Маршрутизация с интернационализацией (i18n)

Примеры

Next.js имеет встроенную поддержку интернационализированной (i18n) маршрутизации начиная с версии v10.0.0. Вы можете предоставить список локалей, локаль по умолчанию и доменные локали, и Next.js автоматически обработает маршрутизацию.

Поддержка i18n маршрутизации в настоящее время предназначена для дополнения существующих решений для интернационализации, таких как react-intl, react-i18next, lingui, rosetta, next-intl, next-translate, next-multilingual, tolgee и другие, упрощая обработку маршрутов и разбор локалей.

Начало работы

Для начала добавьте конфигурацию i18n в ваш файл next.config.js.

Локали представляют собой идентификаторы локалей UTS, стандартизированный формат для определения локалей.

Обычно идентификатор локали состоит из языка, региона и письменности, разделенных дефисом: язык-регион-письменность. Регион и письменность являются необязательными. Пример:

  • en-US - английский язык, используемый в США
  • nl-NL - нидерландский язык, используемый в Нидерландах
  • nl - нидерландский язык без указания региона

Если локаль пользователя nl-BE не указана в вашей конфигурации, пользователь будет перенаправлен на nl, если он доступен, или на локаль по умолчанию в противном случае. Если вы не планируете поддерживать все регионы страны, рекомендуется включать основные локали стран, которые будут использоваться как запасные варианты.

next.config.js
module.exports = {
  i18n: {
    // Все локали, которые вы хотите поддерживать
    // в вашем приложении
    locales: ['en-US', 'fr', 'nl-NL'],
    // Локаль по умолчанию, которая будет использоваться при посещении
    // пути без префикса локали, например `/hello`
    defaultLocale: 'en-US',
    // Список доменов локалей и локаль по умолчанию, которую они
    // должны обрабатывать (требуется только при настройке доменной маршрутизации)
    // Примечание: поддомены должны быть включены в значение домена для сопоставления, например "fr.example.com".
    domains: [
      {
        domain: 'example.com',
        defaultLocale: 'en-US',
      },
      {
        domain: 'example.nl',
        defaultLocale: 'nl-NL',
      },
      {
        domain: 'example.fr',
        defaultLocale: 'fr',
        // Необязательное поле http может использоваться для тестирования
        // доменов локалей локально с http вместо https
        http: true,
      },
    ],
  },
}

Стратегии работы с локалями

Существует две стратегии работы с локалями: маршрутизация по подпути и маршрутизация по домену.

Маршрутизация по подпути

Маршрутизация по подпути добавляет локаль в путь URL.

next.config.js
module.exports = {
  i18n: {
    locales: ['en-US', 'fr', 'nl-NL'],
    defaultLocale: 'en-US',
  },
}

С приведенной выше конфигурацией будут доступны маршруты для en-US, fr и nl-NL, причем en-US является локалью по умолчанию. Если у вас есть pages/blog.js, будут доступны следующие URL:

  • /blog
  • /fr/blog
  • /nl-nl/blog

Локаль по умолчанию не имеет префикса.

Маршрутизация по домену

Используя маршрутизацию по домену, вы можете настроить обслуживание локалей с разных доменов:

next.config.js
module.exports = {
  i18n: {
    locales: ['en-US', 'fr', 'nl-NL', 'nl-BE'],
    defaultLocale: 'en-US',

    domains: [
      {
        // Примечание: поддомены должны быть включены в значение домена для сопоставления
        // например, www.example.com должен использоваться, если это ожидаемое имя хоста
        domain: 'example.com',
        defaultLocale: 'en-US',
      },
      {
        domain: 'example.fr',
        defaultLocale: 'fr',
      },
      {
        domain: 'example.nl',
        defaultLocale: 'nl-NL',
        // Укажите другие локали, которые должны перенаправляться
        // на этот домен
        locales: ['nl-BE'],
      },
    ],
  },
}

Например, если у вас есть pages/blog.js, будут доступны следующие URL:

  • example.com/blog
  • www.example.com/blog
  • example.fr/blog
  • example.nl/blog
  • example.nl/nl-BE/blog

Автоматическое определение локали

Когда пользователь посещает корень приложения (обычно /), Next.js попытается автоматически определить предпочитаемую локаль на основе заголовка Accept-Language и текущего домена.

