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 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):
- Flutter — Common Flutter errors (“A RenderFlex overflowed…”): the layout-overflow condition, its yellow-and-black debug striping, and why a child that exceeds its parent's constraints overflows rather than silently resizing — the exact behaviour a longer string provokes in a fixed-width control.
- Flutter API —
MediaQueryData.textScaler: the system text-scale factor a widget test overrides to simulate accessibility text sizing — the knob our old English suite turned to 2×, which is orthogonal to the string-length axis a longer locale stresses. - Flutter — Internationalizing Flutter apps: the ARB-based localization workflow (message keys, per-locale resources, the generated lookups) behind the multi-language pass described here.
Keep reading: all posts on the WizusLabs Engineering blog.