Создание типобезопасных хелперов

Типобезопасные хелперы в контексте i18next строятся вокруг идеи строгого соответствия между структурой переводов и доступными ключами в коде. Основная цель — исключить обращение к несуществующим ключам и обеспечить проверку параметров интерполяции на уровне TypeScript.

Базовая конфигурация начинается с описания структуры ресурсов:

export const resources = {
  ru: {
    common: {
      greeting: "Привет, {{name}}",
      unread: "Непрочитанные сообщения: {{count}}",
    },
  },
  en: {
    common: {
      greeting: "Hello, {{name}}",
      unread: "Unread messages: {{count}}",
    },
  },
} as const;

Ключевой момент заключается в использовании as const, позволяющего зафиксировать литеральные типы строк. Без этого TypeScript деградирует до широких типов string, что лишает систему смысловой информации о ключах.


Формирование типов ресурсов из структуры переводов

На основе ресурсов создаётся типовая модель, описывающая доступные языки, неймспейсы и ключи:

type Resources = typeof resources;

type AppLocales = keyof Resources; // "ru" | "en"
type AppNamespaces = keyof Resources["ru"]; // "common"

Дальнейшее расширение позволяет извлекать ключи конкретного неймспейса:

type CommonKeys = keyof Resources["ru"]["common"];
// "greeting" | "unread"

Такой подход уже позволяет строить ограниченные интерфейсы для t, но не решает проблему интерполяции.


Типизация интерполяции и параметры перевода

Структура строк в i18next поддерживает плейсхолдеры. Для строгой типизации требуется явное описание параметров:

type CommonInterpolation = {
  greeting: { name: string };
  unread: { count: number };
};

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

type TranslationParams<K extends keyof CommonInterpolation> =
  CommonInterpolation[K];

Функция перевода приобретает строгую сигнатуру:

function t<K extends keyof CommonInterpolation>(
  key: K,
  params: TranslationParams<K>
) {}

Это исключает возможность передачи лишних или отсутствующих параметров.


Обёртка над i18next с типобезопасным t

Базовый экземпляр i18next остаётся нетипизированным по умолчанию, поэтому создаётся обёртка:

import i18next from "i18next";

function typedT<K extends keyof CommonInterpolation>(
  key: K,
  params: TranslationParams<K>
) {
  return i18next.t(key as string, params);
}

Такой слой фиксирует контракт между ключами и параметрами, сохраняя совместимость с внутренним API.


Генерация строгого типа ключей через шаблонные типы

При расширении системы используется извлечение ключей из ресурсов с учётом namespace:

type Join<N extends string, K extends string> = `${N}.${K}`;

type CommonPrefixedKeys = Join<"common", keyof Resources["ru"]["common"]>;
// "common.greeting" | "common.unread"

Это позволяет моделировать реальные ключи i18next, где namespace является частью строки.


Типизация useTranslation

Хук useTranslation требует отдельной обёртки для сохранения строгой типизации:

type UseTranslationReturn = {
  t: typeof typedT;
};

function useTypedTranslation(): UseTranslationReturn {
  const { t } = i18next;

  return {
    t: (key, params) => t(key as string, params),
  };
}

Более строгий вариант предполагает привязку namespace:

type Namespace = "common";

type NamespacedKeys =
  keyof Resources["ru"][Namespace];

function useNamespaceTranslation(ns: Namespace) {
  return {
    t: (key: NamespacedKeys, params?: any) =>
      i18next.t(`${ns}.${key}`, params),
  };
}

Маппинг ключей к параметрам через conditional types

Для устранения ручного описания параметров используется условная типизация:

type ParamsMap = {
  greeting: { name: string };
  unread: { count: number };
};

type ParamsFor<K extends keyof ParamsMap> = ParamsMap[K];

Дальнейшее развитие — автоматизация через сопоставление ключей:

type StrictT = <K extends keyof ParamsMap>(
  key: K,
  params: ParamsFor<K>
) => string;

