Setting up i18n framework (i18next) for web application
i18next is the most common i18n framework in JavaScript ecosystem. Works with React, Vue, Angular, Svelte, Node.js and vanilla JS. Huge plugin ecosystem: backends for loading translations, language detectors, formatters. Unlike react-intl, not tied to one framework.
Installation
# Base package
npm install i18next
# React integration
npm install react-i18next
# Load translations via HTTP
npm install i18next-http-backend
# Detect browser language
npm install i18next-browser-languagedetector
# Cache in localStorage
npm install i18next-localstorage-backend
Initialization
// src/i18n/config.ts
import i18n from 'i18next'
import { initReactI18next } from 'react-i18next'
import HttpBackend from 'i18next-http-backend'
import LanguageDetector from 'i18next-browser-languagedetector'
i18n
.use(HttpBackend)
.use(LanguageDetector)
.use(initReactI18next)
.init({
// Application languages
supportedLngs: ['ru', 'en', 'de', 'uk'],
fallbackLng: 'ru',
defaultNS: 'common',
// Namespaces — split translations by domains
ns: ['common', 'catalog', 'checkout', 'account'],
// Load from server
backend: {
loadPath: '/locales/{{lng}}/{{ns}}.json',
addPath: '/locales/add/{{lng}}/{{ns}}', // for i18next-parser
},
// Detect language
detection: {
order: ['querystring', 'cookie', 'localStorage', 'navigator', 'htmlTag'],
lookupQuerystring: 'lng',
lookupCookie: 'locale',
lookupLocalStorage: 'i18nextLng',
caches: ['localStorage', 'cookie'],
},
interpolation: {
escapeValue: false,
// Formatters for dates, numbers, currency
format(value, format, lng) {
if (format === 'date') {
return new Intl.DateTimeFormat(lng, { dateStyle: 'medium' }).format(value)
}
if (format === 'currency') {
const currency = lng === 'ru' ? 'RUB' : lng === 'uk' ? 'UAH' : 'USD'
return new Intl.NumberFormat(lng, { style: 'currency', currency, maximumFractionDigits: 0 }).format(value)
}
return value
},
},
// Don't warn about missing keys in dev
saveMissing: process.env.NODE_ENV === 'development',
missingKeyHandler(lngs, ns, key) {
console.warn(`[i18next] Missing key: ${ns}:${key} for ${lngs.join(',')}`)
},
})
export default i18n
// src/main.tsx
import './i18n/config' // import before App
import { Suspense } from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
ReactDOM.createRoot(document.getElementById('root')!).render(
<Suspense fallback={<div>Loading...</div>}>
<App />
</Suspense>
)
Translation files
public/locales/
ru/
common.json
catalog.json
checkout.json
en/
common.json
catalog.json
checkout.json
// public/locales/ru/catalog.json
{
"title": "Каталог",
"filter": {
"title": "Фильтры",
"reset": "Сбросить",
"apply": "Применить",
"price_from": "Цена от",
"price_to": "до"
},
"sort": {
"label": "Сортировка",
"price_asc": "Дешевле",
"price_desc": "Дороже",
"popular": "Популярные",
"new": "Новинки"
},
"items_count_one": "{{count}} товар",
"items_count_few": "{{count}} товара",
"items_count_many": "{{count}} товаров",
"items_count_other": "{{count}} товаров",
"empty": "По вашему запросу ничего не найдено",
"price": "{{price, currency}}"
}
Using in React
import { useTranslation, Trans } from 'react-i18next'
function CatalogPage({ count, price }: { count: number; price: number }) {
// Load specific namespace
const { t, i18n } = useTranslation('catalog')
return (
<main>
<h1>{t('title')}</h1>
{/* Pluralization */}
<p>{t('items_count', { count })}</p>
{/* ru, count=3: "3 товара" */}
{/* Formatting via interpolation */}
<p>{t('price', { price })}</p>
{/* ru: "14 990 ₽" */}
{/* Multiple namespaces */}
<FilterPanel />
</main>
)
}
// Trans component for text with HTML/components inside
function PrivacyNote() {
const { t } = useTranslation('common')
return (
<Trans
i18nKey="privacy_note"
components={{
link: <a href="/privacy" className="underline" />,
}}
/>
// JSON: "By clicking, you accept <link>privacy policy</link>"
)
}
Switching language
function LanguageSwitcher() {
const { i18n } = useTranslation()
const changeLanguage = async (lng: string) => {
await i18n.changeLanguage(lng)
// i18next automatically:
// 1. Load needed JSON files if not loaded yet
// 2. Save to localStorage/cookie
// 3. Update all components via React context
document.documentElement.lang = lng
}
return (
<div>
{['ru', 'en', 'de', 'uk'].map(lng => (
<button
key={lng}
onClick={() => changeLanguage(lng)}
disabled={i18n.resolvedLanguage === lng}
>
{lng.toUpperCase()}
</button>
))}
</div>
)
}
SSR: i18next with Node.js
// server/i18n.ts — separate instance for SSR
import i18next from 'i18next'
import Backend from 'i18next-fs-backend'
const serverI18n = i18next.createInstance()
await serverI18n
.use(Backend)
.init({
lng: 'ru',
fallbackLng: 'ru',
ns: ['common', 'catalog'],
backend: {
loadPath: './public/locales/{{lng}}/{{ns}}.json',
},
interpolation: { escapeValue: false },
})
export function createI18nForRequest(locale: string) {
return serverI18n.cloneInstance({ lng: locale })
}
Automatic key extraction
npm install --save-dev i18next-parser
# i18next-parser.config.js
module.exports = {
locales: ['ru', 'en', 'de'],
output: 'public/locales/$LOCALE/$NAMESPACE.json',
input: ['src/**/*.{ts,tsx}'],
keepRemoved: false,
sort: true,
}
npx i18next-parser
Parser finds all t('key'), useTranslation('ns'), <Trans i18nKey="..."> calls and updates JSON files: adds new keys, keeps existing, removes unused.
Namespaces by routes
// Load namespace only for needed page
export async function loader() {
// React Router v6 loader
await i18n.loadNamespaces('checkout')
return null
}
function CheckoutPage() {
const { t } = useTranslation('checkout')
// Namespace guaranteed loaded
}
Timeframe
Installation and basic setup with 2 languages — 1 day. With multiple namespaces, lazy loading by routes, i18next-parser and CI checks — 2–3 days.







