Peça hoje ao Cursor ou ao Claude Code para adicionar i18n a um app Next.js e você provavelmente vai receber middleware.ts, setRequestLocale em todo layout, e useTranslations chamado a partir de uma página async — uma configuração que era correta para o Next.js 15 e que já ficou uma geração defasada no Next.js 16. O globalize.now é uma infraestrutura de localização com IA, então essa é justamente a camada que acompanhamos de perto: a fiação de roteamento mudou, o catálogo que ela serve não. A seguir, o que mudou, como identificar de qual era o seu código gerado veio, e as três correções que tomam uma tarde, não uma reescrita completa.

Nada disso é uma mudança que quebra compatibilidade. E é justamente por isso que passa despercebido com facilidade.

O que mudou no Next.js 16 para i18n?

O arquivo que negocia seu locale mudou de nome. A documentação do Next.js registra a convenção do arquivo middleware como depreciada e renomeada para proxy, chegando na v16.0.0, e a explicação oficial é sobre vocabulário —

Duas observações de comportamento acompanham a renomeação na v16. O Proxy usa o runtime Node.js por padrão, e a opção de configuração por arquivo runtime não está mais disponível ali — se você definir essa opção, o Next.js lança um erro. O Proxy também não é suportado em exportação estática, o que importa se a negociação de locale é o único motivo pelo qual você ainda não está exportando.

Especificamente para o roteamento de locale, o guia de configuração do next-intl agora mostra o arquivo como src/proxy.ts e observa claramente que ele se chamava middleware.ts até o Next.js 16. O import dentro dele permanece o mesmo:

// src/proxy.ts
import createMiddleware from 'next-intl/middleware';
import {routing} from './i18n/routing';

export default createMiddleware(routing);

export const config = {
  matcher: '/((?!api|trpc|_next|_vercel|.*\\..*).*)'
};

Repare na assimetria, porque ela costuma confundir: o arquivo agora é proxy.ts, enquanto o caminho do import continua sendo next-intl/middleware. Renomear o import também é um erro comum — uma correção além da conta.

Por que o código de i18n gerado por IA sempre sai uma versão atrasado?

Porque um agente de codificação prevê o padrão mais representado, e o padrão mais representado é aquele que teve anos para se acumular. Todo post de blog, resposta no Stack Overflow e exemplo no GitHub sobre roteamento de locale no App Router escrito antes do final de 2026 usa middleware.ts. Um modelo que pesa esse corpus contra poucas semanas de documentação da v16 vai recorrer à forma antiga, com toda confiança, sem nenhum aviso de que houve uma renomeação.

É o mesmo mecanismo por trás de um problema sobre o qual já escrevemos, em que o Cursor continua adicionando strings fixas mesmo depois da configuração de i18n — o agente reproduz o código-base estatisticamente comum, não o seu. É também por isso que arquivos de instrução só resolvem parcialmente o problema no GitHub Copilot: as regras lidas pela interface de chat nem sempre são lidas pelo autocompletar inline.

A consequência prática é pontual, mas real. Sua configuração gerada funciona, então nada quebra de forma evidente. Depois você encontra um bug, pesquisa sobre ele, e todas as respostas atuais descrevem arquivos que você não tem.

Como saber de qual era é a minha configuração gerada?

Quatro greps. Rode-os a partir da raiz do projeto e em cerca de um minuto você já vai saber.

# 1. Pre-16 locale negotiation file
ls middleware.ts src/middleware.ts 2>/dev/null

# 2. Legacy static-rendering API
grep -rn "setRequestLocale" app src 2>/dev/null

# 3. Hooks called inside async components
grep -rn -B3 "useTranslations" app | grep -n "async function"

# 4. Which next-intl era the config reads from
grep -rn "root-params\|await params" src/i18n/request.ts 2>/dev/null

Resultados em 1 e 2 indicam um scaffolding anterior à v16. Um resultado em 3 é um erro de runtime genuíno, só esperando a primeira requisição que renderize esse componente. Nenhum resultado em 4 significa que sua configuração de requisição ainda lê o locale da forma antiga.

Preciso renomear middleware.ts para proxy.ts?

Não com urgência, e você não deve fazer isso manualmente. O Next.js já vem com um codemod que renomeia tanto o arquivo quanto a função exportada:

npx @next/codemod@canary middleware-to-proxy .

A renomeação é uma depreciação, não uma remoção, então um middleware.ts já existente continua funcionando. O motivo para rodar o codemod mesmo assim é o custo de manutenção: quando os nomes dos seus arquivos coincidem com a documentação atual, todo resultado de busca futuro se aplica ao seu repositório. Deixe isso desatualizado e você paga um pequeno pedágio em cada sessão de debug, para sempre.

O setRequestLocale está obsoleto no next-intl?

Ele está marcado como legado, o que é uma afirmação mais branda do que obsoleto e vale a pena ler com atenção. A documentação do next-intl descreve setRequestLocale como uma API que existiu até a introdução de next/root-params, diz que ele continua suportado por compatibilidade retroativa, e recomenda usar next/root-params em vez dele.

A forma mais recente lê o locale correspondente diretamente na sua configuração de requisição, em vez de repassá-lo manualmente por todo layout e página:

// src/i18n/request.ts
import * as rootParams from 'next/root-params';
import {notFound} from 'next/navigation';
import {getRequestConfig} from 'next-intl/server';
import {hasLocale} from 'next-intl';
import {routing} from './routing';

