Progetti multilingua in Python (parte 9/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.
Considerazioni varie (parte seconda).
Più lingue contemporaneamente nella stessa applicazione.
Si può fare, certo. Non è uno scenario consueto e certamentegettext
non è nato con questo use case in mente, ma si può fare. La chiave qui è ricordare che quando “installate” gettext
, concretamente non fate altro che collegare il nome _
al metodo gettext
di un oggetto GNUTranslations
(creato da funzioni come gettext.translation
e gettext.install
). Non è necessario installare gettext
nei __builtins__
però: Python ha la nozione di namespace, e ogni namespace può avere il suo _
collegato a una diversa istanza di GNUTranslations
. Potete “installare” lingue diverse in ciascun modulo, in ciascuna classe, in ciascuna funzione:import gettext
class Foo:
_ = gettext.translation('myapp', 'locale', languages=['it']).gettext
def foo(self):
_ = self._
print(_('una stringa'))
def bar():
_ = gettext.translation('myapp', 'locale', languages=['en']).gettext
print(_('una stringa'))
Foo().foo() # stampa la versione italiana della stringa
bar() # stampa la versione inglese
In questo modo, per esempio, potete costruire un’interfaccia grafica dove ogni finestra è tradotta in una lingua diversa.Motivi occasionali per importare i moduli “tradotti” (unit test, documentazione…).
Come abbiamo visto, nelle prime fasi dello sviluppo del codice, quando la vostra preoccupazione è di marcare le stringhe per la traduzione ma non avete ancora installatogettext
, in genere risolvete il problema dei nomi _
e ngettext
mettendo in cima ai moduli qualcosa come:_ = lambda i: i
ngettext = lambda i, j, k: i
In seguito, quando avviate il meccanismo di gettext
, togliete queste definizioni per non “schermare” i nuovi __builtins__
installati da gettext
. Adesso la vostra applicazione funziona, se parte dal giusto entry-point.Ci sono però diverse altre occasioni in cui il vostro codice potrebbe non essere eseguito “nell’ordine giusto”: una suite di test come
unittest
potrebbe importare ed eseguire il codice di un modulo qualsiasi; la stessa cosa potrebbe fare un tool di documentazione come Sphinx per estrarre le docstring; e così via. Questi strumenti inciampano in una raffica di NameError
, visto che non riescono a trovare _
o ngettext
definiti da nessuna parte.Non ci sono delle soluzioni veramente eleganti. Per esempio, potete iniettare voi stessi delle lambda noop nei
__builtins__
all’inizio di un modulo di test:# modulo test.py
import unittest
import builtins
builtins.__dict__['_'] = lambda i:i
builtins.__dict__['ngettext'] = lambda i, j, k: i
from main import * # il modulo che volete testare
class TestMain(unittest.TestCase):
# etc etc
Oppure potete lasciare le lambda nei vari moduli “tradotti”, disattivandole in base a una costante globale:from settings import GETTEXT_IS_ACTIVE
if not GETTEXT_IS_ACTIVE:
_ = lambda i: i
ngettext = lambda i, j, k: i
# etc etc
In questo modo potete impostare la costante globale a True
solo prima di avviare “davvero” la vostra applicazione, lasciandola invece a False
per le normali operazioni di sviluppo, test, etc.
Testare gettext
.
Non dovreste testare gettext
, per lo stesso motivo per cui non dovreste testare math.sqrt
: è il vostro codice che dovete testare, non quello della libreria standard di Python.Piuttosto, occasionalmente potrebbe servirvi “testare” le traduzioni nel senso di avere comunque qualcosa da vedere anche quando i cataloghi non sono ancora pronti: per esempio, per avere un riscontro visivo del cambiamento dello stato di una GUI quando cambiate lingua. Oppure, potreste voler disporre di un valore prevedibile della stringa “tradotta”, da poter inserire in un unit test.
In casi del genere è facile sostituire gli oggetti di
gettext
con dei mock. Questo per esempio “traduce” una stringa anteponendole il codice della lingua. Se non c’è nessuna lingua impostata (perché non avete passato un parametro languages
né avete impostato una variabile d’ambiente opportuna), allora antepone --
alla stringa “tradotta”.import gettext
class DummyGNUTranslations(gettext.NullTranslations):
def __init__(self, language):
self._language = language.upper()
def gettext(self, message):
return self._language + message
def dummy_translation(domain, localedir=None, languages=None,
class_=None, fallback=False, codeset=None):
language = '--' # no language found!
if languages is None:
import os
for envar in ('LANGUAGE', 'LC_ALL', 'LC_MESSAGES', 'LANG'):
val = os.environ.get(envar)
if val:
language = val.split(':')[0]
break
else:
language = languages[0]
return DummyGNUTranslations(language)
gettext.translation = dummy_translation # mock di translation
gettext.translation('myapp', 'locale', languages=['it']).install()
print(_('una stringa')) # stampa "ITuna stringa"
gettext.translation('myapp', 'locale', languages=['en']).install()
print(_('una stringa')) # stampa "ENuna stringa"
gettext.install('myapp', 'locale')
# stampa "--una stringa" se nessuna variabile d'ambiente e' impostata
print(_('una stringa'))
import os
os.environ['LANG'] = 'it'
gettext.install('myapp', 'locale')
print(_('una stringa')) # stampa "ITuna stringa"
Tradurre i campi di un database.
Un’applicazione è talvolta molto più del semplice codice, e una stringa da tradurre può arrivare da posti diversi di un modulo Python. Uno scenario tipico è quando il testo da tradurre è conservato in un database: qui naturalmentegettext
non può arrivare.Va detto che nella maggior parte dei casi, se siete in questo scenario, state anche usando un framework in grado di darvi una mano. Per esempio, considerate l’applicazione web di un giornale online: un contributor carica un articolo che finisce nel database, i traduttori ne producono altre versioni (che stanno sempre nel database), e infine il framework si incarica di mostrare all’utente la versione desiderata. Per compiti del genere, Django ha diverse soluzioni: tra queste, Django Parler e Django Modeltranslation che implementano le due strategie di basso livello di cui parleremo tra poco.
Se non usate nessun framework, come minimo volete accedere al database attraverso un toolkit/ORM come SqlAlchemy. Anche in questo caso, controllate prima se non esistano soluzioni già pronte. Per esempio per SqlAlchemy ci sono SqlAlchemy-i18n e una delle SqlAlchemy-utils, che adottano ciascuno uno dei due approcci normalmente usati in questi casi.
Se non usate neppure un ORM… probabilmente vi conviene ripensarci. Implementare da zero a mano il supporto per la i18n del database non è proprio semplicissimo. Inoltre, se avete bisogno di fornire traduzioni, in genere significa che la vostra è già un’applicazione “importante”, e allora davvero volete affidarvi a soluzioni artigianali? Detto questo, tenente conto che ci sono due approcci tipici per risolvere il problema:
- creare, per ogni tabella da tradurre, tante tabelle “tradotte” quante sono le lingue, con una foreing key verso la tabella “da tradurre”;
- creare, per ciascun campo da tradurre, tante colonne “tradotte” nella stessa tabella quante sono le lingue.
CREATE TABLE articles (id, author, title, text);
CREATE TABLE articles_it (id, title, text, article_id);
CREATE TABLE articles_fr (id, title, text, article_id);
E nel secondo caso:CREATE TABLE articles (id, author, title, title_it, title_fr,
text, text_it, text_fr);
Il primo approccio ha il vantaggio di non modificare la tabella originale; è poi facile aggiungere una nuova lingua all’occorrenza (basta creare una nuova tabella). Lo svantaggio principale è che ogni query che richiede un campo tradotto ha bisogno di un join alla tabella secondaria, e i join notoriamente sono più lenti.Il secondo approccio ha il vantaggio di non aver bisogno di join per la richiesta dei campi tradotti. Tuttavia la tabella finisce per avere un gran numero di colonne, e per aggiungere una lingua occorre fare una
ALTER TABLE
.In genere, se avete bisogno (come probabile) di poche lingue che potete decidere all’inizio, allora il secondo approccio dovrebbe essere preferibile. Se invece prevedete un gran numero di lingue che si aggiungeranno nel tempo, potreste provare il primo approccio.
Tradurre stringhe all’interno di file di testo.
È veramente difficile immaginare uno scenario in cui sia utile inserire stringhe da tradurre in un file di configurazione. Dopo tutto, questi file sono fatti perché l’utente possa modificarli: e se l’utente modifica una stringa da tradurre, allora dovrebbe anche estrarre di nuovo il catalogo.po
, ri-tradurselo e ri-compilarlo. Viceversa, se non volete che l’utente modifichi una stringa da tradurre, allora perché inserirla in un file di configurazione? Mettete la stringa in un settings.py
invece che in un settings.ini
, e traducetela con gettext
come al solito.Un caso speciale sono le varie resources (tipicamente, file xml) che i gui framework possono usare per definire gli elementi dell’interfaccia: questi file contengono tra l’altro anche le varie etichette di pulsanti, menu, etc. Va detto che il framework di solito ha già i meccanismi necessari per tradurre questi file. Per esempio, in wxPython una XmlResource viene automaticamente passata attraverso
wx.GetTranslation
al momento del caricamento. In PyQt, QtDesigner ha la possibilità di produrre file .ui
traducibili.Un altro caso verosimile è quando la stringa da tradurre si trova all’interno di un template. Di nuovo, qui è probabile che stiate usando un framework che già offre soluzioni al riguardo. Per esempio, sia Django che Flask non hanno problemi a tradurre stringhe dentro i template html, o ai file di codice javascript.
Tuttavia una web-app non è l’unico scenario possibile per voler usare un template. In ogni caso, se state usando un template engine, allora è probabile che esista già una soluzione. Per esempio, Jinja2 ha una estensione i18n che fa il lavoro. Se non state usando un template engine per produrre i vostri template… probabilmente vi conviene ripensarci. È un altro di quegli scenari in cui reinventare la ruota non serve a niente.
Detto questo, probabilmente vi conviene cercare di limitare il più possibile l’uso di stringhe “statiche” (da tradurre o meno) all’interno dei template. Dopo tutto, sono appunto dei template: potete mettere queste stringhe in un modulo Python, tradurle con
gettext
e iniettare poi il risultato nel template.Infine, se i file di testo non sono dei template, oppure è scomodo gestirli con un template engine, oppure se proprio preferite evitare il template engine e fare tutto a mano, allora probabilmente l’ultima risorsa che fa al caso vostro è Babel. Questo toolkit estrae stringhe da tradurre da normali moduli Python, ma oltre a questo ha la possibilità di estrarre da altri file: tipicamente si tratta di template html, ma potete scrivere delle “regole di estrazione” personalizzate per qualsiasi tipo di file.
Uso di framework.
Se la vostra applicazione utilizza un framework per l’interfaccia con l’utente, controllate fin da subito se questo dispone già di strumenti propri per il supporto i18n. Per esempio:django.utils.translation
in Django,- Flask Babel per Flask,
pyramid.i18n
in Pyramid,wx.GetTranslation
in wxPython,QTranslator
in PyQt,sphinx-intl
per Sphinx,- eccetera.
gettext
: spesso si tratta di soluzioni più integrate e su misura per le esigenze del framework, e vale la pena di usare quelle.Librerie e programmi esterni.
Oltre agli strumenti di GNU Gettext che abbiamo già visto, ci sono alcune librerie Python che possono aiutarvi con la manipolazione dei cataloghi.po
. Una è Polib che presenta un’interfaccia Python per esplorare, modificare e creare i cataloghi. Un’altra è Babel, che è un toolkit più completo per il supporto i18n e l10n dei progetti Python.Un problema separato sono gli strumenti specifici per i traduttori, che raramente sono in grado di lavorare direttamente sui file
.po
senza rischiare di danneggiarli. L’interfaccia più usata è Poedit, un editor grafico di cataloghi pensato soprattutto per i traduttori.Se poi la traduzione di applicazioni diventa un business davvero complesso, potete dare un’occhiata a qualche translation manager più raffinato, in grado di gestire tutti i passaggi del lavoro in modo integrato: per esempio Transifex o Phraseapp, che però sono a pagamento.
nice blog.
RispondiElimina