Руководство по архитектуре интернационализации. Включает автоматическую типизацию ключей, проверку полноты перевода и динамическую загрузку ресурсов.
Преимущество: Полная типобезопасность — при отсутствии ключа или несовпадении структуры перевода в разных языках проект не скомпилируется.
Все настройки сосредоточены в shared/config/i18n/. Каждый аспект (типы, инициализация, блоки) вынесен в отдельный файл.
Vite: public/locales/[lng]/. Next.js: messages/[lng]/ (рекомендуется) или public/locales/[lng]/. Вложенные папки группируют namespaces (например, booking/order.json).
1export const i18nKey =
2 <Keys extends string>() =>
3 <K extends Keys>(key: K) =>
4 key;TNestedKeyOf для генерации путей к ключам через точку.1export type TDotPrefix<T extends string, P extends string> = P extends ""
2 ? T
3 : `${P}.${T}`;
4
5export type TNestedKeyOf<ObjectType extends object> = {
6 [Key in keyof ObjectType & string]: ObjectType[Key] extends object
7 ? TDotPrefix<TNestedKeyOf<ObjectType[Key]>, Key>
8 : Key;
9}[keyof ObjectType & string];i18n.config.ts эталонные JSON через import type → TResources. Vite: public/locales; Next: messages/ (рекомендуется) или public/locales.1import type common from "../../../../public/locales/en/common.json";
2import type header from "../../../../public/locales/en/header.json";
3import type { TNestedKeyOf } from "./i18n.types";
4
5export type TCommon = typeof common;
6export type THeader = typeof header;
7
8export type TResources = {
9 common: TCommon;
10 header: THeader;
11};
12
13export const NS = ["common", "header"] as const;
14export type TCommonKeys = TNestedKeyOf<TCommon>;1import type common from "../../../../messages/en/common.json";
2import type header from "../../../../messages/en/header.json";
3import type { TNestedKeyOf } from "./i18n.types";
4
5export type TCommon = typeof common;
6export type THeader = typeof header;
7
8export type TResources = {
9 common: TCommon;
10 header: THeader;
11};
12
13export const NS = ["common", "header"] as const;
14export type TCommonKeys = TNestedKeyOf<TCommon>;i18n.blocks.ts.1export const TRANSLATION_BLOCKS = {
2 booking: { folder: "booking", namespaces: ["orders_page"] },
3 shared: { folder: "", namespaces: ["header", "common"] }
4};
5
6export const getNamespacePath = (lng: string, ns: string): string => {
7 // ... логика определения пути к JSON
8 return `/locales/${lng}/${ns}.json`;
9};i18n.init.ts (стек Vite SPA).1import i18n from "i18next";
2import HttpBackend from "i18next-http-backend";
3import { initReactI18next } from "react-i18next";
4import { getNamespacePath } from "./i18n.blocks";
5import { NS } from "./i18n.config";
6
7i18n.use(HttpBackend)
8 .use(initReactI18next)
9 .init({
10 fallbackLng: "en",
11 ns: [...NS],
12 defaultNS: NS[0],
13 backend: {
14 // без allowMultiLoading loadPath вызывается на каждую пару язык + namespace
15 loadPath: (lngs: string[], nss: string[]) =>
16 getNamespacePath(lngs[0], nss[0])
17 }
18 });
19export default i18n;i18n.checker.ts: satisfies ловит отсутствующие ключи, явная проверка через TNestedKeyOf — лишние. Гарантирует, что все языки имеют идентичную структуру ключей.1import type { TCommonKeys, TResources } from "./i18n.config";
2import type { TNestedKeyOf } from "./i18n.types";
3import type common_ru from "../../../../public/locales/ru/common.json";
4
5/** Ошибка компиляции, если T не never — в сообщении видны проблемные ключи */
6type TAssertNoKeys<T extends never> = T;
7
8// Нет ключа из EN или не совпал тип значения → ошибка
9export const RU_TRANSLATION_CHECKER = {
10 common: {} as typeof common_ru,
11} satisfies TResources;
12
13// satisfies не видит лишние ключи — проверяем обе стороны явно
14export type TRuCommonMissingKeys = TAssertNoKeys<
15 Exclude<TCommonKeys, TNestedKeyOf<typeof common_ru>>
16>;
17export type TRuCommonExtraKeys = TAssertNoKeys<
18 Exclude<TNestedKeyOf<typeof common_ru>, TCommonKeys>
19>;Vite SPA → react-i18next + public/locales. Next.js App Router → next-intl + messages.Vite — react-i18next
Vite SPA: useTranslation + Trans. JSON: public/locales/[lng]/.
1import { useTranslation, Trans } from "react-i18next";
2
3export const MyComponent = () => {
4 const { t } = useTranslation("common");
5
6 return (
7 <div>
8 <h1>{t("header.title")}</h1>
9 <Trans
10 ns="common"
11 i18nKey="description.text"
12 components={[<strong key="0" />, <br key="1" />]}
13 />
14 </div>
15 );
16};Next.js — next-intl
Next.js App Router: useTranslations + t.rich. JSON: messages/[lng]/ (рекомендуется) или public/locales/[lng]/ через import() в request.ts.
1"use client";
2
3import { useTranslations } from "next-intl";
4
5export const MyComponent = () => {
6 const t = useTranslations("common");
7
8 return (
9 <div>
10 <h1>{t("header.title")}</h1>
11 {t.rich("description.text", {
12 strong: (chunks) => <strong>{chunks}</strong>,
13 br: () => <br />
14 })}
15 </div>
16 );
17};changeLanguage + localStorage. Next.js: locale routing / next-intl navigation (не i18next.changeLanguage).1import i18n from "./i18n.init";
2
3export const changeLanguage = (lng: string) => {
4 i18n.changeLanguage(lng);
5 localStorage.setItem("i18nextLng", lng);
6};