Comprehensive guide to internationalization architecture. Includes automatic key typing, translation completeness checks, and dynamic resource loading.
Benefit: Total type safety — the project will not compile if a key is missing or translation structures differ across languages.
All settings are centralized in shared/config/i18n/. Each aspect (types, initialization, blocks) is separated into its own file.
Vite: public/locales/[lng]/. Next.js: messages/[lng]/ (recommended) or public/locales/[lng]/. Nested folders group namespaces (e.g., booking/order.json).
1export const i18nKey =
2 <Keys extends string>() =>
3 <K extends Keys>(key: K) =>
4 key;TNestedKeyOf to generate dot-notated key paths.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, import reference JSON via import type and build TResources. Vite: public/locales; Next: messages/ (recommended) or 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 stack).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 catches missing keys, an explicit TNestedKeyOf check catches extra keys. Guarantees that all languages have an identical key structure.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]/ (recommended) or public/locales/[lng]/ loaded via import() in 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 (not i18next.changeLanguage).1import i18n from "./i18n.init";
2
3export const changeLanguage = (lng: string) => {
4 i18n.changeLanguage(lng);
5 localStorage.setItem("i18nextLng", lng);
6};