Desarrollamos tu web presencial SPA DESDE 300€. Si, es una locura. Web SPA DESDE 300€ — Si, es una locura. Hablemos →

Cómo traducir una aplicación web sin inventarte una clave para cada texto

Cómo traducir una aplicación web sin inventarte una clave para cada texto

Toda aplicación multiidioma se enfrenta antes o después a la misma disyuntiva, y casi siempre la resuelve sin darse cuenta de que estaba eligiendo. Cuando aparecen los primeros mil textos ya es tarde para cambiar de idea barato. Aquí verás por qué existe ese dilema, cómo lo resuelve cada tecnología y qué pasa cuando tu aplicación llega a diez mil textos. Los ejemplos van en Laravel porque es uno de los stacks con los que más trabajamos en AndorraDev, pero el problema es idéntico en Symfony, Rails, Django o Next.js.

El problema no es traducir, es identificar cada texto

Cualquier sistema de internacionalización te pide lo mismo: identifica el texto de alguna forma. Y ahí solo hay dos maneras, las dos con un coste que se paga después.

La primera es usar una clave. Escribes dashboard.empty_state_title y en otro archivo defines qué significa. La segunda es usar el texto original como identificador: escribes "No tienes ningún inmueble todavía" y ese literal es la clave.

Ninguna es gratis, y la mayoría de proyectos descubre cuál eligió mal cuando ya hay miles de textos.

Lo que cuesta cada opción

Con claves, el código deja de contarte nada. Abres una plantilla y ves @lang('dashboard.empty_state_title'). Para saber qué pone ahí tienes que ir al archivo de idioma, buscar la clave y volver. Multiplícalo por cada revisión, cada cambio de copy y cada persona nueva que entra al proyecto.

Y hay un efecto peor a largo plazo: nadie borra claves. Como no sabes si dashboard.empty_state_title sigue usándose en algún sitio, se queda. Un archivo de idioma de tres años tiene siempre un tercio de basura que nadie se atreve a tocar.

Con el texto literal, cualquier retoque rompe la traducción. Este es el modelo de gettext, que usan PHP y también WordPress: el msgid es el texto en inglés. Funciona muy bien hasta que alguien quita un punto final. En ese momento el identificador ha cambiado, la traducción existente deja de encontrarse y el usuario ve el texto en inglés sin que nada avise.

En proyectos con copy vivo, que son casi todos, ese modelo significa retraducir por accidente varias veces al año.

Qué hace cada tecnología

gettext apuesta por el texto literal, y lleva décadas arrastrando el problema de la fragilidad. WordPress hereda exactamente eso con su __('Texto', 'textdomain').

ICU MessageFormat, el estándar que hay debajo de medio mundo, va por claves y añade una sintaxis potentísima para plurales y géneros. Resuelve la gramática, no la legibilidad.

i18next en JavaScript, y por extensión next-intl en Next.js, también van por claves con archivos JSON anidados. La experiencia es la misma: el código no dice qué pone.

Rails tiene un truco interesante con las claves perezosas, donde t('.title') se resuelve según la vista en la que estás. Ahorra escritura pero no te dice el contenido.

Y Laravel te deja elegir las dos: __('clave') o __('Texto literal'). Que es lo mismo que decirte que elijas cuál de los dos problemas prefieres.

Escribir la clave y el texto en la misma llamada

La salida es dejar de elegir. Escribes la clave y el texto en la misma llamada, y el sistema usa la clave para identificar y el texto como valor por defecto.

Es lo que hace laratext, un paquete que mantenemos y que usamos en producción:

@text('dashboard.empty_state_title', 'No tienes ningún inmueble todavía')

Lees la plantilla y sabes qué pone. Buscas la clave y la encuentras. Cambias una coma en el texto y la traducción no se rompe, porque la clave no ha cambiado. El coste es escribir un poco más en cada llamada, y a cambio desaparecen los dos problemas.

Los marcadores y los plurales siguen la sintaxis que ya conoces de Laravel:

text('items.count', 'Un inmueble|Tienes :count inmuebles', ['count' => $n]);
Andie recomienda

source_locale es lo que te permite tener los textos del código en un idioma y ejecutar la aplicación en otro, que es el caso típico de un proyecto escrito en inglés que corre en español. Declarándolo, el escáner compara contra el original correcto y solo ve los textos que de verdad han cambiado, así que cada pasada traduce lo nuevo y nada más. Es una línea de configuración y es la que mantiene barato el mantenimiento.

Lo que cambia cuando hay diez mil textos

