DevGang
Авторизоваться

Создание многоязычного веб-сайта с использованием Next.js и next-intl

Для начала я создам новое приложение next.js, используя следующую команду. Если ваша страница уже создана, вы можете перейти к следующему шагу.

npx create-next-app@latest

В этом примере проекта я использую TypeScript, но он работает так же, если вы используете JavaScript, просто игнорируйте ввод текста, который будет встречаться в кодах на протяжении всей статьи.

Установка и настройка next-intl

Создав проект, давайте теперь установим его с помощью следующей команды:

npm i next-intl

Далее нам нужно внести некоторые изменения в нашу файловую структуру:

  • Создайте папку messages внутри нашей папки src, здесь будут расположены различные языковые файлы для нашей страницы. Внутри него мы создадим два файла: pt.json и en.json. Имя каждого файла должно относиться к языкам, которые вы собираетесь использовать на своем веб-сайте.
  • Создайте файл middleware.ts, также в папке src. Этот файл будет отвечать за перенаправление пользовательских запросов.
  • Создайте папку [locale] внутри папки app и переместите файлы .tsx в эту новую папку. Это позволит файлу layout.tsx иметь доступ к предпочитаемой папке пользователя через реквизиты и сможет передавать эту информацию на страницы приложения. Не забудьте обновить импорт этих файлов.

Ваш проект должен иметь структуру папок, подобную этой:

В этом примере мы будем использовать португальский и английский языки, причем английский по умолчанию. В файл middleware.ts вставьте следующий код:

import createMiddleware from 'next-intl/middleware';

export default createMiddleware({
  // Lista de locales suportados pela sua página
  locales: ['en', 'pt'],

  // Locale padrão
  defaultLocale: 'en'
});

export const config = {
  // Ignora as rotas que não devem ser internacionalizadas,
  // como rotas para arquivos de imagem
  matcher: ['/((?!api|_next|_vercel|.*\\..*).*)']
};

Замените код файла layout.tsx следующим:  

import { NextIntlClientProvider } from 'next-intl';
import { Inter } from 'next/font/google';
import { notFound } from 'next/navigation';
import { ReactNode } from 'react';
import '../globals.css';

const inter = Inter({ subsets: ['latin'] });

export function generateStaticParams() {
  return [{ locale: 'en' }, { locale: 'pt' }];
}

interface Props {
  children: ReactNode;
  params: {
    locale: string;
  };
}

export default async function LocaleLayout({
  children,
  params: { locale },
}: Props) {
  let messages;
  try {
    messages = (await import(`../../messages/${locale}.json`)).default;
  } catch (error) {
    notFound();
  }

  return (
    <html lang={locale}>
      <body className={inter.className}>
        <NextIntlClientProvider locale={locale} messages={messages}>
          {children}
        </NextIntlClientProvider>
      </body>
    </html>
  );
}

В этом файле мы получаем через реквизиты locale, в которой должны отображаться страницы, и импортируем соответствующий json-файл с переводами. Затем эта информация передается <NextIntlClientProvider />, что делает эти переводы доступными для дочерних компонентов по запросу каждого из них.

Теперь наполним файлы .json переводами нашей страницы. Переводы этих файлов могут быть организованы так, как вы предпочитаете, вам просто нужно обратить внимание, что все файлы имеют одинаковую структуру и одинаковые ключи, так что единственная разница между файлом и, например, pt.json это en.json значения каждого ключа.

pt.json
{
  "home": {
    "meta": {
      "title": "Create Next App",
      "description": "Gerado pelo create next app"
    },
    "page": {
      "get-started": "Comece editando o arquivo",
      "by": "Por",
      "docs": {
        "title": "Docs",
        "content": "Encontre informações detalhadas sobre os recursos e a API do Next.js."
      },
      "learn": {
        "title": "Aprenda",
        "content": "Aprenda sobre o Next.js em um curso interativo com questionários!"
      },
      "templates": {
        "title": "Templates",
        "content": "Explore o playground do Next.js."
      },
      "deploy": {
        "title": "Deploy",
        "content": "Faça deploy instantaneamente de seu site Next.js em uma URL compartilhável com a Vercel."
      }
    }
  }
}
ru.json
{
  "home": {
    "meta": {
      "title": "Create Next App",
      "description": "Generated by create next app"
    },
    "page": {
      "get-started": "Get started by editing",
      "by": "By",
      "docs": {
        "title": "Docs",
        "content": "Find in-depth information about Next.js features and API."
      },
      "learn": {
        "title": "Learn",
        "content": "Learn about Next.js in an interactive course with quizzes!"
      },
      "templates": {
        "title": "Templates",
        "content": "Explore the Next.js playground."
      },
      "deploy": {
        "title": "Deploy",
        "content": "Instantly deploy yout Next.js site to a shareable URL with Vercel."
      }
    }
  }
}

