DEV Community

Cover image for Criando um site com múltiplos idiomas usando Next.js e next-intl
Adriel
Adriel

Posted on

Criando um site com múltiplos idiomas usando Next.js e next-intl

Recentemente eu comecei a fazer com que a minha página de portfólio exibisse o seu conteúdo tanto em português como em inglês, para tentar deixar ela mais acessível.

Eu uso next.js nela e nesse processo eu acabei optando por utilizar a biblioteca next-intl, que apesar de muito prática tem toda sua documentação em inglês, então eu resolvi escrever esse artigo para ajudar quem está atrás de fazer algo parecido. Vamos ao tutorial.

Criando o app next

Para começar eu vou criar uma nova aplicação next.js usando o comando a seguir, caso você já tenha sua página criada, pode pular para o próximo passo.

npx create-next-app@latest
Enter fullscreen mode Exit fullscreen mode

Eu estou utilizando typescript nesse projeto de exemplo, mas o funcionamento é o mesmo se você estiver usando javascript, basta ignorar as tipagens que vão aparecer nos códigos ao longo do artigo.

Instalando e configurando o next-intl

Com o projeto criado, vamos agora instalar o next usando o comando a seguir:

npm i next-intl
Enter fullscreen mode Exit fullscreen mode

Em seguida precisamos fazer algumas modificações na nossa estrutura de arquivos:

  • Criar uma pasta messages dentro da nossa pasta src, é lá onde ficarão os diferentes arquivos de idioma da nossa página. Dentro dela criaremos dois arquivos: pt.json e en.json. O nome de cada arquivo tem que se referir aos idiomas que você pretende aplicar ao seu site.

  • Criar um arquivo middleware.ts, também na pasta src. Esse arquivo será o responsável por redirecionar as requisições do usuário para o locale correto.

  • Criar uma pasta [locale] dentro da pasta app e mover os arquivos .tsx para dentro dessa nova pasta. Isso permitirá que o arquivo layout.tsx tenha acesso ao locale preferido pelo usuário através das props e possa repassar essa informação para as páginas do app. Lembre-se de atualizar os imports desses arquivos.

Seu projeto deve estar com uma estrutura de pastas parecida com essa:

Estrutura de pastas e arquivos mostrando localização do arquivo middleware.ts, e das pastas messages e [locale]

Para esse exemplo iremos utilizar os idiomas português e inglês, com o inglês como padrão. No arquivo middleware.ts, cole o código a seguir:

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|.*\\..*).*)']
};
Enter fullscreen mode Exit fullscreen mode

Substitua o código do arquivo layout.tsx pelo seguinte:

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

O que estamos fazendo nesse arquivo é pegar via props o locale em que as páginas devem ser exibidas e importando o arquivo json correspondente com as traduções. Essas informações são então passadas para o <NextIntlClientProvider /> que disponibiliza essas traduções para as componentes filhos conforme é solicitado por cada um.

Agora iremos preencher os arquivos .json com as traduções da nossa página. As traduções desses arquivos podem ser organizadas da forma como você preferir, você só deve prestar atenção para que todos os arquivos tenham a mesma estrutura e as mesmas chaves, de forma que o que a única diferença do arquivo pt.json para o en.json, por exemplo, sejam os valores de cada chave.

  • 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."
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode
  • en.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."
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Traduzindo nossa página

No momento eu que eu estou escrevendo esse artigo, o next-intl funciona apenas com componentes do lado do cliente. Então para usar nossas traduções temos que adicionar o 'use client' no topo do nosso arquivo page.tsx.

Feito isso, precisamos agora apenas utilizar o hook useTranslations, exportado pelo next-intl, passando como parâmetro as chaves correspondentes a parte do nosso arquivo json que queremos acessar, nesse caso o conteúdo está em '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
Enter fullscreen mode Exit fullscreen mode

Agora só precisamos colocar cada valor do nosso json no local correto da página usando a função t que recebemos do hook no passo anterior.

O título da seção de documentação, por exemplo, sairia disso:

<h2>Docs <span>-&gt;</span></h2>
Enter fullscreen mode Exit fullscreen mode

Para isso:

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

Aplicando isso para todos os trechos da página com texto, ficariamos com esse arquivo page.tsx no final:

'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>
  );
}
Enter fullscreen mode Exit fullscreen mode

Pronto! Com apenas isso sua página passa a exibir o conteúdo de acordo com a preferência de idioma de cada usuário.

Mas e o metadata?

Se você está exportando metadados personalizados para sua página, usar useTranslations não será diretamente no arquivo page.tsx não será possível, pois enquanto o hook exige que o componente esteja do lado do cliente, para o metadata funcionar o componente deve estar do lado do servidor.

Felizmente você não precisa abrir mão dos seus metadados ou da tradução para múltiplos idiomas, para os dois funcionarem basta que você mova as partes que usam o useTranslations para um outro componente.

Assim, esse novo componente fica do lado do cliente, usando o useTranslations enquanto o page.tsx fica do lado do servidor com o metadata, apenas importando e exibindo o componente recém-criado.

Para o nosso exemplo, eis o que faremos:

  • Criar um componente Description, que será a parte superior da página;
  • Criar um componente LinkCard, que será usado para cada um dos cards da seção inferior da página;
  • Criar os componentes DocsCard, LearnCard, TemplatesCard e DeployCard que usarão o LinkCard para renderizar o conteúdo de cada card da seção inferior;
  • Atualizar nosso arquivo page.tsx removendo o 'use client' e substituindo o conteúdo html pelos componentes que criamos.

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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')}
    />
  );
}
Enter fullscreen mode Exit fullscreen mode

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')}
    />
  );
}

Enter fullscreen mode Exit fullscreen mode

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')}
    />
  );
}
Enter fullscreen mode Exit fullscreen mode

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')}
    />
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

Agora que temos o componente da página do lado do servidor, podemos exportar os metadados da página usando uma variável ou a função generateMetadata.

Nesse caso, como eu quero gerar títulos e descrições específicos para cada idioma, vamos utilizar a segunda opção, porque assim conseguimos pegar o idioma preferido do usuário pelos parâmetros da função e importar os metadados correspondentes.

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
Enter fullscreen mode Exit fullscreen mode

Resultado

E pronto, agora temos uma página com conteúdo em mais de um idioma:

Página web padrão do Next.js com conteúdo em Português

Página web padrão do Next.js com conteúdo em Inglês

Caso queira consultar posteriormente, esse é o repositório com o código desenvolvido ao longo do artigo.

Fontes

Documentação do Next.js sobre Metadata
Documentação do next-intl

Top comments (0)