Internationalization (i18n)
Supporting multiple languages and regions: locales, formatting, RTL, and translation workflows for frontend applications.
Internationalization (i18n) is the design and implementation of your app so it can adapt to different languages and regions. Localization (l10n) is the actual translation and locale-specific formatting. This guide covers the frontend side of both and how to implement them without blocking feature work.
Locales and Language Tags
A locale is a language and optionally a region (e.g. en-US, fr-FR). Use BCP 47 language tags consistently. Detect the user's locale from browser settings, URL, or user preference and store it (URL param, cookie, or app state). Let users override it in settings.
Detection order: Many apps check (1) saved user preference, (2) URL segment or query param, (3) Accept-Language header or navigator.language. Fall back to a default locale (e.g. en) when the requested one isn't supported. Persist the choice so returning users get the same language without re-detecting.
Extracting and Managing Strings
Don't hardcode user-facing strings. Use keys (e.g. welcome.title) and look up the translated string for the current locale. Store translations in JSON or similar; load the right bundle per locale. Support pluralization and interpolation (e.g. "You have items"). Libraries like react-intl, i18next, or formatjs handle this.
Key naming: Use a hierarchy that matches your UI (e.g. checkout.title, checkout.buttons.placeOrder) so translators and developers can find strings easily. Avoid keys that are the same as the English text—if the key is the fallback, you lose the ability to change the default without touching code. Keep keys stable across releases so translation memory tools can reuse prior translations.
Formatting: Dates, Numbers, Currency
Use the Intl API (Intl.DateTimeFormat, Intl.NumberFormat, Intl.RelativeTimeFormat) to format dates, numbers, and currency according to the user's locale. Never concatenate date parts manually—locales differ in order and separators. Same for numbers (decimal and thousands separators) and currency (symbol position, spacing).
Examples: new Intl.DateTimeFormat(locale, { dateStyle: 'long' }).format(date) for a locale-aware date string. For relative time ("2 hours ago"), use Intl.RelativeTimeFormat and pass the current locale. For currency, always pass the currency code (e.g. USD, EUR) and let Intl handle symbol and placement. If you need a specific format (e.g. short date for tables), pass options rather than building the string yourself.
RTL (Right-to-Left)
For Arabic and Hebrew, support RTL layout. Use CSS logical properties (margin-inline-start instead of margin-left) and dir="rtl" on the document or container. Test that layouts flip correctly and that icons and asymmetrical UI are mirrored where appropriate. Consider dir="auto" for user-generated content that might be mixed LTR/RTL.
Logical properties: Prefer margin-inline-start, padding-block-end, and text-align: start so that when dir changes, the layout flips automatically. Avoid hardcoding left/right for layout; use them only for visual effects that shouldn't flip (e.g. a decorative corner). Test RTL early—retrofitting is harder than building with logical properties from the start.
Translation Workflow
Translations are often maintained in a separate system or by non-engineers. Use a workflow that exports source strings, imports translations, and keeps keys in sync. Missing translations should fall back to a default (e.g. source language) and be visible in dev so nothing ships untranslated by accident.
CI and missing keys: In development, you can show the key or highlight missing translations so developers notice. In production, fall back to the default locale or a clearly marked placeholder. Some teams run a build step that fails if a required locale has missing keys for the default locale. Integrate with your TMS (translation management system) so that new keys are exported and updated translations are pulled in on deploy.
Performance: Loading Locale Data
Loading every locale up front can bloat the bundle. Consider loading the active locale's messages asynchronously and showing a minimal shell until they're ready. For Intl, most runtimes already ship with locale data; if you use a polyfill or extra locale data, load only the locales you support. Lazy-loading translation files per locale keeps the initial bundle small and speeds up first load.
Summary
Solid i18n from the start avoids costly rework. Centralize strings, use Intl for formatting, and support RTL so your product can serve a global audience. Invest in a clear locale and key strategy, and wire translations into your build and deployment pipeline. When adding a new locale, test with real content and native speakers early to catch layout and tone issues.
Plurals and Variables
Use a message format that supports plurals (e.g. ICU format: "one item" vs "many items") so translations can be correct in every language. For dynamic values, use placeholders (e.g. "Hello, ") and let the library substitute them—never concatenate translated strings with variables, as word order differs across languages. Keep keys stable when you change copy so translators only update what changed; use key names that describe the context (e.g. "button_submit" not "string_1") so they're easier to maintain. For long content (e.g. help articles), consider storing translations in CMS or separate files and loading them on demand to keep the main bundle small.
Related articles
- Frontend ArchitectureFeature Flags and A/B Testing
Ship behind flags, run experiments, and roll out features gradually. How to implement feature flags and A/B testing in frontend applications.
Read article - Frontend ArchitectureFrontend System Design
A structured framework for approaching frontend system design interviews and real-world architecture decisions.
Read article - Frontend ArchitectureMicro-Frontends
When and how to adopt micro-frontends: implementation approaches, trade-offs, and common challenges.
Read article - Frontend ArchitectureState Management at Scale
Master state architecture for large frontend applications: when to use local vs global state, library trade-offs, server state, and state machine patterns.
Read article