Rubin Aga
Writing

Shipping in five languages, alone

Kapmo ships in English, Spanish, French, German and Italian, and I keep all five up to date on my own. Here's why, and how AI models and DeepL make it possible.

Kapmo is in five languages: English, Spanish, French, German and Italian. With AI, translating an app costs very little now, so it was an easy call.

Why five

English reaches a lot of people, but plenty of people would rather use an app in their own language. French alone covers France, Belgium, part of Switzerland and parts of Africa. Spanish, German and Italian each add millions more. If it makes the app more comfortable for them and it’s a prompt away, I’ll add it. Five is enough for now, though it could be more.

How the app’s text is set up

The app uses i18next. Every piece of text lives in one JSON file per language, about 600 lines each, with English as the fallback. If a key is missing in Italian, you see the English text instead of a blank or a raw key.

When the app starts, it works out your language from your settings, or from your phone if you haven’t picked one, and loads the matching file. You can switch in the app at any time.

The workflow for new text is simple. I add the English string, then ask whichever frontier model I’m working with to add it to the other four files. They’re all good at this, including leaving placeholders like {{name}} alone.

Challenges are different

The app’s own text is fixed, but challenges are generated for each person, so they can’t sit in a JSON file.

When a challenge is generated, it’s written in your language from the start. Translation only comes in when a challenge already exists and someone needs it in another language. Then a separate edge function sends it to DeepL and returns the translation, instead of generating a whole new challenge. That’s cheaper and faster, and the challenge stays the same one.

DeepL gets a bit of help to sound right. Each request includes a line of context saying the text is a short instruction in a photo diary app, asking someone to find or notice something around them. It’s also asked to prefer informal wording, so a challenge reads like a friend’s suggestion in every language, and to keep the original formatting.

The function only takes challenges up to 280 characters. If the source and target language are the same, it doesn’t call DeepL at all. If DeepL fails, it hands back the original text, marked as not translated, so you still see a challenge, just in the language it was written in.

Translations are also cached per language. If you switch the app from English to Italian and back, the challenge on Home switches with it, and it doesn’t ask DeepL again for a language it already has.

The photo feedback works the same way. The model that checks your photo is told to write its feedback in your language, in natural grammar and without mixing languages.

What broke, and how I check it

As far as I know, nothing has broken because of it, and the parts that could break are tested.

The translation function has its own tests, with DeepL faked. One checks that every request carries the right settings: the context line, informal wording and kept formatting. Another checks that when DeepL fails, you get the original text back in its original language. The challenge provider has integration tests that check a cached translation gets reused, and that the challenge on Home changes language when the app’s language does. The rest of the app’s tests run in English, and every language file has the same keys.

The thing I check least is length. German strings are often much longer than English ones, and a button label that fits in English can wrap or get cut off in German. The redesign has an open task to check the longest string on every screen in all five languages before it ships.

Similar

Terms in this post