Localizzazione “che funziona” in Python

In modo da impratichirmi in questo tipo di faccende anche in Python, ho pensato di aggiungere al mio programma/esperimento Minesweeptk il supporto alla localizzazione linguistica. Conosco abbastanza bene il problema in C/C++ e, nei miei progetti professionali in quei linguaggi, uso da tempo la libreria GNU gettext. Su Python c’è il porting di questa libreria (modulo gettext), quindi mi sono subito sentito a casa.

Ispirandosi al principio di jobsiana memoria “it just works”, l’idea di fondo è che l’utente non debba fare nulla, se non lanciare il programma e vederselo comparire davanti con l’interfaccia nella propria lingua: niente comandi da menù e/o opzioni di configurazione! Nel caso in cui le traduzioni nella lingua dell’utente non siano disponibili (cosa credete, siamo pigri da queste parti: per adesso ci sono solo l’inglese e l’italiano ;-)) la lingua di default è l’inglese.

Come determinare la lingua da usare

gettext determina la lingua da usare in base alle variabili di ambiente LANGUAGE, LC_ALL, LC_MESSAGES e LANG, valutate in quest’ordine (ossia la prima che esiste è quella che conta). Queste variabili sono stringhe che si presentano nella forma

xx[_XX][.codifica]

dove:

  • xx è il codice della lingua (it per italiano, en per inglese etc.)
  • XX è il codice del paese, e serve a selezionare il simbolo della valuta, le convenzioni per le date e le ore etc. (Ad esempio: it_IT seleziona italiano e Italia, it_CH italiano e Svizzera, en_UK inglese e Regno Unito, en_US inglese e USA etc.). Se il paese non è indicato si adotta una scelta predefinita (quale? Boh…)
  • codifica è il tipo di codifica da usare per visualizzare i caratteri a video (ad esempio UTF-8, Windows-1252, cp850 etc.). Di solito dipende dalle impostazioni del terminale e/o del proprio ambiente grafico. Anche in questo caso, se non indicata si ricade su una scelta predefinita che, sui sistemi operativi moderni, è quasi sempre UTF-8.

Sui sistemi Linux (e, più in generale, su tutti i sistemi conformi allo standard POSIX) almeno la variabile LANG (l’ultima spiaggia…) è là, pronta ad indicare a gettext la giusta lingua. Ecco cosa succede sul mio sistema Ubuntu:

$ echo $LANG
it_IT.UTF-8

Su Windows e Mac OS X, invece, si può incappare in qualche problema.

Caso particolare: Windows

In Windows la variabile di ambiente LANG, per impostazione predefinita, non è presente. Quindi, a meno che l’utente non l’abbia impostata a mano, gettext finirà probabilmente per mostrare l’interfaccia utente in inglese.

Windows, ovviamente, conosce le informazioni sulla lingua dell’utente, ma queste stanno memorizzate in qualche chiave nel registro di sistema, ed è da lì che devono essere estratte. Fortunatamente a questo ci pensa il modulo Python locale o, meglio, la funzione locale.getdefaultlocale(). locale.getdefaultlocale() restituisce una coppia di valori: il nome del locale (la parte lingua_PAESE) e la codifica dei caratteri da utilizzare. A questo punto è sufficiente impostare la variabile di ambiente LANG in base a questi due valori e il gioco è fatto:

import locale, os
nomelocale, codifica = locale.getdefaultlocale()
os.environ[ 'LANG' ] = nomelocale
if codifica:
    os.environ[ 'LANG' ] += "." + codifica

Caso particolare: Mac OS X

Aspetta un momento: ma Mac OS X non è POSIX? Sì… anzi… lo è “quasi sempre”. Nel senso che se si apre un terminale la variabile LANG è impostata nel modo corretto e quindi, eseguendo un programma Python da terminale con il comando “python nomefile.py”, gettext è in grado di localizzare le traduzioni.

Ma se l’applicazione Python viene impacchettata con py2app (come nel caso del mio Minesweeptk), nessuna delle variabili d’ambiente osservate da gettext sarà impostata. Questo perché le applicazioni native per Mac OS X utilizzano un altro meccanismo per la localizzazione, e non la libreria gettext di GNU.

