Salta al contenuto

Blog ·

Sedicimila chiavi, nove lingue

Tradurre un pannello di controllo non è un problema di testo. I guasti che contano sono muti: un argomento perso, un plurale appiattito, una protezione cieca.

Di Provibr

Il nostro catalogo di messaggi inglese contiene attualmente poco più di sedicimila stringhe. I cataloghi sono nove, uno per lingua, e devono avere esattamente le stesse chiavi. Fa qualcosa come oltre centoquarantamila pezzi di testo che devono restare tutti fedeli gli uni agli altri mentre una dozzina di persone cambia il prodotto sotto di essi.

La cosa interessante di quel problema non è il volume. È che quasi nessuno dei guasti si annuncia. Una traduzione con un refuso viene segnalata da un cliente entro una settimana. Una traduzione che ha perso in silenzio un argomento rende per sempre una frase con un buco dentro, e nessuno apre un ticket per una frase che si legge bene.

Questo è l’insieme di protezioni a cui siamo arrivati, con il ragionamento dietro ciascuna. Non sono legate ad alcun framework; i modi di guastarsi sono una proprietà dell’avere più di una lingua.

La classe di bug che conta

Si parta da quello che ci ha fatto prendere la cosa sul serio.

Una stringa inglese dice Welcome back, {name}. Qualcuno la traduce, e la traduzione è Welcome back. È grammaticale, è una frase ragionevole, e si rende senza errori in qualunque sistema di template esistente. Il parametro semplicemente non compare. Non c’è alcuna eccezione, nessuna riga di log, nessun avviso di chiave mancante.

Quel bug è invisibile a ogni test che verifica se le chiavi esistono, che è il test che quasi tutti scrivono per primo. L’unica cosa che lo trova è un confronto: estrarre l’insieme dei nomi dei segnaposto dalla stringa di riferimento, estrarli dalla traduzione, ed esigere che coincidano.

Una volta costruito quel confronto, si rivelano necessari tre affinamenti, e ciascuno è nato da una stringa reale che era sfuggita.

Confrontare i tipi degli argomenti, non solo i nomi. Un {count} nudo non viene formattato. Stampa il numero grezzo, senza separatore delle migliaia, in qualunque lingua. Si scriva {count, number} e viene formattato. Se il tipo è presente in inglese e assente nella traduzione, il nome coincide, la struttura dei rami coincide, e una lingua rende in silenzio un intero grezzo dove le altre ne rendono uno formattato. Lo stesso vale per le date: un argomento data che ha perso il proprio tipo rende un timestamp ISO in faccia al lettore.

Non confrontare gli stili degli argomenti. Lo stile valuta contro lo stile numero semplice è una scelta di presentazione che cambia legittimamente da lingua a lingua, e una protezione che lo segnala insegna alle persone a ignorare la protezione. Il tipo è strutturale, lo stile è gusto, e solo uno dei due appartiene a un test.

Confrontare i nomi dei rami, non il testo dei rami. Un plurale che il traduttore ha appiattito conserva il proprio argomento, quindi un confronto sui segnaposto lo lascia passare volentieri, mentre lo schermo finisce per mostrare un numero nudo senza alcun sostantivo dopo. Nel nostro catalogo olandese avevamo esattamente questo, ed è così che è stato trovato.

I plurali non sono un problema risolto da copiare

L’istinto è esigere che una traduzione abbia gli stessi rami plurali dell’inglese. L’inglese ne ha due: uno e altro. Quel requisito è corretto per le lingue che ne hanno anch’esse due, ed è precisamente sbagliato per quelle che non li hanno, perché accetta volentieri un catalogo a cui mancano categorie di cui la sua stessa lingua ha bisogno.

Alcune misurazioni, tutte tratte dalle regole standard che accompagnano qualunque runtime moderno:

