
На Next.js Conf команда Vercel анонсировала Next.js 13, который представил новый каталог приложений.
Он включает поддержку Макетов, Серверных компонентов, Потоковой передачи и Поддержка выборки данных.
Потрясающий! Next.js 13 выпущен!
Он кажется довольно быстрым и закладывает основу для безграничной динамики.
Запоздалые мысли…
Звучит неплохо, но, если заглянуть в каталог приложений, похоже, что это полностью новая установка Next.js… не совсем сравнимая со старой…
Что это означает в отношении i18n?
Глядя на документы, кажется, что наши старые подходы больше не будут работать.

Полезные функции, предоставляемые next-i18next (и другими модулями i18n, связанными с Next.js), подобные описанным здесь и здесь, не подходят для этой новой настройки каталога приложений.
Новый подход
В этом разделе вы увидите, как мы можем интернационализировать новый каталог приложений с помощью i18next, react-i18next и i18next-resources-to-backend.npm install i18next react-i18next i18next-resources-to-backend
1. Структура папок
Давайте начнем с создания новой структуры папок, которая использует язык в качестве параметра URL. Так называемый динамический сегмент:
.
└── приложение
└── [lng]
├── вторая страница
| └── page.js
├── layout.js
└── page.js
Файл app/[lng]/page.js может выглядеть так:
импортировать ссылку из «далее/ссылка»
функция экспорта по умолчанию Page({ params: { lng } }) {
return (
‹›
‹h1›Привет!‹/h1›
‹Link href={` /${lng}/second-page`}›
вторая страница
‹/Link›
‹/›
)
}
И файл app/[lng]/second-page/page.js может выглядеть так:
импортировать ссылку из «далее/ссылка»
функция экспорта по умолчанию Page({ params: { lng } }) {
return (
‹›
‹h1›Привет со второй страницы!‹/h1›
‹Link href= {`/${lng}`}›
назад
‹/Ссылка›
‹/›
)
}
Последний файл app/[lng]/layout.js может выглядеть так:
импортировать {каталог} из «i18next»
константные языки = ['en', 'de']
экспортировать асинхронную функцию generateStaticParams() {
return languages.map((lng) =› ({ lng }))
}
функция экспорта по умолчанию RootLayout({
children,
params: {
lng
}
}) {
return (
‹html lang= {lng} dir={dir(lng)}›
‹head /›
‹body›
{children}
‹/body›
‹/html›
)
}
2. Определение языка
Теперь переход к http://localhost:3000/en или http://localhost:3000/de должен что-то показать, а также ссылки на вторую страницу и обратно должны работать, но переход к http://localhost:3000 вернет ошибку 404.
Чтобы исправить это, мы создадим промежуточное ПО Next.js и рефакторинг немного кода:
Давайте сначала создадим новый файл app/i18n/settings.js:
export const fallbackLng = ‘en’
export const languages = [fallbackLng, ‘de’]
Затем адаптируйте файл app/[lng]/layout.js:
импортировать { каталог } из ‘i18next’
импортировать { языки } из ‘../i18n/settings’
экспортировать асинхронную функцию generateStaticParams() {
return languages.map((lng) =› ({ lng }))
}
функция экспорта по умолчанию RootLayout({
children,
params: {
lng
}
}) {
return (
‹html lang= {lng} dir={dir(lng)}›
‹head /›
‹body›
{children}
‹/body›
‹/html›
)
}
И, наконец, создайте файл middleware.js:npm install accept-language
импортировать { NextResponse } из ‘next/server’
импортировать acceptLanguage из ‘accept-language’
импортировать { fallbackLng, languages } из ‘./app/i18n/settings’
acceptLanguage.languages(языки)
export const config = {
matcher: ‘/:lng*’
}
const cookieName = ‘i18next’
промежуточное ПО функции экспорта (req) {
let lng
if (req.cookies.has(cookieName)) lng = acceptLanguage.get(req.cookies.get(cookieName).value)
if (!lng) lng = acceptLanguage.get(req.headers.get('Accept-Language'))
if (!lng) lng = fallbackLng
if (req.nextUrl.pathname === ‘/’) {
return NextResponse.redirect(новый URL(`/${lng}`, req.url))
}
if (req.headers.has('referer')) {
const refererUrl = new URL(req.headers.get('referer'))
const lngInReferer = languages.find((l) = › refererUrl.pathname.startsWith(`/${l}`))
const response = NextResponse.next()
if (lngInReferer) response.cookies.set(cookieName, lngInReferer)
вернуть ответ
}
вернуть NextResponse.next()
}

