German needs a third more room
Translating an app into nine languages is a layout problem before it is a language problem. Short labels grow by a third or more and plurals go from two forms to six.
Habits ships in nine languages, and the work that made that survive was not in the translation files. It was in the layout. A label that fits in English will not fit in German, a sentence with a count in it has two forms in English and six in Arabic, and a screen designed left to right mirrors when the script runs the other way. Every one of those is a layout decision that has to be made before the first translator sees a string, and the tool that makes the decisions stick is an allowance, measured in the longest locale rather than in English, with a screenshot test that fails in that locale rather than the one the developer reads.
How much longer the words get
The expansion is not a guess; it has been measured for decades. The W3C's guidance on text size in translation reproduces the figures IBM published for translation from English into European languages, and they are worse for short strings than for long ones: text of up to ten characters should be expected to grow to between 200 and 300 percent of its English length, 11 to 20 characters to 180 to 200 percent, 21 to 30 to 160 to 180, 31 to 50 to 140 to 160, and only text over 70 characters settles at around 130 percent. The labels on buttons, tabs and settings rows are the short strings, and they are the ones that grow most.
The title of this post undersells it. A third more is what a paragraph needs. A four-letter button label can need three times its width, and a settings row that was designed with the English label snug against its toggle will push the toggle off the screen in German, wrap onto two lines in French, and truncate with an ellipsis in Finnish, all in the same release, all invisible to a developer running the app in English.
Plurals are not two forms
The second surprise is grammatical. English has two plural forms, one item and everything else, and code written in English tends to encode that as a conditional: if the count is one, say this, otherwise say that. Unicode's CLDR plural rules define the categories each language actually needs, and two is the exception, not the rule.
Japanese and Chinese have one form. English, German, Hindi and Turkish have two. French, Spanish and Portuguese have three, with a separate form for large round numbers. Russian, Polish and Czech have four. Irish has five. Arabic and Welsh have six, with distinct forms for zero, one, two, few, many and other.
The practical consequence is that a count never goes into a string by concatenation. It goes through the platform's plural resource, which asks CLDR which category the number falls in for the current locale and picks the translator's form for that category. On Android that is the plurals resource type, and the discipline is that the English file declares only the two forms English needs while the Russian file declares four and the Arabic file six, and the code never knows. The temptation to special-case "1" in code is the bug; the fix is that the code has no opinion about how many forms there are.
The expansion allowance
Knowing the numbers is the easy half. The rule that turns them into a shipped app is the expansion allowance: every label is designed at the width of its longest translation plus a margin, not at the width of its English, and the screenshot test that guards the layout runs in the longest locale rather than in English. Concretely, for each screen, the build renders it in every supported locale, finds the locale whose strings are widest for that screen, and that locale becomes the one whose screenshot is compared against the reference. A layout change that breaks German fails the build in German, before anyone who reads German has seen it.
The allowance has consequences that ripple through the design. Labels that would wrap in the longest locale are shortened in every locale, or the row is given two lines from the start, so that the English version does not look different from the German one in a way that would surprise a user switching languages. Icons with text beside them are laid out so that the text can grow without the icon moving. Toolbars with several actions are designed with the longest locale's labels, which usually means fewer actions per toolbar than an English-only design would allow. None of that is visible in English, and all of it is why the German user sees an app that looks designed rather than translated.
The test itself is worth describing, because it is the part that keeps the allowance honest after launch. Every screen has a reference screenshot per locale, and the build renders each screen in each locale on a fixed device profile and compares. The comparison that gates the merge is the one in the longest locale for that screen, which the build computes rather than a person remembering; the other locales are compared too, but a difference in them is a warning, because a change that fits the longest locale fits the rest. The test catches the two failures that matter, a label that now wraps or truncates and a control that has moved, and it catches them in the language where they happen rather than in the one the developer reads.
Mirroring is the third problem
Right-to-left scripts add a third dimension that no width allowance covers. In Arabic, Hebrew, Persian and Urdu the reading direction reverses, and with it the layout: leading edges become trailing, back arrows point the other way, progress bars fill from the right, and a horizontal list scrolls in the opposite direction. On Android, the platform mirrors a layout automatically if the layout was written in terms of start and end rather than left and right, and Compose's layout primitives follow the same rule; the work is to never write "left" or "right" in a layout, and to check that icons which imply direction, an arrow, a back chevron, a "next" glyph, are marked to mirror while icons that do not, a clock, a checkmark, are not. The screenshot test in a right-to-left locale is the only reliable way to find the one that was missed, and it belongs in the same build as the width test.
Why the layout is where the work lives
The translation file is where people expect localisation to live, and it is the smallest part. The nine languages in Habits are nine files, and the files are maintained by people who know the languages better than I do. What those people cannot do is fix a layout that was designed for English and asked to hold their words. The expansion allowance, the plural resource and the start-and-end discipline are the three decisions that make their work fit, they are made by the person who builds the screens, and they are made once, at design time, for every language the app will ever add. The build that fails in German is how you know the decisions held.
Get new posts by email
Occasional essays on engineering, AI, and building for the people technology leaves behind.
Subscribe with RSS