Такой тип становится основой всей системы безопасных переводов.


Инференс ключей через keyof и const-ресурсы

При использовании единого объекта ресурсов можно вывести типы автоматически:

const typedResources = {
  greeting: "Привет, {{name}}",
  unread: "Непрочитанные: {{count}}",
} as const;

type TypedKeys = keyof typeof typedResources;

Далее создаётся универсальный маппинг интерполяции:

type ExtractParams<T extends string> =
  T extends `{{${infer P}}}` ? P : never;

Однако в реальных условиях применяется более сложный анализ шаблонов, поскольку строки могут содержать несколько параметров.


Универсальный типизированный переводчик

Обобщённая функция, объединяющая ключи и параметры:

type TranslationDictionary = {
  greeting: { name: string };
  unread: { count: number };
};

type TypedTranslate = {
  <K extends keyof TranslationDictionary>(
    key: K,
    params: TranslationDictionary[K]
  ): string;
};

Реализация:

const t: TypedTranslate = (key, params) => {
  return i18next.t(key, params);
};

Поддержка опциональных параметров

Не все переводы требуют интерполяции. Для этого вводится условная логика:

type OptionalParams<K extends keyof TranslationDictionary> =
  keyof TranslationDictionary[K] extends never
    ? []
    : [params: TranslationDictionary[K]];

Функция приобретает перегрузки:

type TypedTranslateFn = {
  <K extends keyof TranslationDictionary>(key: K, ...args: OptionalParams<K>): string;
};

Это позволяет вызывать переводчик без аргументов там, где они не требуются.


Строгая типизация namespaces

При работе с несколькими namespaces создаётся карта:

type Namespaces = {
  common: {
    greeting: { name: string };
    unread: { count: number };
  };
  auth: {
    login: { email: string; password: string };
  };
};

Типизированный переводчик:

type NamespaceKeys<N extends keyof Namespaces> =
  keyof Namespaces[N];

type NsTranslate<N extends keyof Namespaces> = {
  <K extends NamespaceKeys<N>>(
    key: `${N}.${K}`,
    params: Namespaces[N][K]
  ): string;
};

Композиция типизированных хелперов

Для масштабируемых приложений вводится слой композиции:

type AppI18n = {
  t: TypedTranslateFn;
  useNamespace: <N extends keyof Namespaces>(
    ns: N
  ) => NsTranslate<N>;
};

Такой слой обеспечивает единый контракт для всех уровней доступа к переводам.


Ограничение ошибок на уровне компиляции

Типобезопасные хелперы устраняют следующие классы ошибок:

  • обращение к несуществующим ключам
  • передача лишних параметров интерполяции
  • отсутствие обязательных параметров
  • некорректные namespace-строки
  • несоответствие структуры переводов и кода

Каждая из этих проблем переносится в фазу компиляции, исключая выполнение некорректного кода в рантайме.


Интеграция с реальными конфигурациями i18next

При подключении к реальному экземпляру важно сохранить синхронизацию типов и runtime-конфигурации:

i18next.init({
  resources,
  lng: "ru",
  ns: ["common"],
  defaultNS: "common",
});

Типизированный слой остаётся надстройкой, не заменяющей сам i18next, а ограничивающей его использование.


Расширение модели через декларативное слияние типов

Для масштабных проектов используется module augmentation:

declare module "i18next" {
  interface CustomTypeOptions {
    resources: typeof resources;
  }
}

Это позволяет встроить структуру переводов в сам механизм i18next, усиливая проверку типов на уровне всей библиотеки.


Подход к масштабированию типобезопасности

При росте проекта вводится разделение:

  • базовые типы ресурсов
  • типы namespace
  • типы интерполяции
  • обобщённый переводчик
  • специализированные хелперы под доменные модули

Такое разбиение позволяет избежать монолитного типа и поддерживать расширяемость без деградации читаемости системы типов.