How i18n Is Actually Done
Category: Web
i18n (Internationalization) is the engineering work that lets an app support multiple locales. Beyond language, it usually also has to handle differences in units (currency, temperature, distance, etc.), date and time formats, and writing systems (grammar, word breaking, and Arabic's RTL). The more languages you support, the more users you can reach (or maybe it's the other way around).
Unicode support has to start at the bottom
Not just the frontend display — data sent to the backend, the database, the CMS, API responses, error messages, all of it needs to handle Unicode. None of these layers can be the one that breaks.
Locale
A Locale (region) is a Language Code (defined by ISO 639, e.g. en, zh) plus an optional Country Code (defined by ISO 3166, e.g. uk, us, tw).
A product's prototype can start in English or whatever language the team is comfortable with, then add other languages and regions — even currency, dates, addresses, tax rates, phone numbers, legal terms — based on the target market and user feedback. The important principle is build what you need now, leave room for future expansion, and don't over-engineer.
Who should be involved in i18n?
Basically everyone: frontend, backend, QA, PM, Designer.
- Frontend/Backend: build the translation files the UI uses, locale metadata, and agree on the API format (Accept-Language header, or locale in the request, etc.)
- PM: decide which languages/countries the first version should support, whether to support dates/currency, which features need localization, and estimate the cost of maintaining a given requirement
- Designer (UI/UX): whether components or layouts can accommodate translations of different lengths
- QA: whether text gets truncated, whether special characters render correctly, whether there's mojibake, whether any translations are missing
At a larger scale, even Legal, Data, Marketing, and CSM need to get involved. Eventually you realize i18n is a cross-team product capability that solves an engineering problem — it's not the isolated work of a single team.
How is i18n usually done on the frontend?
Building the translation files
The simplest architecture is to pull all the text out of the app into key/string pairs, then pick the right string based on the Locale.
Instead of writing <button>Submit</button>, you write <button>{t("button.submit")}</button>, and build a translation file like
{
"button.submit": "Submit",
"button.cancel": "Cancel",
...
}
Deciding what to name the key is its own problem — same as naming a variable when writing code — usually you settle on a structured/nested path naming convention. As for who should decide the key names? Same answer: get everyone in a meeting.
Treat one language as the Single Source of Truth
Once multiple translation files already exist, this principle helps with ongoing maintenance.
Say English is the source of truth: adding a new translation only requires adding the key and string to the English file. The other language files don't need to change, or only need the new key added.
The reason for this is that if your CI/CD pipeline has a translation management platform or an auto-translation bot, you get scenarios like:
- A new button is added, and the translation platform/bot sees "hey, the content of this key in
zh-TW.jsonis identical toen.json(or it's empty)! Looks like this hasn't been translated yet, send it off for translation." - The English translation changes, and the platform/bot sees "hey, this key's content in
en.jsonwas just updated, but the Chinese still matches the old version, looks like it needs to be retranslated."
This approach guarantees a few things:
- Fallback for missing translations
- Manual translations don't get overwritten by the translation platform or bot
- Engineers maintain one translation file only.
i18n Libraries
There are plenty of existing libraries that handle the frontend i18n work for you — common ones include i18next, react-i18next, next-translate, etc. Which one to pick depends on your needs, app framework, and so on.
What these libraries handle:
- Text translation: a
t('key')-style function that returns the string for the currently selected locale given a key - Locale switching: an API for dynamically switching locales (e.g.
i18n.changeLanguage('zh-TW')), and the UI usually updates automatically or manually afterward, so the i18n layer decides what the UI should display based on locale - Variable interpolation:
t('welcome', { name: 'Alex' })-> "Welcome back, Alex!" - Pluralization: picking singular vs. plural forms based on a number, which can get quite complex across different languages
- Formatting: handling number, date, time, and currency formats for different regions
- Fallback: so a missing key doesn't crash the app
Testing and Performance
Once the i18n architecture and maintenance workflow are on track, as the number of supported locales and the size of the dictionary files grow, you'll need:
- Unit Tests: testing logic around pluralization, date, time, and currency formatting
- Static Analysis: automatically diffing in CI/CD — are the keys fully aligned across files? Are there keys called in code but missing from the JSON? Are there unused, redundant keys?
- Lazy Loading: only asynchronously fetch the JSON file for a given locale on first load or when switching locales
- Caching and splitting: preload shared or high-frequency locales, and use dynamic loading and caching for less-used locales or large pages' translation dictionaries, balancing "data usage" against "smoothness"