Progetti multilingua in Python (parte 5/9).

Attenzione!

Queste guide non sono più aggiornate, e in alcuni punti sono proprio superate.
Il mio libro Python in Windows tratta questi temi, e molto altro, in modo molto più completo e aggiornato. 

Uso di GNU Gettext per Windows.

Probabilmente già lo sapete: questo meccanismo di gestire le traduzioni non è idiomatico di Python, ma è un porting di GNU Gettext. Lo script pygettext.py gestisce solo i casi più semplici: se avete bisogno di strumenti più complessi, dovete scaricare e installare Gettext per Windows. In sostanza si tratta di una serie di utility a riga di comando: se scaricate l’installer, la directory di installazione viene aggiunta alla path di sistema e quindi dalla shell potete fare cose come:
> xgettext -d myapp main.py
invece di scrivere tutto il percorso del programma da invocare. Se scaricate lo zip, potete aggiungere manualmente la directory alla path di sistema, se volete. In sostanza le due versioni sono identiche.
Quando avete installato GNU Gettext for Windows, potete usare:
  • xgettext per produrre cataloghi .po(t), al posto di pygettext.py;
  • msgfmt per produrre cataloghi compilati .mo, al posto di msgfmt.py.

Differenze tra pygettext e xgettext.

xgettext è la utility che produce cataloghi .pot scansionando il vostro codice, analogamente a pygettext che abbiamo già visto. L’uso di questo tool è del tutto analogo a pygettext, e vi rimandiamo alla guida on line per i dettagli. Le differenze notevoli tra i due strumenti sono:
  • pygettext produce file con estensione.pot, xgettext produce subito file .po (è solo una differenza estetica, comunque: in sostanza si tratta comunque di cataloghi-template).
  • pygettext ignora i commenti “per traduttori”, xgettext li include (se passate l’opzione -c alla riga di comando).
  • pygettext produce file encodati nell’encoding locale, xgettext produce file in utf-8.
  • xgettext processa le stringhe plurali: e su questo dobbiamo fare un approfondimento.

Marcatura di forme plurali (seconda parte: cataloghi).

Quando avete marcato delle stringhe con la forma plurale, nel modo che abbiamo visto, xgettext produce queste indicazioni nel catalogo .po(t):
#, fuzzy
msgid ""
msgstr ""
(...)
"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"

(...)

#: main.py:14
msgid "Stringa singolare {} versione originale"
msgid_plural "Stringa plurale {} versione originale"
msgstr[0] ""
msgstr[1] ""
Due cose spiccano subito: l’aggiunta di un nuovo header Plural-Forms:... nell’intestazione del file, e il modo particolare di riportare le stringhe che prevedono il plurale.
Iniziamo dal secondo aspetto: invece della consueta coppia msgid / msgstr, troviamo due “id” per la stringa: uno per il singolare, uno per il plurale. I campi lasciati vuoti per la traduzione (msgstr) sono numerati con gli indici di un array, partendo da 0. Per cominciare, xgettext inserisce due campi di default, perché la maggior parte delle lingue ha una forma per il singolare e una per il plurale. In questi casi di solito si intende che msgstr[0] sia destinata alla traduzione della forma singolare, e msgstr[1] alla forma plurale.
Precisiamo subito che cosa intendiamo per “di solito”, descrivendo l’header Plural-Forms. È vostro compito riempire questo header con i valori appropriati per la lingua della traduzione, prima di compilare il catalogo. Il primo campo, nplurals=INTEGER, deve indicare il numero di forme singolari/plurali: di solito due (singolare e plurale), per le lingue a noi più familiari come l’Italiano o l’Inglese. Questo numero deve corrispondere alle dimensioni dell’array delle scelte msgstr. Quindi, se indicate nplurals=4 nell’header, per ciascuna stringa plurale dovete predisporre quattro campi da riempire per i traduttori: msgstr[0], msgstr[1], msgstr[2] e msgstr[3].
Il secondo campo, plural=EXPRESSION, è un po’ più complicato. Deve contenere un’espressione con la sintassi del linguaggio C, che contiene una variabile n. Questa variabile riceve a runtime il valore del terzo argomento della funzione Python ngettext che abbiamo già visto. Quindi, supponiamo per esempio che nel vostro codice abbiate scritto:
num = 3
s = ngettext('Mario ha {} anno.', 'Mario ha {} anni.', num)
print(s.format(num))
In questo caso la variabile n dell’header plural del catalogo riceve il valore 3. L’espressione deve restituire un numero intero compreso tra 0 e il massimo valore dell’array delle possibilità (nel caso comune, quindi, un valore compreso tra 0 e 1). La funzione ngettxt restituisce quindi la stringa tradotta corrispondente al risultato dell’espressione. Un esempio chiarirà meglio: per lingue come l’Italiano o l’Inglese, che hanno un singolare e un plurale, l’header dovrebbe essere:
"Plural-Forms: nplurals=2; plural= n != 1;"
Se usate questa espressione (come in genere si fa), allora i traduttori compilano il campo msgstr[0] con la forma singolare, e msgstr[1] con la forma plurale. L’espressione in C dell’header restituisce 1 se il valore di n è diverso da 1, e restituisce 0 se n==1. Questo significa che per il valore 1 verrà usata msgstr[0] (la forma singolare) e per tutti gli altri valori (zero compreso) verrà usata la forma plurale contenuta in msgstr[1].
Ma il Francese, per esempio, è già diverso: ha due forme come l’Italiano o l’Inglese, ma usa il singolare anche per lo zero. In questo caso l’header dovrebbe essere:
"Plural-Forms: nplurals=2; plural= n > 1;"
E ci sono molti casi complessi. Se non siete pratici, questa pagina elenca anche le possibilità più esotiche.
Se vi sentite creativi o spericolati potete (ab)usare di queste regole per gestire casi speciali, per esempio le perifrasi che si usano in Italiano o in Inglese per il caso “zero” (che altrimenti di solito rientra nel caso “plurale”). Poniamo di aver marcato questa stringa:
num = 12
s = ngettext('Mario has {} friend.',  # singolare
             'Mario has {} friends.', # plurale (compreso "zero")
             num)
