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

Deferred translation.

Di solito le deferred translation non sono necessarie, tranne che in certi scenari (Django, per esempio) per cui però di solito si usa un framework (Django!) che ha già un supporto specifico per le lazy translation.

Che cosa sono le Deferred translation.

Una traduzione “differita” (deferred) è una stringa che viene marcata per la traduzione (e quindi compare nel catalogo), ma non viene davvero valutata a runtime fin quando non è il momento di usarla davvero. L’esempio classico, ripreso anche dalla documentazione ufficiale di gettext, è qualcosa del genere:
animals = [_('cane'), _('gatto'), _('elefante'), _('cammello'), ...]
i = int(input('scegli un numero:'))
print(animals[i])
In questo caso è chiaro che volete rendere disponibile una traduzione di tutti i nomi della lista, e quindi giustamente li avete marcati. Tuttavia il motore di gettext a runtime tradurrà tutti i nomi, anche se poi in pratica l’utente ne chiede uno solo. Questi casi sono rari, e anche quando succede di solito lo spreco è minimo e si può tollerare. Se però avete una lista di centinaia o migliaia di nomi, allora può essere utile una deferred translation.
Inoltre ci sono scenari più complessi, in cui una deferred translation può essere necessaria: ne parliamo nel prossimo capitolo; per il momento concentriamoci invece su questo esempio.

Primo metodo di deferred translation.

gettext non supporta in modo specifico le deferred translation, ricorrendo alla lazy evaluation delle stringhe da tradurre. Tuttavia non è difficile ricorrere a un hack:
_ = lambda i: i # noop
animals = [_('cane'), _('gatto'), _('elefante'), _('cammello'), ...]
del _
i = int(input('scegli un numero:'))
print(_(animals[i]))
Qui si suppone che, al momento di eseguire questo frammento di codice, voi abbiate già installato il meccanismo di gettext come abbiamo visto, e che quindi in particolare la variabile _ sia già legata a gettext.gettext nel namespace dei __builtins__. Sovrascrivendo temporaneamente _ nel namespace locale (del modulo), voi impedite a gettext di tradurre a runtime le stringhe della lista. Subito dopo voi cancellate la variabile locale _, e questo ripristina il valore di __builtins__._, cosa che permette subito dopo a gettext di tradurre il valore effettivamente scelto.
Notate anche che la marcatura _(animals[i]) non produce nessuna voce “strana” nel catalogo .po, semplicemente perché non è una stringa! Solo a runtime il suo valore verrà determinato come una stringa, sarà passato alla funzione _ (ovvero gettext.gettext) e quindi sarà tradotto.
Purtroppo la documentazione di Python non fa grandi sforzi per chiarire che di solito questo metodo… non funziona! Più precisamente: non funziona quando lo usate all’interno di una funzione, nella quale usate _ anche fuori dal "blocco lambda/del". In altre parole: questo metodo funziona se lavora al livello più alto del modulo:
import gettext, locale, os
os.environ['LANG'] = locale.getdefaultlocale()[0]
gettext.install('myapp', 'locale')

print(_('una stringa'))
_ = lambda i: i  # "blocco lambda/del" >>>
animals = [_('cane'), _('gatto'), _('elefante'), _('cammello'),]
del _            # <<< "blocco lambda/del"
print(_(animals[2]))
print(_('altra stringa'))
Ma nessuno scrive codice Python in questo modo! Se però volete includere il codice nel corpo di una funzione, così:
def main():
    print(_('una stringa'))
    _ = lambda i: i # "blocco lambda/del" >>>
    animals = [_('cane'), _('gatto'), _('elefante'), _('cammello'),]
    del _           # <<< "blocco lambda/del"
    print(_(animals[2]))
    print(_('altra stringa'))
Questo adesso non funziona più: main produce un UnboundLocalError, naturalmente! Potete scorporare il "blocco lambda/del" mettendolo nel namespace di una funzione separata, dove la variabile _ non sia usata al di fuori del blocco:
def get_animals():
    _ = lambda i: i
    animals = [_('cane'), _('gatto'), _('elefante'), _('cammello'),]
    del _
    return animals

def main():
    print(_('una stringa'))
    animals = get_animals()
    print(_(animals[2]))
    print(_('altra stringa'))
Se questo vi sembra brutto da vedere… probabilmente avete ragione. La verità è che gettext non supporta una vera e propria lazy evaluation delle stringhe da tradurre, e un hack come questo può andar bene una tantum, ma non come soluzione strutturata e permanente. Se avete bisogno di qualcosa del genere, potete dare un’occhiata all’implementazione di django.utils.translation.gettext_lazy, per esempio.
Prima di abbandonare del tutto gettext, c’è un altro hack da provare, però.

Secondo metodo di deferred translation.

