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

Типизация переводов в TypeScript-проектах, использующих i18next, решает ключевую проблему интернационализации: отсутствие гарантий корректности ключей и структуры ресурсов на этапе компиляции. При росте количества языков и namespace-структуры риск рассинхронизации между кодом и JSON-файлами переводов становится критическим.


Проблема отсутствия типизации в i18next

Стандартная модель использования i18next опирается на строковые ключи:

t('common.buttons.save')
t('errors.network.timeout')

Такая модель приводит к нескольким классическим проблемам:

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

При увеличении числа локалей и namespace становится невозможным контролировать консистентность вручную.


Идея генерации типов

Генерация типов строится на принципе source of truth = JSON-файлы переводов. Из структуры файлов формируется TypeScript-описание допустимых ключей.

Пример структуры переводов:

locales/
  en/
    common.json
    errors.json
  ru/
    common.json
    errors.json

common.json:

{
  "buttons": {
    "save": "Save",
    "cancel": "Cancel"
  }
}

Из этого формируется тип:

type CommonKeys =
  | 'buttons.save'
  | 'buttons.cancel'

Основные подходы генерации типов

1. Статический парсинг JSON

Наиболее распространённый подход — обход JSON-файлов и построение union-типа.

Алгоритм:

  • рекурсивное чтение структуры JSON
  • формирование пути ключей через точку
  • генерация TypeScript файла

Результат:

export type I18nKeys =
  | 'common.buttons.save'
  | 'common.buttons.cancel'
  | 'errors.network.timeout'

2. Генерация per-namespace типов

Более масштабируемый вариант — разделение типов по namespace:

export type CommonKeys = 'buttons.save' | 'buttons.cancel'
export type ErrorsKeys = 'network.timeout'

И объединение:

export type AppI18nKeys =
  | `common:${CommonKeys}`
  | `errors:${ErrorsKeys}`

Подход удобен для больших приложений с lazy-loading переводов.


3. Автогенерация через CLI-инструменты

На практике используются генераторы, работающие в build-time или watch-режиме:

  • парсинг locales/**.json
  • построение AST типов
  • генерация .d.ts

Типичный pipeline:

JSON → AST → TypeScript types → declaration.d.ts

Интеграция с TypeScript

Module augmentation i18next

i18next поддерживает расширение типов через декларации:

import 'i18next'

declare module 'i18next' {
  interface CustomTypeOptions {
    defaultNS: 'common'
    resources: {
      common: typeof import('../locales/en/common.json')
    }
  }
}

Это позволяет связать реальные JSON-структуры с типами.


Типизация useTranslation

В связке с React:

import { useTranslation } from 'react-i18next'

const { t } = useTranslation<'common'>()

t('buttons.save')

При корректной генерации типов:

  • доступные ключи ограничены union-типом
  • IDE предлагает автодополнение
  • невозможны несуществующие ключи

Обработка вложенных ключей

JSON с глубокой вложенностью:

{
  "auth": {
    "login": {
      "title": "Login",
      "submit": "Sign in"
    }
  }
}

Генератор преобразует в:

type AuthKeys =
  | 'auth.login.title'
  | 'auth.login.submit'

Рекурсивная функция:

type Join<K, P> = K extends string
  ? P extends string
    ? `${K}.${P}`
    : never
  : never

Поддержка plural и context

i18next использует специальные суффиксы:

item
item_plural
item_male
item_female

Генерация типов учитывает:

  • объединение базового ключа
  • сохранение модификаторов как допустимых вариантов

Пример:

type ItemKeys =
  | 'item'
  | 'item_plural'

Более строгая модель:

type PluralKeys<T extends string> = `${T}` | `${T}_plural`

Динамические ключи и ограничения типизации

Проблема возникает при:

t(`errors.${code}`)

Где code — runtime-значение.

Решения:

Ограничение через union

type ErrorCode = 'timeout' | 'offline'

t(`errors.${ErrorCode}`)

Разделение safe/unsafe API

t('errors.network.timeout') // safe
t(dynamicKey as string)     // unsafe

Генерация типов с учётом namespaces

Namespace-структура:

t('common:buttons.save')
t('errors:network.timeout')

Типизация:

type Keys =
  | `common:${CommonKeys}`
  | `errors:${ErrorsKeys}`

Преимущество — поддержка lazy-loading и code-splitting.


CI-интеграция генерации типов

Типичный процесс в pipeline:

  1. загрузка переводов
  2. проверка структуры JSON
  3. генерация .d.ts
  4. сравнение с текущими типами
  5. fail при расхождении

Это предотвращает:

  • забытые ключи
  • рассинхронизацию языков
  • невалидные структуры

Watch-режим разработки

Генерация типов в режиме наблюдения:

locales/**/*.json → watcher → regenerate types

Поведение:

  • изменение JSON мгновенно обновляет типы
  • IDE автоматически пересчитывает автодополнение
  • исключается необходимость ручной генерации

Типизация через template literal types

Ключевой механизм TypeScript:

type DotPath<T extends string> =
  T extends `${infer A}.${infer B}`
    ? A | `${A}.${DotPath<B>}`
    : T

Позволяет формировать рекурсивные ключи без внешних библиотек.


Ошибки при генерации типов

1. Потеря структуры при merge JSON

Разные локали могут содержать:

  • лишние ключи
  • отсутствующие ключи
  • различия в глубине

Типогенератор должен выбирать:

  • union всех языков или
  • базовый язык как эталон

2. Перекрытие ключей

{
  "buttons.save": "Save"
}

и

{
  "buttons": {
    "save": "Save"
  }
}

Такие структуры требуют нормализации перед генерацией.


3. Неконсистентные plural формы

{
  "item": "Item",
  "items": "Items"
}

и

{
  "item": "Item",
  "item_plural": "Items"
}

Генератор должен унифицировать правила.


Архитектурные подходы

Flat-first модель

Все ключи приводятся к строкам:

a.b.c → "a.b.c"

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

  • простая типизация
  • быстрый парсинг

Недостатки:

  • потеря семантики структуры

Tree-first модель

Сохраняется вложенность:

type TranslationTree = {
  auth: {
    login: {
      title: string
    }
  }
}

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

  • ближе к JSON
  • проще валидировать

Недостатки:

  • сложнее использовать в t()

Практическая модель генератора

Типичный генератор включает:

  • reader файлов
  • нормализатор структуры
  • key flattener
  • type builder
  • emitter .d.ts

Пример pipeline:

readLocales()
  → normalize()
  → flattenKeys()
  → buildTypes()
  → writeDeclarations()

Связка с редактором кода

При корректной генерации:

  • автодополнение работает по namespace
  • ошибки отображаются в реальном времени
  • рефакторинг ключей безопасен

Ключевой эффект — превращение i18next из runtime-системы в partially compile-time проверяемую систему.