Neste artigo, vou mostrar de forma objetiva e direta como implementar a internacionalização (i18n) em um projeto Next.js utilizando o next-intl
, sem vincular os idiomas às rotas da URL — ou seja, nada de caminhos como example.com/en
. Essa abordagem evita a necessidade de tratamentos extras caso o usuário altere a URL manualmente.
Utilizaremos um cookie para identificar e armazenar o idioma selecionado pelo usuário. Esse cookie será definido automaticamente no primeiro acesso ao site. Caso o usuário deseje alterar o idioma depois, será possível fazer essa mudança pela plataforma desenvolvida.
Configuração inicial
Caso ainda não tenha um projeto Next.js criado, você pode seguir este tutorial: Como configurar um novo projeto Next.js.
Agora, instale o pacote next-intl
, que nos ajudará a configurar o i18n.
npm install next-intl
Vamos estruturar as pastas do projeto:
├── src
│ ├── app
│ │ ├── layout.tsx
│ │ └── page.tsx
│ ├── components
│ ├── hooks
│ └── i18n
│ ├── locales
│ │ ├── en-US.ts
│ │ └── pt-BR.ts
│ ├── config.ts
│ ├── locale.ts
│ └── request.ts
│ ├── services
│ └── styles
Hoje vamos focar apenas na pasta i18n
. Em um próximo artigo, falarei mais sobre organização de pastas.
1 - Configurando o arquivo layout.tsx
(ponto de entrada)
src/app/layout.tsx
import { Metadata } from 'next'
import { NextIntlClientProvider } from 'next-intl'
import { getLocale, getMessages } from 'next-intl/server'
import { ReactNode } from 'react'
export const metadata: Metadata = {
title: 'NextJS',
description: 'Site do NextJS'
}
async function RootLayout({ children }: { children: ReactNode }) {
const locale = await getLocale()
const messages = await getMessages()
return (
<html lang={locale}>
<body>
<NextIntlClientProvider messages={messages}>
{children}
</NextIntlClientProvider>
</body>
</html>
)
}
Aqui, configuramos o NextIntlClientProvider
, que recebe as mensagens do getMessages()
e o idioma atual com getLocale()
que ira para a tag html, ambos importados do next-intl/server
.
2 - Configurando a pasta i18n
Essa estrutura serve para guardar todas as configurações relacionadas ao uso do i18n no projeto.
2.1 - Arquivo de config.ts
src/i18n/config.ts
export type LocaleProps = (typeof locales)[number]
export const locales = ['en-US', 'pt-BR'] as const
export const defaultLocale: LocaleProps = 'en-US'
Aqui exportamos as tipagens das idiomas que vamos usar no projeto.
2.2 - Arquivo de locale.ts
src/i18n/locale.ts
'use server'
import { cookies } from 'next/headers'
import { defaultLocale, LocaleProps } from './config'
const COOKIE_NAME = `${process.env.NEXT_PUBLIC_PROJECT_NAME}-i18n`
export async function getUserLocale() {
return (await cookies()).get(COOKIE_NAME)?.value || defaultLocale
}
export async function setUserLocale(locale: LocaleProps) {
;(await cookies()).set(COOKIE_NAME, locale)
}
Esse arquivo é feito pra rodar no server, aqui gerenciamos todo buscar e alteração do cookie, ficar por sua escolha colocar o nome do projeto ou não para compor como vai ser chamado no cookie.
2.3 - Arquivo de request.ts
src/i18n/request.ts
import {
useLocale as useNextIntlLocale,
useTranslations as useNextIntlTranslations
} from 'next-intl'
import { getRequestConfig, setRequestLocale } from 'next-intl/server'
import { getUserLocale } from './locale'
export default getRequestConfig(async () => {
const locale = await getUserLocale()
return {
locale,
messages: (await import(`./locales/${locale}.ts`)).default
}
})
export function setLocale(locale: 'pt-BR' | 'en-US') {
setRequestLocale(locale)
}
export function useLocale() {
const locale = useNextIntlLocale()
return locale
}
export function useTranslations() {
const t = useNextIntlTranslations()
return t as (key: string) => string
}
O arquivo request
deixou para buscar e montagem do json dos locales(Que vem da pasta locales que vou explicar logo abaixo) um wrapper
de atualizar a linguagem, buscar a que está ativa no momento e também um para tradução. Esse formato ajuda caso um dia chegue a trocar a lib next-intl
, só preciso trocar nessas funções e todo o projeto continuará funcionando.
2.4 - Pasta locales
Essa pasta contem os arquivos que guardam todos as traduções do nosso projeto.
Por exemplo:
export default {
English: 'English',
Portuguese: 'Portuguese',
'Page not found': 'Page not found',
}
src/i18n/locales/en-US.ts
export default {
English: 'Inglês',
Portuguese: 'Português',
'Page not found': 'Página não encontrada',
}
src/i18n/locales/pt-BR.ts
Um detalhe: utilizo as chaves das traduções como o próprio texto (no padrão inglês). Isso facilita o fallback quando há erro na biblioteca de tradução — ao menos o sistema exibe o texto em inglês. Além disso, evita duplicação e facilita a leitura e manutenção do JSON, especialmente em sistemas grandes.
Exemplo de uso normal.
export default {
notFound: 'Página não encontrada',
pageNotFound: 'Página não encontrada',
}
Isso é fácil de gerenciar em arquivos pequenos, mas vira um grande desafio em projetos grandes.
Exemplo de uso 1
import { useTranslations } from 'next-intl'
export default function HomePage() {
const t = useTranslations()
return (
<div>
<h1>{t('title')}</h1>
<h1>{t('subtitle')}</h1>
</div>
)
}
Exemplo de uso 2
import { useTranslations } from 'next-intl'
export default function HomePage() {
const t = useTranslations()
return (
<div>
<h1>{t('Home page')}</h1>
<h1>{t('Home page subtitle')}</h1>
</div>
)
}
Eu particularmente prefiro o Exemplo de uso 1.
Atenção: O next-intl pode gerar erro se a chave tiver um ponto no final, tipo 'Home page.'. Isso não acontece com a lib react-i18next (tema para um próximo artigo 😄).
Um outro ponto que gosto de fazer com essa arquitetura de organização é separar os json de tradução do projeto.
Nesse exemplo temos duas pasta de i18n uma em cada modulo(Padrão de modulo consistem em centralizar tudo em seus modulos como o de Auth, Home, User, Profile, Company para facilitar uma manutenção e merge posso trabalhar esse ponto em artigos futuro 😄).
Agora vamos ver como ficaria o nosso arquivo de locales.
import Auth from '@/app/(auth)/i18n/en-US'
import Dashboard from '@/app/(dashoard)/i18n/en-US'
export default {
English: 'English',
'Page not found': 'Page not found',
...Auth,
...Dashboard
}
Com o uso do spread, mantemos os arquivos menores e organizados. E como usamos o próprio texto como chave, evitamos conflitos entre módulos.
Vamos usar o que construímos
Para usar em nosso projeto tem algumas abordagem a mais simples seria assim.
import { useTranslations } from 'next-intl'
export default function HomePage() {
const t = useTranslations()
return (
<div>
<h1>{t('Home page')}</h1>
<h1>{t('Home page subtitle')}</h1>
</div>
)
}
Porém esse formato você já viu, vamos melhorar ele?
import { useTranslations } from 'next-intl'
interface TypographyProps {
text: string
className?: string
length?: number
variant?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'p' | 'span' | 'strong'
}
function Typography({ variant, text, className, length }: TypographyProps) {
const Component = variant || 'span'
const t = useTranslations()
const textTranslation = t(text)
return (
<Component className={className}>
{length && textTranslation.length > length ? (
<>{textTranslation.substring(0, length)}...</>
) : (
textTranslation
)}
</Component>
)
}
export { Typography }
Aqui criamos um componente Typography
que ele pode ser qualquer uma das tags html que ele recebe como prop, e o mesmo também recebe uma prop text
que já vai ser traduzida no próprio componente, além disso, pode passar uma lenght
para um limite no texto a ser exibido. Assim não precisa em todo arquivo do seu projeto ficar importando o useTranslations()
esse mesmo formato pode se estender para label de input e etc.
Conclusão
Existem várias formas de organizar e aplicar i18n no seu projeto. Neste artigo, compartilhei um modelo que gosto e acho bastante produtivo. Ele pode não ser o "melhor", mas é funcional, escalável e fácil de manter. Sinta-se à vontade para adaptá-lo e evoluí-lo conforme as necessidades dos seus projetos.
Top comments (0)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.