Decision: second-brain — switch ES/EN con DeepL, clave fuera de prod
Fecha: 2026-08-16 Dominio: IA/ML · i18n
Decisión
Añadir un switch ES/EN a la superficie del grafo (Docs + Cloud). Las
traducciones al español se generan offline con DeepL y se cachean en
i18n/es.json (committeado). La clave DEEPL_API_KEY vive solo en .env
(gitignored) y nunca se despliega a prod: el build de producción solo copia
el es.json ya traducido, jamás llama a DeepL.
Contexto
El grafo (content/nodes/*.md) está en inglés. Se quería poder leerlo en
español sin (a) traducir a mano 162 nodos ni (b) exponer la API key en el
cliente (DeepL no admite llamadas desde el navegador sin filtrar la clave, y
una clave free no debe llegar a prod).
Arquitectura
.env # DEEPL_API_KEY (gitignored — NO se despliega)
.env.example # plantilla
scripts/translate.mjs # DeepL (api-free.deepl.com) → i18n/es.json
i18n/glossary.json # términos técnicos que se conservan en inglés (ver abajo)
i18n/es.json # cache committeado { nodes:{slug:{title,body}}, sections:{id:{title,blurb}} }
scripts/build.mjs # copia i18n/es.json → dist/es.json (sin clave)
site/app/i18n.js # estado de idioma compartido (lang, setLang, nodeTitle, ...)
npm run translate— regenerai18n/es.json(354 strings: 162 nodos ×2 + 15 secciones ×2). Lote de 50 textos por request (límite free).- El frontend carga
es.jsony unnodeTitle()/nodeBody()/secTitle()/secBlurb()resuelve ES↔EN en caliente; el toggle persiste enlocalStoragey dispara un eventolangchangeque re-rutea. - Los títulos de course se traducen vía
leadId(nuevo campo enbuildCourses, el nodo “lead” del componente conexo).
Glosario técnico (la garantía anti “términos raros”)
La API free de DeepL no soporta ignore_tags ni glosarios (devuelve
Value for 'ignore_tags' not supported). Solución: i18n/glossary.json es la
lista única de términos que se conservan verbatim en inglés (backpropagation,
gradient descent, transformer, attention, fine-tuning, embedding, token, dropout,
pooling, learning rate, batch, pipeline, backpressure, acronyms —RNN/CNN/LSTM/
PCA/SVD/SGD/MLP/ETL/MLOps/LLM/RAG— y tools —PyTorch/TensorFlow/Keras/pandas/
Docker/Kubernetes/Airflow/…—).
Mecanismo en translate.mjs:
- Antes de traducir, cada término del glosario se enmascara con un placeholder
⟨K0⟩…⟨Kn⟩(regex case-insensitive, word-boundary, match más largo primero para que “gradient descent” gane a “gradient”). - Si el texto solo contiene placeholders (p.ej. el título “Backpropagation”),
se omite DeepL por completo — el free API desnuda los
⟨⟩e incluso traduce el resto (“⟨K0⟩” → “Kerning cero”). Se conserva el original. - Al devolver,
unprotectrestaura los placeholders con la forma original, tolerando que DeepL reestilice los corchetes (⟨K0⟩→«K0») o los quite.
Resultado verificado: “Backpropagation” → “Backpropagation”, “Gradient Descent” → “Gradient Descent”, “Transformer” → “Transformer”, mientras lo que tiene traducción estándar sí se traduce (“Convolutional Neural Network” → “Red neuronal convolucional”, “Reinforcement Learning” → “Aprendizaje por refuerzo”).
Correr
npm run translate # necesita DEEPL_API_KEY en .env (offline, no en CI)
npm run build # NO necesita la clave — copia el es.json cacheadoTradeoffs / reversión
- Traducción por lote, no incremental:
translateretraduce todo y gasta cuota cada vez (~354 strings). Con 162 nodos es barato; si crece, se añade dedupe por hash de fuente. - Glosario = fuente de verdad: si un término queda mal, se añade a
i18n/glossary.jsony se re-correnpm run translate. No hay otra capa. - Solo conceptos: se traducen títulos/cuerpos de nodo y secciones. Los
open_questions(dudas personales) y el chrome del filtro cloud quedan en EN. Reversible: agregar más claves al dictUIdemain.js.
Related
~/nef/decisions/2026-08-15-second-brain-consolidacion.md(origen del repo)- DeepL Auth: cabecera
Authorization: DeepL-Auth-Key <key>(free = sufijo:fx)