Типизация namespace

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

В базовой конфигурации i18next ресурсы описываются как вложенный объект, где язык содержит набор namespace, а каждый namespace содержит словарь переводов:

const resources = {
  ru: {
    common: {
      ok: "Ок",
      cancel: "Отмена"
    },
    auth: {
      login: "Вход",
      logout: "Выход"
    }
  }
}

Здесь common и auth — это namespace. Именно на этом уровне возникает необходимость типизации, поскольку ключи внутри каждого namespace должны быть строго ограничены заранее определённой структурой.

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

Без TypeScript система i18next позволяет обращаться к любому ключу:

t("auth.signin.button")

Если ключ отсутствует, ошибка обнаруживается только во время выполнения. При масштабировании проекта это приводит к накоплению «мёртвых» ключей и трудноуловимым ошибкам локализации.

TypeScript позволяет перенести проверку в фазу компиляции, но для этого требуется явно описать структуру namespace.

Модульное расширение типов i18next

Основной механизм типизации основан на declaration merging — расширении типов через модуль i18next.

import "i18next";

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

Где common и auth — это объекты или типы, описывающие структуру переводов.

Пример описания ресурсов

const common = {
  ok: "Ок",
  cancel: "Отмена"
} as const;

const auth = {
  login: "Вход",
  logout: "Выход"
} as const;

Использование as const фиксирует литеральные типы строк, что позволяет TypeScript извлекать точные ключи.

Инференс ключей namespace

После объявления ресурсов становится возможным извлечение типов ключей:

type CommonKeys = keyof typeof common;
// "ok" | "cancel"

Это позволяет ограничить функцию перевода допустимыми значениями:

t("common.ok");     // корректно
t("common.cancel"); // корректно
t("common.save");   // ошибка TypeScript

Типизация через рекурсивные структуры

В реальных приложениях переводы часто вложены:

const auth = {
  form: {
    login: "Войти",
    password: "Пароль"
  },
  errors: {
    required: "Обязательное поле"
  }
} as const;

TypeScript автоматически строит глубокую структуру типов:

type AuthKeys =
  | "form.login"
  | "form.password"
  | "errors.required";

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

Default namespace и его влияние на типизацию

i18next поддерживает defaultNS, который используется при отсутствии явного указания пространства имён:

i18n.init({
  defaultNS: "common",
  ns: ["common", "auth"]
});

В типах это отражается через:

interface CustomTypeOptions {
  defaultNS: "common";
}

Теперь вызовы без namespace интерпретируются как относящиеся к common:

t("ok"); // интерпретируется как common.ok

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

Жёсткое связывание namespace и ресурсов

Наиболее строгий вариант типизации предполагает явное описание всех namespace:

interface Resources {
  common: {
    ok: string;
    cancel: string;
  };
  auth: {
    login: string;
    logout: string;
  };
}

И последующее подключение:

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

Такой подход полностью исключает возможность обращения к несуществующим namespace или ключам.

Проблема масштабирования типов

При росте приложения ручное описание типов становится избыточным. Тогда используется генерация типов из JSON-файлов переводов.

Пример структуры файлов:

locales/
  ru/
    common.json
    auth.json

Скрипт генерации формирует типы:

type Resources = {
  common: typeof import("./locales/ru/common.json");
  auth: typeof import("./locales/ru/auth.json");
};

Это обеспечивает синхронизацию runtime и compile-time структуры.

Namespace в контексте ключей с префиксами

i18next поддерживает обращение с явным namespace через двоеточие:

t("auth:login");

В TypeScript это также может быть отражено через шаблонные типы:

type NamespacedKey<N extends string, K extends string> = `${N}:${K}`;

Пример:

type AuthLoginKey = NamespacedKey<"auth", "login">;
// "auth:login"

Это позволяет строго моделировать строковый API i18next.

Разделение namespace по модулям приложения

В архитектуре крупных приложений namespace часто соответствуют модулям:

  • common — общие элементы UI
  • auth — авторизация
  • dashboard — панель управления
  • profile — профиль пользователя

Каждый модуль хранит собственные типы:

export const dashboard = {
  title: "Панель",
  widgets: {
    cpu: "ЦП",
    memory: "Память"
  }
} as const;

Типы объединяются на уровне глобального ресурса:

type AppResources = {
  common: typeof common;
  auth: typeof auth;
  dashboard: typeof dashboard;
};

Проверка корректности ключей при рефакторинге

Одним из ключевых преимуществ типизации namespace является безопасный рефакторинг. При изменении структуры переводов:

// было
auth.login

// стало
auth.form.login

TypeScript автоматически выявляет все устаревшие обращения:

t("auth.login"); // ошибка компиляции

Это предотвращает скрытые ошибки локализации в runtime.

Ограничения типизации namespace

Несмотря на мощь TypeScript, существуют ограничения:

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

Для динамических сценариев остаётся fallback:

t(dynamicKey as string);

что частично снижает безопасность.

Комбинирование namespace и контекстных ключей

В сложных интерфейсах ключи могут зависеть от контекста:

type PageNamespace = "auth" | "dashboard";

function translate<N extends PageNamespace>(ns: N, key: string) {
  return t(`${ns}:${key}`);
}

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

Типизация как часть архитектуры локализации

Namespace в типизированном виде становится не просто организационной единицей, а частью архитектуры приложения. Он связывает:

  • структуру файлов переводов
  • API функции t
  • модульную организацию кода
  • систему сборки TypeScript

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