Internationalization (i18n)
Cross-framework guidelines for building multi-language frontend applications. These conventions apply to Angular, React and Vue projects in this community.
- Ship every user-facing string through a translation layer (no hardcoded text)
- Keep keys stable, structured and meaningful
- Support locale-aware formatting (dates, numbers, currencies, plurals)
- Make adding a new language a configuration change, not a refactor
Recommended libraries
Section titled “Recommended libraries”| Stack | Library |
|---|---|
| React | react-i18next / react-intl / lingui |
| Angular | @angular/localize / @ngx-translate/core |
| Vue | vue-i18n |
| Shared | ICU MessageFormat for plurals & gender |
Pick one per project and stick with it.
Project structure
Section titled “Project structure”src/ i18n/ en/ common.json auth.json checkout.json es/ common.json auth.json checkout.json index.ts → loader / configuration- One file per feature or domain (
auth,checkout,profile) - Mirror the same key tree across every locale
- Keep
enas the source of truth; other locales are translated from it
Key naming
Section titled “Key naming”- Use
camelCaseordot.notation, not full sentences - Group by feature, then by component or screen
- Keep keys descriptive of intent, not of the literal text
// ✅ Good{ "checkout": { "summary": { "title": "Order summary", "totalLabel": "Total" }, "actions": { "confirm": "Confirm purchase" } }}
// ❌ Bad{ "confirmPurchaseButtonOnTheCheckoutSummary": "Confirm purchase"}Writing translatable strings
Section titled “Writing translatable strings”- Never concatenate translated fragments — use interpolation
- Use ICU MessageFormat for plurals and gender
- Move punctuation, spacing and casing inside the translation
// ✅ Goodt('cart.itemCount', { count }) // "{count, plural, one {# item} other {# items}}"
// ❌ Bad`${t('cart.youHave')} ${count} ${t('cart.items')}`Locale-aware formatting
Section titled “Locale-aware formatting”Use the platform Intl APIs (or the i18n library wrappers around them) for:
Intl.DateTimeFormat→ dates and timesIntl.NumberFormat→ numbers, currencies, percentagesIntl.RelativeTimeFormat→ “hace 2 horas” / “2 hours ago”Intl.PluralRules→ plural categories
Never format with manual string templates — they break in other locales.
Adding a new language
Section titled “Adding a new language”- Duplicate the
en/folder into the new locale code (e.g.pt/) - Translate every value, keeping keys identical
- Register the locale in the i18n config and language switcher
- Verify pluralization rules and date/number formats
- Add a smoke test that renders a representative screen in the new locale
Code review checklist
Section titled “Code review checklist”- No hardcoded user-facing strings
- Keys are stable, descriptive and grouped by feature
- Plurals use ICU, not
if (count === 1) - Dates, numbers and currencies use
Intl(or library equivalent) - All locales have the same keys (no missing translations)
- RTL languages render correctly if supported
- Strings render without overflow at ~30% longer (German, Spanish often expand)
Testing
Section titled “Testing”- Snapshot or render tests run with at least two locales
- A “pseudo-locale” build (e.g.
en-XA) catches missing or hardcoded strings - Lint or CI rule that fails on missing keys across locales