Если обнаружена локаль, отличная от локали по умолчанию, пользователь будет перенаправлен:

  • При маршрутизации по подпути: На путь с префиксом локали
  • При маршрутизации по домену: На домен с указанной локалью по умолчанию

При маршрутизации по домену, если пользователь с заголовком Accept-Language fr;q=0.9 посещает example.com, он будет перенаправлен на example.fr, так как этот домен обрабатывает локаль fr по умолчанию.

При маршрутизации по подпути пользователь будет перенаправлен на /fr.

Добавление префикса для локали по умолчанию

С Next.js 12 и Middleware мы можем добавить префикс для локали по умолчанию с помощью обходного решения.

Например, вот файл next.config.js с поддержкой нескольких языков. Обратите внимание, что локаль "default" добавлена намеренно.

next.config.js
module.exports = {
  i18n: {
    locales: ['default', 'en', 'de', 'fr'],
    defaultLocale: 'default',
    localeDetection: false,
  },
  trailingSlash: true,
}

Затем мы можем использовать Middleware для добавления пользовательских правил маршрутизации:

middleware.ts
import { NextRequest, NextResponse } from 'next/server'

const PUBLIC_FILE = /\.(.*)$/

export async function middleware(req: NextRequest) {
  if (
    req.nextUrl.pathname.startsWith('/_next') ||
    req.nextUrl.pathname.includes('/api/') ||
    PUBLIC_FILE.test(req.nextUrl.pathname)
  ) {
    return
  }

  if (req.nextUrl.locale === 'default') {
    const locale = req.cookies.get('NEXT_LOCALE')?.value || 'en'

    return NextResponse.redirect(
      new URL(`/${locale}${req.nextUrl.pathname}${req.nextUrl.search}`, req.url)
    )
  }
}

Этот Middleware пропускает добавление префикса по умолчанию для API Routes и публичных файлов, таких как шрифты или изображения. Если запрос сделан к локали по умолчанию, мы перенаправляем на наш префикс /en.

Отключение автоматического определения локали

Автоматическое определение локали можно отключить:

next.config.js
module.exports = {
  i18n: {
    localeDetection: false,
  },
}

Когда localeDetection установлен в false, Next.js больше не будет автоматически перенаправлять на основе предпочитаемой локали пользователя и будет предоставлять информацию о локали только на основе домена или пути, как описано выше.

Доступ к информации о локали

Вы можете получить доступ к информации о локали через роутер Next.js. Например, используя хук useRouter(), доступны следующие свойства:

  • locale содержит текущую активную локаль.
  • locales содержит все настроенные локали.
  • defaultLocale содержит настроенную локаль по умолчанию.

При предварительном рендеринге страниц с getStaticProps или getServerSideProps информация о локали предоставляется в контексте, передаваемом в функцию.

При использовании getStaticPaths настроенные локали предоставляются в параметре контекста функции под locales, а настроенная локаль по умолчанию — под defaultLocale.

Переход между локалями

Вы можете использовать next/link или next/router для перехода между локалями.

Для next/link можно указать проп locale для перехода на другую локаль отличную от текущей активной. Если проп locale не указан, при клиентских переходах будет использоваться текущая активная локаль. Например:

import Link from 'next/link'

export default function IndexPage(props) {
  return (
    <Link href="/another" locale="fr">
      Перейти на /fr/another
    </Link>
  )
}

При использовании методов next/router напрямую вы можете указать локаль, которая должна быть использована, через параметры перехода. Например:

import { useRouter } from 'next/router'

export default function IndexPage(props) {
  const router = useRouter()

  return (
    <div
      onClick={() => {
        router.push('/another', '/another', { locale: 'fr' })
      }}
    >
      Перейти на /fr/another
    </div>
  )
}

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

import { useRouter } from 'next/router'
const router = useRouter()
const { pathname, asPath, query } = router
// Изменяем только локаль, сохраняя всю остальную информацию о маршруте, включая параметры запроса href
router.push({ pathname, query }, asPath, { locale: nextLocale })

Подробнее о структуре объекта для router.push см. здесь.

Если у вас есть href, который уже включает локаль, вы можете отключить автоматическую обработку префикса локали:

