Tota aplicació multiidioma s'enfronta tard o d'hora a la mateixa disjuntiva, i gairebé sempre la resol sense adonar-se que estava triant. Quan apareixen els primers mil textos ja és tard per canviar d'idea barat. Aquí veuràs per què existeix aquest dilema, com el resol cada tecnologia i què passa quan la teva aplicació arriba a deu mil textos. Els exemples van en Laravel perquè és un dels stacks amb què més treballem a AndorraDev, però el problema és idèntic a Symfony, Rails, Django o Next.js.
El problema no és traduir, és identificar cada text
Qualsevol sistema d'internacionalització et demana el mateix: identifica el text d'alguna manera. I aquí només hi ha dues maneres, totes dues amb un cost que es paga després.
La primera és fer servir una clau. Escrius dashboard.empty_state_title i en un altre fitxer defineixes què significa. La segona és fer servir el text original com a identificador: escrius "Encara no tens cap immoble" i aquest literal és la clau.
Cap de les dues és gratis, i la majoria de projectes descobreix quina va triar malament quan ja hi ha milers de textos.
Què costa cada opció
Amb claus, el codi deixa d'explicar-te res. Obres una plantilla i veus @lang('dashboard.empty_state_title'). Per saber què hi posa has d'anar al fitxer d'idioma, buscar la clau i tornar. Multiplica-ho per cada revisió, cada canvi de copy i cada persona nova que entra al projecte.
I hi ha un efecte pitjor a llarg termini: ningú esborra claus. Com que no saps si dashboard.empty_state_title encara es fa servir en algun lloc, es queda. Un fitxer d'idioma de tres anys sempre té una tercera part d'escombraries que ningú gosa tocar.
Amb el text literal, qualsevol retoc trenca la traducció. Aquest és el model de gettext, que fan servir PHP i també WordPress: el msgid és el text en anglès. Funciona molt bé fins que algú treu un punt final. En aquell moment l'identificador ha canviat, la traducció existent deixa de trobar-se i l'usuari veu el text en anglès sense que res avisi.
En projectes amb copy viu, que són gairebé tots, aquest model significa retraduir per accident diverses vegades l'any.
Què fa cada tecnologia
gettext aposta pel text literal, i fa dècades que arrossega el problema de la fragilitat. WordPress hereta exactament això amb el seu __('Text', 'textdomain').
ICU MessageFormat, l'estàndard que hi ha sota mig món, va per claus i afegeix una sintaxi potentíssima per a plurals i gèneres. Resol la gramàtica, no la llegibilitat.
i18next en JavaScript, i per extensió next-intl a Next.js, també van per claus amb fitxers JSON imbricats. L'experiència és la mateixa: el codi no diu què hi posa.
Rails té un truc interessant amb les claus mandroses, on t('.title') es resol segons la vista on ets. Estalvia escriptura però no t'explica el contingut.
I Laravel et deixa triar totes dues: __('clau') o __('Text literal'). Que és el mateix que dir-te que triïs quin dels dos problemes prefereixes.
Escriure la clau i el text a la mateixa crida
La sortida és deixar de triar. Escrius la clau i el text a la mateixa crida, i el sistema fa servir la clau per identificar i el text com a valor per defecte.
És el que fa laratext, un paquet que mantenim i que fem servir en producció:
@text('dashboard.empty_state_title', 'Encara no tens cap immoble')
Llegeixes la plantilla i saps què hi posa. Busques la clau i la trobes. Canvies una coma al text i la traducció no es trenca, perquè la clau no ha canviat. El cost és escriure una mica més a cada crida, i a canvi desapareixen els dos problemes.
Els marcadors i els plurals segueixen la sintaxi que ja coneixes de Laravel:
text('items.count', 'Un immoble|Tens :count immobles', ['count' => $n]);
source_locale és el que et permet tenir els textos del codi en un idioma i executar l'aplicació en un altre, que és el cas típic d'un projecte escrit en anglès que corre en castellà. Declarant-lo, l'escàner compara contra l'original correcte i només veu els textos que de debò han canviat, així que cada passada tradueix el que és nou i res més. És una línia de configuració i és la que manté barat el manteniment.
Què canvia quan hi ha deu mil textos
Amb vint textos qualsevol mètode funciona. El problema apareix a escala, i és aquí on es veuen les costures.
A Abodara, una plataforma que hem construït, hi ha 9.950 crides de text repartides en tres idiomes. A Hominer el plantejament és el mateix. A aquest volum, cap persona manté els fitxers d'idioma a mà.
El que cal és un escàner que llegeixi el codi i compari amb el que hi ha traduït. Amb això pots:
- Veure què falta abans de gastar un cèntim, amb una passada en sec que no crida el traductor
- Traduir només allò nou i avisar dels textos que han canviat, en comptes de refer-ho tot
- Esborrar les claus que ja no es fan servir, que és el que resol les escombraries acumulades del principi
- Congelar una clau concreta quan la traducció automàtica no ha encertat i l'has corregida a mà
Una clau construïda sobre la marxa és invisible
Hi ha un parany que apareix a tots els sistemes amb escàner, i convé saber-lo abans i no després.
Una clau construïda sobre la marxa és invisible. L'escàner llegeix el codi de manera estàtica, així que això no ho veu:
return text("activity.{$this->type->value}", $default);
No és només que no la tradueixi: és que tampoc pot avisar-te que falta. L'idioma que no la tingui li ensenyarà la clau en cru a l'usuari i res t'ho dirà. La versió que sí que funciona és avorrida i explícita:
return match ($this->type->value) {
'created' => text('activity.created', ':actor va crear :item'),
'deleted' => text('activity.deleted', ':actor va eliminar :item'),
};
Què hi ha al mercat i en què es diferencia
Convé separar tres problemes que es confonen contínuament, perquè cada eina en resol només un.
Els textos de la interfície són els d'aquest article. Laravel ja porta les dues formes, fitxers PHP amb clau i fitxers JSON amb el text original com a clau, així que l'elecció existeix des del primer dia encara que ningú no la faci conscientment. Fora de PHP, gettext és l'estàndard històric i fa dècades que va triar la segona.
El contingut de la base de dades és un altre problema, i aquí la referència és spatie/laravel-translatable, que desa les traduccions d'un atribut en una columna JSON. No competeix amb l'anterior: el nom d'un producte no viu en un fitxer d'idioma.
La gestió del procés de traducció la cobreixen plataformes com Crowdin, Lokalise o Weblate, que són les que de debò calen quan qui tradueix no toca el codi. Funcionen amb qualsevol de les dues formes d'identificar, sempre que hagis triat abans de tenir deu mil textos.
Laratext, que és el que mantenim, ataca justament la costura del primer problema: escriure la clau i el text a la mateixa crida, per no haver de triar entre una clau il·legible i un literal que es trenca en canviar una coma. Si la teva aplicació té dos-cents textos, amb el que porta el framework en tens de sobres.
I si no treballes amb Laravel
El principi es trasllada, encara que canviïn les eines.
- A Next.js, next-intl i i18next van per claus. Pots recuperar la llegibilitat fent servir el text original com a part de la clau o mantenint el fitxer de l'idioma font al costat del codi. I hi ha escàners com
i18next-parserque fan la feina de detectar què falta. - A WordPress, estàs lligat a gettext i a la seva fragilitat. La mitigació real és disciplina: congela els textos abans de traduir i tracta qualsevol canvi de copy com el que és, una retraducció.
- A qualsevol stack, el criteri que importa és el mateix: que el codi continuï sent llegible, que un canvi menor no trenqui res i que puguis saber quines claus sobren. Si el teu sistema no et dona les tres coses, al cap de dos anys ho pagues.
On és el codi
El paquet està publicat a Packagist, el codi a GitHub i la documentació del paquet recull totes les opcions. És gratuït i va sortir d'aquest problema exacte en un projecte de client.
Si estàs muntant una aplicació multiidioma i vols estalviar-te aquest dolor des del principi, a aplicacions web a mida i desenvolupament Laravel expliquem com ho plantegem. I si prefereixes explicar-ho directament, escriu-nos.