CGChristoph Griehl
Resume
WorkExpertiseWritingAboutContact
← All writing
Web Development · Technical

A Comprehensive Approach to Internationalizing Applications

Getting i18n right across the client, the backend, and the team workflow — without turning your codebase into a string graveyard.

CG
Christoph Griehl
Senior Full-Stack Engineer
Sep 15, 20233 min read
A mobile phone on world travel
A mobile phone on world travel

Internationalization tends to arrive late and loud: a deal in a new market, or a partner who needs the product in their language by Friday. By then the codebase is full of hard-coded strings, dates formatted three different ways, and a backend that only ever imagined one locale. Here is how I approach i18n so it stays maintainable instead of becoming a tax on every future feature.

Start at the boundary, not the strings

The instinct is to start replacing visible text with translation keys. That is the easy part. The harder part is everywhere a locale silently leaks in: number and currency formatting, dates and time zones, pluralization, sort order, and text direction. Decide early where the locale boundary sits in your system, and treat anything that crosses it as locale-aware by default.

In practice that means a single resolved locale value, threaded from the request through to rendering — never re-derived halfway down the stack from a guess.

On the client: a library and a single source of truth

Pick a well-established library rather than rolling your own. I reach for i18next: it is framework-agnostic, battle-tested, and you inherit the community’s hard-won edge cases. Then keep the setup boring. A single JSON file of translations is usually enough; reach for namespaces, nested keys, or backend-loaded bundles only when a real constraint — like bundle size — forces it. Each of those adds a way to lose track of a key.

Give keys structure with a self-explanatory, BEM-inspired convention such as userSettingsPage.languageSwitch, and let tooling keep them honest. The i18next-parser scans the codebase, removes dead keys, and adds new ones; on TypeScript projects, generating a key type turns a missing translation into a compile error instead of a production surprise.

{
  "userSettingsPage.languageSwitch": "Language",
  "checkout.items": "{count, plural, one {# item} other {# items}}",
  "checkout.total": "Total: {amount, number, ::currency/EUR}"
}

Using the ICU message format for plurals and interpolation keeps grammatical logic out of your components and in the one place translators can actually reach it.

A missing translation should degrade gracefully to a readable default — never to a raw key staring back at your user.

The backend speaks locale too

Emails, PDFs, validation errors, and notifications all originate server-side, and they need the same locale machinery as the client. Storing a language preference in the database is fine, but querying it on every request does not scale in a stateless service. A cleaner order of resolution:

  1. Read the locale from a custom header on the request.
  2. Fall back to the user’s stored preference in the database.
  3. For public endpoints, fall back to the browser’s Accept-Language header.
  4. Fall back to a default locale for anything left.

Then thread that one resolved value into every background job. The most common production bug here is a perfectly localized UI sending its confirmation email in the wrong language.

Make translation a team workflow

Engineers should not be the bottleneck for copy in a dozen languages. A platform like locize lets non-technical teammates edit translations directly, and a webhook can open a pull request automatically — leaving the team to review and merge. Extract keys in CI, keep the default locale complete and reviewed, and fail the build on missing keys for shipped locales.

What I’d do differently

If I could tell past-me one thing: add the locale boundary on day one, even with a single language. Retrofitting i18n is mostly archaeology — finding every place a string, a date, or a number slipped through. Building the seam upfront costs almost nothing and saves a brutal refactor later. Boring infrastructure, applied early, wins again.

UIUXFreelance
CG
Christoph Griehl

Senior full-stack engineer in Germany, working across AI/RAG systems, geospatial software, document intelligence, and data-heavy web platforms.

Keep reading
May 23, 2023 · Personal

Remote Engineering on the Road

Read article →
Apr 16, 2023 · Technical

Building a Production Web Component Library

Read article →