При переходе к корневому пути / теперь будет проверяться наличие файла cookie с последним выбранным языком, в качестве резервного варианта будет проверяться заголовок Accept-Language, а последним резервным вариантом является определенный резервный язык.
Обнаруженный язык будет использоваться для перенаправления. на соответствующую страницу.
3. Инструментарий i18n
Давайте подготовим i18next в файле app/i18n/index.js:
Здесь мы не используем синглтон i18next, а создаем новый экземпляр при каждом вызове useTranslation, потому что во время компиляции кажется, что все выполняется параллельно. Наличие отдельного экземпляра обеспечит согласованность переводов.
import { createInstance } from 'i18next'
import resourcesToBackend from 'i18next-resources-to-backend'
import { initReactI18next } from 'react-i18next/initReactI18next'
import {getOptions} from ' ./настройки'
const initI18next = async (lng, ns) =› {
const i18nInstance = createInstance()
await i18nInstance
.use(initReactI18next)
.use(resourcesToBackend((язык, пространство имен ) =› import(`./locales/${language}/${namespace}.json`)))
.init(getOptions(lng, ns))
return i18nInstance
}
экспортировать асинхронную функцию useTranslation(lng, ns, options = {}) {
const i18nextInstance = await initI18next(lng, ns)
return {
t: i18nextInstance.getFixedT(lng, Array.isArray (ns) ?ns[0] : ns, options.keyPrefix),
i18n: i18nextInstance
}
}
В файле app/i18n/settings.js мы добавим параметры i18next:
export const fallbackLng = ‘en’
export const languages = [fallbackLng, ‘de’]
export const defaultNS = ‘translation’
функция экспорта getOptions (lng = fallbackLng, ns = defaultNS) {
return {
// отладка: true,
supportLngs: languages,
fallbackLng,
lng,< br /> резервный NS: defaultNS,
defaultNS,
ns
}
}
Подготовим несколько файлов перевода:
.
└── app
└── i18n
└── локали
├── en
| ├── translation.json
| └── second-page.json
└── de
├── translation.json
└── second-page.json
app/i18n/locales/en/translation.json:
{
«title»: «Привет!»,
«to-second-page»: «На вторую страницу»
}
app/i18n/locales/de/translation.json:
{
«название»: «Hello Leute!»,
«на вторую страницу»: «Zur zweiten Seite»
}
app/i18n/locales/en/second-page.json:
{
«title»: «Привет со второй страницы!»,
«back-to-home»: «Вернуться на главную»
}
app/i18n/locales/de/second-page.json:
{
«title»: «Hallo von der zweiten Seite!»,
«back-to-home»: «Zurück zur Hauptseite»
}
Теперь мы готовы использовать это на наших страницах…
Страницы сервера могут async таким образом, мы можем ожидать ответа useTranslation.
app/[lng]/page.js:
импортировать ссылку из ‘next/link’
импортировать {useTranslation } из ‘../i18n’
асинхронная функция экспорта по умолчанию Page({ params: {lng } }) {
const { t } = await useTranslation(lng)
return (
‹›
‹h1›{t ('title')}‹/h1›
‹Link href={`/${lng}/second-page`}›
{t('to-second-page')}
‹/Ссылка›
‹/›
)
}
app/[lng]/second-page/page.js:
импортировать ссылку из ‘next/link’
импортировать {useTranslation } из ‘../../i18n’
экспортировать асинхронную функцию по умолчанию Page({ params: { lng } }) {
const { t } = await useTranslation (lng, 'вторая страница')
return (
‹›
‹h1›{t('title')}‹/h1›
‹Link href={`/${lng}`}›
{t('back-to-home')}
‹/Ссылка›
‹/›
)
}