Con veinte textos cualquier método funciona. El problema aparece a escala, y ahí es donde se ven las costuras.

En Abodara, una plataforma que hemos construido, hay 9.950 llamadas de texto repartidas en tres idiomas. En Hominer el planteamiento es el mismo. A ese volumen, ninguna persona mantiene los archivos de idioma a mano.

Lo que hace falta es un escáner que lea el código y compare con lo que hay traducido. Con eso puedes:

  • Ver qué falta antes de gastar un céntimo, con una pasada en seco que no llama al traductor
  • Traducir solo lo nuevo y avisar de los textos que han cambiado, en vez de rehacerlo todo
  • Borrar las claves que ya no se usan, que es lo que resuelve la basura acumulada del principio
  • Congelar una clave concreta cuando la traducción automática no ha acertado y la has corregido a mano

Una clave construida sobre la marcha es invisible

Hay una trampa que aparece en todos los sistemas con escáner, y conviene saberla antes y no después.

Una clave construida sobre la marcha es invisible. El escáner lee el código de forma estática, así que esto no lo ve:

return text("activity.{$this->type->value}", $default);

No es solo que no la traduzca: es que tampoco puede avisarte de que falta. El idioma que no la tenga le enseñará la clave en crudo al usuario y nada te lo dirá. La versión que sí funciona es aburrida y explícita:

return match ($this->type->value) {
    'created' => text('activity.created', ':actor creó :item'),
    'deleted' => text('activity.deleted', ':actor eliminó :item'),
};

Qué hay en el mercado y en qué se diferencia

Conviene separar tres problemas que se confunden todo el rato, porque cada herramienta resuelve solo uno.

Los textos de la interfaz son los de este artículo. Laravel ya trae las dos formas, ficheros PHP con clave y ficheros JSON con el texto original como clave, así que la elección existe desde el primer día aunque nadie la haga conscientemente. Fuera de PHP, gettext es el estándar histórico y eligió hace décadas la segunda.

El contenido de la base de datos es otro problema, y ahí la referencia es spatie/laravel-translatable, que guarda las traducciones de un atributo en una columna JSON. No compite con lo anterior: el nombre de un producto no vive en un fichero de idioma.

La gestión del proceso de traducción la cubren plataformas como Crowdin, Lokalise o Weblate, que son las que de verdad hacen falta cuando quien traduce no toca el código. Funcionan con cualquiera de las dos formas de identificar, siempre que hayas elegido antes de tener diez mil textos.

Laratext, que es el que mantenemos, ataca justo la costura del primer problema: escribir la clave y el texto en la misma llamada, para no tener que elegir entre una clave ilegible y un literal que se rompe al cambiar una coma. Si tu aplicación tiene doscientos textos, con lo que trae el framework vas sobrado.

Y si no trabajas con Laravel

El principio se traslada, aunque cambien las herramientas.

  • En Next.js, next-intl y i18next van por claves. Puedes recuperar la legibilidad usando el texto original como parte de la clave o manteniendo el archivo del idioma fuente al lado del código. Y hay escáneres como i18next-parser que hacen el trabajo de detectar lo que falta.
  • En WordPress, estás atado a gettext y a su fragilidad. La mitigación real es disciplina: congela los textos antes de traducir y trata cualquier cambio de copy como lo que es, una retraducción.
  • En cualquier stack, el criterio que importa es el mismo: que el código siga siendo legible, que un cambio menor no rompa nada y que puedas saber qué claves sobran. Si tu sistema no te da las tres cosas, a los dos años lo pagas.

Dónde está el código

El paquete está publicado en Packagist, el código en GitHub y la documentación del paquete recoge todas las opciones. Es gratis y salió de este problema exacto en un proyecto de cliente.

Si estás montando una aplicación multiidioma y quieres evitarte este dolor desde el principio, en aplicaciones web a medida y desarrollo Laravel explicamos cómo lo planteamos. Y si prefieres contarlo directamente, escríbenos.

Escrito por
Edu Lazaro
Edu Lazaro
Founder & Lead Developer en AndorraDev

Desarrollador full-stack con más de 15 años de experiencia en Laravel, React, Node.js y arquitecturas cloud. Ayudo a empresas en Andorra a construir su presencia digital.

Partner de diseño · ionospace.
Necesitas ayuda? ×
Andie by AndorraDev
Asistente IA + equipo humano
Asistente IA de AndorraDev
Andie
Hola! Soy Andie, el asistente IA de AndorraDev. ¿En qué puedo ayudarte? Si necesitas hablar con Edu, solo pídelo.
17:28