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
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
Em seguida precisamos fazer algumas modificações na nossa estrutura de arquivos:
Criar uma pasta
messages
dentro da nossa pastasrc
, é lá onde ficarão os diferentes arquivos de idioma da nossa página. Dentro dela criaremos dois arquivos:pt.json
een.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 pastasrc
. Esse arquivo será o responsável por redirecionar as requisições do usuário para o locale correto.Criar uma pasta
[locale]
dentro da pastaapp
e mover os arquivos.tsx
para dentro dessa nova pasta. Isso permitirá que o arquivolayout.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:
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|.*\\..*).*)']
};
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>
);
}
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'
.
'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:
<h2>Docs <span>-></span></h2>
Para isso:
<h2>{t('docs.title')} <span>-></span></h2>
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')}
<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>-></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>-></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>-></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>-></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
eDeployCard
que usarão oLinkCard
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')}
<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>-></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>
);
}
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/</span><span class="p">${</span><span class="nx">params</span><span class="p">.</span><span class="nx">locale</span><span class="p">}</span><span class="s2">.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:
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)