При использовании i18next архитектура проекта становится
критически важной частью системы локализации. Неправильная организация
переводов быстро приводит к дублированию ключей, конфликтам между
модулями, росту объёма JSON-файлов и усложнению поддержки. Грамотно
построенная структура позволяет:
Минимальная структура проекта с i18next обычно выглядит
следующим образом:
src/
├── i18n/
│ ├── index.js
│ ├── config.js
│ ├── locales/
│ │ ├── en/
│ │ │ └── common.json
│ │ └── ru/
│ │ └── common.json
│ └── services/
│ └── languageDetector.js
│
├── components/
├── pages/
└── app.js
i18nКаталог i18n обычно выделяется как отдельный центр
управления локализацией.
Пример:
i18n/
├── config.js
├── index.js
├── locales/
├── services/
├── formatters/
└── helpers/
Назначение файлов:
| Файл/каталог | Назначение |
|---|---|
config.js |
Конфигурация i18next |
index.js |
Инициализация |
locales/ |
Файлы переводов |
services/ |
Детектирование языка |
formatters/ |
Форматирование дат, валют |
helpers/ |
Вспомогательные функции |
Чаще всего используется отдельный файл инициализации:
// i18n/index.js
import i18n from 'i18next'
import { initReactI18next } from 'react-i18next'
import enCommon from './locales/en/common.json'
import ruCommon from './locales/ru/common.json'
i18n
.use(initReactI18next)
.init({
resources: {
en: {
common: enCommon,
},
ru: {
common: ruCommon,
},
},
lng: 'ru',
fallbackLng: 'en',
ns: ['common'],
defaultNS: 'common',
interpolation: {
escapeValue: false,
},
})
export default i18n
localesОсновной каталог локализации.
Типичная структура:
locales/
├── en/
│ ├── common.json
│ ├── auth.json
│ ├── dashboard.json
│ └── profile.json
│
├── ru/
│ ├── common.json
│ ├── auth.json
│ ├── dashboard.json
│ └── profile.json
│
└── de/
├── common.json
├── auth.json
├── dashboard.json
└── profile.json
Каждый язык хранится отдельно.
Каждый модуль приложения имеет собственный namespace.
Namespace — логическое разделение переводов.
Например:
auth.json
dashboard.json
profile.json
Это позволяет:
auth{
"login": "Вход",
"logout": "Выход",
"email": "Электронная почта",
"password": "Пароль"
}
Использование:
t('login', { ns: 'auth' })
Или:
const { t } = useTranslation('auth')
t('login')
{
"save": "Сохранить",
"cancel": "Отмена",
"delete": "Удалить"
}
Подходит для небольших проектов.
{
"buttons": {
"save": "Сохранить",
"cancel": "Отмена"
},
"messages": {
"success": "Успешно",
"error": "Ошибка"
}
}
Использование:
t('buttons.save')
{
"auth": {
"loginButton": "Войти"
}
}
{
"button1": "Войти"
}
Ключ должен отражать смысл, а не внешний вид элемента.
В крупных приложениях переводы часто располагаются рядом с функциональными модулями.
Пример:
src/
├── features/
│ ├── auth/
│ │ ├── components/
│ │ ├── pages/
│ │ └── locales/
│ │ ├── en.json
│ │ └── ru.json
│ │
│ └── profile/
│ ├── components/
│ └── locales/
│ ├── en.json
│ └── ru.json
Преимущества:
Даже при модульной архитектуре обычно создаётся единая точка регистрации.
Пример:
import authEn from '../features/auth/locales/en.json'
import authRu from '../features/auth/locales/ru.json'
export const resources = {
en: {
auth: authEn,
},
ru: {
auth: authRu,
},
}
В крупных проектах нельзя загружать все переводы сразу.
Для этого используется backend.
npm install i18next-http-backend
public/
└── locales/
├── en/
│ ├── common.json
│ └── auth.json
│
└── ru/
├── common.json
└── auth.json
import i18n from 'i18next'
import Backend from 'i18next-http-backend'
i18n.use(Backend).init({
backend: {
loadPath: '/locales/{{lng}}/{{ns}}.json',
},
})
const { t } = useTranslation('dashboard')
При открытии страницы namespace загрузится автоматически.
Типичная структура React + i18next:
src/
├── i18n/
│ ├── index.js
│ └── locales/
│
├── hooks/
├── components/
├── pages/
├── layouts/
└── features/
В Next.js локализация часто интегрируется отдельно.
src/
├── i18n/
├── public/
│ └── locales/
│
├── pages/
├── components/
└── features/
В monorepo локализация может быть вынесена в отдельный пакет.
packages/
├── ui/
├── api/
├── shared/
└── i18n/
├── locales/
├── config/
└── helpers/
Преимущества:
Практически всегда создаётся namespace common.
Пример:
{
"save": "Сохранить",
"cancel": "Отмена",
"loading": "Загрузка"
}
Он содержит:
Иногда компонент содержит собственные переводы.
Пример:
components/
└── DatePicker/
├── index.jsx
└── locales/
├── en.json
└── ru.json
Подход полезен для:
Плохой пример:
common.json
Размер:
5000 строк
Проблемы:
Лучше:
common/
├── buttons.json
├── messages.json
├── validation.json
└── navigation.json
Можно собирать namespace автоматически.
Пример:
import buttons from './common/buttons.json'
import messages from './common/messages.json'
export default {
...buttons,
...messages,
}
В TypeScript часто создают типизацию ключей.
Структура:
i18n/
├── locales/
├── types/
│ └── i18next.d.ts
└── index.ts
import 'i18next'
import common from './locales/ru/common.json'
declare module 'i18next' {
interface CustomTypeOptions {
defaultNS: 'common'
resources: {
common: typeof common
}
}
}
Преимущества:
При большом количестве языков важно соблюдать единообразие.
Пример:
locales/
├── en/
├── ru/
├── de/
├── fr/
├── es/
└── zh/
У каждого языка должны быть одинаковые namespace.
Проблема:
ru/auth.json
содержит:
{
"login": "Вход"
}
а:
en/auth.json
не содержит ключ login.
Это приводит к fallback и ошибкам интерфейса.
Часто создают скрипты:
scripts/
└── validateLocales.js
Скрипт:
function compareKeys(base, target) {
for (const key in base) {
if (!(key in target)) {
console.log(`Missing key: ${key}`)
}
}
}
В очень больших проектах вводят уровни:
locales/
├── core/
├── pages/
├── widgets/
├── forms/
└── modals/
Это помогает:
Иногда переводы выносятся:
Примеры сервисов:
Частый вариант:
locales/
├── static/
└── remote/
Где:
static — базовые переводы;remote — обновляемые переводы.Для SSR важно:
Структура:
i18n/
├── server.js
├── client.js
└── shared.js
// server.js
i18n.init({
preload: ['en', 'ru'],
})
// client.js
i18n.init({
lng: 'ru',
})
Пример:
fallbackLng: {
'de-CH': ['fr', 'en'],
default: ['en'],
}
Структура каталогов:
locales/
├── en/
├── fr/
├── de/
└── de-CH/
Неправильно:
{
"price": "Цена: {{value}} ₽"
}
Лучше:
{
"price": "Цена: {{value}}"
}
А формат валюты вынести отдельно:
new Intl.NumberFormat('ru-RU', {
style: 'currency',
currency: 'RUB',
})
i18n/
├── formatters/
│ ├── currency.js
│ ├── date.js
│ └── number.js
В multi-platform проектах:
locales/
├── mobile/
├── web/
├── desktop/
└── shared/
Плохо:
{
"saveButton": "Сохранить",
"saveBtn": "Сохранить",
"save": "Сохранить"
}
Лучше:
{
"save": "Сохранить"
}
Рекомендуемые варианты:
auth
profile
dashboard
settings
navigation
validation
errors
Нежелательные:
data
stuff
misc
other
temp
Часто выделяют отдельные namespace.
locales/
└── ru/
├── errors.json
└── validation.json
Пример:
{
"required": "Поле обязательно",
"email": "Некорректный email"
}
На раннем этапе:
common.json
На среднем:
common/
auth/
profile/
На крупном:
features/
domains/
widgets/
microfrontends/
Архитура локализации должна расти вместе с приложением.