LinguaChe cosa sorprende
FranceseLo zero prende il ramo singolare. «0 machine», non «0 machines».
UcrainoQuattro categorie. Lo zero ricade in many, insieme ai numeri da 5 a 20; 21 è one e 22 è few.
RumenoDa 2 a 19 è few, 20 è other, e 101 è di nuovo few.
AraboSei categorie, divise a 0, 1, 2, da 3 a 10 e da 11 a 99.

La riga del rumeno ha una conseguenza che si manifesta come volume grezzo. Il rumeno mette una preposizione davanti al sostantivo sopra il diciannove, quindi una frase che in inglese non ha bisogno di alcun plurale in rumeno ne ha bisogno. Contando i messaggi dei nostri cataloghi che contengono un plurale: l’inglese ne ha 319, il rumeno 498. 179 messaggi hanno bisogno in rumeno di una costruzione plurale di cui l’inglese non ha alcun bisogno. L’ucraino ne aggiunge 13 di suoi, il francese 4.

Eccone uno, ed è un inglese del tutto ordinario:

text
en: Start {days}-day trial
uk: Почати пробний період на {days, plural, one {# день} few {# дні} many {# днів} other {# дня}}

Nulla nella frase inglese lascia intuire che sia un problema di plurali. Una protezione che ricava le proprie aspettative dall’inglese non può in alcun modo vedere questo.

Quindi la seconda protezione gira per ogni lingua contro il proprio catalogo: per ogni plurale che vi si trova, ogni categoria che quella lingua può davvero produrre è gestita, per nome o con un ramo a valore esatto?

Quella protezione ha bisogno di un limite deliberato, altrimenti diventa inutile. Il francese e lo spagnolo hanno una categoria many che si applica soltanto ai multipli esatti di un milione. Esigerla ovunque aggiungerebbe centinaia di rami che nessun essere umano vedrà mai, in un prodotto che conta server, indirizzi, membri del team e giorni. Perciò la verifica è guidata da campioni: chiede quali categorie siano raggiungibili per i conteggi che una chiave vede realisticamente, e c’è un piccolo elenco esplicito di chiavi che contano più in alto, così il meccanismo è pronto il giorno in cui servirà a qualcosa.

Quell’elenco al momento è vuoto, ed è una misurazione anziché una svista. I due candidati che sembrano poter crescere molto contano byte, e la regola francese vuole un multiplo esatto di un milione; un file di esattamente 1.000.000 di byte è una coincidenza, non un caso.

La protezione che in silenzio non faceva nulla

E ora quella scomoda, perché è il guasto di cui parla tutto questo articolo, capitato agli strumenti stessi.

Abbiamo una verifica per cui ogni chiamata di traduzione nel codice si riferisce a una chiave che esiste. Analizza il sorgente, tiene traccia di quale oggetto traduttore appartenga a quale namespace, e cerca ogni chiave. Riferiva di coprire diverse migliaia di chiavi. Era verde.

Saltava anche ogni chiamata proveniente da un pattern di destrutturazione, perché il codice che legava un traduttore a un namespace gestiva solo un identificatore semplice e ignorava in silenzio qualunque altra cosa. Nessun legame, quindi nessuna chiave da verificare, quindi nessun fallimento. Il pattern che saltava era proprio quello usato in quasi tutta la nostra applicazione.

L’abbiamo scoperto perché qualcuno ha indovinato il nome di una chiave, l’ha scritto, e il test è rimasto verde.

Ne sono uscite due regole, e ora le applichiamo entrambe a ogni analisi statica che scriviamo.

Un analizzatore che incontra una forma che non comprende deve fallire rumorosamente, non saltarla in silenzio. «Non riconosciuto» è un rilievo, non un’operazione nulla. L’intero valore dello strumento sta nell’aver guardato tutto.

Dimostri la protezione rossa prima di fidarsi che sia verde. Rompa qualcosa di proposito e guardi il test fallire. Una verifica che non ha mai fallito non è stata provata; è stata soltanto eseguita.

Un parente stretto di quella lezione: il nome di un marchio nei nostri cataloghi era scritto in quattro modi diversi, e l’unico punto dell’intero codice che conosceva la grafia corretta la conosceva come eccezione in una lista di permessi. L’unico componente che aveva la risposta giusta era configurato per distogliere lo sguardo. Un’eccezione non è una protezione, e la correzione di solito consiste nel rovesciare la cosa: enunciare la regola, e lasciare che le eccezioni siano dati appesi a essa.

Analizzare il proprio formato dei messaggi è più difficile di quanto sembri

L’estrazione dei segnaposto con un’espressione regolare funziona finché non funziona più, e il modo in cui fallisce è istruttivo perché fallisce sulle migliori stringhe.

Un ramo select il cui testo è una sola parola somiglia esattamente a un argomento, per chi cerca pattern. Il nostro vecchio scanner sollevava falsi allarmi sui messaggi scritti con più cura di tutto il catalogo e, allo stesso tempo, perdeva argomenti reali dentro i rami, perché un’espressione regolare non sa contare le parentesi graffe. Sbagliava in entrambe le direzioni insieme.

Il sostituto percorre le graffe come si deve e ricorre dentro i corpi dei rami. Sono trenta righe anziché una, ed è la differenza fra una protezione di cui le persone si fidano e una protezione che le persone aggirano a forza di annotazioni.

La tipografia è una protezione per lingua, non condivisa

L’ultima categoria non può essere verificata per confronto, perché è esattamente la cosa che differisce fra le lingue. Il francese vuole uno spazio unificatore prima dei due punti, del punto e virgola, del punto esclamativo e del punto interrogativo. L’ucraino vuole uno specifico carattere apostrofo che somiglia agli altri due apostrofi che probabilmente si hanno sulla tastiera. Lo spagnolo apre una domanda con un segno rovesciato, nel punto in cui la domanda comincia anziché all’inizio della frase. Il rumeno usa virgolette proprie e diacritici con la virgola sotto.

Nulla di tutto ciò è confrontabile fra lingue, quindi ogni lingua che ha regole riceve la propria verifica. Sembra una duplicazione e non lo è: l’alternativa è un’unica protezione generica che può affermare soltanto l’intersezione di nove insiemi di regole, che a fini pratici non è nulla.

Un avvertimento da chi lo ha fatto in un repository grande: uno spazio unificatore è invisibile, e parecchi strumenti lo convertiranno premurosamente in uno spazio normale lungo il percorso. Li inseriamo con uno script proprio per questo motivo, e verifichiamo dopo che siano ancora lì.

Che cosa costruiremmo per primo, e in che ordine

Se si parte da una sola lingua e ci si aspetta di aggiungerne altre, questo è l’ordine che ci avrebbe risparmiato di più.

  1. Le stesse chiavi in ogni catalogo, verificate in CI. Costa poco, ed è la fondazione su cui poggia il resto.
  2. Confronto dei segnaposto e dei tipi degli argomenti rispetto alla lingua di riferimento. È quello che intercetta i bug silenziosi.
  3. Confronto dei nomi dei rami per plurali e select, così un plurale appiattito è un fallimento.
  4. Copertura dei plurali per lingua, guidata dalle regole della lingua stessa, prima che arrivi la prima lingua che ha bisogno di più categorie di quella di riferimento.
  5. Tipografia per lingua, una piccola verifica per ogni lingua che abbia convenzioni proprie.
  6. Una verifica statica che ogni chiave riferita nel codice esista e, cosa più importante, che sia onesta sulle chiamate che non è riuscita a comprendere.

Il tema che attraversa tutti e sei è che i bug di traduzione non sollevano eccezioni. Rendono. Ogni protezione qui esiste perché qualcosa si è reso alla perfezione, in una lingua che nessuno nella stanza legge, e ha detto qualcosa di leggermente diverso da ciò che intendevamo.

Inizi con un host e un agente

Crei un account, installi l’agente con un solo comando e colleghi il suo primo host Proxmox. Il periodo di prova dura 14 giorni e non chiede alcun dato di pagamento.