Progetti multilingua in Python (parte 6/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. 

Attivare gettext nel codice Python.

Dopo una lunga digressione sugli strumenti di GNU Gettext e le possibilità ulteriori che offrono rispetto ai semplici script distribuiti con Python, riprendiamo il nostro percorso originale: avevamo ottenuto i cataloghi compilati .mo, uno per ciascuna lingua tradotta, e li avevamo messi al loro posto nella directory locale. È arrivato il momento di attivare il meccanismo di gettext nel nostro codice, e vedere finalmente le stringhe tradotte.

Riassunto e preparazione.

Prima di proseguire, ripetiamo rapidamente tutti i passaggi a partire da un modulo Python nuovo, per semplificare:
# -*- coding: utf-8 -*-
_ = lambda i: i   # TODO eliminare... 
ngettext = lambda i, j, k: i  # TODO eliminare...

def main():
    print(_('stringa non-ascii èéàòì: versione originale'))
    print(_('stringa interpolata {}: versione originale').format(123))
    num = 1
    s = ngettext('stringa singolare {}: versione originale', 
                 'stringa plurale {}: versione originale', num)
    print(s.format(num))

if __name__ == '__main__':
    main()
Verificate prima di tutto che questo codice funziona normalmente:
> py main.py
stringa non-ascii èéàòì: versione originale
stringa interpolata 123: versione originale
stringa singolare 1: versione originale
A questo punto supponiamo di voler fornire due traduzioni: non importa in quale lingua, diciamo Inglese e Italiano. (Ehm sì, abbiamo notato: il codice originale è già in Italiano. Ma è solo una prova, e non ci mettiamo a tradurre davvero. E una traduzione italiana comunque ci fa comodo per sperimentare, come vedremo, visto che dovrebbe essere quella automaticamente riconosciuta nel “locale” del nostro computer).
Create due cataloghi .po con:
> xgettest -d myapp main.py
Oppure usate pygettext.py, ma in questo caso le stringhe plurali non verranno riconosciute automaticamente e dovrete inserirle voi stessi a mano. A questo punto “traducete” il catalogo risultante come segue:
(...)
"Plural-Forms: nplurals=2; plural=n != 1;"

#: main.py:7
msgid "stringa non-ascii èéàòì: versione originale"
msgstr "stringa non-ascii èéàòì: versione INGLESE"

#: main.py:8
msgid "stringa interpolata {}: versione originale"
msgstr "stringa interpolata {}: versione INGLESE"

#: main.py:11
msgid "stringa singolare {}: versione originale"
msgid_plural "stringa plurale {}: versione originale"
msgstr[0] "stringa singolare {}: versione INGLESE"
msgstr[1] "stringa plurale {}: versione INGLESE"
“Traducete” allo stesso modo il catalogo nella versione italiana (scrivete semplicemente “versione ITALIANA” ovunque… le regole del plurale sono identiche).
Compilate entrambi i cataloghi, e collocateli nella directory locale:
myapp 
      |- main.py
      |
      |- locale
                |- it
                |     |- LC_MESSAGES
                |                    |- myapp.mo
                |- en
                      |- LC_MESSAGES
                                     |- myapp.mo
Fatto questo, siamo pronti a partire. Beh, quasi.

Due scenari per la traduzione.

Occorre prima distinguere tra due scenari diversi. Nella maggior parte dei casi, l’applicazione è “monolingua”: viene semplicemente tradotta in una delle lingue disponibili, scelta all’inizio. La selezione iniziale della lingua può essere fatta leggendo un file di configurazione, una costante in un modulo settings.py, etc. Ma più frequentemente si interroga il “locale” dell’utente per scoprire quale lingua preferisce, come vedremo tra poco.
Nel secondo scenario, l’applicazione è “plurilingua” nel senso che l’utente ha la possibilità di cambiare lingua a runtime, scegliendo tra quelle disponibili. Come nel caso precedente, potrebbe iniziare con la lingua “locale” o indicata da un setting; successivamente però ha la possibilità di cambiarla in qualsiasi momento.

Alcune osservazioni su locale.

In entrambi questi scenari, di solito si desidera sapere qual è la lingua “predefinita” per l’utente: per impostarla in modo definitivo in un programma “monolingua”, o per cominciare da questa in un programma “plurilingua”. Questa informazione fa parte delle impostazioni di l10n (localizzazione) che si ricavano con il modulo locale. Detta così non sembra difficile, ma occorre fare alcune precisazioni:
>>> import locale
>>> locale.getlocale()
(None, None)
Ops! Prima precisazione: nello spirito di Unix, il “locale” di una applicazione non è automaticamente impostato. Le applicazioni che vogliono utilizzare un locale per rappresentare le varie informazioni (date, numeri con la virgola, etc.), devono attivarlo per prima cosa. In particolare, una chiamata “a vuoto” a locale.setlocale imposta il locale predefinito per l’utente:
>>> locale.setlocale(locale.LC_ALL, '')
'Italian_Italy.1252'
>>> locale.getlocale()
('Italian_Italy', '1252')
In questa tupla, il primo elemento indica il locale attivato, il secondo l’encoding di default. E qui va fatta una seconda precisazione: i nomi dei locale in Windows sono diversi da quelli tradizionali Unix, e Python non gestisce molto bene questa differenza, per esempio:
>>> locale.setlocale(locale.LC_ALL, 'en-US')
'en-US'
>>> # il locale *viene impostato*, ma...
>>> locale.getlocale()
(...)
ValueError: unknown locale: en-US
>>> # ma d'altra parte...
>>> locale.setlocale(locale.LC_ALL, 'en')
'en'
>>> locale.getlocale()
('en_US', 'ISO8859-1')
Cambiare locale a runtime per certi codici richiede qualche astuzia. D’altra parte cambiare locale a runtime non è un’operazione comune e… questo articolo parla di i18n, non di l10n! Se avete bisogno di approfondire la cosa, potete partire di qui. A noi basta sapere che una chiamata iniziale a locale.setlocale(locale.LC_ALL, '') imposta il locale predefinito dell’utente senza troppi problemi.
E tuttavia… questo non serve ancora a nulla! Almeno per il nostro scopo di tradurre l’applicazione. Infatti gettext non chiede automaticamente al locale di sistema quale lingua dovrebbe usare, ma preferisce cercare invece in una serie di variabili d’ambiente: in ordine LANGUAGE, LC_ALL, LC_MESSAGES, e infine LANG. Il problema è che nessuna di queste variabili è impostata in Windows! Vuol dire che dobbiamo pensarci noi “a mano”: per esempio,
import os, locale
locale.setlocale(locale.LC_ALL, '')
os.environ['LANG'] = locale.getlocale()[0]
Queste righe all’inizio del vostro programma garantiranno che quando gettext cercherà un’indicazione nella variabile LANG, ci troverà la lingua del locale predefinito. Se preferite tuttavia non impostare il locale, potete ottenere lo stesso risultato con
import os, locale
os.environ['LANG'] = locale.getdefaultlocale()[0]
In un’ottica cross-platform, probabilmente conviene usare LANG perché è l’ultimo posto dove gettext va a cercare. Se proprio avete paura di pestare i piedi a qualche impostazione preesistente su altre piattaforme, dovreste fare qualcosa come:
try:
    os.environ['LANG']
except KeyError:
    os.environ['LANG'] = etc, etc.
Impostare inizialmente il locale e poi cambiarlo successivamente a runtime si può fare, anche se è uno scenario inconsueto. Se dopo aver cambiato il locale desiderate anche aggiornare la lingua della traduzione, dovete riavviare il meccanismo di gettext: e qui c’è un altro intoppo.
Naturalmente la variabile LANG non si aggiorna da sola automaticamente quando cambiate il locale: dovreste cambiarne a mano il valore tutte le volte. Ma fare questo può essere sbagliato dal punto di vista della compatibilità cross-platform, ma soprattutto non otterrete sempre l’effetto desiderato. Infatti gettext cerca la lingua nelle variabili LANGUAGE, LC_ALL e LC_MESSAGES, prima di arrivare a LANG: in Windows, dove nessuna di queste variabili esiste, va tutto bene. Ma su altre piattaforme probabilmente gettext troverà un valore settato prima di arrivare a LANG: e imposterà di nuovo la lingua di default, con ogni probabilità.
Potreste cambiare direttamente il valore di LANGUAGES, per essere sicuri. Ma il punto è che queste variabili d’ambiente sono fatte per contenere il nome della lingua di default nel locale predefinito dell’utente, e non dovrebbero essere cambiate neppure quando cambiate il locale. Possiamo fare un’eccezione per LANG, viste le esigenze di Windows, ma solo se associamo a questa il valore della lingua di default. Usare LANG o le altre variabili per comunicare a gettext una lingua arbitraria, diversa da quella di default, non è del tutto cross-compatibile. Niente paura, però: se avete bisogno di cambiare lingua a runtime, allora gettext vi mette a disposizione degli strumenti adeguati, indipendentemente dal locale impostato e dalle variabili d’ambiente.
Infine, se non vi importa di sapere qual è la lingua predefinita nel locale dell’utente (perché, per esempio, volete affidarvi solo a un file di configurazione), allora naturalmente questi passaggi non sono necessari.

Primo scenario: tradurre un’applicazione “monolingua”.

Il modulo gettext mette a disposizione due API: una più semplice, che replica in sostanza l’uso di GNU Gettext. La seconda più “pythonica” e complessa, basata sulle classi. In realtà la “vecchia API” dietro le quinte fa uso di quella class-based, e in pratica serve solo a chi è abituato ai nomi tradizionali (gettext.gettext, etc.).
La prima API trova impiego in pratica solo se il vostro scenario è una applicazione “monolingua” da tradurre nella lingua di default del locale dell’utente: in sostanza dovete solo avviare una volta il meccanismo di gettext all’inizio e poi non ci pensate più. Anche per questo tipo di applicazione, tuttavia, l’API class-based è probabilmente da preferirsi.
Vediamo in ogni caso come si usa l’API più semplice, immaginando appunto lo scenario “monolingua”. Riprendiamo il modulo main.py, e modifichiamo la parte finale come segue:
if __name__ == '__main__':
    import locale, os
    os.environ['LANG'] = locale.getdefaultlocale()[0]
    import gettext
    gettext.bindtextdomain('myapp', 'locale')
    gettext.textdomain('myapp')
    _ = gettext.gettext
    ngettext = gettext.ngettext
    main()
La logica delle prime due righe è stata discussa nel paragrafo precedente: in sostanza ci prepariamo a dare a gettext un posto (la variabile d’ambiente LANG) dove cercare la lingua in cui tradurre il nostro programma. Se preferite impostare la lingua da un file di configurazione o in qualsiasi altro modo, potreste (ma non dovreste!) scrivere:
lang = mysettings.LANGUAGE # leggo da un file di configurazione...
os.environ['LANG'] = lang  # non fatelo pero'! non e' cross-compatibile
Come abbiamo già spiegato nel paragrafo precedente, questa tecnica non è consigliabile. In Windows funziona senz’altro, ma su altre piattaforme probabilmente no: gettext troverà un valore per la lingua di default e lo applicherà, prima di arrivare a cercare in LANG. Il modo giusto per impostare una lingua arbitraria è usare l’API class-based di gettext, come vedremo.
gettext.bindtextdomain collega il “dominio” dei vostri cataloghi alla directory in cui cercarli. Il “dominio”, come abbiamo visto, nel nostro caso è myapp (corrisponde al nome myapp.mo dei cataloghi che gettext dovrà cercare). La directory nel nostro caso è locale: la path è relativa al modulo corrente (main.py). Se avete bisogno di specificare una path più complessa, potete ricorrere alle consuete manipolazioni con os.path:
from os.path import join, abspath, dirname
# se la dir "locale" e' al livello superiore del package del progetto:
locale_path = abspath(join(dirname(__file__), '..', 'locale'))
gettext.bindtextdomain('myapp', locale_path)
Notate che se non passate una path per la directory “locale”, Python assume una generica [sys.prefix]/share/locale, che non è molto utile. La documentazione consiglia di usare sempre gettext.bindtextdomain e specificare una directory precisa, magari con una path assoluta.
gettext.textdomain imposta il dominio per i cataloghi delle traduzioni (nel nostro caso myapp, come già sappiamo). Una volta fatto quest’ultimo passo, gettext è pronto a partire. Con le ultime due istruzioni colleghiamo a gettext le variabili che abbiamo usato per marcare le stringhe (_ e ngettext), e siamo a posto.
Non dimenticatevi di cancellare le righe che assegnavano provvisoriamente _ e ngettext a delle lambda noop! Fatto questo…
> py main.py
stringa non-ascii èéàòì: versione ITALIANA
stringa interpolata 123: versione ITALIANA
stringa singolare 1: versione ITALIANA
Come per magia, le stringhe appaiono nella versione “tradotta”. Se invece selezionate il locale inglese, per esempio con
os.environ['LANG'] = 'en' # non fatelo pero'! non e' cross-compatibile
otterrete naturalmente:
> py main.py
stringa non-ascii èéàòì: versione INGLESE
stringa interpolata 123: versione INGLESE
stringa singolare 1: versione INGLESE
Se manca la traduzione di una stringa nella lingua desiderata, viene visualizzata la versione originale. Potete verificarlo facilmente inserendo nel nostro main.py una nuova stringa marcata. Esiste la possibilità di attivare un meccanismo di fallback più complesso, ma conviene usare l’API class-based di gettext per questo: ci torneremo tra poco.
Se avete marcato stringhe per la traduzione anche in altri moduli, allora dovete importare gettext all’inizio di ciascun modulo:
import gettext
_ = gettext.gettext
# ed eventualmente anche: 
ngettext = gettext.ngettext
Vedremo che invece l’API class-based permette di installare facilmente i nomi necessari nei __builtins__ globali (un hack che certamente potete fare “a mano” anche voi con l’API tradizionale, volendo). Il meccanismo di gettext deve comunque essere avviato solo una volta, all’inizio del programma: per esempio, come abbiamo fatto sopra, nell’entry-point del modulo main della vostra applicazione.
Infine, per completezza, ecco il modo equivalente di avviare un’applicazione “monolingua” usando l’API class-based, che esaminiamo nel paragrafo seguente:
if __name__ == '__main__':
    import locale, os
    os.environ['LANG'] = locale.getdefaultlocale()[0]
    import gettext
    gettext.install('myapp', 'locale', names=['ngettext'])
    main()
Con questa tecnica non è necessario importare gettext negli altri moduli della vostra applicazione.

Secondo scenario: tradurre un’applicazione “plurilingua”.

Se avete la necessità di cambiare lingua a runtime, o anche semplicemente se avete stringhe marcate in più di un modulo e non avete voglia di importare gettext ovunque, allora vi conviene usare l’API class-based, che offre qualche possibilità in più.
Questa API si sviluppa intorno a due classi NullTranslation e GNUTranslation che in realtà non avete bisogno di usare direttamente: rimandiamo alla documentazione per i dettagli. Un’istanza di queste classi rappresenta una singola “regola per la traduzione”: un set di istruzioni che dicono a gettext dove si trovano i cataloghi, quali lingue usare etc. Tuttavia è più facile manipolare queste classi attraverso alcune funzioni helper messe a disposizione da gettext.

Uso di gettext.translation.

La più interessante e complessa di queste funzioni è gettext.translation, la cui signature completa è:
gettext.translation(domain, localedir=None, languages=None, 
                    class_=None, fallback=False, codeset=None) 
Questa funzione restituisce un’istanza di (GNU/Null)Translation pronta all’uso. I suoi argomenti sono:
  • domain è il “dominio” dell’applicazione, come ormai sappiamo (nel nostro caso, myapp);
  • localedir è la path della directory locale, analogamente a quanto abbiamo già visto;
  • languages è una lista di codici di lingue da considerare: gettext cercherà una traduzione in ordine dalla prima all’ultima. Se languages=None allora gettext proverà solo a fornire la traduzione nella lingua “di default” (cercando nelle variabili d’ambiente LANGUAGE, LC_ALL, LC_MESSAGES e LANG come sappiamo);
  • class_ indica quale classe la funzione dovrebbe istanziare: il default None produce un’istanza di GNUTranslation, che è in genere quello che si vuole;
  • fallback determina il comportamento da usare se non viene trovato nessun catalogo per le lingue indicate: se è False solleva un OSError, mentre se è True la funzione restituisce un’istanza di NullTranslation che “traduce” sempre restituendo la stringa originale;
  • codeset non dovrebbe più essere usato in Python 3.
L’istanza di GNUTranslation restituita da questa funzione ha un metodo install che appunto la “installa”, avviando il processo di traduzione di gettext. Il metodo install in sostanza procede a iniettare il nome _ nei __builtins__ globali, in modo che non ci sia bisogno di fare esplicitamente la manipolazione dei nomi come abbiamo fatto prima (_ = gettext.gettext etc.). Il metodo install riceve un parametro opzionale names per installare a piacere altri nomi di gettext oltre a _.
Di conseguenza, il modo tipico per avviare il meccanismo di traduzione è scrivere qualcosa del genere all’inizio del nostro programma:
import gettext
t = gettext.translation('myapp', 'locale', fallback=True)
t.install(names=['ngettext'])
Questo crea una semplice “regola di traduzione”, specificando il dominio e la collocazione della directory locale, e imponendo di restituire la stringa originale se manca il catalogo. Infine, installa nei __builtins__ globali i nomi _ e ngettext. È tutto ciò che occorre per far partire la macchina: come vedremo, in questo scenario molto semplice è ancora più conveniente usare la funzione globale gettext.install per fare tutto questo in un colpo solo.
Vediamo però qualche scenario più complicato:
t = gettext.translation('myapp', 'locale', languages=['en', 'fr'],
                        fallback=True)
Questo traduce in Inglese come prima scelta: se una stringa non ha traduzione inglese, allora prova la traduzione francese; altrimenti restituisce la stringa originale.
def_lang = locale.getdefaultlocale()[0]
t = gettext.translation('myapp', 'locale', languages=[def_lang, 'en'], 
                        fallback=True)
Questo traduce nella lingua del locale dell’utente, e in mancanza di questa in Inglese, e come ultima risorsa restituisce la stringa originale.
def_lang = locale.getdefaultlocale()[0]
def_trans = gettext.translation('myapp', 'locale', languages=[def_lang],
                                fallback=True)
en_trans = gettext.translation('myapp', 'locale', languages=['en'], 
                               fallback=True)
fr_trans = gettext.translation('myapp', 'locale', languages=['fr'], 
                               fallback=True)
def_trans.install(names=['ngettext'])
Questo prepara tre “regole di traduzione” pronte all’uso: una per la lingua di default, una per l’Inglese, una per il Francese. Quindi inizia a installare la prima. Nel corso del programma, per esempio in risposta a un input dell’utente, è possibile cambiare al volo la lingua di traduzione semplicemente installando un’altra “regola”:
(...)
fr_trans.install(names=['ngettext'])
Quando le regole (ovvero le lingue disponibili) sono poche, è possibile preparare tutte le istanze di gettext.GNUTranslation all’inizio, come abbiamo appena fatto. Se l’utente può scegliere tra decine di traduzioni, conviene naturalmente generare l’istanza su richiesta.

Uso di gettext.install.

Se non avete bisogno di cambiare lingua a runtime, e non vi servono regole di fallback, non è necessario usare gettext.translation per avere un’istanza della classe da installare. In questi casi semplici, gettext.install fa tutto il lavoro in un colpo solo:
os.environ['LANG'] = locale.getdefaultlocale()[0]
gettext.install('myapp', 'locale', names=['ngettext'])
Notate che non è possibile passare un parametro languages a gettext.install per specificare la lingua di traduzione: gettext si limiterà a guardare nelle variabili d’ambiente, quindi dobbiamo impostarne una. Nell’esempio qui sopra abbiamo scelto come sempre LANG, impostata alla lingua di default. Per le ragioni di cross-compatibilità già esposte, potreste ma non dovreste impostare in questo modo una lingua specifica diversa da quella di default. Se avete bisogno di indicare una lingua specifica, allora non dovreste usare gettext.install ma passare da gettext.translation. Infine, se gettext non trova il catalogo per la lingua indicata, userà una istanza di NullTranslation che restituisce sempre la stringa originale.
Ancora una nota: gettext.install installa la lingua che trova, con il parametro fallback=True, che abbiamo discusso nel paragrafo precedente. In pratica, se non trova il catalogo della traduzione, restituisce la lingua originale della stringa.
In definitiva gettext.install non è più “potente” dell’API tradizionale (non class-based) di gettext che abbiamo visto sopra: l’unico vantaggio è che è più compatta, e installa _ (ed eventualmente ngettext) nei __builtins__ globali. In pratica però gettext.install vi permette di fare le stesse cose dell’API “semplice”. Se il vostro scenario è più complesso, usate gettext.translation.

Uso di gettext.find.

L’ultima funzione globale dell’API class-based di gettext è find. Questa funzione accetta in input una lista di lingue, e cerca se ci sono i cataloghi corrispondenti. La sua signature completa è:
gettext.find(domain, localedir=None, languages=None, all=False)
I parametri sono:
  • domain è il dominio dell’applicazione, come sappiamo;
  • localedir è la path della directory locale;
  • languages è una lista di codici di lingua, come abbiamo visto per gettext.translation;
  • se all è False la funzione restituisce la prima lingua per cui trova un catalogo, tra quelle indicate (oppure None se non trova nessun catalogo); se invece all è True, allora la funzione restituisce una lista di tutte le lingue trovate.
Per esempio, nel nostro caso (dove abbiamo installato le traduzioni italiana e inglese):
# stampa locale/en/LC_MESSAGES/myapp.mo
print(gettext.find('myapp', 'locale', 
                   languages=['de', 'en', 'fr', 'it']))
# stampa ['locale/en/LC_MESSAGES/myapp.mo', 
#         'locale/it/LC_MESSAGES/myapp.mo']
print(gettext.find('myapp', 'locale', 
                   languages=['de', 'en', 'fr', 'it'], all=True))
Internamente questa funzione è utilizzata da gettext al momento di istanziare una classe GNUTranslation, per verificare quali lingue sono effettivamente disponibili tra quelle richieste. Voi potete usarla per sapere in anticipo come stanno le cose, ed eventualmente intervenire:
default_lang = locale.getdefaultlocale()[0]
preferred_lang = ask_user('Che lingua preferisci?')
if not gettext.find('myapp', 'locale', languages=[preferred_lang]):
    emit_warning('La lingua scelta non è disponibile...')
t = gettext.translation('myapp', 'locale', fallback=True, 
                        languages=[preferred_lang, default_lang])
t.install(names=['ngettext'])
Notate che se chiamate gettext.find lasciando languages=None, la funzione restituisce la prima lingua che trova nelle consuete variabili d’ambiente (se all=False), o tutte quelle che vi trova (se all=True).

Commenti

Post più popolari