La documentazione di gettext menziona un altro modo di risolvere il problema della deferred translation: marcare le stringhe da tradurre in modo deferred con un simbolo diverso dal solito _:
_d = lambda i: i # noop
print(_('una stringa'))
animals = [_d('cane'), _d('gatto'), _d('elefante'), _d('cammello'),]
print(_(animals[2]))
print(_('altra stringa'))
Questo hack è, in un certo senso, l’inverso del precedente. Qui usiamo la variabile _d per marcare una stringa da tradurre in modo deferred. La variabile è definita come una noop e non sovrascrive la consueta _: quindi da un lato gettext non cercherà di tradurre a runtime una stringa marcata con _d (un simbolo che per lui non significa nulla), e dall’altro non si rischia nessun UnboundLocalError (perché _d è definita nel namespace del modulo, e non è mai cancellata).
Il problema è che adesso le stringhe marcate _d non finiranno nel catalogo .po, naturalmente: i vari tool di estrazione cercano solo le stringhe marcate con _.
Potete aggiungere le stringhe mancanti nel catalogo a mano, beninteso. Ma per fortuna non siete costretti a farlo: tool come xgettext (e anche pygettext.py) hanno opzioni per personalizzare i simboli di marcatura da cercare nel codice. Per esempio, se volete estrarre sia le stringhe marcate con _ sia quelle marcate con _d, potete usare xgettext in questo modo:
> xgettext -d myapp --keyword=_d main.py
L’opzione --keyword=_d aggiunge la nostra marcatura _d alle altre consuete keyword da cercare nel codice per produrre il catalogo. Una volta che il catalogo è stato composto, potete tradurlo e compilarlo come di consueto.
Questo secondo hack è senz’altro più “pulito” e raccomandabile del primo: magari non sostituisce una soluzione di lazy evaluation alla Django, ma è più che sufficiente per le situazioni più semplici.

Valutazione delle variabili tradotte e deferred translation.

Ecco invece uno scenario più complesso, che è poi anche il motivo per cui Django ha bisogno di lazy translation. Il punto è che non c’è niente di magico in _ (ovvero in gettext.gettext): è una funzione, che restituisce la stringa tradotta quando viene chiamata. Il problema è che non tutte le espressioni Python sono valutate allo stesso momento. Per chiarirci: se assegnate una stringa tradotta a una variabile, allora siete alla mercé del momento esatto in cui Python valuta quella variabile, e quindi chiama la funzione _: se in quel momento il meccanismo di gettext non è ancora pronto, un NameError sarà inevitabile.
Per esempio, questo funziona senza problemi:
def main():
    s = _('una stringa')
    print(s)

if __name__ == '__main__': 
    import gettext, locale, os
    os.environ['LANG'] = locale.getdefaultlocale()[0]
    gettext.install('myapp', 'locale')
    main()
Ma questo invece non funziona, perché gli attributi di classe sono valutati a load time:
class Main:
    s = _('una stringa') # -> NameError: name '_' is not defined 

    def main(self):
        print(self.s)

if __name__ == '__main__': 
    import gettext, locale, os
    os.environ['LANG'] = locale.getdefaultlocale()[0]
    gettext.install('myapp', 'locale')
    Main().main()
Prima che gettext abbia avuto modo di installare _ nei __builtins__, l’attributo di classe viene valutato e l’inesistente funzione _ viene eseguita.
Questo funziona di nuovo, invece, perché anche il codice a livello di modulo viene valutato a load time:
import gettext, locale, os
os.environ['LANG'] = locale.getdefaultlocale()[0]
gettext.install('myapp', 'locale')

class Main:
    s = _('una stringa') # nessun problema questa volta

    def main(self):
        print(self.s)

if __name__ == '__main__': 
    Main().main()
Ma chiaramente non sempre potete o volete installare gettext a livello di modulo. Può farvi comodo una deferred translation, a questo punto:
_d = lambda i: i  # _d e' definita, quindi niente NameError

class Main:
    s = _d('una stringa') # marchiamo la stringa per la traduzione...

    def main(self):
        print(_(self.s)) # ...ma la traduciamo solo all'ultimo momento

if __name__ == '__main__': 
    import gettext, locale, os
    os.environ['LANG'] = locale.getdefaultlocale()[0]
    gettext.install('myapp', 'locale')
    Main().main()

Cambio di lingua a runtime e deferred translation.

E che dire dei cambi di lingua “al volo”? Quando re-installiamo gettext con una nuova lingua, questo non basta certo a provocare la ri-valutazione a cascata di tutte le variabili già valutate:
import gettext
t_it = gettext.translation('myapp', 'locale', languages=['it'])
t_en = gettext.translation('myapp', 'locale', languages=['en'])

t_it.install()
s = _('una stringa')
print(s) # stampa la traduzione italiana

