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.