Relatório preliminar. O universo pode ter mudado desde a coleta dos dados.
i18n end-to-end: registry de conteúdo em vez de arquivos de tradução soltos
Este site falava um idioma e tinha o outro de enfeite: o conteúdo inteiro vivia em src/content/pt-BR, e cada página fazia import fixo desse barrel. O botão de idioma traduzia o chrome — nada mais. Em três commits isso mudou para tradução de ponta a ponta: o mesmo conteúdo, em EN e PT-BR, servido pela mesma rota. Este texto registra o porquê do desenho, a armadilha concreta que o Next.js 16 nos pregou no meio do caminho, e como a paridade virou obrigação do compilador em vez de disciplina de equipe.
Por que um registry, e não arquivos de tradução soltos
A alternativa clássica — arquivos JSON de tradução por namespace, resolvidos por chave em runtime — resolve o problema de lookup e cria outro: a chave que não existe. A tradução some em silêncio, ou cai num fallback que ninguém percebe até um usuário ver a string em inglês no meio do texto em português. Não há garantia estrutural de que os dois idiomas cubram o mesmo conjunto de strings.
O tneemo.com foi pelo caminho oposto: o conteúdo é um barrel tipado por idioma, e os dois barrels satisfazem a mesma interface (ContentModule), exportada de src/content/index.ts. Um Record<Locale, ContentModule> liga cada idioma ao seu barrel. Se o barrel EN esquece um campo, tsc falha antes do build — a paridade é um erro de tipo, não uma promessa. O registry é a fonte única de verdade: páginas, projetos, arsenal, linha do tempo, entradas do laboratório e siteConfig (que carrega o pages com todo texto de UI) vivem no mesmo contrato tipado.
Isso custa mais no início (definir o contrato, tipar os dois lados) e paga em toda mudança: adicionar uma string ao PagesCopy obriga os dois idiomas a responderem, na mesma compilação.
A armadilha do Next 16: funções não cruzam a fronteira
Aqui está o erro que quase virou lenda interna. As strings de UI viviam em UiCopy, em src/lib/ui.ts, e o layout servidor passava esse objeto como prop para componentes client. O problema: UiCopy continha funções:
langSwitchAria: (target: string) => string;
countRecords: (n: number) => string;
O SiteHeader é um client component ("use client"). Todo objeto que cruza a fronteira servidor→cliente é serializado — e função não é serializável. Sintoma: erro de build do Next.js 16 apontando que a prop não pode ser transmitida a um client component. Diagnóstico: o objeto de copy parecia dado, mas carregava closures. Correção (commit 1e196c8): funções viraram dados — langSwitchLabel (string pronta) e templates com placeholder countRecordsTpl: "{n} public records", interpolados no ponto de uso. Dado cruza fronteira; código não.
A regra que ficou: copy é dado, não código. Se uma string precisa de interpolação, ela é um template; se precisa de lógica, a lógica mora no componente.
Paridade sem duplicar rotas
Antes, cada página era uma rota com texto fixo. Agora a rota é uma só — /sobre, /linha-do-tempo, qualquer uma — e o conteúdo é decidido na hora do request:
getContent()emsrc/lib/locale.tslê o cookietneemo:lang, normaliza para"pt-BR"ou"en"e devolve o barrel inteiro daquele idioma;- páginas e
generateMetadatachamamawait getContent()e tiram dalisiteConfig,labEntries,navigationetc.; - o botão de idioma escreve o cookie e chama
router.refresh()— sem trocar de rota, sem duplicar a árvore de rotas.
siteConfig.pages (tipo PagesCopy) concentra todo texto de UI por rota — hero, labels, CTA, category labels — populado nos dois idiomas. Como os dois barrels satisfazem o mesmo tipo, uma string nova não traduzida em EN não compila. O cookie escolhe o idioma; o tipo garante que ambos existem.
O resultado: o mesmo conteúdo em dois idiomas, uma rota por página, e a paridade mantida por tsc, não por checklist. Se a fronteira servidor→cliente reaparecer em algum componente novo, a regra é a mesma: dado atravessa, função não.