O arquivo translation.json começa pequeno. Depois, cada tela acrescenta suas próprias chaves, dois componentes usam title para coisas diferentes e ninguém sabe se checkout.payment.failed ainda pertence ao checkout ou a uma regra compartilhada.

Quando isso acontece, o problema não é o react-i18next sozinho. É a falta de uma fronteira de propriedade para as mensagens. Em uma aplicação grande, organize as traduções por funcionalidade, mantenha os nomes estáveis e faça o TypeScript e o CI conferirem o contrato.

Este é um recorte da visão geral de internacionalização em React e no backend. Aqui o foco é mais estreito: como decidir onde uma chave vive e como descobrir cedo que uma mudança quebrou o catálogo.

Diagrama mostra uma aplicação React organizando chaves de tradução por funcionalidade, namespace e verificação no CI.

Resposta curta

  • Use um namespace por funcionalidade ou área do produto, não um arquivo gigante por idioma.
  • Prefira chaves semânticas e estáveis, como checkout.payment.failed, em vez da frase em inglês.
  • Derive os tipos do locale-fonte e use keyPrefix para reduzir caminhos repetidos no componente.
  • Verifique chaves ausentes, strings hardcoded e tipos gerados antes do merge.

Por que um único arquivo de tradução começa a falhar?

O problema de um catálogo único aparece em três lugares: propriedade, carregamento e revisão. A documentação do react-i18next recomenda separar arquivos de tradução em namespaces e carregá-los quando a tela precisa deles (react-i18next, "Multiple Translation Files").

Um arquivo por idioma parece simples enquanto o produto tem poucas telas. Com o crescimento, a lista mistura navegação, cobrança, onboarding e mensagens de erro. O nome da chave deixa de mostrar quem pode alterá-la, e uma revisão pequena passa a tocar em um arquivo que várias equipes editam.

Namespaces também são uma fronteira de carregamento. O hook useTranslation pode receber um namespace, e o runtime pode carregar apenas esse conjunto quando a rota é renderizada. Isso não obriga todo projeto a fazer lazy loading, mas deixa a decisão explícita em vez de amarrar cada idioma a um pacote monolítico.

O ponto não é criar dezenas de arquivos porque a pasta parece organizada. Um namespace precisa representar uma área que tem dono, ciclo de mudança e contexto próprio. Se duas telas sempre mudam juntas e compartilham o mesmo vocabulário, talvez pertençam ao mesmo namespace.

Como escolher namespaces por funcionalidade?

Comece pela tela ou pelo fluxo que o usuário reconhece. checkout, account e reports são fronteiras mais úteis do que buttons, labels e messages, porque ligam o catálogo ao trabalho de uma equipe e à parte do produto que o usuário está usando.

Uma estrutura simples pode ser esta:

src/locales/
├── en-US/
│   ├── common.json
│   ├── checkout.json
│   └── account.json
├── pt-BR/
│   ├── common.json
│   ├── checkout.json
│   └── account.json
└── es/
    ├── common.json
    ├── checkout.json
    └── account.json

common deve ser pequeno. Coloque nele termos realmente compartilhados, como ações do shell ou estados usados em várias áreas. Se cada nova tela joga texto em common, você apenas trocou um arquivo gigante por um namespace gigante.

Dentro de checkout.json, agrupe as mensagens por contexto de uso:

{
  "summary": {
    "title": "Revise seu pedido",
    "total": "Total",
    "submit": "Confirmar pedido"
  },
  "payment": {
    "failed": "Não foi possível confirmar o pagamento",
    "retry": "Tentar novamente"
  }
}

O namespace diz qual funcionalidade é dona da chave. O caminho interno explica a tela ou o estado. Essa combinação é mais fácil de procurar do que um catálogo em que error, title e button aparecem dezenas de vezes.

Chaves semânticas ou frases como chave: qual escolher?

Escolha um estilo e registre a escolha. O i18next usa por padrão uma notação baseada em chaves e também documenta chaves em linguagem natural como alternativa (i18next, "Getting started").

