JSON Internationalization (i18n): File Format, i18next, and Best Practices
Last updated:
JSON is the universal format for i18n translation files. Every major JavaScript internationalization library — i18next, react-i18next, Vue i18n, next-intl — reads translations from JSON, whether bundled at build time or fetched from a CDN. This guide covers how to structure those JSON files, wire up i18next, handle pluralization and interpolation, split translations with namespaces, format numbers and dates with the Intl API, and manage translation files at scale.
JSON Translation File Format
i18n JSON files contain key-value pairs where keys are translation identifiers and values are locale-specific strings. Two common structures:
// en/translation.json — flat keys (simple projects)
{
"welcome": "Welcome to our app",
"signIn": "Sign in",
"signOut": "Sign out",
"errors.required": "This field is required",
"errors.email": "Enter a valid email address"
}
// en/translation.json — nested keys (recommended for large projects)
{
"welcome": "Welcome to our app",
"nav": {
"home": "Home",
"dashboard": "Dashboard",
"settings": "Settings"
},
"auth": {
"signIn": "Sign in",
"signOut": "Sign out",
"forgotPassword": "Forgot password?"
},
"errors": {
"required": "This field is required",
"email": "Enter a valid email address",
"minLength": "Must be at least {{min}} characters"
}
}Each locale gets its own parallel file with identical keys and locale-specific values. Arabic (RTL) example:
// ar/translation.json
{
"welcome": "مرحباً بك في تطبيقنا",
"nav": {
"home": "الرئيسية",
"dashboard": "لوحة التحكم",
"settings": "الإعدادات"
}
}i18next Setup and Usage
// i18n.ts
import i18next from 'i18next'
import { initReactI18next } from 'react-i18next'
import enTranslation from './locales/en/translation.json'
import frTranslation from './locales/fr/translation.json'
import arTranslation from './locales/ar/translation.json'
i18next
.use(initReactI18next)
.init({
resources: {
en: { translation: enTranslation },
fr: { translation: frTranslation },
ar: { translation: arTranslation },
},
lng: navigator.language.split('-')[0], // 'en', 'fr', 'ar'
fallbackLng: 'en',
interpolation: { escapeValue: false }, // React already escapes
})
// Component usage
import { useTranslation } from 'react-i18next'
function Header() {
const { t, i18n } = useTranslation()
return (
<header>
<h1>{t('welcome')}</h1>
<nav>
<a href="/home">{t('nav.home')}</a>
<a href="/dashboard">{t('nav.dashboard')}</a>
</nav>
<button onClick={() => i18n.changeLanguage('ar')}>العربية</button>
<button onClick={() => i18n.changeLanguage('en')}>English</button>
</header>
)
}Interpolation and Variable Substitution
Use {"{{variableName}}"} in JSON values. i18next substitutes runtime values at the call site, so translators can reorder variables to match their language's grammar.
// en/translation.json
{
"greeting": "Hello, {{name}}!",
"items": "You have {{count}} items in your cart",
"price": "Total: {{amount, currency}}",
"lastLogin": "Last login: {{date, datetime}}",
"welcome_back": "Welcome back, {{name}}! You have {{unread}} unread messages."
}// Usage
t('greeting', { name: 'Alice' })
// → "Hello, Alice!"
t('price', { amount: 9.99, formatParams: { amount: { currency: 'USD' } } })
// → "Total: $9.99"
// Nested interpolation with formatting
t('lastLogin', { date: new Date(), formatParams: { date: { dateStyle: 'long' } } })
// → "Last login: January 15, 2026"Pluralization
Append an ICU plural category suffix to the base key. Pass {"{ count }"} to t() and i18next selects the correct form automatically.
// en/translation.json — ICU plural categories
{
"apple_one": "1 apple",
"apple_other": "{{count}} apples",
"file_zero": "No files",
"file_one": "1 file",
"file_other": "{{count}} files",
"notification_one": "You have 1 new notification",
"notification_other": "You have {{count}} new notifications"
}// ar/translation.json — Arabic has 6 plural categories
{
"apple_zero": "لا تفاح",
"apple_one": "تفاحة واحدة",
"apple_two": "تفاحتان",
"apple_few": "{{count}} تفاحات",
"apple_many": "{{count}} تفاحة",
"apple_other": "{{count}} تفاحة"
}t('apple', { count: 0 }) // → "No apples" (uses apple_zero if defined, else apple_other)
t('apple', { count: 1 }) // → "1 apple"
t('apple', { count: 5 }) // → "5 apples"| Language | Plural categories |
|---|---|
| English | one, other |
| French | one, many, other |
| Russian | one, few, many, other |
| Arabic | zero, one, two, few, many, other |
| Chinese / Japanese | other (no pluralization) |
Namespacing and Code Splitting
Namespaces split large translation sets into separate JSON files by feature or page, reducing initial bundle size and keeping files manageable.
// Multiple namespace files
// locales/en/common.json — shared across all pages
// locales/en/dashboard.json — dashboard-specific
// locales/en/errors.json — error messages
// i18next config with namespaces
i18next.init({
ns: ['common', 'dashboard', 'errors'],
defaultNS: 'common',
resources: {
en: {
common: require('./locales/en/common.json'),
dashboard: require('./locales/en/dashboard.json'),
errors: require('./locales/en/errors.json'),
}
}
})
// Usage with namespace prefix
const { t } = useTranslation(['dashboard', 'common'])
t('dashboard:metrics.title') // from dashboard namespace
t('common:nav.home') // from common namespace
t('errors:required') // from errors namespace
// Next.js i18n: lazy-load namespace per page
export async function getStaticProps({ locale }) {
return {
props: {
...(await serverSideTranslations(locale, ['common', 'dashboard'])),
},
}
}Number, Date, and Currency Formatting
The browser-native Intl API handles locale-aware formatting with zero bundle cost. Use it alongside your JSON translation files.
// Using Intl directly (no library needed)
const price = 1234567.89
// Currency
new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(price)
// → "$1,234,567.89"
new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(price)
// → "1.234.567,89 €"
new Intl.NumberFormat('ja-JP', { style: 'currency', currency: 'JPY' }).format(12500)
// → "¥12,500"
// Date formatting
const date = new Date('2026-01-15')
new Intl.DateTimeFormat('en-US', { dateStyle: 'long' }).format(date) // → "January 15, 2026"
new Intl.DateTimeFormat('ar-SA', { dateStyle: 'long' }).format(date) // → "١٥ يناير ٢٠٢٦"
new Intl.DateTimeFormat('ja-JP', { dateStyle: 'long' }).format(date) // → "2026年1月15日"
// Relative time
const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' })
rtf.format(-1, 'day') // → "yesterday"
rtf.format(-3, 'month') // → "3 months ago"
rtf.format(2, 'week') // → "in 2 weeks"JSON i18n File Management and Tooling
| Tool | Purpose | Key feature |
|---|---|---|
| i18next-parser | Extract t('key') from source | Auto-generate missing keys |
| Crowdin | Translation platform | Git sync, machine translation |
| Lokalise | Translation management | REST API, webhooks, GitHub integration |
| Weblate | Self-hosted TMS | Open source, Git-based |
| Tolgee | Developer-first TMS | In-context translation |
# i18next-parser — extract translation keys from source
npx i18next-parser --config i18next-parser.config.js
# i18next-parser.config.js
module.exports = {
input: ['src/**/*.{ts,tsx}'],
output: 'public/locales/$LOCALE/$NAMESPACE.json',
locales: ['en', 'fr', 'de', 'ja', 'ar'],
defaultNamespace: 'translation',
keepRemoved: false, // remove keys not found in source
}Definitions
- i18n (internationalization)
- The process of designing software so it can be adapted to different languages and regions without code changes. Contrasted with l10n (localization), which is the adaptation itself — translating strings, formatting dates and numbers, and adjusting layout for a specific locale.
- ICU plural categories
- The Unicode Common Locale Data Repository (CLDR) defines 6 plural categories: zero, one, two, few, many, other. English uses only one and other. Arabic uses all 6. i18next maps the {count} value to the correct category using the locale's plural rules and selects the matching key suffix.
- namespace
- An i18next concept for splitting translations into separate JSON files by feature or page. Reduces initial bundle size and organizes large translation sets. The default namespace is typically "translation"; additional namespaces like "dashboard" or "errors" are loaded on demand.
- interpolation
- Variable substitution within translation strings. i18next uses {{variableName}} syntax — the library replaces placeholders with runtime values passed in the second argument to t(). Interpolation lets translators reorder variables to match their language's grammar without changing source code.
- Intl API
- A set of browser-native JavaScript APIs for locale-sensitive formatting: Intl.NumberFormat (numbers, currency, percentages), Intl.DateTimeFormat (dates, times), Intl.RelativeTimeFormat ("3 days ago"), and Intl.PluralRules (plural category selection). Available in all modern browsers without additional dependencies or bundle cost.
FAQ
What is the standard JSON format for i18n translation files?
The standard format uses flat key-value pairs: {"{ "key": "value" }"}, or nested objects: {"{ "ns": { "key": "value" } }"} accessed as t('ns.key'). Arrays support ordered lists: {"{ "list": ["item1", "item2"] }"}. i18next's format is the de facto standard for JavaScript projects. CLDR plural categories appear as key suffixes: key_one, key_other (English); key_zero through key_other (Arabic has 6). Interpolation uses {"{{variable}}"} syntax. Nested keys are recommended for large projects as they group related strings and make files easier to navigate.
How does pluralization work in i18n JSON?
Append an ICU plural category suffix to the base key: apple_one and apple_other in English, or apple_zero through apple_other in Arabic (6 categories). Pass {"{ count }"} to t() and i18next automatically selects the correct key using the locale's CLDR plural rules. Use new Intl.PluralRules('ar').select(3) to see which category a number maps to (returns "few"). English uses only one and other; Arabic uses all six CLDR categories; Chinese and Japanese use only other (no grammatical pluralization).
How do I handle RTL languages in i18n JSON?
RTL is a CSS and HTML concern, not a JSON concern. Translation values in JSON files are just strings — the JSON structure for Arabic, Hebrew, Farsi, and Urdu is identical to LTR locales. To activate RTL layout: set dir="rtl" on the HTML element or a wrapper div when the active locale is RTL. Use CSS logical properties (margin-inline-start instead of margin-left) so layout automatically mirrors for RTL. Detect RTL locales programmatically: new Intl.Locale(locale).textInfo?.direction === 'rtl'.
What is the difference between i18next namespaces?
Namespaces split translation files by feature or page, reducing initial bundle size and organizing large translation sets. A common namespace holds shared strings (navigation, buttons, labels). A dashboard namespace holds dashboard-specific strings. An errors namespace holds error messages. Each page loads only its required namespaces: useTranslation(['dashboard', 'common']). Access a specific namespace with a colon prefix: t('dashboard:metrics.title'). In Next.js, specify namespaces per page in serverSideTranslations(locale, ['common', 'dashboard']).
How do I add interpolation variables to JSON translation strings?
Use {"{{variableName}}"} syntax in JSON values: {"{ "greeting": "Hello, {{name}}!" }"}. Pass the variable in the t() call: {"t('greeting', { name: 'Alice' })"} produces "Hello, Alice!". For nested property access: {"{{user.name}}"}. For formatted values: {"{{amount, currency}}"} with formatParams. i18next also supports $t('otherKey') inside a value for string composition. Never concatenate translated strings with JavaScript string concatenation — use interpolation so translators can reorder variables for their language's grammar.
How do I lazy-load i18n JSON files to reduce bundle size?
Use the i18next-http-backend plugin: it loads JSON from /locales/{lng}/{ns}.json on demand instead of bundling all translations at build time. Combine with namespace splitting so each page requests only its required namespaces. In Next.js: next-i18next handles lazy loading per page via serverSideTranslations. In Vite: use dynamic import('./locales/en.json') inside a language-switch handler. The backend plugin caches loaded namespaces in memory to avoid re-fetching on language switches within the same session.
How do I keep i18n JSON files in sync across languages?
Use i18next-parser to auto-extract translation keys from source code — it scans for t() calls and generates or updates JSON files for each locale, adding new keys and removing obsolete ones. Missing keys in non-default locales render as the raw key string at runtime, making gaps visible during development. Translation management services (Crowdin, Lokalise, Tolgee, Weblate) sync with your Git repo and flag untranslated strings. Add a CI check that compares key sets across locale files: any key in en.json absent in fr.json indicates an untranslated string.
What is the Intl API and does it replace i18n JSON files?
The Intl API is a set of browser-native JavaScript APIs for locale-sensitive formatting: Intl.NumberFormat for numbers and currency, Intl.DateTimeFormat for dates and times, Intl.RelativeTimeFormat for relative time ("3 days ago"), and Intl.PluralRules for plural category selection. It handles formatting but NOT translation — you still need i18n JSON files for translated text. Intl data is built into all modern browsers with no additional bundle cost. Use both together: JSON files for translated text, Intl APIs for locale-aware formatting of numbers and dates within those strings.
Further reading and primary sources
- i18next documentation — Complete i18next reference: initialization, plugins, interpolation, pluralization, and framework integrations
- CLDR Plural Rules — Unicode CLDR plural category definitions and rules for all supported languages
- MDN Intl API — Browser-native Intl API reference: NumberFormat, DateTimeFormat, RelativeTimeFormat, PluralRules
- JSON Schema Patterns (Jsonic) — JSON Schema validation patterns for ensuring i18n JSON files have the correct structure
- JSON to Markdown (Jsonic) — Convert JSON translation data to Markdown tables and documentation formats