Chapter 19·Shipping translations·~6 min
Pseudolocalization workbench
Pseudolocalization replaces 'Sign in to continue' with '[!! ßîğň ìñ ţö çöńţïñúé !!]' before any real translation is bought: same shape, obviously not English. Anything that doesn't transform is a hardcoded string. Anything that overflows is a layout that won't fit German.
The problem
The classic localization bug is a string that never entered the translation pipeline: hardcoded in a JSX attribute or a server error message, it surfaces the day after launch, when a French user sends a screenshot of half-English UI. Pseudolocalization finds these strings before launch, by running the app pretending to be translated.
How it works
A fake locale that finds real bugs
Pseudolocalization turns English strings into readable-but-strange English before a single translator is hired. Every part of the transform hunts a specific bug class. Accents (Ḥéļļö) mark text that flowed through the i18n layer, so anything still plainly readable in the UI is a hardcoded string that bypassed it. Padding simulates German-scale growth and finds truncation. Delimiters like ⟦…⟧ make silent clipping visible at a glance. An RTL flip previews mirroring long before any Arabic translation is bought.
A serious transform is also careful about what it does not touch. {placeholders}, HTML tags, URLs, and printf tokens pass through unaccented, because a mangled {count} does not fail QA: it crashes at runtime.
It belongs in CI rather than in a quarterly audit because string bugs regress one commit at a time. A pseudoloc build that someone (or a screenshot diff) checks weekly catches the new hardcoded string while its author still remembers writing it.
The demo
Try these, in order
Each step reproduces one specific failure in the demo below.
- 1With the default JSON payload and “Accent” mode, read the items_in_cart string in the output. The ICU skeleton ({count, plural, =0 {…} one {…} other {…}}) keeps its keywords intact while the branch text gets accented. A transform that accents “plural” breaks the runtime.
- 2In the workbench at the top, switch to “Expand (40%)”. Every string gets padded. This is the text-expansion smoke test: any layout that clips here will clip in German.
- 3Switch to “RTL flip”. Strings render right-to-left with bidi control characters: a cheap first look at what a layout does under RTL before anyone buys an Arabic translation.
- 4Paste a strings file from a real project into the source box. Anything that comes out un-accented in the app UI is a hardcoded string that never went through the i18n layer: the class of bug pseudoloc exists to catch.
- 5Scroll to “A real screen under pseudoloc”. With its “Expand (40%)” button active, compare the “Upgrade plan” button in the Source card with its twin in the Pseudo-translated card. The pseudo button clips its label: the button has a fixed width, and the padded string overflows it. The textarea only showed a longer string; the screen shows the truncation it causes.
- 6In the same section, click “RTL flip”, then “Accent”. Under RTL flip the whole pseudo card mirrors: badge, text alignment, and button swap sides. Under Accent nothing clips and nothing moves: accents only prove the strings went through the i18n layer, and finding the layout breaks is the other two modes' job.
The transform leaves variables ({name}), HTML tags, and URLs untouched, as any real pseudo-translator must.
A real screen under pseudoloc
Storage
Almost fullYour workspace is using 9.3 GB of its 10 GB quota.
Older file versions are removed first when you run out.
⟦·· Šţøŕàĝé ··⟧
⟦·· Åĺɱøšţ ƒúĺĺ ··⟧⟦·········· Ýøúŕ ŵøŕķšþàçé íš úšíñĝ 9.3 ĜƁ øƒ íţš 10 ĜƁ ǫúøţà. ··········⟧
⟦··········· Øĺðéŕ ƒíĺé ṽéŕšíøñš àŕé ŕéɱøṽéð ƒíŕšţ ŵĥéñ ýøú ŕúñ øúţ. ···········⟧
Under Expand (40%), the fixed-width button clips its label, the same clipping a German translation would cause. Under RTL flip, the whole card mirrors: badge, text alignment, and button move to the other side. All of this appears before a team buys a single Arabic translation.
The full transformation chain
How to use this in CI
1. Add a build step that produces a pseudo-translated locale file from the English source (the function in src/lib/pseudoloc.ts is a starting point, and production tools wrap ICU MessageFormat-aware parsers).
2. Run the screenshot / visual-regression test suite against the pseudo locale on every PR.
3. Fail the build if any English text appears un-transformed in the screenshots, or if any element overflows when expanded by 40%.
The short version
Pseudolocalize in CI, before any money goes to translators. It finds hardcoded strings, concatenation, and brittle layouts while they are still cheap to fix.
What to do about it
- Add pseudoloc as a CI mode. A build that runs with
LOCALE=qps-ploc(the standard pseudo-locale) should be screenshot-diffed against the English baseline. Any text that stays unchanged is flagged for review. - Use accent mode to find missing strings, expand mode to find layouts that won't fit longer translations, and RTL mode to find CSS that's not direction-aware.
- Preserve ICU variables, HTML tags, URLs, and printf placeholders; pseudo-translating those breaks the runtime resolver. The transform should treat them as protected regions.
- Make this a pre-merge gate on PRs that touch user-facing code.
Where this comes up
Who it concerns
Moments
- ·PR review for user-facing features
- ·Pre-launch QA
- ·First localization conversation with a new team
Use the tool: String audit · run the send-out checks on a real file →
Field note
This is old, proven tooling: Windows has shipped built-in pseudo-locales (qps-ploc and its variants) since Vista, so teams could test localizability without waiting for translations. If an OS ships that as a first-class feature, a web app can afford it as a CI step.