Para uma aplicação mantida por várias pessoas, chaves semânticas costumam separar o texto exibido da identidade usada pelo código. payment.failed continua apontando para o mesmo conceito quando a frase muda, e o tradutor pode receber uma mensagem diferente sem exigir uma busca pelo texto antigo.

Isso não significa transformar cada frase em uma taxonomia profunda. Evite caminhos que descrevem toda a árvore de componentes, como pages.checkout.components.payment.form.card.error. A estrutura deve sobreviver a uma refatoração visual.

Também evite chaves vagas. title dentro de checkout.summary é legível. title em common quase nunca é. Se uma mensagem tem interpolação, plural ou contexto, deixe essa informação explícita no recurso e teste os casos que mudam a frase entre idiomas.

Como dar tipagem às chaves com TypeScript?

O i18next pode inferir chaves e valores a partir de um objeto de recursos e expor esse formato por module augmentation. A documentação oficial mostra a configuração de CustomTypeOptions, resources, defaultNS e keyPrefix (i18next, "TypeScript").

Use um locale como fonte do contrato. Os outros idiomas precisam acompanhar suas chaves, mas não devem redefinir a forma que o código conhece. Um exemplo ilustrativo:

// src/i18n/resources.ts
export const resources = {
  "en-US": {
    common: {
      cancel: "Cancel",
    },
    checkout: {
      summary: {
        title: "Review your order",
        submit: "Place order",
      },
    },
  },
} as const;

export default resources;
// src/@types/i18next.d.ts
import "i18next";
import resources from "../i18n/resources";

declare module "i18next" {
  interface CustomTypeOptions {
    defaultNS: "common";
    resources: (typeof resources)["en-US"];
  }
}

No componente, o namespace e o prefixo reduzem o caminho que cada chamada precisa repetir:

import { useTranslation } from "react-i18next";

export function OrderSummary() {
  const { t } = useTranslation("checkout", { keyPrefix: "summary" });

  return (
    <section aria-labelledby="order-summary-title">
      <h2 id="order-summary-title">{t("title")}</h2>
      <button type="submit">{t("submit")}</button>
    </section>
  );
}

Esse exemplo é ilustrativo. O nome do locale-fonte, o carregamento dos recursos e a configuração do bundler dependem da aplicação. Antes de adotar a tipagem, execute tsc --noEmit no projeto e confirme que uma chave inexistente falha no mesmo lugar em que o código seria revisado.

Se o catálogo for grande, a documentação atual do i18next também descreve enableSelector: "optimize" para consultas com selector e conjuntos extensos de tradução. Essa opção muda a API que o time usa, então trate-a como uma decisão de migração, não como uma linha para copiar sem medir o impacto no código.

Como manter os idiomas sincronizados?

Tipar o locale-fonte não prova que pt-BR e es têm todas as chaves. A verificação precisa comparar os recursos e também localizar strings que escaparam do fluxo de tradução.

O i18next-cli, mantido pelo mesmo projeto, reúne extração de chaves, geração de tipos, sincronização de locales e linting (i18next, "i18next-cli"). Um pipeline inicial pode ser:

npx i18next-cli status
npx i18next-cli extract --ci
npx i18next-cli types --ci
npx tsc --noEmit

O comando status ajuda a enxergar a cobertura por idioma e namespace. extract --ci pode falhar quando os arquivos não refletem as chaves encontradas no código. types --ci evita que as definições geradas fiquem desatualizadas. O tsc fecha a parte que depende do uso tipado no componente.

Há um limite importante: análise estática não entende toda chave construída dinamicamente, como t(`error.${code}`). O README do CLI lista esse caso entre as limitações do relatório de chaves não usadas (i18next, "i18next-cli"). Para esses fluxos, use um mapa explícito de chaves permitidas ou um teste que percorra os códigos válidos.

Não bloqueie o merge porque uma tradução editorial ainda está pendente se o produto tem um estado de fallback aceitável. Bloqueie quando a aplicação perdeu a chave, o namespace não carrega, a interpolação não bate ou uma tela introduziu texto visível fora do contrato. São falhas diferentes e merecem mensagens diferentes no CI.

