Voltar para as notas

Uma fonte bilíngue para 26 estudos de caso

Os 26 estudos de caso deste portfólio viviam em dois arquivos espelhados à mão: projects.ts (3.358 linhas, PT) e projects.en.ts (3.348 linhas, EN). Toda edição precisava ser feita duas vezes, e nada garantia que as duas cópias continuassem estruturalmente iguais. Esta nota documenta a unificação em uma fonte bilíngue única — e como provar que a migração não perdeu um byte.

O diagnóstico

Antes de mexer, um script comparou as duas árvores estruturalmente. O resultado confirmou o risco: 1.616 folhas diferiam entre os idiomas (textos traduzidos), 485 eram idênticas (slugs, URLs, stack) — e havia um drift real: um projeto tinha 14 keywords de SEO em PT e apenas 12 em EN. Exatamente o tipo de divergência silenciosa que a duplicação manual produz.

O formato unificado

O merge gerou uma fonte única onde cada folha divergente vira um par { pt, en } e cada valor idêntico é armazenado uma vez:

tagline: {
  pt: "BI operacional para SLA, backlog e incidentes.",
  en: "Operational BI for SLA, backlog, and incidents.",
},
stack: ["Power BI", "SQL", "DAX"],

Uma função localizeProject projeta a árvore para o idioma pedido, e os dois arquivos antigos viraram fachadas de 12 linhas — os consumidores (sitemap, página de projeto) não mudaram uma linha.

A prova de que nada se perdeu

Migração de conteúdo à mão não se revisa no olho. A verificação teve três camadas:

  1. No gerador: localize(merged, "pt") e localize(merged, "en") foram comparados por deep-equal contra os dois arrays originais, ordem incluída.
  2. Independente: os arquivos legados foram extraídos do git HEAD e comparados por JSON.stringify contra a saída das novas fachadas — idêntico byte a byte, inclusive na ordem das chaves.
  3. Permanente: testes de invariantes agora travam o formato — slugs únicos por idioma, paridade de contagem, nenhuma folha bilíngue vazia e resolução cruzada de slugs entre locales.

O detalhe de tipos que quase passou

O tipo Bilingual<Project> precisou ser distributivo: campos opcionais (string | undefined) e uniões discriminadas (kind: "video" | "embed") quebravam a versão ingênua do tipo mapeado. A regra que funcionou: apenas string puro pode virar par { pt, en }; literais de união permanecem intactos para o TypeScript continuar discriminando as variantes.

Drift estrutural entre idiomas agora é impossível por construção — as traduções vivem lado a lado, e o compilador valida o arquivo inteiro.