E, anche peggio, la soluzione indicata per Windows non risolve, perché la funzione locale.getdefaultlocale(), su Mac OS X, restituisce invariabilmente (None, None). Credo si tratti di un bug, anche se non ho trovato conferme. O, perlomeno, sarebbe fortemente auspicabile che tale funzione si comportasse in maniera analoga a come si comporta su Windows.

Allora, come fare?

Piuttosto che usare gettext le linee guida ufficiali indicano di utilizzare il meccanismo nativo di Mac OS X, attraverso il package PyObjC e qualche altra diavoleria. Ma, se si ha un’applicazione Python già funzionante su Linux e Windows e non si vuole riscrivere mezza roba solo per andare su Mac, la soluzione che ho escogitato è questa: ottenere le informazioni richieste lanciando un programma esterno che sappia dialogare con il sistema operativo. Il programma esterno in questione, che consente di accedere alle impostazioni predefinite dell’utente, è defaults (preinstallato su Mac OS X).

E il codice Python che è equivalente a quello già visto per Windows è:

import subprocess, os
pp = subprocess.Popen( [ 'defaults', 'read', '-g', 'AppleLocale' ], stdout = subprocess.PIPE )
os.environ[ 'LANG' ] = pp.communicate()[ 0 ].strip()

Ossia:

  • la seconda riga crea un processo che esegue il comando “defaults read -g AppleLocale” ed instrada l’output del nuovo processo su una pipe [il comando in oggetto stampa sul suo standard output la lingua scelta dall’utente corrente]
  • la terza riga preleva dalla pipe l’output del processo appena creato (pp.communicate()[0]), elimina eventuali spazi o new line in testa e in fondo (.strip()) e assegna il risultato alla variabile di ambiente LANG.

Missione compiuta.

Mettiamo i pezzi insieme

Inserendo in testa alla vostra applicazione Python il codice che segue, e lasciando il resto uguale alla versione che funziona su Linux, si è a posto, in prima approssimazione, su tutti i sistemi operativi:

import os
if not 'LANG' in os.environ:
    import sys
    if sys.platform == 'darwin':
        # Sotto sotto Mac OS X si chiama Darwin...
        import subprocess
        pp = subprocess.Popen( [ 'defaults', 'read', '-g', 'AppleLocale' ], stdout = subprocess.PIPE )
        os.environ[ 'LANG' ] = pp.communicate()[ 0 ].strip()
    else:
        # Questa parte viene sempre eseguita su Windows ma, se la variabile LANG
        # non fosse impostata, funziona anche su POSIX
        import locale
        loc, cp = locale.getdefaultlocale()
        os.environ[ 'LANG' ] = loc
        if cp:
            os.environ[ 'LANG' ] += '.' + cp
import gettext
gettext.bindtextdomain('myapplication', '/path/to/my/language/directory')
gettext.textdomain('myapplication')
_ = gettext.gettext
# ...
print _('This is a translatable string.')

Riferimenti

Si spinge fortemente il novizio, come ho fatto io…, a consultare la documentazione ufficiale Python sui moduli gettext, locale, subprocess ed os; gli articoli Python: gettext doesn’t load translations on Windows e How can you get the system default language/locale, in a py2app packaged Python app on Mac OS X?; la pagina del manuale in linea di defaults.

Ovviamente si sottintende una certa confidenza con gettext e gli strumenti correlati (comandi msg*).

PS: if you are interested in translating Minesweeptk into your own language, please add a comment to this post. I’ll contact you as soon as possible!

Advertisements

Rispondi

Inserisci i tuoi dati qui sotto o clicca su un'icona per effettuare l'accesso:

Logo WordPress.com

Stai commentando usando il tuo account WordPress.com. Chiudi sessione / Modifica )

Foto Twitter

Stai commentando usando il tuo account Twitter. Chiudi sessione / Modifica )

Foto di Facebook

Stai commentando usando il tuo account Facebook. Chiudi sessione / Modifica )

Google+ photo

Stai commentando usando il tuo account Google+. Chiudi sessione / Modifica )

Connessione a %s...