DEV Community

Cover image for Como implementar internacionalização (i18n) em um projeto Next.js sem alterar a URL
Junior
Junior

Posted on

Como implementar internacionalização (i18n) em um projeto Next.js sem alterar a URL

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

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

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

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

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

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

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

src/i18n/locales/en-US.ts

export default {
  English: 'Inglês',
  Portuguese: 'Português',
 'Page not found': 'Página não encontrada',
}
Enter fullscreen mode Exit fullscreen mode

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

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

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

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.

Por exemplo.
Imagem de como ficar as pastas

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

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

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

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.