
← Voltar ao blog
Como uma pessoa mantém sete localidades sem editar manualmente cada arquivo JSON e quais mercados de contratação tornaram valer a pena construir.
Um portfólio apenas em inglês é uma escolha válida. Da mesma forma, enviar sete localidades quando você é uma pessoa mantendo o repositório: não é “gratuito”, mas é legível se você tratar o idioma como escopo do produto, não como uma caixa de seleção, e automatizar as partes chatas.
Este post é a versão longa: quais idiomas, por que eu os escolhi e exatamente o que o código faz (packages/types, packages/i18n, scripts/translate.mjs, apps/web/scripts/translate-blog.mjs).
Eu não escolhi localidades porque um modelo veio com elas. Eu não otimizei para “maior número de falantes em todo o mundo” como um único número. Eu comecei pela geografia de contratação: olhei para países e regiões onde os desenvolvedores são contratados (empregadores remotos, mercados de tecnologia domésticos fortes, centros da UE) e perguntei quais idiomas eu poderia realisticamente enviar para que esses leitores pudessem usar o site em sua língua materna, não apenas em inglês. Essa é uma escolha de produto sobre quem pode avaliar esse portfólio, não decoração.
Inglês como padrão ainda é não negociável: é o denominador comum mais amplo para tecnologia. As seis localidades não inglesas neste repositório são português, espanhol, alemão, polonês, holandês e estoniano, listadas em LOCALE_CONFIG em packages/types/src/index.ts. Juntamente com en, isso são sete localidades de superfície.
A intenção é direta: se alguém que pode contratar ou colaborar comigo chega aqui de Brasil ou Portugal, Espanha ou América Latina, Alemanha, Áustria ou Suíça, Polônia, Holanda ou Estônia, eu quero que leia em um idioma que corresponda a como eles realmente leem quando é importante, enquanto ainda aceitando que um es cobre muitos países e um pt cobre dois mercados muito diferentes. Isso é um compromisso, não ignorância. Um código de localidade não pode cobrir todos os registros culturais desses idiomas, e eu não estou fingindo que possa.
Cada frase soa como se um tradutor humano gastasse uma hora nela? Não. Tradução automática e lotes assistidos por LLM fazem parte da pilha. A promessa é mais estreita e, acho, mais honesta: cobertura deliberada, diferenças revisáveis no git e cópia que não é silenciosamente apenas em inglês para leitores que preferem outro idioma materno.
Tudo o que precisa saber “quais idiomas existem” deve derivar de LOCALE_CONFIG: chaves en, pt, es, de, pl, nl, et, cada uma com name, bcp47, e um código franc para helpers de detecção de idioma. O pacote i18n re-exporta essa configuração e constrói locales como Object.keys(LOCALE_CONFIG); veja packages/i18n/src/index.ts.
Dicionários de tempo de execução são JSON estáticos por localidade em packages/i18n/src/messages/, importados por meio de um mapeador de importação para que os empacotadores possam resolver cada arquivo. O helper htmlLangFromLocale mapeia uma localidade para uma tag BCP 47 para <html lang>. Essa é a história de “fonte única de verdade”: tipos + arquivos JSON + aplicativo Next todos alinhados na mesma enumeração de localidades.
Se você adicionar uma localidade, você estende LOCALE_CONFIG, adiciona um novo arquivo JSON e conecta o importador, então executa o pipeline de tradução. Não há lista de tempo de execução oculta em outro lugar que possa divergir.
// packages/types/src/index.ts
export const LOCALE_CONFIG = {
en: { name: "English", bcp47: "en", franc: "eng" },
pt: { name: "Português", bcp47: "pt", franc: "por" },
es: { name: "Español", bcp47: "es", franc: "spa" },
de: { name: "Deutsch", bcp47: "de", franc: "deu" },
pl: { name: "Polski", bcp47: "pl", franc: "pol" },
nl: { name: "Nederlands", bcp47: "nl", franc: "nld" },
et: { name: "Eesti", bcp47: "et", franc: "est" },
} as const;
export type Locale = keyof typeof LOCALE_CONFIG;
en.json, arquivo de bloqueio, MT/LLM híbridoA cópia de UI autoritativa vive em packages/i18n/src/messages/en.json. Outros idiomas são gerados; a regra do projeto é explícita: edite apenas o inglês, então execute pnpm translate a partir da raiz do repositório, que executa scripts/translate.mjs com o ambiente carregado de apps/web/.env.local (veja o script package.json raiz).
translate.mjs é longo por bons motivos. Ele mantém scripts/translate.lock.json: hashes por localidade de seções da fonte em inglês para que quando você alterar uma parte de en.json, você não envie cegamente todo o dicionário para uma API. Isso importa quando en.json cresce (sobre páginas, projetos, experiência) e você se importa com custo e repetibilidade.
O script também remove chaves que desapareceram do inglês para que os arquivos de localidade não acumulem ramos órfãos, e pode remover termos conhecidos não traduzíveis (nomes próprios, marcas de tecnologia) antes de uma solicitação, então os mescla de volta; veja o conjunto DO_NOT_TRANSLATE e helpers relacionados no arquivo.
A escolha de ferramentas foi impulsionada por custo e ajuste. LibreTranslate é auto-hospedável via Docker sem custo por token, o que o torna o ajuste certo para strings curtas e informativas como nav, meta e errors; essas chaves não precisam de nuances de nível LLM, e pagar por caractere para DeepL ou Google Translate para elas se somaria sem adicionar qualidade. Groq lida com o caminho LLM porque sua inferência é rápida e barata para trabalho em lote em comparação com OpenAI ou Anthropic no mesmo volume; o modelo padrão TRANSLATE_MODEL é um modelo pequeno (llama-4-scout) selecionado especificamente para manter o gasto por execução baixo, não o mesmo modelo usado para o chat.
O script não envia tudo para Groq. Um objeto CONFIG codifica a política real:
groqKeyPatterns (por exemplo, about.sections.*.body, home.hero.subheadline, projects.items.*.problem, experience.items.*.impact) passam por Groq para cada localidade via shouldUseGroqForKey. Títulos de emprego (experience.items.*.role) também são sempre Groq, porque a tradução automática pura geralmente retorna títulos com aparência inglesa inalterados.libreKeyPrefixes (por exemplo, meta, nav, errors, experience.labels), com LIBRETRANSLATE_URL padrão para um host local amigável ao Docker, a menos que você o substitua.Então: cada localidade obtém qualidade LLM em campos narrativos; LibreTranslate lida com as chaves informativas onde a tradução automática é precisa o suficiente. Isso é um compromisso de custo e qualidade incorporado no código, não um comentário em um README.
// scripts/translate.mjs
function shouldUseGroqForKey(localeCode, key) {
return (
matchesDotPattern(key, "experience.items.*.role") ||
CONFIG.groqKeyPatterns.some((pat) => matchesDotPattern(key, pat))
);
}
As duas chaves Groq são intencionalmente separadas. GROQ_API_KEY vive na produção e serve o chat do portfólio em tempo de execução; execuções de lote de tradução que aumentam o uso de tokens comeria na mesma taxa limite e afetaria os usuários reais. TRANSLATE_GROQ_API_KEY é uma chave diferente usada apenas em execuções de lote locais, com sua própria cota. O fallback para GROQ_API_KEY existe para conveniência quando nenhuma chave dedicada é configurada, não como a configuração pretendida.
TRANSLATE_MODEL escolhe o modelo para tradução em lote (padrão meta-llama/llama-4-scout-17b-16e-instruct no cabeçalho do script), independente de GROQ_MODEL usado pelo chat. O padrão é deliberadamente um modelo pequeno e rápido para manter o gasto por execução baixo. Há controles para tamanho do lote, repetições de 429 e registro; leia o bloco de comentários no início do arquivo em scripts/translate.mjs antes de ajustá-lo.
O uso de API para Groq (e hospedagem de LibreTranslate se você executá-lo localmente) é dinheiro real. A vitória aqui é controle e automação, não mágica. O arquivo de bloqueio é o que torna isso gerenciável: seções que não mudaram em inglês não vão para nenhuma API na próxima execução.
Posts de blog não são traduzidos com o mesmo roteador híbrido. pnpm translate:blog executa apps/web/scripts/translate-blog.mjs, que lê apps/web/content/blog/en/*.mdx e, para cada localidade de destino em TARGET_LOCALES (pt, es, de, pl, nl, et), chama Groq com um prompt estrito: preserve a estrutura do frontmatter YAML, mantenha date e hero idênticos à fonte em inglês, traduza título, descrição, tags e corpo, não estrague URLs. Arquivos de saída ficam em apps/web/content/blog/<locale>/.
Não há caminho LibreTranslate nesse script hoje; a tradução de blog é lote LLM por arquivo, com --locale, --file, --force e --delay para reduzir a dor do limite de taxa. No tempo de leitura, Next apenas carrega o MDX para a localidade da rota; nenhuma chamada Groq acontece quando um visitante abre uma página de blog.
O aplicativo Next.js usa o segmento [locale] em apps/web/app/[locale]/. Componentes de servidor chamam normalizeLocale no parâmetro para que valores desconhecidos revertam para en, então getDictionary(locale) carrega o módulo JSON correspondente de packages/i18n. É por isso que cada modelo de página parece o mesmo estruturalmente: todos lêem cópia do dicionário, não strings inglesas codificadas em JSX (com exceções raras que o linter sinaliza).
Rotas de blog combinam essa localidade com o slug: posts publicados são lidos de content/blog/<locale>/<slug>.mdx quando o slug é habilitado em publish.json. Então, “suportar sete localidades” não é apenas scripts de tradução; também é rotas estáticas + arquivos de conteúdo estáticos que devem existir para cada idioma que você se importa.
A promessa de qualidade é mais estreita do que o polimento literário: pipelines consistentes, saída revisável no git e priorização honesta: campos narrativos pt/es via Groq, tudo o mais via MT, a menos que a chave corresponda ao padrão de função. Isso não é um modo de falha; é um escopo declarado.
Comece com quem você quer ler e quais mercados de contratação importam para você, não com “quantas bandeiras”. A lista de localidades é uma decisão de produto; trate-a como tal.
Comece pequeno. Uma localidade extra (provavelmente aquela onde você tem conexões profissionais ou está ativamente procurando emprego) é suficiente para validar o pipeline. Adicione uma entrada LOCALE_CONFIG, adicione o arquivo JSON, execute pnpm translate. Se a qualidade da saída for aceitável para essa localidade, você tem um caminho repetível. Você não precisa de seis localidades não inglesas para provar que a arquitetura funciona.
Decida sua política híbrida cedo. LibreTranslate é rápido e gratuito se você executá-lo localmente; Groq adiciona qualidade em campos narrativos, mas custa tokens e adiciona latência. Se você está enviando apenas uma ou duas localidades não inglesas, Groq-only para todo o dicionário é mais simples do que construir a lógica de roteamento em shouldUseGroqForKey. A complexidade híbrida em translate.mjs só vale a pena quando você tem chaves suficientes e localidades suficientes que a variação de qualidade de MT realmente importe entre elas.
Mantenha a fonte em inglês canônica e a saída revisável por diff. O arquivo de bloqueio em scripts/translate.lock.json não é cerimônia opcional; é o que permite que você execute pnpm translate sem gastar tokens novamente em seções que não mudaram. Qualquer pipeline que retraduz o dicionário inteiro em cada execução atingirá problemas de custo e limite de taxa antes que seu portfólio tenha um segundo leitor. Confirme os arquivos de localidade gerados para que você possa revisar cada mudança de tradução em uma solicitação de pull normal.
Então, implemente uma fonte em inglês, arquivos satélites gerados e scripts que você pode executar em CI ou localmente, para que seu portfólio permaneça um produto que você pode evoluir, não uma pilha de cópias editadas manualmente.
A implementação não é a parte difícil. LOCALE_CONFIG, dois scripts de lote, um arquivo de bloqueio: qualquer engenheiro decente pode construir isso em um fim de semana. A parte mais difícil é decidir antecipadamente que o idioma é uma restrição de produto, não um recurso que você adiciona quando o resto está “pronto”.
Se você tratar a i18n como decoração, você a enviará exatamente uma vez e nunca mais a tocará. Se você tratar como escopo, com automação, diferenças revisáveis e uma lista honesta do que não cobre, e permanecer manutenível, e você permanecer no controle do que seu portfólio comunica aos leitores que você pode nunca conhecer em inglês.
Essa é a aposta real aqui: não que cada frase soe como se um tradutor humano gastasse uma hora nela, mas que o site funcione para alguém em Varsóvia ou Tallinn que prefira não ler em inglês quando é importante. Se essa aposta valerá a pena, é TBD.