t_en.install()
print(s) # stampa ANCORA la traduzione italiana!
s = _('una stringa') # rivaluto...
print(s) # adesso stampa la traduzione inglese
Se intendete sviluppare un’applicazione “plurilingua” (nel senso che abbiamo già definito: in grado di cambiare lingua a runtime), dovete fare molta attenzione a questa trappola. In pratica, tutte le espressioni “tradotte” assegnate a variabili dovrebbero essere tradotte in modalità deferred:
_d = lambda i: i
import gettext
t_it = gettext.translation('myapp', 'locale', languages=['it'])
t_en = gettext.translation('myapp', 'locale', languages=['en'])
s = _d('una stringa') # marco, ma non traduco...

t_it.install()
print(_(s))           # ...traduco in Italiano...

t_en.install()
print(_(s))           # ...traduco in Inglese

Deferred translation fatte bene.

Se avete bisogno di ricorrere a deferred translation in modo massiccio, prima o poi questi hack vi verranno scomodi. Quello di cui avete bisogno è un vero e proprio sistema di lazy string applicato a gettext. Va detto che purtroppo l’implementazione di gettext non è molto flessibile per una necessità come questa. La “vecchia API”, come abbiamo detto, dietro le quinte istanzia un nuovo oggetto GNUTranslation a ogni chiamata a gettext.gettext. Ma anche la nuova API class-based utilizza un nuovo oggetto a ogni cambio di lingua.
Ora, un sistema di lazy string in genere collega una stringa a un oggetto “fornitore di differenti versioni” della stringa: questo “oggetto fornitore” è mutabile, per esempio un dizionario. Quando chiamate la stringa, dietro le quinte l’oggetto fornitore viene interrogato per sapere qual è la versione più recente da visualizzare. In effetti un oggetto GNUTranslation all’interno contiene proprio un dizionario: tuttavia finché non cambiate lingua, il dizionario resta invariato; e quando cambiate lingua il dizionario non viene semplicemente aggiornato, ma cambia proprio (perché entra in funziona un nuovo oggetto GNUTranslation).
Per esempio questa implementazione non è adatta a gettext, nonostante disponga perfino di una invitante funzione dal nome make_lazy_gettext. Come riporta la documentazione, se l’oggetto fornitore è un semplice dizionario, allora tutto bene: ma gettext è più complesso di così. Vale la pena di riprendere l’esempio della documentazione per capire il problema:
>>> from speaklater import make_lazy_gettext
>>> translations = {'hello': 'ciao'}
>>> lazy = make_lazy_gettext(lambda: translations.get)
>>> s = lazy('hello')
>>> print(s)
ciao
>>> translations['hello'] = 'ola!' # se gettext facesse questo...
>>> print(s)
ola!
Se gettext funzionasse in questo modo, sarebbe perfetto. Il problema è che invece, quando cambiate lingua, gettext cambia anche completamente il dizionario. E a questo punto, anche il collegamento esistente alla lazy string salta:
>>> # ...
>>> print(s)
ciao
>>> translations = {'hello': 'ola!'} # ...ma gettext fa *questo*!
>>> print(s)
ciao
>>> # ops.
Non è impossibile patchare o re-implementare gettext in modo da avere un unico oggetto che aggiorna il suo dizionario a ogni cambio di lingua. Alcune implementazioni che potete trovare su internet abbandonano proprio gettext in favore di soluzioni più "lazy-oriented".
Nel frattempo questa soluzione (da un’idea di Peter Otten) invece funziona:
class LazyTranslation:
    def __init__(self, message):
        self.message = message

    def __str__(self):
        if _ is LazyTranslation: # gettext non e' ancora attivo
            return self.message
        return _(self.message)

    @classmethod
    def install(cls):
        import builtins
        builtins.__dict__['_'] = cls 
L’idea è che la stringa da tradurre sia contenuta in una classe che ricalcola la traduzione ogni volta che ne si chiede una rappresentazione (__str__). Inizialmente userete l’alias _ per LazyTranslation (il metodo install fa questo per voi), in modo da marcare le stringhe come di consueto: quando poi gettext si installerà nella stessa variabile, anche LazyTranslation attiverà il suo meccanismo di traduzione dinamica:
LazyTranslation.install()  # adesso "_" e' LazyTranslation

s = _('una stringa')
print(s)  # gettext non attivo, stampa la stringa identica

import gettext
gettext.translation('myapp', 'locale', languages=['it']).install()
print(s)  # stampa la versione italiana della stringa

gettext.translation('myapp', 'locale', languages=['en']).install()
print(s)  # stampa la versione inglese della stringa
Dovete ricordare però che adesso LazyTranslation non è più una stringa vera e propria: se volete conservare le operazioni consuete sulle stringhe, dovete re-implementarle:
class LazyTranslation:
    # (...)
    def __len__(self):
        return len(self.message)
    # etc. etc.

Commenti

Post più popolari