4. Переключатель языка
Теперь давайте определим переключатель языка в компоненте нижнего колонтитула:
app/[lng]/components/Footer/index.js:
импортировать ссылку из 'next/link'
импортировать {Trans} из 'react-i18next/TransWithoutContext'
импортировать {языки} из '../../../i18n/settings'
импортировать {useTranslation} из '../../../i18n'
export const Footer = async ({ lng }) =› {
const { t } = await useTranslation (lng, 'footer')
return (
‹footer style={{ marginTop: 50 }}›
‹Trans i18nKey="languageSwitcher" t={t}›
Переключение с ‹strong›{{lng}}‹/strong› на:{' '}
‹/ Trans›
{languages.filter((l) =› lng !== l).map((l, index) =› {
return (
‹span key={l} ›
{index › 0 && (' or ')}
‹Link href={`/${l}`}›
{l}
‹/Link›< br /> ‹/span›
)
})}
‹/footer›
)
}
Как видите, мы также можем использовать компонент react-i18next Trans.
Новое пространство имен:
app/i18n/locales/en/footer.json:
{
«languageSwitcher»: «Переключиться с ‹1›{{lng}}‹/1› на: «
}
app/i18n/locales/de/footer.json:
{
«languageSwitcher»: «Wechseln von ‹1›{{lng}}‹/1› nach: «
}
И добавьте этот компонент нижнего колонтитула на страницы:
app/[lng]/page.js:
импортировать ссылку из ‘next/link’
импортировать { useTranslation } из ‘../i18n’
импортировать { нижний колонтитул } из ‘./components/Footer’
асинхронная функция экспорта по умолчанию Page({ params: {lng } }) {
const { t } = await useTranslation(lng)
return (
‹›
‹h1›{t ('title')}‹/h1›
‹Link href={`/${lng}/second-page`}›
{t('to-second-page')}
‹/Link›
‹Footer lng={lng}/›
‹/›
)
}
app/[lng]/second-page/page.js:
импортировать ссылку из ‘next/link’
импортировать {useTranslation} из ‘../../i18n’
импортировать {нижний колонтитул} из ‘../components/Footer’
экспортировать асинхронную функцию по умолчанию Page({ params: { lng } }) {
const { t } = await useTranslation (lng, 'вторая страница')
return (
‹›
‹h1›{t('title')}‹/h1›
‹Link href={`/${lng}`}›
{t('back-to-home')}
‹/Link›
‹Footer lng={lng}/›
‹/›
)
}

