Типизация переводов в TypeScript-проектах, использующих i18next, решает ключевую проблему интернационализации: отсутствие гарантий корректности ключей и структуры ресурсов на этапе компиляции. При росте количества языков и namespace-структуры риск рассинхронизации между кодом и JSON-файлами переводов становится критическим.
Стандартная модель использования i18next опирается на строковые ключи:
t('common.buttons.save')
t('errors.network.timeout')
Такая модель приводит к нескольким классическим проблемам:
При увеличении числа локалей и 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'
Наиболее распространённый подход — обход JSON-файлов и построение union-типа.
Алгоритм:
Результат:
export type I18nKeys =
| 'common.buttons.save'
| 'common.buttons.cancel'
| 'errors.network.timeout'
Более масштабируемый вариант — разделение типов по namespace:
export type CommonKeys = 'buttons.save' | 'buttons.cancel'
export type ErrorsKeys = 'network.timeout'
И объединение:
export type AppI18nKeys =
| `common:${CommonKeys}`
| `errors:${ErrorsKeys}`
Подход удобен для больших приложений с lazy-loading переводов.
На практике используются генераторы, работающие в build-time или watch-режиме:
locales/**.json.d.tsТипичный pipeline:
JSON → AST → TypeScript types → declaration.d.ts
i18next поддерживает расширение типов через декларации:
import 'i18next'
declare module 'i18next' {
interface CustomTypeOptions {
defaultNS: 'common'
resources: {
common: typeof import('../locales/en/common.json')
}
}
}
Это позволяет связать реальные JSON-структуры с типами.
В связке с React:
import { useTranslation } from 'react-i18next'
const { t } = useTranslation<'common'>()
t('buttons.save')
При корректной генерации типов:
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
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-значение.
Решения:
type ErrorCode = 'timeout' | 'offline'
t(`errors.${ErrorCode}`)
t('errors.network.timeout') // safe
t(dynamicKey as string) // unsafe
Namespace-структура:
t('common:buttons.save')
t('errors:network.timeout')
Типизация:
type Keys =
| `common:${CommonKeys}`
| `errors:${ErrorsKeys}`
Преимущество — поддержка lazy-loading и code-splitting.
Типичный процесс в pipeline:
.d.tsЭто предотвращает:
Генерация типов в режиме наблюдения:
locales/**/*.json → watcher → regenerate types
Поведение:
Ключевой механизм TypeScript:
type DotPath<T extends string> =
T extends `${infer A}.${infer B}`
? A | `${A}.${DotPath<B>}`
: T
Позволяет формировать рекурсивные ключи без внешних библиотек.
Разные локали могут содержать:
Типогенератор должен выбирать:
{
"buttons.save": "Save"
}
и
{
"buttons": {
"save": "Save"
}
}
Такие структуры требуют нормализации перед генерацией.
{
"item": "Item",
"items": "Items"
}
и
{
"item": "Item",
"item_plural": "Items"
}
Генератор должен унифицировать правила.
Все ключи приводятся к строкам:
a.b.c → "a.b.c"
Преимущества:
Недостатки:
Сохраняется вложенность:
type TranslationTree = {
auth: {
login: {
title: string
}
}
}
Преимущества:
Недостатки:
Типичный генератор включает:
.d.tsПример pipeline:
readLocales()
→ normalize()
→ flattenKeys()
→ buildTypes()
→ writeDeclarations()
При корректной генерации:
Ключевой эффект — превращение i18next из runtime-системы в partially compile-time проверяемую систему.