Реализация интернационализации в Next.js

Примеры

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

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

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

Для начала добавьте конфигурацию 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 позволяет установить куки NEXT_LOCALE=the-locale, которые имеют приоритет над заголовком accept-language. Эти куки можно установить с помощью переключателя языка, и когда пользователь вернётся на сайт, при перенаправлении с / будет использоваться локаль, указанная в куки, для перехода в правильное местоположение локали.

Например, если пользователь предпочитает локаль 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.