import Link from 'next/link'

export default function IndexPage(props) {
  return (
    <Link href="/fr/another" locale={false}>
      Перейти на /fr/another
    </Link>
  )
}

Использование куки NEXT_LOCALE

Next.js поддерживает переопределение заголовка accept-language с помощью куки NEXT_LOCALE=the-locale. Этот куки можно установить с помощью переключателя языка, и когда пользователь вернется на сайт, он будет использовать локаль, указанную в куки, при перенаправлении с / на правильное расположение локали.

Например, если пользователь предпочитает локаль fr в заголовке accept-language, но установлен куки NEXT_LOCALE=en, при посещении / пользователь будет перенаправлен на расположение локали en до тех пор, пока куки не будет удален или истечет его срок действия.

Оптимизация для поисковых систем

Поскольку Next.js знает, на каком языке пользователь посещает сайт, он автоматически добавляет атрибут lang к тегу <html>.

Next.js не знает о вариантах страницы, поэтому вам нужно самостоятельно добавлять метатеги hreflang с помощью next/head. Подробнее о hreflang можно узнать в документации Google для веб-мастеров.

Как это работает со статической генерацией?

Обратите внимание, что интернационализированная маршрутизация не интегрируется с output: 'export', так как не использует уровень маршрутизации Next.js. Гибридные приложения Next.js, которые не используют output: 'export', полностью поддерживаются.

Динамические маршруты и страницы с getStaticProps

Для страниц, использующих getStaticProps с динамическими маршрутами, все варианты локалей страницы, которые должны быть предварительно отрендерены, должны быть возвращены из getStaticPaths. Наряду с объектом params, возвращаемым для paths, вы также можете вернуть поле locale, указывающее, какую локаль вы хотите отрендерить. Например:

pages/blog/[slug].js
export const getStaticPaths = ({ locales }) => {
  return {
    paths: [
      // Если `locale` не указан, будет сгенерирована только локаль по умолчанию
      { params: { slug: 'post-1' }, locale: 'en-US' },
      { params: { slug: 'post-1' }, locale: 'fr' },
    ],
    fallback: true,
  }
}

Для автоматически статически оптимизированных и нединамических страниц с getStaticProps версия страницы будет сгенерирована для каждой локали. Это важно учитывать, так как это может увеличить время сборки в зависимости от количества локалей, настроенных внутри getStaticProps.

Например, если у вас настроено 50 локалей и 10 нединамических страниц с getStaticProps, это означает, что getStaticProps будет вызван 500 раз. 50 версий каждой из 10 страниц будут сгенерированы во время каждой сборки.

Чтобы уменьшить время сборки динамических страниц с getStaticProps, используйте режим fallback. Это позволяет возвращать только самые популярные пути и локали из getStaticPaths для предварительного рендеринга во время сборки. Затем Next.js будет строить оставшиеся страницы во время выполнения по мере их запроса.

Автоматически статически оптимизированные страницы

Для автоматически статически оптимизированных страниц версия страницы будет сгенерирована для каждой локали.

Нединамические страницы с getStaticProps

Для нединамических страниц с getStaticProps версия генерируется для каждой локали, как описано выше. getStaticProps вызывается для каждой локали, которая рендерится. Если вы хотите исключить определенную локаль из предварительного рендеринга, вы можете вернуть notFound: true из getStaticProps, и эта версия страницы не будет сгенерирована.

export async function getStaticProps({ locale }) {
  // Вызов внешнего API для получения постов.
  // Можно использовать любую библиотеку для получения данных
  const res = await fetch(`https://.../posts?locale=${locale}`)
  const posts = await res.json()

  if (posts.length === 0) {
    return {
      notFound: true,
    }
  }

  // Возвращая { props: posts }, компонент Blog
  // получит `posts` как проп во время сборки
  return {
    props: {
      posts,
    },
  }
}

Ограничения конфигурации i18n

  • locales: всего 100 локалей
  • domains: всего 100 элементов доменов локалей

Полезно знать: Эти ограничения были добавлены изначально для предотвращения потенциальных проблем с производительностью во время сборки. Вы можете обойти эти ограничения с помощью пользовательской маршрутизации, используя Middleware в Next.js 12.