Перевод нашей страницы

На данный момент я пишу эту статью, next-intl работает только с компонентами на стороне клиента. Поэтому, чтобы использовать наши переводы, нам нужно добавить 'use client' вверху нашего файла page.tsx.

Как только это будет сделано, нам просто нужно использовать хук useTranslations, экспортированный next-intl, передав в качестве параметра ключи, соответствующие той части нашего json-файла, к которой мы хотим получить доступ, в данном случае содержимое находится в формате 'home.page'.

'use client';

import Image from 'next/image';
import styles from '../page.module.css';
import { useTranslations } from 'next-intl';

export default function Home() {
  const t = useTranslations('home.page');

  // resto do arquivo

Теперь нам просто нужно поместить каждое значение в нашем json в правильное место на странице, используя функцию, t которую мы получили от перехватчика на предыдущем шаге.

Например, заголовок раздела документации будет выглядеть так:

<h2>Docs <span>-&gt;</span></h2>

Для этого:

<h2>{t('docs.title')} <span>-&gt;</span></h2>

Применив это ко всем разделам страницы с текстом, мы получим следующий файл page.tsx:  

'use client';

import { useTranslations } from 'next-intl';
import Image from 'next/image';
import styles from '../page.module.css';

export default function Home() {
  const t = useTranslations('home.page');

  return (
    <main className={styles.main}>
      <div className={styles.description}>
        <p>
          {t('get-started')}&nbsp;
          <code className={styles.code}>src/app/[locale]/page.tsx</code>
        </p>
        <div>
          <a
            href='https://vercel.com?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app'
            target='_blank'
            rel='noopener noreferrer'
          >
            {t('by') + ' '}
            <Image
              src='/vercel.svg'
              alt='Vercel Logo'
              className={styles.vercelLogo}
              width={100}
              height={24}
              priority
            />
          </a>
        </div>
      </div>

      <div className={styles.center}>
        <Image
          className={styles.logo}
          src='/next.svg'
          alt='Next.js Logo'
          width={180}
          height={37}
          priority
        />
      </div>

      <div className={styles.grid}>
        <a
          href='https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app'
          className={styles.card}
          target='_blank'
          rel='noopener noreferrer'
        >
          <h2>
            {t('docs.title')} <span>-&gt;</span>
          </h2>
          <p>{t('docs.content')}</p>
        </a>

        <a
          href='https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app'
          className={styles.card}
          target='_blank'
          rel='noopener noreferrer'
        >
          <h2>
            {t('learn.title')}
            <span>-&gt;</span>
          </h2>
          <p>{t('learn.content')}</p>
        </a>

        <a
          href='https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app'
          className={styles.card}
          target='_blank'
          rel='noopener noreferrer'
        >
          <h2>
            {t('templates.title')} <span>-&gt;</span>
          </h2>
          <p>{t('templates.content')}</p>
        </a>

        <a
          href='https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app'
          className={styles.card}
          target='_blank'
          rel='noopener noreferrer'
        >
          <h2>
            {t('deploy.title')} <span>-&gt;</span>
          </h2>
          <p>{t('deploy.content')}</p>
        </a>
      </div>
    </main>
  );
}

Готово! После этого ваша страница начнет отображать контент в соответствии с языковыми предпочтениями каждого пользователя.

А как насчет метаданных?

Если вы экспортируете пользовательские метаданные на свою страницу, использование useTranslations непосредственно в файле page.tsx будет невозможно, поскольку, хотя перехватчик требует, чтобы компонент находился на стороне клиента, для работы метаданных компонент должен быть на стороне сервера.

К счастью, вам не нужно отказываться от своих метаданных или перевода на несколько языков: чтобы и то, и другое работало, вам просто нужно переместить части, которые их используют, useTranslations в другой компонент.

Таким образом, этот новый компонент находится на стороне клиента, используя useTranslations метаданные page.tsx на стороне сервера, просто импортируя и отображая вновь созданный компонент.

Для нашего примера вот что мы сделаем:

  • Создайте компонент Description, который будет верхней частью страницы;
  • Создайте компонент LinkCard, который будет использоваться для каждой из карточек в нижней части страницы;
  • Создайте компоненты DocsCardLearnCardTemplatesCard и DeployCard они будут использоваться LinkCard для рендеринга содержимого каждой карточки в нижней части;
  • Обновите наш файл page.tsx, удалив 'use client' и заменив html-содержимое созданными нами компонентами.
src/components/Description.tsx
'use client';

import styles from '@/app/page.module.css';
import { useTranslations } from 'next-intl';
import Image from 'next/image';

export default function Description() {
  const t = useTranslations('home.page');

  return (
    <div className={styles.description}>
      <p>
        {t('get-started')}&nbsp;
        <code className={styles.code}>src/app/[locale]/page.tsx</code>
      </p>
      <div>
        <a
          href='https://vercel.com?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app'
          target='_blank'
          rel='noopener noreferrer'
        >
          {t('by') + ' '}
          <Image
            src='/vercel.svg'
            alt='Vercel Logo'
            className={styles.vercelLogo}
            width={100}
            height={24}
            priority
          />
        </a>
      </div>
    </div>
  );
}
src/components/LinkCard.tsx
import styles from '@/app/page.module.css';

interface Props {
  link: string;
  title: string;
  content: string;
}

export default function LinkCard({ content, link, title }: Props) {
  return (
    <a
      href={link}
      className={styles.card}
      target='_blank'
      rel='noopener noreferrer'
    >
      <h2>
        {title}
        <span>-&gt;</span>
      </h2>
      <p>{content}</p>
    </a>
  );
}
src/components/DocsCard.tsx
'use client';

import { useTranslations } from 'next-intl';
import LinkCard from './LinkCard';

export default function DocsCard() {
  const t = useTranslations('home.page');

  return (
    <LinkCard
      content={t('docs.content')}
      link='https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app'
      title={t('docs.title')}
    />
  );
}
src/components/LearnCard.tsx
'use client';

import { useTranslations } from 'next-intl';
import LinkCard from './LinkCard';

export default function LearnCard() {
  const t = useTranslations('home.page');

  return (
    <LinkCard
      content={t('learn.content')}
      link='https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app'
      title={t('learn.title')}
    />
  );
}
src/components/TemplatesCard.tsx
'use client';

import { useTranslations } from 'next-intl';
import LinkCard from './LinkCard';

export default function TemplatesCard() {
  const t = useTranslations('home.page');

  return (
    <LinkCard
      content={t('docs.content')}
      link='https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app'
      title={t('docs.title')}
    />
  );
}
src/components/DeployCard.tsx
'use client';

import { useTranslations } from 'next-intl';
import LinkCard from './LinkCard';

export default function DeployCard() {
  const t = useTranslations('home.page');

  return (
    <LinkCard
      content={t('deploy.content')}
      link='https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app'
      title={t('deploy.title')}
    />
  );
}
src/app/[locale]/page.tsx
import DeployCard from '@/components/DeployCard';
import Description from '@/components/Description';
import DocsCard from '@/components/DocsCard';
import LearnCard from '@/components/LearnCard';
import TemplatesCard from '@/components/TemplatesCard';
import Image from 'next/image';
import styles from '../page.module.css';

export default function Home() {
  return (
    <main className={styles.main}>
      <Description />

      <div className={styles.center}>
        <Image
          className={styles.logo}
          src='/next.svg'
          alt='Next.js Logo'
          width={180}
          height={37}
          priority
        />
      </div>

      <div className={styles.grid}>
        <DocsCard />
        <LearnCard />
        <TemplatesCard />
        <DeployCard />
      </div>
    </main>
  );
}

Теперь, когда у нас есть серверный компонент страницы, мы можем экспортировать метаданные страницы с помощью переменной или функции generateMetadata.

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

src/app/[locale]/page.tsx
// imports
import { Metadata } from 'next';

interface MetadataProps {
  params: { locale: string };
  searchParams: {};
}

export async function generateMetadata({
  params,
}: MetadataProps): Promise<Metadata> {
  const messages = (await import(`@/messages/${params.locale}.json`)).default;
  return messages.home.meta;
}

// resto do arquivo

Результат

Теперь у нас есть страница с контентом на нескольких языках:

Репозиторий с кодом, разработанным на протяжении всей статьи.

Источник:

#JavaScript
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться