Skip to content
The Handover

WritingRootnotes

Multi-language support for an agent writing interface code

Why concatenating a sentence breaks translation, and the assumptions in code that only hold for one language.

How this project handles language. Source locale: {{source_locale}}. Shipping: {{supported_locales}}.

Read this even if nothing is translated yet. Almost all the cost of localisation is incurred while writing code that was never localised, and almost all of it is avoidable at the time for free.

The one rule that matters most

Never build a sentence by concatenation. Not with string addition, not with template literals stitching fragments, not by rendering a phrase in three parts around a variable.

"You have " + count + " unread messages"        // three fragments
"You have {count} unread messages"              // one string, one parameter

The first cannot be translated. Word order differs between languages, so a translator receiving three fragments has no way to reorder them; the number’s grammatical effect on surrounding words differs; and the fragments arrive without context, so the translator cannot even tell what they form.

The second is translatable, because it is a complete sentence with a named parameter — which is what a translation system is built to handle.

The same applies to fragments assembled in the interface: a label beside a value beside a unit is three strings that a translator cannot rearrange.

Assumptions in code that hold only for one language

Pluralisation is not two cases. English has one and other; other languages have up to six categories, and some have none. Use the platform’s plural mechanism with a count parameter rather than an if count === 1 branch, which is a two-case assumption baked into the logic.

Text length changes dramatically. Translations commonly run a third longer than English, and some considerably more. A button sized to its English label will overflow. Design and test with a long-text pseudo-locale rather than discovering this after translation is paid for.

Word order changes, which is the concatenation problem again in a different place — anywhere the code assumes a subject comes before an object, or a label before a value.

Not all writing goes left to right. If right-to-left languages are in scope, layout must mirror: not just text direction but the whole interface, including icons that imply direction. Use logical rather than physical properties — start and end rather than left and right — from the beginning, since retrofitting is substantial.

Capitalisation rules differ, and some scripts have no case at all. Never apply title case programmatically to translated text.

Sorting is not alphabetical. It is locale-dependent, and a naive sort produces an order that reads as broken to a native speaker. Use the platform’s collation.

Formats belong to the locale, not to you

Dates, times, numbers, currencies, percentages, units, names and addresses all vary. Do not hand-format them:

  • Dates and times — order, separators, calendar and time zone all differ. Use the platform formatter with a locale.
  • Numbers — the decimal separator and the digit grouping differ, and getting those backwards changes the value rather than the appearance.
  • Currency — symbol placement, spacing and the code itself. Store the currency alongside the amount rather than assuming one.
  • Names — do not assume a given name and a family name, in that order, in two fields. Many people have one name; many have several; ordering varies.
  • Addresses and postcodes have no universal shape. A form assuming one is a form some people cannot complete.

Working with the strings

Keys describe meaning, not English. errors.payment.card_expired survives a copy change; you_card_has_expired becomes a lie the moment the wording changes.

Give translators context. A string alone is ambiguous — “Open” is a verb on a button and an adjective on a status. Add a description and where it appears; without it, translators guess, and guess differently each time.

Editing a source string invalidates its translations. This is the one that surprises people: changing punctuation in the source silently leaves every translation stale, or worse, still-applied and now subtly wrong. If a change is cosmetic, say so; if it changes meaning, the translations need redoing.

Never machine-translate interface copy without review. It produces confident, fluent, occasionally wrong text — and the errors cluster in short strings, where context is thinnest, which is most of an interface.

Do not translate error codes, log messages or identifiers. Diagnostics are for engineers and are best in one language; user-facing messages are separate from them.

Even when there is one language today

The cheap things, all free at the time and expensive later:

  • No concatenated sentences.
  • All user-visible strings in one place rather than inline.
  • Meaningful keys.
  • Platform formatters for dates and numbers.
  • Logical layout properties rather than physical ones.

This is the whole of it. A codebase that does these is translatable when someone decides to translate it; one that does not needs a project first.

Where to stop and ask

  • Adding a locale, which is an ongoing commitment to translate everything subsequently, not a one-off.
  • Changing a source string that has been translated, where the question is whether the meaning changed.
  • Removing a locale, which affects people currently using it.
  • Anything where the layout cannot accommodate a longer translation — that is a design conversation, not a licence to abbreviate the source into something terse and unclear.