A web app that translates English text input into Pig Latin.
Pig Latin is a language game with the following rules:
ay
added to the end.
way
added to the end.
way
are not modified.
Clone the repository.
git clone https://github.com/ythecombinator/piglatin-translator
cd
into the directory.
cd piglatin-translator
Install the project dependencies:
yarn
# or
npm install
Start the development server:
yarn start
# or
npm run start
Head over to localhost:3000 in your browser of choice.
To run tests, do:
yarn test
# or
npm run test
I tried to benefit from a declarative functional-based approach because:
Some teams might not feel comfortable with the adoption of a functional utility belt like ramda. We could use normal side-effectful imperative approaches combined with an immutability helper, like immer, instead.
For example, let's get handleCaseOf
transformer:
At a first glance, it might seem a bit difficult to reason about its behavior because of the number of functions involved. Another way of achieving the same behavior would be:
const handleCaseOf = (original: string) => (parsed: string) => {
const lettersParsed = parsed.split("");
const lettersOriginal = original.split("");
lettersOriginal.forEach((letter, index) => {
if (!punctuationRegex.test(letter) && letter === letter.toUpperCase()) {
lettersParsed[index] = lettersParsed[index].toUpperCase();
}
});
return lettersParsed.join("");
};
We could rewrite it wrapping with produce
utility from immer which would guarantee immutability but we would miss advantages from ramda like auto-curried functions which help us with partial application and function composition.
Both are linear when it comes to the sentence length (number of characters passed as input).
Although this application core is quite simple, it's designed in a way adding new rules/behaviors is trivial, given it only requires you to:
translator.transformers
applyTransformers
functionThere are more testing approaches relevant for this app which are out of scope for its first iteration. To mention:
To ensure correctness, we could do a mathematical proof, using induction. This would be the safest approach but also the hardest one. Unless we use a proof assistant, like Coq or Isabelle, the proof is done manually. The problem of this approach is that the proof is related to the mathematical function, and not the TypeScript implementation. Therefore, the property can be proved correct but the implementation may still be wrong.
To tackle this, we could use property-based testing, which consists of generative testing. With them, we don't need to supply specific example inputs with expected outputs (like the ones in the overview section) as with unit tests. Instead, we could define properties about the code and use a generative-testing engine (e.g., fast-check) to create randomized inputs to ensure the defined properties are correct.
The unit tests under .spec
files still have their place, though. They are important for the early stages of TDD (like the MVP of this translator web app, for example) because they serve as anchor points to ensure that the development efforts proceed as desired (like accomplishing what is described in the provided Pig Latin spec).
Eventually, these example-based tests end up being passive tests; the tests become part of a regression suite and provide no new information about the functionality. Property-based tests, however, are always active tests as they generate new data each time the test suite is run.
We could come up with some custom arbitraries for each of our rulese.g.
way
And then check against them.
For our components, we could shallow test our hooks with Enzyme.
For example, we could have tests in our Translator
component to ensure that originalText
state changes properly on TextField
events and also to ensure that useEffect
properly reacts to these updating translatedText
state.
To ensure the UI itself behaves as expected, we could have Cypress (or similar) automatically testing functionality as close as possible to a real user.