export default getRequestConfig(async ({locale}) => {
  if (!locale) {
    const paramValue = await rootParams.locale();
    if (hasLocale(routing.locales, paramValue)) {
      locale = paramValue;
    } else {
      notFound();
    }
  }

  return {locale};
});

next/root-params está disponível por padrão no Next.js 16.3 e versões posteriores; em versões anteriores, é preciso habilitá-lo por meio de experimental.rootParams. Siga a configuração dessa forma e a renderização estática vem de graça, desde que você continue exportando generateStaticParams para o segmento [locale].

A abordagem antiga exigia chamar setRequestLocale em todas as páginas e layouts que você queria renderizar de forma estática, antes de qualquer outra chamada do next-intl, porque o Next.js renderiza layouts e páginas de forma independente. É uma regra que um agente de IA esquece já no quinto arquivo que escreve. Ao eliminar essa exigência, elimina-se toda essa classe de bug.

Por que o useTranslations quebra no meu Server Component assíncrono?

Porque hooks não podem ser chamados a partir de componentes async, e useTranslations é um hook. Essa é uma restrição do React Server Components, não uma peculiaridade do next-intl, e a resposta do next-intl é um conjunto paralelo de funções que podem ser aguardadas (awaitable):

// Async component — await the server API
import {getTranslations} from 'next-intl/server';

export default async function ProfilePage() {
  const user = await fetchUser();
  const t = await getTranslations('ProfilePage');
  return <h1>{t('title', {username: user.name})}</h1>;
}
// Non-async component — the hook is correct here
import {useTranslations} from 'next-intl';

export default function UserDetails({user}) {
  const t = useTranslations('UserProfile');
  return <h2>{t('title')}</h2>;
}

getFormatter, getNow, getTimeZone, getMessages e getLocale seguem o mesmo padrão. Vale a pena parar no segundo exemplo: um componente não assíncrono e sem recursos interativos é um componente compartilhado, e o next-intl resolve a implementação correta dependendo se ele é renderizado no servidor ou no cliente. Então usar useTranslations em um Server Component não é um erro — chamá-lo em um componente async é que é.

Por que recebo o erro de contexto do NextIntlClientProvider?

As notas de solução de problemas do next-intl apontam duas causas, e elas pedem correções opostas. Ou o componente realmente está rodando no cliente sem nenhum provider acima dele — nesse caso, envolva-o e passe as mensagens que ele precisa —, ou ele acabou entrando no grafo de módulos do cliente quando você esperava renderização no servidor — nesse caso, passe-o via children a partir de um Server Component, em vez de importá-lo dentro de um.

O segundo caso é o que costuma acontecer em apps gerados por IA, porque os agentes aplicam 'use client' generosamente para fazer a interatividade funcionar, e a diretiva é contagiosa ao longo do grafo de imports. O padrão preferido é traduzir no servidor e repassar as strings já prontas através da fronteira:

import {useTranslations} from 'next-intl';
import Expandable from './Expandable'; // 'use client'

export default function FAQEntry() {
  const t = useTranslations('FAQEntry');
  return <Expandable title={t('title')}>{t('description')}</Expandable>;
}

Se algum componente realmente precisa de mensagens no cliente, você pode delimitar um provider apenas para essa subárvore, em vez de enviar todas as mensagens para o navegador — usar messages={null} no provider raiz não envia nenhuma.

O que tudo isso não muda?

Seus arquivos de locale. Cada correção acima é fiação de roteamento e renderização; o JSON que seu app carrega permanece intocado por tudo isso. E esse é o ponto que vale a pena levar consigo, porque a fiação é um trabalho de uma tarde, que muda uma vez por ano, enquanto o catálogo é o que se deteriora toda semana à medida que nova UI é lançada.

É essa a divisão para a qual construímos. O next-intl e seus vizinhos servem traduções em tempo de execução — não competimos com eles, e se você ainda está escolhendo entre eles, next-intl vs react-i18next vs Lingui detalha os prós e contras. O globalize.now atua uma camada acima, produzindo as chaves e os arquivos de locale que esses runtimes leem, e é por isso que a integração com Next.js é indiferente a se a sua negociação de locale vive em middleware.ts ou em proxy.ts.

A conversão é feita uma única vez e acontece dentro do app: você conecta o repositório, o globalize.now converte o código-base uma vez e abre um pull request com o catálogo. Depois disso, jobs de push traduzem as novas unidades do catálogo à medida que surgem — que é justamente o modo de falha descrito em por que os arquivos de tradução ficam constantemente fora de sincronia, e o motivo pelo qual vale a pena fechar essa lacuna antes do seu terceiro idioma, não depois.

Se o seu projeto já tem uma configuração do next-intl feita manualmente, já escrevemos exatamente sobre o que mexemos e o que não mexemos. Se ainda não tem, o passo a passo com o Cursor é o caminho mais curto — com a ressalva de que o arquivo gerado por ele leva o nome da convenção antiga, então rode o codemod depois.

Por onde começar

Rode os quatro greps. Se você obtiver resultados nos dois primeiros, rode o codemod, mova sua configuração de requisição para next/root-params, e estará atualizado. Depois, dê uma olhada nos arquivos de locale, porque essa é a parte que ainda vai continuar se desatualizando no mês que vem. Se você está lançando um app construído com IA e quer que o catálogo seja gerenciado em vez de mantido manualmente, a página para vibe coders traz a visão geral, e os preços estão em uma página própria.

O globalize.now transforma textos fixos do app em arquivos de locale prontos para tradução e os mantém atualizados a cada novo lançamento.

Experimente o globalize.now grátis