Структура проекта с i18next

При использовании i18next архитектура проекта становится критически важной частью системы локализации. Неправильная организация переводов быстро приводит к дублированию ключей, конфликтам между модулями, росту объёма JSON-файлов и усложнению поддержки. Грамотно построенная структура позволяет:

  • масштабировать приложение без хаоса в переводах;
  • разделять переводы по модулям;
  • подключать языки динамически;
  • упрощать работу команды;
  • переиспользовать ключи;
  • поддерживать SSR, SPA и мобильные приложения.

Базовая структура проекта

Минимальная структура проекта с 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

Namespace — логическое разделение переводов.

Например:

auth.json
dashboard.json
profile.json

Это позволяет:

  • уменьшать размер файлов;
  • загружать переводы частями;
  • избегать конфликтов ключей;
  • упрощать навигацию.

Пример namespace 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": "Войти"
}

Ключ должен отражать смысл, а не внешний вид элемента.


Feature-based архитектура

В крупных приложениях переводы часто располагаются рядом с функциональными модулями.

Пример:

src/
├── features/
│   ├── auth/
│   │   ├── components/
│   │   ├── pages/
│   │   └── locales/
│   │       ├── en.json
│   │       └── ru.json
│   │
│   └── profile/
│       ├── components/
│       └── locales/
│           ├── en.json
│           └── ru.json

Преимущества:

  • независимость модулей;
  • удобный lazy loading;
  • простая миграция feature-модулей;
  • высокая масштабируемость.

Центральное подключение namespace

Даже при модульной архитектуре обычно создаётся единая точка регистрации.

Пример:

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,
  },
}

Lazy loading переводов

В крупных проектах нельзя загружать все переводы сразу.

Для этого используется backend.

Установка

npm install i18next-http-backend

Структура проекта при lazy loading

public/
└── locales/
    ├── en/
    │   ├── common.json
    │   └── auth.json
    │
    └── ru/
        ├── common.json
        └── auth.json

Конфигурация backend

import i18n from 'i18next'
import Backend from 'i18next-http-backend'

i18n.use(Backend).init({
  backend: {
    loadPath: '/locales/{{lng}}/{{ns}}.json',
  },
})

Динамическая загрузка namespace

const { t } = useTranslation('dashboard')

При открытии страницы namespace загрузится автоматически.


Структура для React-приложений

Типичная структура React + i18next:

src/
├── i18n/
│   ├── index.js
│   └── locales/
│
├── hooks/
├── components/
├── pages/
├── layouts/
└── features/

Структура для Next.js

В Next.js локализация часто интегрируется отдельно.

src/
├── i18n/
├── public/
│   └── locales/
│
├── pages/
├── components/
└── features/

Структура для monorepo

В monorepo локализация может быть вынесена в отдельный пакет.

packages/
├── ui/
├── api/
├── shared/
└── i18n/
    ├── locales/
    ├── config/
    └── helpers/

Преимущества:

  • единая система переводов;
  • повторное использование;
  • синхронизация между приложениями.

Общие namespace

Практически всегда создаётся namespace common.

Пример:

{
  "save": "Сохранить",
  "cancel": "Отмена",
  "loading": "Загрузка"
}

Он содержит:

  • общие кнопки;
  • базовые сообщения;
  • стандартные подписи;
  • системные тексты.

Изоляция переводов компонентов

Иногда компонент содержит собственные переводы.

Пример:

components/
└── DatePicker/
    ├── index.jsx
    └── locales/
        ├── en.json
        └── ru.json

Подход полезен для:

  • UI-библиотек;
  • переиспользуемых компонентов;
  • npm-пакетов.

Организация больших JSON-файлов

Плохой пример:

common.json

Размер:

5000 строк

Проблемы:

  • сложно искать;
  • высокий риск конфликтов;
  • неудобный merge;
  • сложная поддержка.

Разбиение по доменам

Лучше:

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

В TypeScript часто создают типизацию ключей.

Структура:

i18n/
├── locales/
├── types/
│   └── i18next.d.ts
└── index.ts

Расширение типов i18next

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

Скрипт:

  • сравнивает ключи;
  • ищет отсутствующие переводы;
  • проверяет вложенность;
  • валидирует JSON.

Пример проверки

function compareKeys(base, target) {
  for (const key in base) {
    if (!(key in target)) {
      console.log(`Missing key: ${key}`)
    }
  }
}

Каталогизация переводов

В очень больших проектах вводят уровни:

locales/
├── core/
├── pages/
├── widgets/
├── forms/
└── modals/

Это помогает:

  • структурировать код;
  • разграничивать ответственность;
  • упрощать навигацию.

Хранение переводов вне репозитория

Иногда переводы выносятся:

  • в CMS;
  • в облачные сервисы;
  • в базы данных.

Примеры сервисов:

  • locize;
  • Phrase;
  • Lokalise;
  • Crowdin.

Гибридная архитектура

Частый вариант:

locales/
├── static/
└── remote/

Где:

  • static — базовые переводы;
  • remote — обновляемые переводы.

Организация переводов для SSR

Для SSR важно:

  • загружать namespace заранее;
  • избегать асинхронных задержек;
  • синхронизировать сервер и клиент.

Структура:

i18n/
├── server.js
├── client.js
└── shared.js

Отдельные конфигурации

Сервер

// server.js
i18n.init({
  preload: ['en', 'ru'],
})

Клиент

// client.js
i18n.init({
  lng: 'ru',
})

Организация fallback-языков

Пример:

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": "Сохранить"
}

Нейминг namespace

Рекомендуемые варианты:

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/

Архитура локализации должна расти вместе с приложением.