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:
- No gerador:
localize(merged, "pt")elocalize(merged, "en")foram comparados por deep-equal contra os dois arrays originais, ordem incluída. - Independente: os arquivos legados foram extraídos do
git HEADe comparados porJSON.stringifycontra a saída das novas fachadas — idêntico byte a byte, inclusive na ordem das chaves. - 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.