Fluxo mostra uma chave saindo do componente, passando pelo namespace e chegando ao CI que verifica tipos e idiomas.

O que verificar antes de aceitar uma nova chave?

Uma revisão de tradução precisa responder duas perguntas: a chave está no lugar certo e o comportamento continua correto em cada locale? O i18next descreve namespaces como agrupamentos lógicos e explica a ordem de resolução entre idioma, namespace e fallback (i18next, "Translation Resolution").

Use esta lista para uma mudança de produto:

  1. Propriedade: a chave pertence a uma funcionalidade concreta? Se é usada em duas áreas, há uma razão para ser compartilhada?
  2. Nome: o nome descreve o conceito e continua válido se o componente mudar de lugar?
  3. Parâmetros: interpolação, plural e contexto têm testes para os valores que mudam a frase?
  4. Carga: a tela pede o namespace correto e trata o estado enquanto ele ainda não chegou?
  5. Paridade: todos os idiomas esperados têm a mesma chave ou uma exceção documentada?
  6. Layout: uma tradução mais longa cabe no botão, no menu e no mobile?
  7. Remoção: quando a funcionalidade some, a chave antiga também sai ou fica registrada como dívida?

Esse checklist é curto de propósito. Ele liga a revisão do JSON ao comportamento que o usuário vê. Para fluxos de tela, um teste de navegador também ajuda: separe o mock de API do teste de contrato no Playwright e use a navegação real para conferir idioma, fallback e conteúdo visível.

Quando a estrutura precisa mudar?

Não reorganize todos os arquivos só porque o produto cresceu. Mude a estrutura quando uma equipe não consegue descobrir a dona de uma chave, quando o carregamento exige enviar catálogos de telas não usadas ou quando a verificação de paridade demora mais que a própria alteração.

Uma migração segura começa com um namespace novo e uma fronteira clara. Mova uma funcionalidade por vez, deixe um alias temporário apenas quando a compatibilidade for necessária e remova o caminho antigo depois que os consumidores forem atualizados. A documentação do i18next oferece keyPrefix e getFixedT para reduzir repetição sem esconder de onde a chave veio (i18next, "API").

Se a equipe não consegue explicar a diferença entre common, shared e global, não crie outro desses nomes. Dê um exemplo concreto, registre o dono e faça a próxima revisão usar a regra. Uma convenção pequena e aplicada vale mais que uma árvore perfeita que ninguém respeita.

Perguntas frequentes

Devo criar um namespace para cada componente React?

Não por padrão. Um componente reutilizável pode ter texto próprio, mas o namespace deve acompanhar uma unidade de propriedade e carregamento. Criar um arquivo por componente aumenta a coordenação e pode espalhar uma frase que deveria ser entendida como um fluxo. Comece por funcionalidade, depois divida quando o tamanho ou o carregamento justificar.

Chaves semânticas são melhores que usar a frase em inglês?

Não existe uma escolha universal. Chaves semânticas separam texto e identidade, o que ajuda quando a copy muda e quando várias pessoas mantêm o catálogo. Chaves naturais podem ser mais rápidas em um projeto pequeno. O risco aparece ao misturar estilos ou depender de uma frase como identificador estável.

TypeScript garante que todos os idiomas têm a tradução?

Não sozinho. A tipagem pode conferir o recurso que o código conhece, mas a paridade entre arquivos de idioma precisa de extração, status, um teste próprio ou outra etapa do pipeline. Mantenha essas duas verificações separadas para saber se falhou o uso da chave ou a entrega de uma tradução.

Conclusão

Em uma aplicação grande, chaves de tradução são parte da arquitetura do produto. Dê a cada funcionalidade um namespace compreensível, use nomes que sobrevivam a refatorações e escolha um locale-fonte para alimentar o contrato TypeScript.

Depois, automatize o que é fácil esquecer: extração, paridade, tipos e strings hardcoded. O objetivo não é produzir mais arquivos. É permitir que uma pessoa encontre a mensagem, entenda quem a mantém e descubra uma quebra antes que o usuário descubra primeiro.

Fontes consultadas