We now ship in 8 locales (English, Spanish, French, German, Portuguese-BR, Japanese, Korean, Arabic). Getting from 1 to 3 was easy. From 3 to 5 got hard. From 5 to 8 required rewriting most of what we'd built. This is the pattern we landed on — the specific architecture that would have saved us months if we'd built it first. Practical over theoretical.
What we built wrong initially
Our first internationalization was 90 minutes on a Friday afternoon. Wrapped every string in t('key'); extracted to JSON; had a contractor translate to Spanish. Shipped. Felt easy.
Then we added French. Discovered: some plurals didn't work (we'd hardcoded singular/plural in code). Fixed those manually.
Added German. Discovered: half our UI buttons overflowed their fixed-width containers. German is 30-50% longer than English. Fixed by giving buttons flex widths.
Added Japanese. Discovered: our date formatting was hardcoded to MM/DD/YYYY. Discovered: our text was mixed with fonts that don't include Japanese glyphs. Discovered: some placeholder text was truncated with ellipsis; Japanese characters wider than expected.
Added Korean. Similar issues to Japanese but different. Started noticing translator turnaround was slow (2-3 weeks per string batch); half the strings we added never got translated; users saw English fallback everywhere.
Added Arabic. Everything broke. RTL. Our CSS was full of margin-left and padding-right; layouts mirrored badly; icons pointed the wrong way; text got cut off.
At this point we stopped and rebuilt.
What we rebuilt
Seven pieces. All decisions we'd make differently if starting today.
Piece 1: ICU MessageFormat for everything. Not t('key') with variable interpolation; ICU. Handles plurals (all languages), gender, select, formatting. Migration was painful; new work never regresses.
Piece 2: CSS logical properties everywhere. margin-inline-start not margin-left. Works in LTR (behaves like margin-left) and RTL (behaves like margin-right). No conditional CSS; no separate RTL stylesheet.
Piece 3: Explicit context for every string. Description field in every translation key. “Submit” as button vs. “Submit” as verb in a sentence: different translations. Translators can't know without description; without context, they guess wrong.
Piece 4: Automated string extraction on merge. CI extracts changed strings; pushes to Lokalise; translators see updates within a day of merge. Reduced turnaround from 2-3 weeks to 2-3 days. Consistency more important than any single sprint push.
Piece 5: Locale-aware everything (Intl APIs). Dates: Intl.DateTimeFormat. Numbers: Intl.NumberFormat. Currency: Intl.NumberFormat with currency style. Lists: Intl.ListFormat. Never hardcoded formats; never string-formatted from templates.
Piece 6: Pseudo-locale for UI testing. Every layout gets tested with a pseudo-locale that's 40% longer with accented characters and RTL markers. Catches layout breakage before real locales; fast feedback.
Piece 7: Orphaned message cleanup as CI check. CI job: extract used keys from code; compare to translation files; report unused. Prevents translation-file bloat and translator-time waste on strings nobody uses.
What we do differently now
Concrete decisions, in order of what would-save-us-most-time:
Design for text expansion from day one. Every text container: flexible width. Every layout: tolerant of 50% longer strings. This is a design-time decision, not runtime. Test with pseudo-locale continuously.
Never concatenate. “Hello, ” + name + “!” breaks in every language with different word order. Use ICU: “{name}, hello!”. Translator can reorder.
Never assume plurals are singular/plural. ICU plural syntax; even for English. Consistent; safe for any language you might add.
Never hardcode dates/currencies/numbers. Intl APIs. Non-negotiable.
Always test with 3+ locale families. English (baseline), German (long text), Japanese (different script + reading), Arabic (RTL). Catches most issues before shipping.
Provide context in every key. Description field mandatory. Screenshots when possible.
Automate the pipeline. Manual translator handoff bottlenecks everything. Automation from PR merge to translator visibility is the highest-value infra work.
What Claude Code changed
Specific pieces that got materially cheaper:
i18n-strategist subagent — i18n-strategist for architecture questions. “How should we structure our string keys?” produces trade-offs; not just an answer. Prevents mistakes we'd otherwise make.
String extraction and refactoring — Migrating 8,000 t() calls to ICU MessageFormat was originally estimated at 3 sprints. With Claude Code handling mechanical conversion (human review): 1 sprint.
Context descriptions — Adding descriptions to 8,000 existing translation keys was previously never-going-to-happen. Claude Code drafts descriptions from surrounding code context; humans review. Half-done in a week.
RTL migration — Migrating 340 CSS files from directional properties (margin-left) to logical (margin-inline-start): Claude Code + tests catches most; humans review edge cases.
Nothing revolutionary. Each individual thing is modest. Cumulative effect: i18n went from “major project every quarter” to “background maintenance in normal work.”
What still doesn't work
Honest about the limits:
Marketing copy tone. Direct translation of marketing copy sounds wrong in most languages. Requires transcreation (creative adaptation) not translation. Slower; more expensive; unavoidable for high-visibility content.
Culturally-specific concepts. Idioms, humor, cultural references. Some concepts don't translate; require rewriting for target culture. Product copy usually fine; marketing and brand copy require care.
Right-to-left number handling. Numbers in Arabic content follow specific bidi rules; Unicode bidi algorithm gets most of this right but edge cases (dates mixed with numbers, phone numbers, currency codes) require explicit handling.
Legal disclaimers. Legal text differs by jurisdiction. Not just translation; different legal requirements per market. Handled by legal team; product team can't just translate US disclaimers into Spanish and call it done.
What to do with this
If you're at 1 locale considering a second: rebuild i18n foundation first. Cheaper now than at locale 4.
If you're at 2-3 locales and things are working: audit your foundation before adding a 4th. Fix pluralization (ICU), fix formatting (Intl), plan for RTL if it's on the roadmap.
If you're at 5+ locales and it's chaotic: rebuild in place. Painful but pays back quickly. What we did.
Combined with the i18n-strategist subagent and the full i18n guide, this is where our practice landed. Not perfect. Sustainable.