How to translate your API with Shapeless
As a global business, Azimo supports nine languages, which means we need a pretty robust translation process. Buttons on mobile devices, for instance, have a character limit. The clear and concise wording that our copywriter uses in English might be an entire paragraph in German, or translate into something offensive or silly in my native language, Polish 😂.
Once translations are ready, they are sent to our products via translation “keys” — a string of numbers and letters that tells our API what text we need to display to the user.
During a recent project, our API was supposed to return an object with translation keys in place of labels. We built and deployed solution based on this requirement. Yet when the frontend team started work, we realised that it would be much better to translate the response on the backend and deliver the labels in the correct language.
At first we wanted to write some logic on every object to allow it to translate itself, but this quickly resulted in huge, cumbersome object graphs that only slowed us down. The following simple technique allowed us to separate the logic of translations from the returned objects.
Let’s define our translations first as:
And a simple response:
The simplest approach would look like this:
Problem solved… but only for this simple use case. What if we wanted to create a more complex response? Adding another field would require manual code changes every time. Building a tree of objects would end up with a nightmare of
copy methods (or using lenses - but it’s still a lot of code).
One might be tempted to create a method for translating an object with something like:
But then you would have to remember to add the logic every time you add an object to the graph.
What we really need is a generic interface:
Cool, but how to implement this? Maybe like this:
Now we have abstracted away the logic of translating the object. A good start, sure, but still we need to write logic for every other object we want to call the function on.
Enter the magic of Shapeless. It allows us to treat any object as a modifiable tree. So the same code would look like this:
The most important part here is line 23, where we replace the translated string. The rest of the code is a standard way of defining a “type class” in Scala. In essence:
- Line 4 — we define how to convert a
case classto an
HList, which allows us to treat every field as an element in the list and then go through the list recursively checking if we have a
Translatedefined for that field
- Line 11 — how to treat the end of the list, since we go through the list recursively we need to know what to do when we encounter the end
- Line 15 — is the bread and butter recursive processing of the list, first apply the logic at the head and connect it with the tail of the list, which is in essence another list (and so on)
This works for our simple example:
But what happens when we add an Int field to our response? Well, it crashes:
Error:(14, 31) could not find implicit value for parameter translate: com.azimo.Translate[com.azimo.Response]
val translator = Translate[Response]
Error:(14, 31) not enough arguments for method apply: (implicit translate: com.azimo.Translate[com.azimo.Response])com.azimo.Translate[com.azimo.Response] in object Translate.
Unspecified value parameter translate.
val translator = Translate[Response]
Why is that? We have no definition for
Translate[Int] in our
object Translate and while going through the list the compiler could not find a representation for our case class that could be derived from our “type class” definition. Adding a simple line would solve the compilation problems:
This solution has some nice advantages:
- You know that your objects “translate” properly on the compilation level (does not compile does not work).
- It’s fairly generic and after you do some adjustments when your objects change, it should work for more and more cases (think
- Less error prone since less copy-paste is required
Disadvantages (sadly there are always some 🙄):
- More complex code, until you are proficient with this kind of coding
- With big object graphs you need to start digging deeper in to Shapeless — check
In the interests of keeping this article short, I skipped
Coproduct and error handling (I suggest returning
Either instead of
translate method) but the solution I’ve described might be a good base for you own translation logic. If you have alternative solutions or questions, feel free to comment below.