WizusLabs Engineering · Craft

The locale that breaks your layout

You lay the interface out in English, you test the overflow in English, and everything fits. Then you add a language whose words are longer — German, usually — and a control that behaved for your entire development cycle quietly runs off its own edge. Nothing errors. The layout just breaks, in a language you were not looking at.

By WizusLabs Engineering · 2026-07-27 · ~8 min read

Here is the trap we walked into, stated as plainly as we can: an overflow test written in English — even a careful one that cranks the system text size all the way up to 2× — is not a test of a longer language. It proves that English survives being enlarged. It says nothing at all about what happens when a control laid out to fit “Manage privacy choices” is handed “Datenschutz-Einstellungen verwalten” instead. Those are two different failure modes that look like one, and the gap between them is exactly where a shipped-in-five-languages app grows a layout bug that its author never sees, because its author reads English.

The bug that only speaks German

The concrete setting: we localized Embergrid, our offline-first grid-bomb arcade game, from English and Vietnamese up to five languages — English, Vietnamese, Spanish, Portuguese, and German. That is a full pass: roughly 1,568 message keys per language, all at parity, plus the in-game language picker, the store metadata, and the bundled accessibility statement. Translation itself is the well-understood part. The part that bites is that translated strings are not the same length as the strings your layout was built around, and German is the reliable worst case. German builds nouns by welding smaller nouns together into one unbroken word, so a phrase that English spreads across three short tokens arrives as a single long one. “Accessibility” becomes “Barrierefreiheit.” “Manage privacy choices” becomes “Datenschutz-Einstellungen verwalten.” The meaning is identical; the number of pixels it demands is not.

Now put that longer string into a control that was sized, pixel for pixel, to the English one. A button, a settings row, a HUD label, a dialog title — anything laid out with a fixed or tightly-bounded width. The English text fit with padding to spare, so nobody thought of it as tight. The German text does not fit, so it overflows: the framework clips it, or throws the yellow-and-black overflow stripes that every Flutter developer recognizes, or shoves a neighbouring widget off the screen. And here is the part that surprised us most — it does all of this at the default text scale, 1.0×, before the user has touched a single accessibility setting. The overflow is not an edge case triggered by a large-text preference. It is the normal, first-launch appearance of the app for a German speaker.

Why 2× English is not the same as 1.0× German

It is tempting to assume an accessibility text-scale test already covers this — surely if the layout survives English at 2× it survives German at 1×, because German is only about a third longer? It does not, and the reason is worth being precise about, because it is the whole lesson. Scaling English makes every glyph bigger while leaving the shape of the text unchanged: the words and their break opportunities are exactly where they were, just larger, so the layout engine still has the same places to wrap. A longer locale changes the shape itself. A German compound is frequently one long, unbreakable token with no space in the middle for the layout to fold on. You are not asking the box to hold bigger letters; you are asking it to hold a single word that is wider than the box, with nowhere to break it. Those stress the layout along completely different axes.

So the two tests answer two different questions. “Does English survive 2×?” is a question about vertical growth and wrapping headroom. “Does German fit at 1×?” is a question about a single horizontal run exceeding a fixed width. An app can pass the first cleanly and fail the second on the first screen, and ours would have. The character distribution is the tell: enlarged English is still English-shaped, and a genuinely longer language is a different distribution of widths that your English corpus, at any scale, never contained.

The same fixed-width control, sized for English, overflows in German Two identical controls of the same width. The English label "Manage privacy choices" fits inside with room to spare. The German translation "Datenschutz-Einstellungen verwalten" is longer and runs past the right edge of the same box, clipping at the boundary — a layout overflow that happens at the default 1.0x text scale, before any accessibility text scaling is applied. One control, one width, two languages English (en) Manage privacy choices fits, with padding to spare German (de) Datenschutz-Einstellungen verwalten the compound word runs past the box edge → RenderFlex overflow at 1.0× scale
Same control, same width, two languages: the box was sized to the English string, so the longer German compound runs past its edge — and it does so at the default 1.0× text scale, before any accessibility scaling is even in the picture.

