Bilíngue desde o schema — por que i18n não pode ser um plugin
Bilíngue desde o schema
Há duas formas de um software falar dois idiomas, e elas são tão diferentes que mereciam nomes distintos.
A primeira é a tradução de interface: os botões e rótulos mudam de idioma, mas os dados — os posts, os emails, as páginas — continuam num idioma só. A segunda é o bilinguismo de conteúdo: o sistema entende que o mesmo post, o mesmo email, a mesma landing page existem em versões por idioma, e trata cada versão como um cidadão de primeira classe.
A maioria das ferramentas faz a primeira e chama de "suporte a i18n". A LaunchWithAgency faz a segunda, e a faz no lugar onde ela de fato precisa ser feita: no schema do banco. Este artigo é sobre por que essa diferença não é um detalhe, e por que i18n adicionado depois nunca alcança i18n projetado desde o início.
O sintoma: a coluna locale
Abra qualquer uma das tabelas centrais da suíte e você encontra a mesma coluna. Em blog_posts: locale, com default pt. Em email_templates: locale, parte da identidade do template. Em landing_pages: locale, e — repare — a restrição de unicidade da tabela é sobre (ownerId, slug, locale), não só (ownerId, slug). Em newsletter_subscribers: locale, guardando o idioma de cada assinante individual.
Essa coluna repetida não é um acaso. Ela é uma decisão tomada uma vez, na fundação, e mantida com disciplina por todo o schema: idioma é um atributo dos dados, não uma configuração da conta.
A diferença é enorme. Se o idioma fosse uma configuração da conta — "esta conta é em português" — então o conteúdo seria monolíngue por construção, e falar inglês exigiria uma segunda conta, ou um plugin, ou um truque. Como o idioma é uma coluna na linha, cada post, cada template, cada página carrega o próprio idioma. PT e EN não competem por um espaço de configuração; eles coexistem como linhas vizinhas na mesma tabela.
Como duas versões compartilham uma identidade
O detalhe mais elegante dessa arquitetura é como ela liga as duas versões de um mesmo conteúdo sem fundi-las.
Pegue um post de blog bilíngue. Ele é, fisicamente, duas linhas em blog_posts: uma com locale: pt, outra com locale: en. São dois corpos de texto, dois títulos, duas descrições de SEO — porque traduzir não é copiar, é reescrever, e cada idioma merece o próprio texto trabalhado.
Mas as duas linhas compartilham o mesmo slug. O slug é a identidade conceitual do post — "este é o artigo sobre X" — e o locale é a variante. A unicidade no banco é garantida sobre a combinação dos dois: você pode ter meu-artigo/pt e meu-artigo/en convivendo, e o sistema sabe que são o mesmo artigo em dois idiomas. É exatamente isso que permite servir o conteúdo em /blogs/meu-artigo para um visitante em português e /en/blogs/meu-artigo para um em inglês, com a URL espelhando a estrutura e o leitor podendo trocar de idioma sem se perder.
> Duas linhas, um slug. A tradução não é uma cópia escondida nem um campo extra — é uma irmã da linha original, com a mesma identidade e o próprio texto.
O blog deste site é a prova viva: cada post é um par de arquivos Markdown — nome.md em português, nome.en.md em inglês — ambos declarando o mesmo slug no frontmatter. O arquivo que você está lendo tem uma irmã em inglês. A convenção de arquivos espelha exatamente a convenção do banco.
Por que i18n não pode ser um plugin
Aqui está a tese central, e ela é uma afirmação sobre o momento da decisão.
Quando um software nasce monolíngue, o idioma é uma suposição que se infiltra em todo lugar: nas chaves primárias, nas URLs, nos índices, nas consultas, nas regras de unicidade, na lógica de "buscar o post tal". Cada uma dessas suposições foi escrita assumindo que existe um conteúdo por slug. Adicionar um segundo idioma depois significa caçar e revisar cada uma dessas suposições — e basta uma escapar para o bilinguismo vazar: dois idiomas brigando pelo mesmo slug, uma URL que não sabe qual versão servir, um índice que trata as traduções como duplicatas.
É por isso que i18n adicionado depois quase sempre tem cara de remendo. Não é incompetência de quem remenda — é que a suposição monolíngue está espalhada demais para ser totalmente arrancada. O plugin de tradução fica eternamente lutando contra uma fundação que assume um idioma só.
A LaunchWithAgency evita essa luta não a vencendo, mas não a começando. A coluna locale está no schema desde a primeira migration. As regras de unicidade já incluem o idioma. As URLs já têm o prefixo /en. Nenhuma consulta jamais assumiu um conteúdo por slug. O bilinguismo não é um recurso que a suíte tem — é uma propriedade que a suíte é, porque a fundação foi construída sabendo dele.
Por que isso importa para quem opera
Esta não é uma curiosidade de engenharia. É uma capacidade de negócio.
Uma agência que atende clientes em mais de um país, um time de produto com mercado na América Latina e nos Estados Unidos, um fundador que quer publicar em português e alcançar leitores em inglês — todos eles precisam que o conteúdo seja genuinamente bilíngue, não que a interface tenha um seletor de idioma. Eles precisam que o post tenha uma versão em inglês com SEO próprio, que o email de boas-vindas saia no idioma do assinante, que a landing page exista nas duas línguas com URLs limpas.
A LaunchWithAgency entrega isso sem plugin, sem segunda conta, sem truque — porque a decisão foi tomada no único lugar onde i18n pode ser tomado de verdade: no schema, antes da primeira linha de conteúdo existir. Operação séria atravessa fronteiras. Um software sério precisa nascer sabendo disso.