Criando um site com múltiplos idiomas usando Next.js e next-intl
Tags: webdev, tutorial, nextjs, frontend
Published: 2023-11-08 16:16:48

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.

```bash
npx create-next-app@latest
```

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:

```bash
npm i next-intl
```

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]](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/66y1g2kneusuxrzajzah.png)

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:

```typescript
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|.*\\..*).*)']
};
```

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

```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>
  );
}
```

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:
```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."
      }
    }
  }
}
```

- en.json:
```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."
      }
    }
  }
}
```

## 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'`.

```tsx
'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
```

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:

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

Para isso:

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

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

```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>
  );
}
```

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`:**

```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`:**

```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`:**

```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`:**

```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`:**

```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`:**

```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`:**

```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>
  );
}
```

Agora que temos o componente da página do lado do servidor, podemos [exportar os metadados da página usando uma variável](https://nextjs.org/docs/app/building-your-application/optimizing/metadata#static-metadata) ou a [função generateMetadata](https://nextjs.org/docs/app/building-your-application/optimizing/metadata#dynamic-metadata).

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`:**

```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
```

## 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ijxzbivahhv5cyn30588.png)


![Página web padrão do Next.js com conteúdo em Inglês](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/izv7et3c18gsf1qv5nwq.png)

Caso queira consultar posteriormente, esse é o [repositório com o código desenvolvido ao longo do artigo](https://github.com/br-adriel/devto-next-intl-article).

## Fontes

[Documentação do Next.js sobre Metadata](https://nextjs.org/docs/app/building-your-application/optimizing/metadata)
[Documentação do next-intl](https://next-intl-docs.vercel.app/docs/getting-started/app-router-client-components)