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 certamente gettext 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 installato gettext, 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 naturalmente gettext 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.
Nel primo caso avete quindi qualcosa come
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:
Studiate che cosa vi offre il framework, prima di ricorrere a 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.

Commenti

Post più popolari