Автор:
    БозданиС:2025-10-05ПослСднСС обновлСниС:2026-05-31

    ΠŸΠ΅Ρ€Π΅Π²ΠΎΠ΄ вашСго сайта Π½Π° Next.js 15 с использованиСм next-intl ΠΈ Intlayer | Π˜Π½Ρ‚Π΅Ρ€Π½Π°Ρ†ΠΈΠΎΠ½Π°Π»ΠΈΠ·Π°Ρ†ΠΈΡ (i18n)

    Π­Ρ‚ΠΎ руководство ΠΏΡ€ΠΎΠ²Π΅Π΄Π΅Ρ‚ вас Ρ‡Π΅Ρ€Π΅Π· Π»ΡƒΡ‡ΡˆΠΈΠ΅ ΠΏΡ€Π°ΠΊΡ‚ΠΈΠΊΠΈ использования next-intl Π² ΠΏΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠΈ Next.js 15 (App Router) ΠΈ ΠΏΠΎΠΊΠ°ΠΆΠ΅Ρ‚, ΠΊΠ°ΠΊ Π½Π°Π»ΠΎΠΆΠΈΡ‚ΡŒ Intlayer свСрху для Π½Π°Π΄Π΅ΠΆΠ½ΠΎΠ³ΠΎ управлСния ΠΏΠ΅Ρ€Π΅Π²ΠΎΠ΄Π°ΠΌΠΈ ΠΈ Π°Π²Ρ‚ΠΎΠΌΠ°Ρ‚ΠΈΠ·Π°Ρ†ΠΈΠΈ.

    Π‘ΠΌΠΎΡ‚Ρ€ΠΈΡ‚Π΅ сравнСниС Π² next-i18next vs next-intl vs Intlayer.

    • Для Π½Π°Ρ‡ΠΈΠ½Π°ΡŽΡ‰ΠΈΡ…: слСдуйтС ΠΏΠΎΡˆΠ°Π³ΠΎΠ²Ρ‹ΠΌ Ρ€Π°Π·Π΄Π΅Π»Π°ΠΌ, Ρ‡Ρ‚ΠΎΠ±Ρ‹ ΠΏΠΎΠ»ΡƒΡ‡ΠΈΡ‚ΡŒ Ρ€Π°Π±ΠΎΡ‡Π΅Π΅ многоязычноС ΠΏΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠ΅.
    • Для Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊΠΎΠ² срСднСго уровня: ΠΎΠ±Ρ€Π°Ρ‚ΠΈΡ‚Π΅ Π²Π½ΠΈΠΌΠ°Π½ΠΈΠ΅ Π½Π° ΠΎΠΏΡ‚ΠΈΠΌΠΈΠ·Π°Ρ†ΠΈΡŽ payload ΠΈ Ρ€Π°Π·Π΄Π΅Π»Π΅Π½ΠΈΠ΅ сСрвСрной/клиСнтской части.
    • Для ΠΎΠΏΡ‹Ρ‚Π½Ρ‹Ρ… Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊΠΎΠ²: ΠΎΠ±Ρ€Π°Ρ‚ΠΈΡ‚Π΅ Π²Π½ΠΈΠΌΠ°Π½ΠΈΠ΅ Π½Π° ΡΡ‚Π°Ρ‚ΠΈΡ‡Π΅ΡΠΊΡƒΡŽ Π³Π΅Π½Π΅Ρ€Π°Ρ†ΠΈΡŽ, middleware, ΠΈΠ½Ρ‚Π΅Π³Ρ€Π°Ρ†ΠΈΡŽ SEO ΠΈ Ρ…ΡƒΠΊΠΈ Π°Π²Ρ‚ΠΎΠΌΠ°Ρ‚ΠΈΠ·Π°Ρ†ΠΈΠΈ.

    Π§Ρ‚ΠΎ ΠΌΡ‹ рассмотрим:

    • Настройка ΠΈ структура Ρ„Π°ΠΉΠ»ΠΎΠ²
    • ΠžΠΏΡ‚ΠΈΠΌΠΈΠ·Π°Ρ†ΠΈΡ Π·Π°Π³Ρ€ΡƒΠ·ΠΊΠΈ сообщСний
    • ИспользованиС клиСнтских ΠΈ сСрвСрных ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚ΠΎΠ²
    • ΠœΠ΅Ρ‚Π°Π΄Π°Π½Π½Ρ‹Π΅, sitemap, robots для SEO
    • Middleware для ΠΌΠ°Ρ€ΡˆΡ€ΡƒΡ‚ΠΈΠ·Π°Ρ†ΠΈΠΈ ΠΏΠΎ Π»ΠΎΠΊΠ°Π»ΠΈ
    • Π”ΠΎΠ±Π°Π²Π»Π΅Π½ΠΈΠ΅ Intlayer свСрху (CLI ΠΈ автоматизация)

    Настройка вашСго прилоТСния с использованиСм next-intl

    УстановитС зависимости next-intl -

    bash
    npm install next-intl
    
    bash
    .
    β”œβ”€β”€ locales
    β”‚   β”œβ”€β”€ en
    β”‚   β”‚  β”œβ”€β”€ common.json
    β”‚   β”‚  └── about.json
    β”‚   β”œβ”€β”€ fr
    β”‚   β”‚  β”œβ”€β”€ common.json
    β”‚   β”‚  └── about.json
    β”‚   └── es
    β”‚      β”œβ”€β”€ common.json
    β”‚      └── about.json
    └── src
        β”œβ”€β”€ i18n.ts
        β”œβ”€β”€ middleware.ts
        β”œβ”€β”€ app
        β”‚   └── [locale]
        β”‚       β”œβ”€β”€ layout.tsx
        β”‚       └── about
        β”‚           └── page.tsx
        └── components
            β”œβ”€β”€ ClientComponentExample.tsx
            └── ServerComponent.tsx
    

    Настройка ΠΈ Π·Π°Π³Ρ€ΡƒΠ·ΠΊΠ° ΠΊΠΎΠ½Ρ‚Π΅Π½Ρ‚Π°

    Π—Π°Π³Ρ€ΡƒΠΆΠ°ΠΉΡ‚Π΅ Ρ‚ΠΎΠ»ΡŒΠΊΠΎ Ρ‚Π΅ пространства ΠΈΠΌΡ‘Π½, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Π΅ Π½ΡƒΠΆΠ½Ρ‹ вашим ΠΌΠ°Ρ€ΡˆΡ€ΡƒΡ‚Π°ΠΌ, ΠΈ провСряйтС Π»ΠΎΠΊΠ°Π»ΠΈ Π½Π° Ρ€Π°Π½Π½Π΅ΠΌ этапС. По возмоТности Π΄Π΅Ρ€ΠΆΠΈΡ‚Π΅ сСрвСрныС ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚Ρ‹ синхронными ΠΈ отправляйтС Π½Π° ΠΊΠ»ΠΈΠ΅Π½Ρ‚ Ρ‚ΠΎΠ»ΡŒΠΊΠΎ Π½Π΅ΠΎΠ±Ρ…ΠΎΠ΄ΠΈΠΌΡ‹Π΅ сообщСния.

    src/i18n.ts
    import { getRequestConfig } from "next-intl/server";
    import { notFound } from "next/navigation";
    
    export const locales = ["en", "fr", "es"] as const;
    export const defaultLocale = "en" as const;
    
    async function loadMessages(locale: string) {
      // Π—Π°Π³Ρ€ΡƒΠΆΠ°ΠΉΡ‚Π΅ Ρ‚ΠΎΠ»ΡŒΠΊΠΎ Ρ‚Π΅ пространства ΠΈΠΌΡ‘Π½, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Π΅ Π½ΡƒΠΆΠ½Ρ‹ Π²Π°ΡˆΠ΅ΠΌΡƒ layout/страницам
      const [common, about] = await Promise.all([
        import(`../locales/${locale}/common.json`).then((m) => m.default),
        import(`../locales/${locale}/about.json`).then((m) => m.default),
      ]);
    
      return { common, about } as const;
    }
    
    export default getRequestConfig(async ({ locale }) => {
      if (!locales.includes(locale)) notFound();
    
      return {
        messages: await loadMessages(locale),
      };
    });
    
    src/app/[locale]/layout.tsx
    import type { ReactNode } from "react";
    import { locales } from "@/i18n";
    import {
      getLocaleDirection,
      unstable_setRequestLocale,
    } from "next-intl/server";
    
    export const dynamic = "force-static";
    
    export function generateStaticParams() {
      return locales.map((locale) => ({ locale }));
    }
    
    export default async function LocaleLayout({
      children,
      params,
    }: {
      children: ReactNode;
      params: { locale: string };
    }) {
      const { locale } = params;
    
      // Π£ΡΡ‚Π°Π½ΠΎΠ²ΠΈΡ‚ΡŒ Π°ΠΊΡ‚ΠΈΠ²Π½ΡƒΡŽ локаль запроса для этого сСрвСрного Ρ€Π΅Π½Π΄Π΅Ρ€Π° (RSC)
      unstable_setRequestLocale(locale);
    
      const dir = getLocaleDirection(locale);
    
      return (
        <html lang={locale} dir={dir}>
          <body>{children}</body>
        </html>
      );
    }
    
    src/app/[locale]/about/page.tsx
    import { getTranslations, getMessages, getFormatter } from "next-intl/server";
    import { NextIntlClientProvider } from "next-intl";
    import pick from "lodash/pick";
    import ServerComponent from "@/components/ServerComponent";
    import ClientComponentExample from "@/components/ClientComponentExample";
    
    export const dynamic = "force-static";
    
    export default async function AboutPage({
      params,
    }: {
      params: { locale: string };
    }) {
      const { locale } = params;
    
      // БообщСния Π·Π°Π³Ρ€ΡƒΠΆΠ°ΡŽΡ‚ΡΡ Π½Π° сторонС сСрвСра. ΠžΡ‚ΠΏΡ€Π°Π²Π»ΡΠ΅ΠΌ ΠΊΠ»ΠΈΠ΅Π½Ρ‚Ρƒ Ρ‚ΠΎΠ»ΡŒΠΊΠΎ Π½Π΅ΠΎΠ±Ρ…ΠΎΠ΄ΠΈΠΌΠΎΠ΅.
      const messages = await getMessages();
      const clientMessages = pick(messages, ["common", "about"]);
    
      // Π‘Ρ‚Ρ€ΠΎΠ³ΠΎ сСрвСрныС ΠΏΠ΅Ρ€Π΅Π²ΠΎΠ΄Ρ‹/Ρ„ΠΎΡ€ΠΌΠ°Ρ‚ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅
      const tAbout = await getTranslations("about");
      const tCounter = await getTranslations("about.counter");
      const format = await getFormatter();
    
      const initialFormattedCount = format.number(0);
    
      return (
        <NextIntlClientProvider locale={locale} messages={clientMessages}>
          <main>
            <h1>{tAbout("title")}</h1>
            <ClientComponentExample />
            <ServerComponent
              formattedCount={initialFormattedCount}
              label={tCounter("label")}
              increment={tCounter("increment")}
            />
          </main>
        </NextIntlClientProvider>
      );
    }
    

    ИспользованиС Π² клиСнтском ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚Π΅

    Рассмотрим ΠΏΡ€ΠΈΠΌΠ΅Ρ€ клиСнтского ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚Π°, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ ΠΎΡ‚ΠΎΠ±Ρ€Π°ΠΆΠ°Π΅Ρ‚ счСтчик.

    ΠŸΠ΅Ρ€Π΅Π²ΠΎΠ΄Ρ‹ (структура повторяСтся; Π·Π°Π³Ρ€ΡƒΠΆΠ°ΠΉΡ‚Π΅ ΠΈΡ… Π² сообщСния next-intl ΠΏΠΎ Π²Π°ΡˆΠ΅ΠΌΡƒ ΡƒΡΠΌΠΎΡ‚Ρ€Π΅Π½ΠΈΡŽ)

    locales/en/about.json
    {
      "counter": {
        "label": "Π‘Ρ‡Π΅Ρ‚Ρ‡ΠΈΠΊ",
        "increment": "Π£Π²Π΅Π»ΠΈΡ‡ΠΈΡ‚ΡŒ"
      }
    }
    
    locales/fr/about.json
    {
      "counter": {
        "label": "Π‘Ρ‡Ρ‘Ρ‚Ρ‡ΠΈΠΊ",
        "increment": "Π£Π²Π΅Π»ΠΈΡ‡ΠΈΡ‚ΡŒ"
      }
    }
    

    ΠšΠ»ΠΈΠ΅Π½Ρ‚ΡΠΊΠΈΠΉ ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚

    src/components/ClientComponentExample.tsx
    "use client";
    
    import React, { useState } from "react";
    import { useTranslations, useFormatter } from "next-intl";
    
    const ClientComponentExample = () => {
      // ΠžΠ±Π»Π°ΡΡ‚ΡŒ видимости нСпосрСдствСнно для Π²Π»ΠΎΠΆΠ΅Π½Π½ΠΎΠ³ΠΎ ΠΎΠ±ΡŠΠ΅ΠΊΡ‚Π°
      const t = useTranslations("about.counter");
      const format = useFormatter();
      const [count, setCount] = useState(0);
    
      return (
        <div>
          <p>{format.number(count)}</p>
          <button
            aria-label={t("label")}
            onClick={() => setCount((count) => count + 1)}
          >
            {t("increment")}
          </button>
        </div>
      );
    };
    

    НС Π·Π°Π±ΡƒΠ΄ΡŒΡ‚Π΅ Π΄ΠΎΠ±Π°Π²ΠΈΡ‚ΡŒ сообщСниС "about" Π² клиСнтскиС сообщСния страницы (Π²ΠΊΠ»ΡŽΡ‡Π°ΠΉΡ‚Π΅ Ρ‚ΠΎΠ»ΡŒΠΊΠΎ Ρ‚Π΅ пространства ΠΈΠΌΡ‘Π½, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Π΅ Π΄Π΅ΠΉΡΡ‚Π²ΠΈΡ‚Π΅Π»ΡŒΠ½ΠΎ Π½ΡƒΠΆΠ½Ρ‹ Π²Π°ΡˆΠ΅ΠΌΡƒ ΠΊΠ»ΠΈΠ΅Π½Ρ‚Ρƒ).

    ИспользованиС Π² сСрвСрном ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚Π΅

    Π­Ρ‚ΠΎΡ‚ UI-ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚ являСтся сСрвСрным ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚ΠΎΠΌ ΠΈ ΠΌΠΎΠΆΠ΅Ρ‚ Π±Ρ‹Ρ‚ΡŒ ΠΎΡ‚Ρ€Π΅Π½Π΄Π΅Ρ€Π΅Π½ Π²Π½ΡƒΡ‚Ρ€ΠΈ клиСнтского ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚Π° (страница β†’ ΠΊΠ»ΠΈΠ΅Π½Ρ‚ β†’ сСрвСр). ΠŸΠΎΠ΄Π΄Π΅Ρ€ΠΆΠΈΠ²Π°ΠΉΡ‚Π΅ Π΅Π³ΠΎ синхронным, пСрСдавая Π·Π°Ρ€Π°Π½Π΅Π΅ вычислСнныС строки.

    src/components/ServerComponent.tsx
    type ServerComponentProps = {
      formattedCount: string;
      label: string;
      increment: string;
    };
    
    const ServerComponent = ({
      formattedCount,
      label,
      increment,
    }: ServerComponentProps) => {
      return (
        <div>
          <p>{formattedCount}</p>
          <button aria-label={label}>{increment}</button>
        </div>
      );
    };
    

    ΠŸΡ€ΠΈΠΌΠ΅Ρ‡Π°Π½ΠΈΡ:

    • ВычисляйтС formattedCount Π½Π° сторонС сСрвСра (Π½Π°ΠΏΡ€ΠΈΠΌΠ΅Ρ€, const initialFormattedCount = format.number(0)).
    • Π˜Π·Π±Π΅Π³Π°ΠΉΡ‚Π΅ ΠΏΠ΅Ρ€Π΅Π΄Π°Ρ‡ΠΈ Ρ„ΡƒΠ½ΠΊΡ†ΠΈΠΉ ΠΈΠ»ΠΈ нСсСриализуСмых ΠΎΠ±ΡŠΠ΅ΠΊΡ‚ΠΎΠ² Π² сСрвСрныС ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚Ρ‹.
    src/app/[locale]/about/layout.tsx
    import type { Metadata } from "next";
    import { locales, defaultLocale } from "@/i18n";
    import { getTranslations } from "next-intl/server";
    
    function localizedPath(locale: string, path: string) {
      return locale === defaultLocale ? path : "/" + locale + path;
    }
    
    export async function generateMetadata({
      params,
    }: {
      params: { locale: string };
    }): Promise<Metadata> {
      const { locale } = params;
      const t = await getTranslations({ locale, namespace: "about" });
    
      const url = "/about";
      const languages = Object.fromEntries(
        locales.map((locale) => [locale, localizedPath(locale, url)])
      );
    
      return {
        title: t("title"),
        description: t("description"),
        alternates: {
          canonical: localizedPath(locale, url),
          languages: { ...languages, "x-default": url },
        },
      };
    }
    
    // ... ΠžΡΡ‚Π°Π»ΡŒΠ½Π°Ρ Ρ‡Π°ΡΡ‚ΡŒ ΠΊΠΎΠ΄Π° страницы
    
    src/app/sitemap.ts
    import type { MetadataRoute } from "next";
    import { locales, defaultLocale } from "@/i18n";
    
    const origin = "https://example.com";
    
    const formatterLocalizedPath = (locale: string, path: string) =>
      locale === defaultLocale ? origin + path : origin + "/" + locale + path;
    
    export default function sitemap(): MetadataRoute.Sitemap {
      const aboutLanguages = Object.fromEntries(
        locales.map((l) => [l, formatterLocalizedPath(l, "/about")])
      );
    
      return [
        {
          url: formatterLocalizedPath(defaultLocale, "/about"),
          lastModified: new Date(),
          changeFrequency: "monthly",
          priority: 0.7,
          alternates: { languages: aboutLanguages },
        },
      ];
    }
    
    src/app/robots.ts
    import type { MetadataRoute } from "next";
    import { locales, defaultLocale } from "@/i18n";
    
    const origin = "https://example.com";
    // Ѐункция для Π³Π΅Π½Π΅Ρ€Π°Ρ†ΠΈΠΈ ΠΏΡƒΡ‚Π΅ΠΉ со всСми локалями
    const withAllLocales = (path: string) => [
      path,
      ...locales
        .filter((locale) => locale !== defaultLocale)
        .map((locale) => "/" + locale + path),
    ];
    
    export default function robots(): MetadataRoute.Robots {
      // Π—Π°ΠΏΡ€Π΅Ρ‚ доступа ΠΊ ΠΎΠΏΡ€Π΅Π΄Π΅Π»Ρ‘Π½Π½Ρ‹ΠΌ путям для всСх Π»ΠΎΠΊΠ°Π»Π΅ΠΉ
      const disallow = [
        ...withAllLocales("/dashboard"),
        ...withAllLocales("/admin"),
      ];
    
      return {
        rules: { userAgent: "*", allow: ["/"], disallow },
        host: origin,
        sitemap: origin + "/sitemap.xml",
      };
    }
    

    Middleware для ΠΌΠ°Ρ€ΡˆΡ€ΡƒΡ‚ΠΈΠ·Π°Ρ†ΠΈΠΈ Π»ΠΎΠΊΠ°Π»Π΅ΠΉ

    Π”ΠΎΠ±Π°Π²ΡŒΡ‚Π΅ middleware для ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠΈ опрСдСлСния Π»ΠΎΠΊΠ°Π»ΠΈ ΠΈ ΠΌΠ°Ρ€ΡˆΡ€ΡƒΡ‚ΠΈΠ·Π°Ρ†ΠΈΠΈ:

    src/middleware.ts
    import createMiddleware from "next-intl/middleware";
    import { locales, defaultLocale } from "@/i18n";
    
    export default createMiddleware({
      locales: [...locales],
      defaultLocale,
      localeDetection: true,
    });
    
    export const config = {
      // ΠŸΡ€ΠΎΠΏΡƒΡΡ‚ΠΈΡ‚ΡŒ API, Π²Π½ΡƒΡ‚Ρ€Π΅Π½Π½ΠΈΠ΅ части Next ΠΈ статичСскиС рСсурсы
      matcher: ["/((?!api|_next|.*\\..*).*)"],
    };
    

    Π›ΡƒΡ‡ΡˆΠΈΠ΅ ΠΏΡ€Π°ΠΊΡ‚ΠΈΠΊΠΈ

    • УстановитС html Π°Ρ‚Ρ€ΠΈΠ±ΡƒΡ‚Ρ‹ lang ΠΈ dir: Π’ src/app/[locale]/layout.tsx вычисляйтС dir с ΠΏΠΎΠΌΠΎΡ‰ΡŒΡŽ getLocaleDirection(locale) ΠΈ Π·Π°Π΄Π°Π²Π°ΠΉΡ‚Π΅ <html lang={locale} dir={dir}>.
    • РаздСляйтС сообщСния ΠΏΠΎ namespace: ΠžΡ€Π³Π°Π½ΠΈΠ·ΡƒΠΉΡ‚Π΅ JSON ΠΏΠΎ локалям ΠΈ namespace (Π½Π°ΠΏΡ€ΠΈΠΌΠ΅Ρ€, common.json, about.json).
    • ΠœΠΈΠ½ΠΈΠΌΠΈΠ·ΠΈΡ€ΡƒΠΉΡ‚Π΅ Π½Π°Π³Ρ€ΡƒΠ·ΠΊΡƒ Π½Π° ΠΊΠ»ΠΈΠ΅Π½Ρ‚: На страницах отправляйтС Π² NextIntlClientProvider Ρ‚ΠΎΠ»ΡŒΠΊΠΎ Π½Π΅ΠΎΠ±Ρ…ΠΎΠ΄ΠΈΠΌΡ‹Π΅ пространства ΠΈΠΌΡ‘Π½ (Π½Π°ΠΏΡ€ΠΈΠΌΠ΅Ρ€, pick(messages, ['common', 'about'])).
    • ΠŸΡ€Π΅Π΄ΠΏΠΎΡ‡ΠΈΡ‚Π°ΠΉΡ‚Π΅ статичСскиС страницы: ЭкспортируйтС export const dynamic = 'force-static' ΠΈ Π³Π΅Π½Π΅Ρ€ΠΈΡ€ΡƒΠΉΡ‚Π΅ статичСскиС ΠΏΠ°Ρ€Π°ΠΌΠ΅Ρ‚Ρ€Ρ‹ для всСх locales.
    • Π‘ΠΈΠ½Ρ…Ρ€ΠΎΠ½Π½Ρ‹Π΅ сСрвСрныС ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚Ρ‹: ΠŸΠ΅Ρ€Π΅Π΄Π°Π²Π°ΠΉΡ‚Π΅ Π·Π°Ρ€Π°Π½Π΅Π΅ вычислСнныС строки (ΠΏΠ΅Ρ€Π΅Π²Π΅Π΄Ρ‘Π½Π½Ρ‹Π΅ ΠΌΠ΅Ρ‚ΠΊΠΈ, ΠΎΡ‚Ρ„ΠΎΡ€ΠΌΠ°Ρ‚ΠΈΡ€ΠΎΠ²Π°Π½Π½Ρ‹Π΅ числа), Π° Π½Π΅ асинхронныС Π²Ρ‹Π·ΠΎΠ²Ρ‹ ΠΈΠ»ΠΈ нСсСриализуСмыС Ρ„ΡƒΠ½ΠΊΡ†ΠΈΠΈ.

    РСализация Intlayer ΠΏΠΎΠ²Π΅Ρ€Ρ… next-intl

    УстановитС зависимости intlayer:

    bash
    npm install intlayer @intlayer/sync-json-plugin --save-dev
    

    Π‘ΠΎΠ·Π΄Π°ΠΉΡ‚Π΅ Ρ„Π°ΠΉΠ» ΠΊΠΎΠ½Ρ„ΠΈΠ³ΡƒΡ€Π°Ρ†ΠΈΠΈ intlayer:

    intlayer.config.ts
    import { type IntlayerConfig, Locales } from "intlayer";
    import { syncJSON } from "@intlayer/sync-json-plugin";
    
    const config: IntlayerConfig = {
      internationalization: {
        locales: [Locales.ENGLISH, Locales.FRENCH, Locales.SPANISH], // Π»ΠΎΠΊΠ°Π»ΠΈ для ΠΈΠ½Ρ‚Π΅Ρ€Π½Π°Ρ†ΠΈΠΎΠ½Π°Π»ΠΈΠ·Π°Ρ†ΠΈΠΈ
        defaultLocale: Locales.ENGLISH, // локаль ΠΏΠΎ ΡƒΠΌΠΎΠ»Ρ‡Π°Π½ΠΈΡŽ
      },
      ai: {
        apiKey: process.env.OPENAI_API_KEY, // ΠΊΠ»ΡŽΡ‡ API для AI
      },
      plugins: [
        // Π‘ΠΈΠ½Ρ…Ρ€ΠΎΠ½ΠΈΠ·ΠΈΡ€ΡƒΠΉΡ‚Π΅ структуру ΠΏΠ°ΠΏΠΎΠΊ ΠΏΠΎ namespace с Intlayer
        syncJSON({
          format: "icu",
          source: ({ key, locale }) => `./locales/${locale}/${key}.json`, // ΠΏΡƒΡ‚ΡŒ ΠΊ JSON-Ρ„Π°ΠΉΠ»Π°ΠΌ с ΠΏΠ΅Ρ€Π΅Π²ΠΎΠ΄Π°ΠΌΠΈ
        }),
      ],
    };
    
    export default config;
    

    Π”ΠΎΠ±Π°Π²ΡŒΡ‚Π΅ скрипты Π² package.json:

    package.json
    {
      "scripts": {
        "i18n:fill": "intlayer fill", // Π·Π°ΠΏΠΎΠ»Π½Π΅Π½ΠΈΠ΅ ΠΏΠ΅Ρ€Π΅Π²ΠΎΠ΄ΠΎΠ² с ΠΏΠΎΠΌΠΎΡ‰ΡŒΡŽ AI
        "i18n:test": "intlayer test" // ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° ΠΎΡ‚ΡΡƒΡ‚ΡΡ‚Π²ΡƒΡŽΡ‰ΠΈΡ…/Π½Π΅ΠΊΠΎΡ€Ρ€Π΅ΠΊΡ‚Π½Ρ‹Ρ… ΠΏΠ΅Ρ€Π΅Π²ΠΎΠ΄ΠΎΠ²
      }
    }
    

    ΠŸΡ€ΠΈΠΌΠ΅Ρ‡Π°Π½ΠΈΡ:

    • intlayer fill: ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅Ρ‚ вашСго AI-ΠΏΡ€ΠΎΠ²Π°ΠΉΠ΄Π΅Ρ€Π° для заполнСния ΠΎΡ‚ΡΡƒΡ‚ΡΡ‚Π²ΡƒΡŽΡ‰ΠΈΡ… ΠΏΠ΅Ρ€Π΅Π²ΠΎΠ΄ΠΎΠ² Π½Π° основС настроСнных Π»ΠΎΠΊΠ°Π»Π΅ΠΉ.
    • intlayer test: провСряСт ΠΎΡ‚ΡΡƒΡ‚ΡΡ‚Π²ΡƒΡŽΡ‰ΠΈΠ΅/Π½Π΅Π΄Π΅ΠΉΡΡ‚Π²ΠΈΡ‚Π΅Π»ΡŒΠ½Ρ‹Π΅ ΠΏΠ΅Ρ€Π΅Π²ΠΎΠ΄Ρ‹ (ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠΉΡ‚Π΅ Π² CI).

    Π’Ρ‹ ΠΌΠΎΠΆΠ΅Ρ‚Π΅ Π½Π°ΡΡ‚Ρ€ΠΎΠΈΡ‚ΡŒ Π°Ρ€Π³ΡƒΠΌΠ΅Π½Ρ‚Ρ‹ ΠΈ ΠΏΡ€ΠΎΠ²Π°ΠΉΠ΄Π΅Ρ€ΠΎΠ²; смотритС Intlayer CLI.