Enseñarle a una app a hablar en un idioma que todavía no conoce
Greenhouse tenía un problema de i18n esperando en el backlog desde hace un tiempo: en algún momento agregar español y portugués, ya que puedo revisar ambos idiomas yo mismo. Un spike de principios de julio delimitó el alcance, encontró unas 170 cadenas traducibles, y recomendó dividir el trabajo en dos partes: primero arreglar un problema arquitectónico en cómo se guardan los nombres de etapa, y después hacer la traducción real más adelante, una vez que la app fuera algo más que una build local que corro yo mismo. Esta semana retomé la parte de la traducción, y resultó ser una buena lección sobre cómo delimitar el trabajo a algo que se pueda terminar de verdad en una sola sentada.
El conteo de cadenas siguió subiendo
Antes de tocar cualquier código, volví a inventariar toda la superficie traducible de la app, ya que el spike original tenía varias semanas y desde entonces se había publicado bastante. El número pasó de unas 170 cadenas a unas 230: cuatro pantallas nuevas (una vista de cosecha, un tablero kanban, una pantalla de recuperación de bóveda dañada, y un diálogo de reglas) que todavía no existían cuando corrí el spike. Ese tipo de desvío es exactamente la razón por la que no confío en una estimación vieja sin volver a revisarla.
La pregunta más grande de alcance era qué significa realmente “agregar español y portugués” como una sola unidad de trabajo. Extraer cada cadena hardcodeada hacia un sistema de traducción es mecánico y totalmente verificable por una máquina: la build compila, las pruebas siguen pasando, nada cambia visualmente. Escribir el español y el portugués reales no es mecánico. Palabras como “Budding” y “Leafing” no tienen equivalentes limpios en ninguno de los dos idiomas, y quiero revisar esas decisiones de palabras con cuidado, no aprobar a ciegas un borrador de máquina.
Así que lo dividí en tres partes en vez de hacerlo todo en un solo issue: extracción y herramientas ahora, con inglés como único idioma publicado; los mensajes de error del lado de Rust más adelante, ya que están armados como cadenas preformateadas en lo profundo del backend y necesitan un cambio de arquitectura real, no solo un archivo de traducción; y el contenido real en español/portugués más adelante todavía, como su propio issue donde pueda sentarme con las palabras y tomármelas en serio.
Paralelizar la parte aburrida
Una vez delimitado a “extraer cada cadena, mantener el inglés exactamente igual,” el trabajo era mecánico pero estaba repartido en diecinueve archivos. Lo dividí en una tarea por archivo y las corrí en paralelo, todas con las mismas instrucciones: encontrar cada cadena visible al usuario, ponerle nombre, reemplazarla por una llamada de función, no tocar el texto renderizado, y nunca usar {@html} aunque hiciera más fácil armar una oración, ya que un par de estos diálogos renderizan nombres de proyecto y notas escritas por el usuario, y meter un template literal dentro de HTML crudo es exactamente el tipo de atajo que después se convierte en un bug de inyección real.
Esa restricción importó más de lo que esperaba. Un diálogo registra un touch con una oración como “Logs a touch on your project name and sinks it to the bottom of your worklist,” con el nombre del proyecto en negrita. El atajo tentador es una sola cadena con la negrita metida a través de HTML crudo. En cambio, la etiqueta de negrita se queda como markup real en el template, y la oración alrededor se divide en una parte de “antes” y una de “después,” cada una su propia cadena traducible. Un poco más incómodo de escribir, bastante más seguro de publicar.
Un fallo silencioso que parecía un éxito
La parte verdaderamente frustrante llegó durante la verificación, después de que los diecinueve archivos estuvieran terminados y mergeados. La build compiló. Sin errores. Y sin embargo, cada cadena traducida en la app compilada estaba indefinida.
Resultó que a mi configuración del proyecto, escrita a mano, le faltaba una opción que el plugin de traducción necesita para saber dónde encontrar los archivos de traducción. Sin eso, el plugin no da error, simplemente decide en silencio que no hay mensajes para compilar. Una build limpia con cero contenido traducido se ve exactamente igual que una build limpia con todo traducido, hasta que se lee de verdad la salida compilada. Solo lo detecté porque fui a mirar el archivo generado directamente en vez de confiar en el check verde. Una vez arreglado eso, apareció un segundo problema de inmediato: la sintaxis de plural que había usado para “5 items” versus “1 item” no era la que este plugin en particular espera, quería su propia forma declarativa en vez del atajo estándar. Ese al menos tuvo la decencia de fallar de forma ruidosa.
Lección
Que una build pase no es lo mismo que una build haciendo lo que uno cree que está haciendo. “Cero errores” solo dice que nada se rompió, no que algo haya funcionado, y las dos afirmaciones están más lejos entre sí de lo que parecen en el momento. Además: cuando una parte del trabajo tiene naturalmente un lado que se puede verificar por máquina y otro que solo se puede verificar con criterio, generalmente es señal de que en realidad son dos issues disfrazados de uno solo.
Lecturas relacionadas
Agregando español y portugués a una app de escritorio, y quedando atrapado en mi propia suposición
Un cambio de idioma basado en recarga probado contra la app compilada real, un archivo de configuración que no parecía valioso hasta que lo fue, y una palabra en español redactada por máquina que yo nunca diría.
Los errores nunca fueron el problema. Las cadenas de texto sí.
Cada error que Greenhouse le mostraba a los usuarios nació como una cadena de texto en inglés formateada a mano en Rust. El arreglo: enviar la forma del problema por el cable, no una oración sobre él.
Bandeja de semillas: las ideas que desaparecieron durante una semana
Capturar una idea en Greenhouse hacía que desapareciera por completo hasta el séptimo día. La solución fue una zona con cuenta regresiva - y la disciplina de no agregar un botón para saltársela.