print(s.format(num))
Nel catalogo myapp.po (per la traduzione italiana) potremmo scrivere:
(...)
"Plural-Forms: nplurals=3; plural=n == 0 ? 0 : n == 1 ? 1 : 2;"
(...)
#: main.py:10
msgid "Mario has {} friend."
msgid_plural "Mario has {} friends."
msgstr[0] "Mario non ha nessun amico."
msgstr[1] "Mario ha {} amico."
msgstr[2] "Mario ha {} amici."
In questo caso abbiamo indicato 3 forme plurali, di cui la prima (msgstr[0]) copre il caso speciale per “zero”. Sia chiaro comunque che non potete usare una regola per certe stringhe, e un’altra per altre: una volta specificato nell’header che ci sono 3 forme plurali, tutte le stringhe marcate con ngettext devono avere 3 possibili traduzioni per quella lingua (eventualmente anche uguali: ma i traduttori devono aver chiaro come comportarsi).
Una domanda: e se volessimo gestire più di due forme plurali nella lingua originale (quella in cui è scritto il codice)? Purtroppo non è possibile: ngettext accetta solo un argomento per la forma singolare, e uno per la forma plurale. Quindi se volessimo coprire il caso precedente anche nel codice originale, dovremmo discriminare “a mano” il caso per lo “zero”:
num = 12
if num == 0:
    s = _('Mario has no friends.')        
else:
    s = ngettext('Mario has {} friend.', 'Mario has {} friends.', num)
print(s.format(num))
Questo però produrrebbe due stringhe distinte nel catalogo:
(...)
"Plural-Forms: nplurals=3; plural=n == 0 ? 0 : n == 1 ? 1 : 2;"
(...)
#: main.py:8
msgid "Mario has no friends."
msgstr "Mario non ha nessun amico."

#: main.py:10
msgid "Mario has {} friend."
msgid_plural "Mario has {} friends."
msgstr[0] "Mario non ha nessun amico."
msgstr[1] "Mario ha {} amico."
msgstr[2] "Mario ha {} amici."
Abbiamo mantenuto comunque la regola per le tre forme plurali, ma non ha importanza: il caso di msgstr[0] non verrà mai raggiunto in realtà, perché quando num==0 il codice Python seleziona “a mano” la stringa semplice.
Questo dovrebbe sconsigliarvi, in generale, dall’usare più di due forme plurali solo per gestire casi speciali: forzando un po’ la mano a ngettext potete coprire questi casi speciali solo nelle lingue tradotte, ma non in quella del codice originale. In realtà dovreste usare più di due forme plurali solo per le lingue (tradotte) che effettivamente lo richiedono (l’Arabo ha cinque tipi di plurale…). Il codice sorgente dovrebbe essere scritto in una lingua che ha solo due forme plurali, come l’Italiano (o meglio, l’Inglese).

Aggiornare i cataloghi.

Oltre a xgettext (per generare cataloghi .po(t) e msgfmt (per generare cataloghi compilati .mo), GNU Gettext comprende altre utility interessanti: rimandiamo alla documentazione per un elenco completo.
Un esame più approfondito merita msgmerge, che serve ad aggiornare cataloghi già prodotti per tener conto di modifiche e integrazioni successive, man mano che il vostro lavoro sul codice va avanti. Per capire come funziona, produciamo prima di tutto un catalogo del codice di esempio che abbiamo scritto:
> xgettext -d myapp main.py
Chiamiamo questo catalogo old_myapp.po per distinguerlo. Volendo, possiamo anche fare qualche esperimento, traducendo qualche stringa. Adesso modifichiamo il nostro codice Python aggiungendo una stringa e cambiandone un’altra:
...
    print(_('stringa MODIFICATA versione originale')) # era 'semplice'
...
    print(_('una nuova stringa! versione originale'))
E generiamo un nuovo catalogo, come prima:
> xgettext -d myapp main.py
Adesso possiamo usare msgmerge per unire i due cataloghi:
> msgmerge old_myapp.po myapp.po > new_myapp.po
Come si intuisce, il primo parametro è il vecchio catalogo, il secondo è quello nuovo, e a destra dell’operatore di re-direzionamento > scriviamo il nome del file risultante dove comparirà l’output di msgmerge.
Aprite new_myapp.po e confrontatelo con old_myapp.po: le traduzioni già fatte sono conservate, e le nuove stringhe (o quelle modificate) hanno il commento fuzzy per aiutare i traduttori a trovarle più facilmente. Una stringa “fuzzy”, nel gergo di Gettext, è una stringa la cui traduzione non è considerata sicura, e andrebbe ricontrollata.
Accertatevi che msgmerge non abbia combinato dei pasticci (a volte capita) ed eventualmente correggete a mano il nuovo catalogo. Quando siete pronti, potete buttare via i due cataloghi vecchi e rinominare new_myapp.po in myapp.po, pronto per essere riconsegnato ai traduttori.
Aggiornare i cataloghi è sempre una questione delicata. Da un lato non volete iniziare troppo tardi a sottoporre il materiale ai traduttori, per non rallentare la distribuzione finale del programma. Dall’altro non è piacevole per i traduttori ricevere continui aggiornamenti che li costringono a rivedere il lavoro già fatto. Cercate di mantenere un equilibrio tra queste due esigenze.

Commenti

Post più popolari