The safety net had one blind spot

We are not careless about overflow; we had tests for it. A suite of widget tests pumped the densest screens — the HUD, settings, results, pause, the dialogs — and asserted that nothing overflowed, and it ran those assertions all the way up to 2× text scale. It was a real net and it had caught real bugs. It had exactly one flaw: every one of those tests pumped Locale('en'). The net proved, thoroughly and repeatedly, that English does not overflow. When we added three longer locales, the code that renders them was never asked the question. German at 1.0× — visibly the longest of the five, and the one most likely to break — walked straight through the gap the suite left open, because the suite did not know the gap was there.

This is the recurring shape of an automated check, and we have written about it before: a gate only ever catches what you told it to look at. A green run does not mean “the layout is safe.” It means “the specific inputs we enumerated did not overflow.” The distance between those two sentences is precisely the set of locales we forgot to enumerate. The test was not wrong. Its coverage was a claim about English that we had quietly started reading as a claim about the app, and those stopped being the same thing the moment a second alphabet-width showed up.

The fix is a loop, not a redesign

The reassuring part is how cheap the honest fix turned out to be. We did not redesign a single screen or invent a per-language layout. We added one permanent, parameterized regression guard — locale_overflow_test.dart — that takes the overflow-assertion harness we already trusted and runs it across the new locales, de, es, and pt, over the same highest-density surfaces (HUD, settings, results, pause, and the dialogs), at both the default and the maximum text scale. That is 42 cases in total, and once the guard existed, all 42 passed: German, our binding worst case, is verified overflow-free on those surfaces. The whole change is a loop over a list of locales wrapped around a check we had already written. The expensive thing was never the test; it was not knowing we needed it.

There is a small strategy inside the loop worth stealing. You do not have to exhaustively test every locale you ship to catch this class of bug — you have to test the longest one. If the surface holds the longest string in your longest language without overflowing, the shorter languages almost always follow for free, because they were never the constraint. German earns its place at the front of the queue for the same reason a load test uses your heaviest payload: it is the one most likely to fail, so verifying it buys you the most confidence per case. Test the worst case on purpose, and the ordinary cases come along.

A locale is not done when it is translated

The durable lesson outlasts this one app and this one language. We used to think of adding a locale as a content task — get the strings translated, reach key parity, ship. It is not. Translation gets you a language the app can say; it does not get you a language the app can show, and the gap between those two is a layout question the translator was never in a position to answer. So we moved the finish line. A longer-than-English locale is not done at translation parity; it is done when its layout has been verified not to overflow — and the way you guarantee that is to bundle a locale-overflow guard into the same change that adds the locale, not to promise yourself you will get to it later. Later is when it ships broken.

This sits alongside the rest of how we try to make correctness a property of the process rather than of anyone's memory — the same instinct as building an engine so its bugs reproduce on command rather than trusting we will remember how they happened. A rule you carry in your head fails the first busy release; a test that runs the longest word in your longest language, every time, does not. The longest word in the language you do not read is the one that breaks the layout you were sure was fine — so make the machine read it for you. The longer version of how these small shipping disciplines compound is on the WizusLabs Engineering blog.

Notes

This is a first-hand account of a localization pass on our own game, not a general Flutter tutorial. The specifics — five languages (English, Vietnamese, Spanish, Portuguese, German), roughly 1,568 message keys per language at parity, a parameterized guard of 42 cases across five surfaces at two text scales — are our own project facts, taken from the work itself, not benchmarks or public figures. The German strings quoted (“Barrierefreiheit,” “Datenschutz-Einstellungen verwalten”) are the real translations that triggered the reasoning. It deliberately contains no invented metrics: we have attached no overflow-count, percentage of screens affected, or render-timing number to any of it, because the point is the failure mode and the guard against it, not a statistic we did not measure. The diagram is a schematic to make the overflow legible; it is not a screenshot of a measured layout.

Sources

The framework mechanisms this post leans on are documented by their maintainers (living documents; accessed 2026-07-27):

Keep reading: all posts on the WizusLabs Engineering blog.

← Back to the Blog