Freesbe

Shipping 25 languages with English as the key

By Freesbe3 min read

Freesbe’s apps speak 25 languages, from Spanish and Japanese to Hindi, Arabic and Hebrew. We added the first ten about ten days into the project and the next fourteen a couple of days later. That only worked because of a few early decisions about how translation fits into the code.

The decision: the English sentence is the key

There are two common ways to mark text for translation. The first uses ids:

<Button>{t("composer.send_button")}</Button>

with a separate English file that says "composer.send_button": "Send". The second uses the English sentence itself:

<Button>{t("Send")}</Button>

We chose the second. Each language’s catalog maps the English sentence to its translation: "Send": "Enviar".

Why:

  • Code reads like the product. In a code review you see exactly what the person will read, not an id you have to look up. When most of your code is written by AI agents and read by people, that matters even more.
  • A missing translation still makes sense. If a language hasn’t caught up with a new sentence, people see the English, not composer.send_button.
  • No naming debates. Nobody has to decide whether it’s send_button or composer.send.

The usual objection is that the same English word can mean two things (“Archive” the verb and “Archive” the noun). In practice it’s rare, and the fix is to write clearer English, which helps the English readers too.

Plurals, done by the language, not by us

English has two plural forms: “1 message”, “2 messages”. Russian and Polish have more, Arabic has six, and Japanese effectively has one. Hard-coding count === 1 ? … : … is wrong for most of the world.

So plural messages use the language’s own rules from the browser’s Intl.PluralRules. A translation provides a form for each category its language uses (“one”, “few”, “many”, “other”), and the app picks the right one.

Right to left is more than dir="rtl"

Arabic and Hebrew read right to left, and switching the page direction is the easy 20 percent. The rest is a long list of small things:

  • Logical CSS everywhere. We write ms-2 (margin at the start) instead of ml-2 (margin on the left), ps-3 instead of pl-3, text-start instead of text-left. Then the layout flips by itself.
  • Each message decides its own direction. A Hebrew speaker in an English workspace writes some messages in each language. Every message gets dir="auto", so each one reads the right way.
  • Names are isolated. A sentence like “Maya joined” in a right-to-left language, with a left-to-right name in the middle, can come out scrambled: punctuation jumps to the wrong side, numbers attach to the wrong word. Every value inserted into a translated sentence is wrapped in invisible Unicode isolation marks, so it keeps its own direction without disturbing the sentence.
  • Icons that point somewhere flip. A “back” arrow points right in Arabic. A play button does not.

Keeping 25 catalogs in step

Every feature adds new text, and a feature can ship in English first, so translation never blocks a release. A script finds every t("…") in the code and lists what each language is still missing, so nothing is forgotten. First drafts of new translations are made by AI and checked in context in the running app.

With many branches adding strings to the same catalogs every day, merge conflicts were constant. We fixed that with a custom git merge driver that merges catalogs string by string, which we wrote about in Running many AI coding agents on one repo.

What we turned down

  • Id keys. The industry default, and the reason so many apps show settings.title somewhere when a translation is missing.
  • Translating on the fly. Machine-translating text as the page loads sounds easy, but it’s slow, costs per view, and gives different words for the same button on different days.
  • English only until we’re bigger. Adding languages later means finding every hard-coded string in a large codebase. Adding them early meant every string was marked from the start.

What we’d tell another team

Mark every string for translation from the first screen, even if you only ship English. Use logical CSS from day one. And test your app in a right-to-left language at least once a week: it’s the fastest way to find layout that only works by accident.

To change Freesbe’s language, see Language in the help center.

Try all of it free with up to 10 people.

No card, no time limit. Invite your team and see what sticks.