🥳 Отлично, вы только что создали свой первый переключатель языков!
5. Клиентская сторона
До сих пор мы создавали только серверные страницы.
Так как же выглядят клиентские страницы?
Поскольку клиентские компоненты не могут async реагировать, нам нужно внести некоторые коррективы.
Давайте представим файл app/i18n/client.js:
«использовать клиент»
import i18next from 'i18next'
import { initReactI18next, useTranslation as useTranslationOrg } from 'react-i18next'
import resourcesToBackend from 'i18next-resources-to-backend'
import { getOptions } from ' ./настройки'
//
i18next
.use(initReactI18next)
.use(resourcesToBackend((язык, пространство имен) => import(`./locales/${language}/${namespace}.json `)))
.init(getOptions())
функция экспорта useTranslation(lng, ns, options) {
if (i18next.resolvedLanguage !== lng) i18next.changeLanguage(lng)
return useTranslationOrg(ns, options)
}
На стороне клиента нормальный синглтон i18next в порядке. Он будет инициализирован только один раз. И мы можем использовать «обычный» хук useTranslation. Мы просто оборачиваем его, чтобы иметь возможность передать в языке.
Нам также нужно создать 2 версии компонента Footer.
.
└── приложение
└── [lng]
└── компоненты
└── Нижний колонтитул
├── client.js
├ ── FooterBase.js
└── index.js
app/[lng]/components/Footer/FooterBase.js:
импортировать ссылку из ‘next/link’
импортировать {Trans} из ‘react-i18next/TransWithoutContext’
импортировать {языки} из ‘../../../i18n/settings’
export const FooterBase = ({ t, lng }) =› {
return (
‹footer style={{ marginTop: 50 }}›
‹Trans i18nKey="languageSwitcher" t={ t}›
Переключиться с ‹strong›{{lng}}‹/strong› на:{' '}
‹/Trans›
{languages.filter((l) =› lng !== l).map((l, index) =› {
return (
‹span key={l}›
{index › 0 && (' or ')}< br /> ‹Link href={`/${l}`}›
{l}
‹/Link›
‹/span›
)
} )}
‹/footer›
)
}
Серверная часть продолжает использовать версию async, app/[lng]/components/Footer/index.js:
импортировать { useTranslation } из ‘../../../i18n’
импортировать { FooterBase } из ‘./FooterBase’
export const Footer = async ({lng}) =› {
const { t } = await useTranslation(lng, 'footer')
return ‹FooterBase t={t} lng={lng} /›
}
Клиентская часть будет использовать новую версию i18n/client, app/[lng]/components/Footer/client.js:
«использовать клиент»
импортировать { FooterBase } из ‘./FooterBase’
импортировать { useTranslation } из ‘../../../i18n/client’
export const Footer = ({ lng }) =› {
const { t } = useTranslation(lng, 'footer')
return ‹FooterBase t={t} lng={lng} /›
}
Клиентская страница может выглядеть так — app/[lng]/client-page/page.js:
«использовать клиент»
import Link from 'next/link'
import { useTranslation } from '../../i18n/client'
import { Footer } from '../components/Footer/client'
> импортировать {useState} из «реагировать»
функция экспорта по умолчанию Page({ params: {lng } }) {
const { t } = useTranslation(lng, 'client-page')
const [counter, setCounter] = useState(0)
return (
‹›
‹h1›{t('title')}‹/h1›
‹p›{t('counter', { count: counter })} ‹/p›
‹div›
‹button onClick={() =› setCounter(Math.max(0, counter — 1))}›-‹/button›
‹button onClick={() =› setCounter(Math.min(10, counter + 1))}›+‹/button›
‹/div›
‹Link href={`/${lng} `}›
‹button type="button"›
{t('back-to-home')}
‹/button›
‹/Link›
‹Footer lng={lng} /›
‹/›
)
}
С некоторыми ресурсами перевода:
app/i18n/locales/en/client-page.json:
{
«title»: «Клиентская страница»,
«counter_one»: «выбран один»,
«counter_other»: «выбрано {{count}}»,
«counter_zero ”: “ничего не выбрано”,
“назад к дому”: “Вернуться к дому”
}
app/i18n/locales/de/client-page.json:
{
«title»: «Client Seite»,
«counter_one»: «eines ausgewählt»,
«counter_other»: «{{count}} ausgewählt»,
«counter_zero ”: “keines ausgewählt”,
“обратно домой”: “Zurück zur Hauptseite”
}
И ссылка на нашей начальной странице — app/[lng]/page.js:
импортировать ссылку из ‘next/link’
импортировать { useTranslation } из ‘../i18n’
импортировать { нижний колонтитул } из ‘./components/Footer’
асинхронная функция экспорта по умолчанию Page({ params: {lng } }) {
const { t } = await useTranslation(lng)
return (
‹›
‹h1›{t ('title')}‹/h1›
‹Link href={`/${lng}/second-page`}›
{t('to-second-page')}
‹/Link›
‹br /›
‹Link href={`/${lng}/client-page`}›
{t('to-client-page' )}
‹/Link›
‹Footer lng={lng}/›
‹/›
)
}
… с ресурсами для перевода:
app/i18n/locales/en/translation.json:
{
«title»: «Привет!»,
«to-second-page»: «На вторую страницу»,
«to-client-page»: «На страницу клиента»
}
app/i18n/locales/de/translation.json:
{
«title»: «Hello Leute!»,
«to-second-page»: «Zur zweiten Seite»,
«to-client-page»: «Zur clienteitigen Seite»
}
🎉🥳 Поздравляем 🎊🎁
Результат должен выглядеть так:

🧑💻 Полный код примера приложения можно найти здесь.
6. Бонус

изменить процесс локализации
Подключитесь к потрясающей системе управления переводами и управляйте своими переводами вне вашего кода.
Синхронизируем файлы перевода с locize. Это можно сделать по запросу, на CI-сервере или перед развертыванием приложения.
Что сделать, чтобы достичь этого шага:
- в locize: регистрация на https://locize.app/register и логин
- in locize: создать новый проект
- установить locize-cli (
npm i locize-cli) - в locize: добавьте все ваши дополнительные языки (это также можно сделать через API или с помощью миграционной команды locize-cli)
Используйте locize-cli
Используйте команду locize download, чтобы всегда загружать опубликованные локализованные переводы в локальный репозиторий (app/i18n/locales) перед объединением приложения. пример
Кроме того, вы также можете использовать команду locize sync для синхронизации вашего локального репозитория (app/i18n/locales) с тем, что опубликовано на locize. пример
🎉🥳 Поздравляем 🎊🎁
Надеюсь, вы узнали кое-что новое об i18n в новой настройке каталога приложений: Next.js, i18next, react-i18next, react-i18next, i18next-resources-to-backend. и современные рабочие процессы локализации.
Так что если вы хотите вывести свою тему i18n на новый уровень, стоит попробовать платформу управления локализацией — locize.
Основатели locize также являются создателями i18next. Таким образом, используя locize, вы напрямую поддерживаете будущее i18next.