Skip to content

Instantly share code, notes, and snippets.

@ricpol
Last active March 22, 2023 15:24
  • Star 5 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save ricpol/109139e14b9835d479ba03ee13e170f6 to your computer and use it in GitHub Desktop.
Come tradurre il codice Python con gettext.

**NOTA IMPORTANTE Questi appunti sono ormai molto datati e incompleti. Li lascio qui per ragioni storiche e affettive, ma davvero, non prendeli alla lettera e non sorprendetevi se ci sono cose sbagliate. Il mio libro Python in Windows parla di tutte queste cose, e di molto altro ancora, in modo più aggiornato, completo e corretto. Vi suggerisco almeno di scaricare il capitolo introduttivo gratuito per farvi un'idea degli argomenti trattati. **

Progetti Python multi-lingua con Gettext.

Il supporto di Python per la traduzione multi-lingua del codice, specialmente in ambiente Windows, è... un po' ruvido. A peggiorare le cose, le guide e gli articoli che si trovano in giro sono spesso approssimativi e quasi sempre pensati per il mondo Linux. Chi scrive ha messo da parte nel tempo un po' di appunti, trucchi e scorciatoie: raccogliamo le cose essenziali da sapere. Questa non è una guida completa, ma può essere un buon punto di partenza. La piattaforma di riferimento è Windows, ma in pratica il 95% di quello che diremo vale dappertutto.

Indice.

  • Cose da sapere prima di tutto.
    • Internazionalizzazione e localizzazione.
    • Che cosa tradurre, che cosa no.
  • Primo: marcare le stringhe per la traduzione.
    • Problemi nella marcatura delle stringhe.
    • Marcatura di forme plurali (prima parte: il codice).
  • Secondo: produrre il template .pot del catalogo.
  • Terzo: produrre i cataloghi .po per i traduttori.
  • Quarto: compilare i cataloghi (da .po a .mo).
  • Uso di GNU Gettext per Windows.
    • Differenze tra pygettext e xgettext.
    • Marcatura di forme plurali (seconda parte: cataloghi).
    • Aggiornare i cataloghi.
  • Quinto: attivare gettext nel codice Python.
    • Riassunto e preparazione.
    • Due scenari per la traduzione.
    • Alcune osservazioni su locale.
    • Primo scenario: tradurre un'applicazione "monolingua".
    • Secondo scenario: tradurre un'applicazione "plurilingua".
      • Uso di gettext.translation.
      • Uso di gettext.install.
      • Uso di gettext.find.
  • Deferred translation.
    • Che cosa sono le Deferred translation.
      • Primo metodo di deferred translation.
      • Secondo metodo di deferred translation.
    • Valutazione delle variabili tradotte e deferred translation.
    • Cambio di lingua a runtime e deferred translation.
    • Deferred translation fatte bene.
  • Considerazioni varie.
    • Troppi literals nel codice?
    • Problemi con la "lingua di default".
    • Conoscere la lingua usata a runtime.
    • Traduzioni, GUI e design MCV.
    • Lingua, locale, layout.
    • Altre considerazioni sul cambio di lingua a runtime.
    • Più lingue contemporaneamente nella stessa applicazione.
    • Motivi occasionali per importare i moduli "tradotti" (unit test, documentazione...).
    • Testare gettext.
    • Tradurre i campi di un database.
    • Tradurre stringhe all'interno di file di testo.
    • Uso di framework.
    • Librerie e programmi esterni.

Cose da sapere prima di tutto.

Riassumiamo in breve gli aspetti preliminari che dovreste già conoscere, almeno in linea generale.

Internazionalizzazione e localizzazione.

Sono due concetti diversi. La "localizzazione" (localization, in breve l10n) riguarda gli adattamenti che un programma deve fare per adeguarsi alle convenzioni in uso nel "locale" dell'utente. La l10n riguarda aspetti diversi, dal modo in cui si scrivono i numeri (i separatori decimali e i raggruppamenti delle cifre), a quello in cui si scrivono le date, alle convenzioni per i valori monetari. Il supporto di Python per la l10n è nel modulo locale della libreria standard. Potete dare un'occhiata alla documentazione per farvi un'idea del problema (e delle soluzioni). Questa guida non riguarda la l10n: tuttavia dovremo tornare sul modulo locale perché ci sono dei dettagli importanti anche per noi.

La "internazionalizzazione" (internationalization, in breve i18n) riguarda in modo specifico la traduzione del codice (almeno per le parti che devono essere visibili per l'utente finale) in lingue differenti. Il supporto di Python per la i18n è nel modulo gettext della libreria standard, che offre in buona sostanza i meccanismi per selezionare la "lingua corrente" e tradurre di conseguenza quel che c'è da tradurre in modo trasparente.

Un programma potrebbe quindi essere "localizzato" ma non "tradotto", o viceversa. È una scelta vostra, se fornire o meno il supporto per questi aspetti. Non fatevi ingannare dall'uso generico del termine "localizzato", in Italiano, per dire "tradotto". In questa guida useremo sempre i termini più specifici.

Che cosa tradurre, che cosa no.

In breve: dovete tradurre tutto quello che l'utente finale dovrà vedere, ma non una riga di più. Se il vostro programma ha un'interfaccia grafica (una GUI), traducete le etichette dei pulsanti e dei menu, per esempio.

Non traducete le docstring, non traducete i messaggi che finiscono nei log, non traducete i messaggi delle eccezioni (cose del tipo: raise MyException('something went wrong!'), etc.). Se scrivete una libreria di backend, in genere non dovete tradurre nulla.

In ogni caso, tenete presente che marcare una stringa per la traduzione è una cosa diversa da tradurla (vedremo presto questo nel dettaglio). Non siete obbligati a tradurre concretamente, ma è utile predisporre comunque il meccanismo di traduzione nell'eventualità che in futuro qualcuno (magari voi stessi) abbia voglia di mettersi effettivamente a tradurre. Aggiungere una traduzione a cose fatte è molto semplice. D'altra parte è lungo e noioso modificare il codice per inserire i meccanismi per la traduzione, una volta che il progetto è finito.

Primo: marcare le stringhe per la traduzione.

Questa è la parte più semplice, ed è anche il solo aspetto di cui vi occuperete per la maggior parte del tempo.

Per convenzione, una stringa da tradurre si marca "impacchettandola" nella funzione _: proprio così, il nome della funzione è un singolo carattere di undescore. Questo mantiene il codice il più compatto e leggibile possibile. Di conseguenza:

print('hello world!')    # questa stringa non sara' tradotta 
print(_('hello world!')) # questa invece si'

Fin quando siete impegnati nella scrittura del codice, marcare le stringhe in questo modo è tutto quel che dovete fare. Non è il caso di impostare il meccanismo di gettext fin da subito. In particolare, non è necessario importare gettext all'inizio dei vostri moduli.

L'unica astuzia necessaria, fin quando non decidete di attivare il meccanismo di traduzione di gettext, è dare un significato provvisorio alla variabile _: altrimenti, quando eseguite il codice ottenete una raffica di NameError. Potete dire a Python che _ è una noop, almeno per il momento: all'inizio di tutti i moduli che contengono stringhe marcate, aggiungete questa riga:

_ = lambda i: i #TODO cancellare questo quando gettext entra in funzione.

Una nota a margine: molte guide consigliano di inserire invece snippet come import gettext; _ = gettext.gettext. Funziona, ma non è molto furbo. Si tratta comunque di una soluzione provvisoria perché, come vedremo, in genere volete usare metodi più raffinati per installare i meccanismi di gettext.

Problemi nella marcatura delle stringhe.

Se volete marcare per la traduzione stringhe interpolate, potete farlo. Ma ricordate che le nuove comodissime "f-string" non funzionano con i meccanismi che useremo più tardi. Quindi, per esempio:

name = 'Mario'
age = 20
s = _(f'Sono {name} e ho {age} anni') # questo purtroppo non si puo' fare
s = _('Sono {name} e ho {age} anni').format(name=name, age=age) # ok

Quando interpolate più di una variabile con format, dovreste sempre indicarle esplicitamente senza fidarvi dell'ordine in cui sono inserite: nella traduzione, in qualche lingua, l'ordine potrebbe non essere mantenuto identico. Quindi:

s = _('Ho {} anni.').format(age) # bene
s = _('Ho {age} anni.').format(age=age) # meglio, aiuta i traduttori
s = _('Sono {name} e ho {age} anni').format(name=name, age=age) # ok
s = _('Sono {} e ho {} anni').format(name, age) # decisamente no

In quale lingua dovrebbe essere scritta la stringa "originale" nel codice? Detto francamente: in Inglese. Chiaramente per noi è più comodo pensare all'Italiano come lingua "di default" e poi fornire la traduzione in Inglese e altre lingue. Potete farlo, certo: ma la regola è che il codice andrebbe sempre scritto in Inglese.

Se scrivete caratteri non-ASCII (lettere accentate, etc.) nelle stringhe marcate per la traduzione, allora dovete inserire la dichiarazione di encoding all'inizio del modulo. Notate che questo non dovrebbe essere necessario: se manca la dichiarazione di encoding, Python 3 suppone che il modulo sia encodato in utf-8. Purtroppo però i meccanismi di gettext si rompono se non trovano la dichiarazione esplicita. Quindi:

# -*- coding: utf-8 -*-

s = _('Questa -è- una stringa non-ascii.')

In alcuni casi volete lasciare un commento per i traduttori: è utile quando la stringa è breve e risulta ambigua o enigmatica se vista fuori contesto. Potete inserire un normale commento Python immediatamente prima della stringa marcata:

# translators: this is a verb, not a noun. 
s = _('Exit')

Questo potrebbe aiutare qui a tradurre "Exit" con "Esci" invece di "Uscita". Il prefisso "translators:" è convenzionale, e si usa per segnalare a chi legge il codice che il commento è rivolto ai traduttori. Purtroppo però gli strumenti predefiniti di Python (pygettext, come vedremo tra poco) non rilevano la presenza di questi commenti e non li inseriscono quindi nel file riservato ai traduttori. Se per il vostro progetto i commenti sono fondamentali, dovete installare e usare altri strumenti (xgettext è l'alternativa più comune, e ne faremo cenno più in là).

Marcatura di forme plurali (prima parte: il codice).

Il primo consiglio qui è: cercate di evitare questo problema, se potete. Le forme plurali sono un ginepraio di complicazioni.

Se una stringa ha bisogno di essere variata al plurale, e non dovete tradurla, potete naturalmente discriminare con qualcosa del genere:

num = 3
if num == 1:
   print('Mario ha {} anno.'.format(num))
else:
    print('Mario ha {} anni.'.format(num))

Se questa stringa deve essere tradotta, potete mantenere questa strategia e lasciare due stringhe separate. Tuttavia gettext supporta le varianti plurali in modo più furbo, con la funzione ngettext:

num = 3
s = ngettext('Mario ha {} anno.', 'Mario ha {} anni.', num)
print(s.format(num))

Come si vede, ngettext vuole 3 argomenti: la stringa nella versione singolare, la stringa nella versione plurale, e la variabile discriminante. Una volta che ngettext ha selezionato la versione giusta a seconda di quanto vale num, occorre naturalmente ancora interpolare la stringa con il consueto metodo format.

Se usate ngettext nel vostro codice, occorre evitare i NameError fin quando non sarete pronti ad attivare il meccanismo di gettext. Potete importare direttamente il nome: from gettext import ngettext. Oppure potete provvisoriamente trasformare anche ngettext in una noop che seleziona sempre il primo elemento:

ngettext = lambda i, j, k: i # TODO via questa riga quando attivo gettext

Vale la pena ripeterlo: le stringhe plurali sono complicate e andrebbero evitate quando possibile. Oltre alla necessità di usare una funzione diversa dalla normale _, i plurali sono più complessi da tradurre, anche perché molte lingue prevedono più di una forma di plurale (ne riparleremo). In genere, semplificare un po' è sempre possibile: "Anni di Mario: {num}." risolve il problema con una sola stringa senza ricorrere a meccanismi complicati.

Il meccanismo di ngettext resta comunque valido anche nei casi in cui, per coincidenza, la forma singolare e plurale sono uguali. Per esempio, dovete comunque scrivere:

s = ngettext('Ho visto {} re.', 'Ho visto {} re.', num)

Anche se in Italiano "re" è invariabile, altre lingue faranno distinzione ("king/kings", etc.).

Infine, tenete conto che purtroppo gli strumenti predefiniti di Python non sono in grado di produrre un catalogo con le stringhe plurali (vedremo subito che cosa è un catalogo, e quali sono gli strumenti predefiniti di Python, non preoccupatevi). Quindi, anche se gettext supporta senza problemi le stringhe plurali una volta che il catalogo è stato creato, tuttavia se volete usarle dovete installare degli strumenti separati (vedremo quali) per produrre i cataloghi.

Secondo: produrre il template .pot del catalogo.

Quando avete marcato "abbastanza" stringhe per la traduzione, è ora di iniziare a produrre i file che darete ai traduttori. Non è necessario completare il codice prima di tradurlo: potete cominciare a tradurre, e poi produrre integrazioni e revisioni man mano che il lavoro va avanti. D'altra parte, i traduttori potrebbero stancarsi di star dietro alle vostre revisioni, quindi dovreste fare attenzione a non cominciare troppo presto il lavoro di produzione dei cataloghi.

L'obiettivo è produrre un file di traduzione per ciascuna lingua (oltre all'originale con cui avete scritto il codice). Il primo passo però è quello di produrre un template da cui ricavare poi i singoli file.

Tradizionalmente il template ha estensione .pot, e i singoli file per ciascuna lingua hanno lo stesso nome del template ma con estensione .po. Alcuni tool producono subito un template con estensione .po. In ogni caso, si tratta sempre di semplici file di testo.

Python dispone di un piccolo script che scansiona il vostro codice e produce un template .pot. In Windows si trova in .../Python36/Tools/i18n/pygettext.py. Per testarlo, scriviamo prima di tutto un modulo Python che riassume alcuni tipi di stringhe di cui abbiamo parlato finora:

# -*- coding: utf-8 -*-
_ = lambda i: i   # TODO eliminare in seguito...
ngettext = lambda i, j, k: i   # TODO eliminare in seguito...

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

    # translators: questo e' un commento
    print(_('stringa commentata versione originale'))

if __name__ == '__main__':
    main()

Chiamate questo file test.py, fatelo girare e controllate che in effetti funziona: senza gettext né traduzioni ancora installate, semplicemente i vari print producono le stringhe originali, interpolate dove occorre.

Quindi, dalla shell invocate pygettext.py:

> py "C:/Program Files/Python36/Tools/i18n/pygettext.py" -d myapp test.py

Se non volete scrivere ogni volta tutto il percorso potete aggiungere la directory alla PYTHONPATH, creare un collegamento o semplicemente copiare lo script nella directory corrente. L'opzione -d specifica il "dominio" del catalogo da creare: in Windows come vedremo questo non è molto importante, ma per compatibilità con il mondo Linux conviene specificarlo. In pratica potete passare il nome della vostra applicazione (qui myapp): l'effetto concreto è che il catalogo prodotto sarà myapp.pot. Infine, passate come argomenti i nomi dei file che volete scansionare. Se avete marcato più di un file, non vi conviene generare cataloghi separati per ciascuno: passate invece tutti i nomi dei file, in successione (pygettext.py -d myapp foo.py bar.py baz.py). Ci sono altre opzioni che potete scoprire con la documentazione online (pygettext.py --help).

Il template myapp.pot sarà creato nella directory corrente. Apritelo con un editor e trovere qualcosa del genere:

# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR ORGANIZATION
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"POT-Creation-Date: 2018-03-31 19:06+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=cp1252\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: pygettext.py 1.5\n"


#: test.py:7
msgid "stringa semplice versione originale"
msgstr ""

#: test.py:8
msgid "stringa non-ascii: èéàòì versione originale"
msgstr ""

#: test.py:9
msgid "stringa interpolata {} versione originale"
msgstr ""

#: test.py:10
msgid ""
"stringa\n"
"            multilinea\n"
"            versione originale"
msgstr ""

#: test.py:21
msgid "stringa commentata versione originale"
msgstr ""

Il vostro compito adesso è modificare questo template, prima di copiarlo e distribuirlo ai traduttori. Nelle prime righe di commento, sostituite il titolo del progetto e i metadati relativi: potete aggiungere righe di commento a piacere. Il resto del file è composto da coppie msgid / msgstr che contengono rispettivamente le stringhe originali estratte dal vostro codice, e le traduzioni da inserire. Le stringhe sono delimitate da virgolette doppie (escapabili all'occorrenza: \"). Una stringa multilinea si crea dalla concatenazione di più stringhe successive (senza righe bianche in mezzo).

La prima coppia msgid / msgstr è destinata per convenzione a "tradurre" la stringa vuota "": la "traduzione" contiene ancora dei metadati, alcuni dei quali spetta al traduttore completare. La cosa importante da notare è la riga:

"Content-Type: text/plain; charset=cp1252\n"

In effetti pygettext produce file .pot nell'encoding del sistema (per un Windows italiano, cp1252 appunto) e non in utf-8. Questo succede anche se la dichiarazione di encoding nei moduli Python è per utf-8. Potete controllare con il vostro editor che in effetti il file .pot è salvato in cp1252.

Qui si pone un problema, specialmente in Windows. Se il traduttore siete voi stesso, fate un po' come credete. Ma se distribuite il file a un traduttore esterno, dovete capire che probabilmente lo aprirà con il Blocco Note, se non addirittura con Word. In questo caso, l'encoding cp1252 tutto sommato potrebbe andar bene: è difficile spiegare a un non-tecnico come aprire e salvare file utf-8 con il Blocco Note. Tenete conto però che se mandate il file a un traduttore all'estero, non è detto che il suo Blocco Note usi cp1252 di default (già nei Paesi dell'Est Europa, per esempio, non è così).

È una questione di convenienza, vostra e dei traduttori. Se preferite avere utf-8, non vi resta che cambiare l'encoding del file con il vostro editor, e ri-salvarlo. In questo caso, correggete per scrupolo anche l'indicazione del charset dentro il file. Dovete però spiegare ai vostri traduttori come usare un file encodato in utf-8 (o forse, meglio ancora, dire loro di installare un editor come Sublime Text o NotePad++ che lavorano nativamente in utf-8, e lasciar perdere il Blocco Note). Va detto comunque che esistono anche degli editor grafici specializzati, riservati ai traduttori, che permettono di lavorare sui cataloghi senza aprire direttamente il file: ne riparleremo.

Le successive coppie msgid / msgstr contengono le stringhe da tradurre, precedute da una riga di commento che indica il file e la riga di provenienza (ai traduttori non importa, ma a voi può far comodo). Notate che, come abbiamo detto, pygettext non supporta i commenti "translator" che avete inserito nel codice. Inoltre pygettext non supporta le stringhe plurali.

Per il momento non dovete toccare nulla oltre ai metadati. Una volta apportate le modifiche opportune, salvate e chiudete il template .pot.

Terzo: produrre i cataloghi .po per i traduttori.

Questa è la parte più semplice, almeno per voi. I file per i traduttori sono delle copie del template .pot. Per convenzione, dovreste cambiare la loro estensione in .po, conservando il nome identico. Forse vi conviene temporaneamente scegliere l'estensione .po.txt, se i vostri traduttori non sono esperti. In ogni caso, quando il traduttore vi riconsegna il file tradotto, cambiate l'estensione in .po.

Il traduttore dovrebbe compilare i metadati di sua competenza, e soprattutto tradurre, si capisce. Da parte vostra, dovete fare attenzione che i traduttori rispettino le parentesi di interpolazione nelle stringhe, riportandole anche nelle traduzioni.

Quarto: compilare i cataloghi (da .po a .mo).

Per essere utilizzabili da gettext, i cataloghi .po devono essere compilati in un formato binario. Python dispone di uno script adatto allo scopo:

> py "C:/Program Files/Python36/Tools/i18n/msgfmt.py" myapp.po

Lo script msgfmt.py produce un catalogo compilato con estensione .mo: nel nostro caso, myapp.mo. Ripetete l'operazione per ciascun catalogo, nelle varie lingue (attenzione a non confondervi, visto che hanno tutti lo stesso nome!).

Dove devono stare questi cataloghi compilati (che, ripetiamo: hanno tutti lo stesso nome)? Per convenzione, stanno in una directory locale che ha una struttura interna ben precisa. Supponiamo che la nostra applicazione sia (per il momento) tradotta in due lingue: inglese e francese. Allora la directory locale ha questa struttura:

locale 
      |- en
      |    |- LC_MESSAGES
      |                  |- myapp.mo, myapp.po
      |
      |- fr
           |- LC_MESSAGES
                         |- myapp.mo, myapp.po

In sostanza, dentro locale c'è una directory per ciascuna lingua (indicata con il consueto codice a due lettere). Dentro la directory della lingua, c'è solo una directory LC_MESSAGES, e dentro questa vive il nostro catalogo compilato myapp.mo. Per convenienza, potete conservare lì anche il catalogo non compilato myapp.po, ma a gettext non serve. Fin quando sviluppate il codice, va bene così; quando in futuro vorrete distribuirlo, toglierete i file .po lasciando solo i cataloghi compilati.

E la directory locale, dove deve stare? Qui c'è un intoppo. Nel mondo Linux, la consuetudine è avere una locale unica, centralizzata, dentro cui vivono i cataloghi di molti programmi: per questo è importante che i cataloghi abbiano un nome univoco (un "dominio", come abbiamo visto). Dentro locale/en/LC_MESSAGES, per esempio, potete trovare molti cataloghi oltre a quello della vostra applicazione. In Windows però questa consuetudine non esiste. Potete mettere locale nella directory/package principale della vostra applicazione, o anche un livello più su, se preferite mantenere solo il codice Python dentro la directory/package:

myapp
     |- myapp
     |       |- (e qui tutti i moduli Python)
     |- locale
              |- (e qui tutti i cataloghi)

Se volete sviluppare in modo cross-platform, avete un piccolo problema di distribuzione: in Windows potete mantenere locale "vicina" all'applicazione, ma in Linux dovreste mettere i cataloghi nella directory centralizzata. Il problema è che questa directory varia a seconda delle diverse distribuzioni. Il punto è che comunque potete dire a Python di cercare le traduzioni in una directory specifica, come vedremo (e in Windows è senz'altro necessario): potete seguire lo schema di Windows anche su Linux, se non volete starci troppo a pensare. Del resto, molte applicazioni cross-platform in Linux fanno proprio questo, tenendosi "vicini" i propri cataloghi senza cercare di indovinare caso per caso dov'è la directory centralizzata.

Una volta predisposta la directory locale e compilati i cataloghi .mo, siamo pronti per attivare il meccanismo di gettext nel nostro programma Python. Prima di vedere come si fa, conviene adesso aprire una parentesi su qualche strumento più avanzato rispetto ai rudimentali script pygettext.py e msgfmt.py che abbiamo usato finora.

Uso di GNU Gettext per Windows.

Probabilmente già lo sapete: questo meccanismo di gestire le traduzioni non è idiomatico di Python, ma è un porting di GNU Gettext. Lo script pygettext.py gestisce solo i casi più semplici: se avete bisogno di strumenti più complessi, dovete scaricare e installare Gettext per Windows. In sostanza si tratta di una serie di utility a riga di comando: se scaricate l'installer, la directory di installazione viene aggiunta alla path di sistema e quindi dalla shell potete fare cose come:

> xgettext -d myapp main.py

invece di scrivere tutto il percorso del programma da invocare. Se scaricate lo zip, potete aggiungere manualmente la directory alla path di sistema, se volete. In sostanza le due versioni sono identiche.

Quando avete installato GNU Gettext for Windows, potete usare:

  • xgettext per produrre cataloghi .po(t), al posto di pygettext.py;
  • msgfmt per produrre cataloghi compilati .mo, al posto di msgfmt.py.

Differenze tra pygettext e xgettext.

xgettext è la utility che produce cataloghi .pot scansionando il vostro codice, analogamente a pygettext che abbiamo già visto. L'uso di questo tool è del tutto analogo a pygettext, e vi rimandiamo alla guida on line per i dettagli. Le differenze notevoli tra i due strumenti sono:

  • pygettext produce file con estensione.pot, xgettext produce subito file .po (è solo una differenza estetica, comunque: in sostanza si tratta comunque di cataloghi-template).
  • pygettext ignora i commenti "per traduttori", xgettext li include (se passate l'opzione -c alla riga di comando).
  • pygettext produce file encodati nell'encoding locale, xgettext produce file in utf-8.
  • xgettext processa le stringhe plurali: e su questo dobbiamo fare un approfondimento.

Marcatura di forme plurali (seconda parte: cataloghi).

Quando avete marcato delle stringhe con la forma plurale, nel modo che abbiamo visto, xgettext produce queste indicazioni nel catalogo .po(t):

#, fuzzy
msgid ""
msgstr ""
(...)
"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"

(...)

#: main.py:14
msgid "Stringa singolare {} versione originale"
msgid_plural "Stringa plurale {} versione originale"
msgstr[0] ""
msgstr[1] ""

Due cose spiccano subito: l'aggiunta di un nuovo header Plural-Forms:... nell'intestazione del file, e il modo particolare di riportare le stringhe che prevedono il plurale.

Iniziamo dal secondo aspetto: invece della consueta coppia msgid / msgstr, troviamo due "id" per la stringa: uno per il singolare, uno per il plurale. I campi lasciati vuoti per la traduzione (msgstr) sono numerati con gli indici di un array, partendo da 0. Per cominciare, xgettext inserisce due campi di default, perché la maggior parte delle lingue ha una forma per il singolare e una per il plurale. In questi casi di solito si intende che msgstr[0] sia destinata alla traduzione della forma singolare, e msgstr[1] alla forma plurale.

Precisiamo subito che cosa intendiamo per "di solito", descrivendo l'header Plural-Forms. È vostro compito riempire questo header con i valori appropriati per la lingua della traduzione, prima di compilare il catalogo. Il primo campo, nplurals=INTEGER, deve indicare il numero di forme singolari/plurali: di solito due (singolare e plurale), per le lingue a noi più familiari come l'Italiano o l'Inglese. Questo numero deve corrispondere alle dimensioni dell'array delle scelte msgstr. Quindi, se indicate nplurals=4 nell'header, per ciascuna stringa plurale dovete predisporre quattro campi da riempire per i traduttori: msgstr[0], msgstr[1], msgstr[2] e msgstr[3].

Il secondo campo, plural=EXPRESSION, è un po' più complicato. Deve contenere un'espressione con la sintassi del linguaggio C, che contiene una variabile n. Questa variabile riceve a runtime il valore del terzo argomento della funzione Python ngettext che abbiamo già visto. Quindi, supponiamo per esempio che nel vostro codice abbiate scritto:

num = 3
s = ngettext('Mario ha {} anno.', 'Mario ha {} anni.', num)
print(s.format(num))

In questo caso la variabile n dell'header plural del catalogo riceve il valore 3. L'espressione deve restituire un numero intero compreso tra 0 e il massimo valore dell'array delle possibilità (nel caso comune, quindi, un valore compreso tra 0 e 1). La funzione ngettxt restituisce quindi la stringa tradotta corrispondente al risultato dell'espressione. Un esempio chiarirà meglio: per lingue come l'Italiano o l'Inglese, che hanno un singolare e un plurale, l'header dovrebbe essere:

"Plural-Forms: nplurals=2; plural= n != 1;"

Se usate questa espressione (come in genere si fa), allora i traduttori compilano il campo msgstr[0] con la forma singolare, e msgstr[1] con la forma plurale. L'espressione in C dell'header restituisce 1 se il valore di n è diverso da 1, e restituisce 0 se n==1. Questo significa che per il valore 1 verrà usata msgstr[0] (la forma singolare) e per tutti gli altri valori (zero compreso) verrà usata la forma plurale contenuta in msgstr[1].

Ma il Francese, per esempio, è già diverso: ha due forme come l'Italiano o l'Inglese, ma usa il singolare anche per lo zero. In questo caso l'header dovrebbe essere:

"Plural-Forms: nplurals=2; plural= n > 1;"

E ci sono molti casi complessi. Se non siete pratici, questa pagina elenca anche le possibilità più esotiche.

Se vi sentite creativi o spericolati potete (ab)usare di queste regole per gestire casi speciali, per esempio le perifrasi che si usano in Italiano o in Inglese per il caso "zero" (che altrimenti di solito rientra nel caso "plurale"). Poniamo di aver marcato questa stringa:

num = 12
s = ngettext('Mario has {} friend.',  # singolare
             'Mario has {} friends.', # plurale (compreso "zero")
             num)
print(s.format(num))

Nel catalogo myapp.po (per la traduzione italiana) potremmo scrivere:

(...)
"Plural-Forms: nplurals=3; plural=n == 0 ? 0 : n == 1 ? 1 : 2;"
(...)
#: main.py:10
msgid "Mario has {} friend."
msgid_plural "Mario has {} friends."
msgstr[0] "Mario non ha nessun amico."
msgstr[1] "Mario ha {} amico."
msgstr[2] "Mario ha {} amici."

In questo caso abbiamo indicato 3 forme plurali, di cui la prima (msgstr[0]) copre il caso speciale per "zero". Sia chiaro comunque che non potete usare una regola per certe stringhe, e un'altra per altre: una volta specificato nell'header che ci sono 3 forme plurali, tutte le stringhe marcate con ngettext devono avere 3 possibili traduzioni per quella lingua (eventualmente anche uguali: ma i traduttori devono aver chiaro come comportarsi).

Una domanda: e se volessimo gestire più di due forme plurali nella lingua originale (quella in cui è scritto il codice)? Purtroppo non è possibile: ngettext accetta solo un argomento per la forma singolare, e uno per la forma plurale. Quindi se volessimo coprire il caso precedente anche nel codice originale, dovremmo discriminare "a mano" il caso per lo "zero":

num = 12
if num == 0:
    s = _('Mario has no friends.')        
else:
    s = ngettext('Mario has {} friend.', 'Mario has {} friends.', num)
print(s.format(num))

Questo però produrrebbe due stringhe distinte nel catalogo:

(...)
"Plural-Forms: nplurals=3; plural=n == 0 ? 0 : n == 1 ? 1 : 2;"
(...)
#: main.py:8
msgid "Mario has no friends."
msgstr "Mario non ha nessun amico."

#: main.py:10
msgid "Mario has {} friend."
msgid_plural "Mario has {} friends."
msgstr[0] "Mario non ha nessun amico."
msgstr[1] "Mario ha {} amico."
msgstr[2] "Mario ha {} amici."

Abbiamo mantenuto comunque la regola per le tre forme plurali, ma non ha importanza: il caso di msgstr[0] non verrà mai raggiunto in realtà, perché quando num==0 il codice Python seleziona "a mano" la stringa semplice.

Questo dovrebbe sconsigliarvi, in generale, dall'usare più di due forme plurali solo per gestire casi speciali: forzando un po' la mano a ngettext potete coprire questi casi speciali solo nelle lingue tradotte, ma non in quella del codice originale. In realtà dovreste usare più di due forme plurali solo per le lingue (tradotte) che effettivamente lo richiedono (l'Arabo ha cinque tipi di plurale...). Il codice sorgente dovrebbe essere scritto in una lingua che ha solo due forme plurali, come l'Italiano (o meglio, l'Inglese).

Aggiornare i cataloghi.

Oltre a xgettext (per generare cataloghi .po(t) e msgfmt (per generare cataloghi compilati .mo), GNU Gettext comprende altre utility interessanti: rimandiamo alla documentazione per un elenco completo.

Un esame più approfondito merita msgmerge, che serve ad aggiornare cataloghi già prodotti per tener conto di modifiche e integrazioni successive, man mano che il vostro lavoro sul codice va avanti. Per capire come funziona, produciamo prima di tutto un catalogo del codice di esempio che abbiamo scritto:

> xgettext -d myapp main.py

Chiamiamo questo catalogo old_myapp.po per distinguerlo. Volendo, possiamo anche fare qualche esperimento, traducendo qualche stringa. Adesso modifichiamo il nostro codice Python aggiungendo una stringa e cambiandone un'altra:

...
    print(_('stringa MODIFICATA versione originale')) # era 'semplice'
...
    print(_('una nuova stringa! versione originale'))

E generiamo un nuovo catalogo, come prima:

> xgettext -d myapp main.py

Adesso possiamo usare msgmerge per unire i due cataloghi:

> msgmerge old_myapp.po myapp.po > new_myapp.po

Come si intuisce, il primo parametro è il vecchio catalogo, il secondo è quello nuovo, e a destra dell'operatore di re-direzionamento > scriviamo il nome del file risultante dove comparirà l'output di msgmerge.

Aprite new_myapp.po e confrontatelo con old_myapp.po: le traduzioni già fatte sono conservate, e le nuove stringhe (o quelle modificate) hanno il commento fuzzy per aiutare i traduttori a trovarle più facilmente. Una stringa "fuzzy", nel gergo di Gettext, è una stringa la cui traduzione non è considerata sicura, e andrebbe ricontrollata.

Accertatevi che msgmerge non abbia combinato dei pasticci (a volte capita) ed eventualmente correggete a mano il nuovo catalogo. Quando siete pronti, potete buttare via i due cataloghi vecchi e rinominare new_myapp.po in myapp.po, pronto per essere riconsegnato ai traduttori.

Aggiornare i cataloghi è sempre una questione delicata. Da un lato non volete iniziare troppo tardi a sottoporre il materiale ai traduttori, per non rallentare la distribuzione finale del programma. Dall'altro non è piacevole per i traduttori ricevere continui aggiornamenti che li costringono a rivedere il lavoro già fatto. Cercate di mantenere un equilibrio tra queste due esigenze.

Quinto: 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).

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.

Considerazioni varie.

Abbiamo terminato la nostra panoramica sull'uso di gettext e sulle tecniche di traduzione più consuete. Aggiungiamo qui alcune considerazioni e consigli sparsi.

Troppi literals nel codice?

Forse un po' in ritardo, dobbiamo avvertire che molti programmatori considerano cattiva pratica disseminare il codice di literals, e quindi anche di stringhe, che siano tradotte o meno. È lo stesso problema dei "numeri magici" nel codice. Più corretto sarebbe usare delle costanti il cui valore è attribuito in un modulo Python separato, o addirittura in una sorgente di dati esterna (un resource file, un file di configurazione, etc.).

Questo è senz'altro vero, ma l'opinione di chi scrive è più morbida. Prima di tutto, se le stringhe devono essere tradotte, allora è più facile usare gettext se queste sono in un modulo Python, invece che in un file esterno (vedremo comunque qualche scenario di questo tipo). In secondo luogo, è senz'altro possibile raggruppare tutte le stringhe in un modulo separato e richiamarle altrove per mezzo di costanti: ma di solito questo complica la leggibilità del codice, non la semplifica:

if foo > bar:
    # trovate piu' chiaro questo...
    emit_warning(WARNING104)
    # ...o questo?
    emit_warning('Your foo is getting too big. Soon it will explode.')

È vero che un codice ben scritto (che rispetta la Pep8, che usa nomi di variabile chiari...) si dovrebbe comprendere da solo; è vero che dove il codice non è chiaro dovrebbero esserci dei commenti; ma è anche vero che talvolta, quando ci sono già delle stringhe destinate a spiegare all'utente quello che sta succedendo, tutto diventa molto più facile anche per chi legge il codice sorgente.

Piuttosto c'è da dire questo: se nel vostro programma ci sono troppi punti diversi (moduli, classi, funzioni...) che hanno al loro interno delle stringhe tradotte, allora probabilmente state sbagliando qualcosa nell'architettura. I compiti dell'interfaccia utente (e quindi il codice CLI o GUI) dovrebbero essere organizzati in pochi punti ben riconoscibili, e non sparpagliati in giro nel codice.

Ancora a questo proposito, una particolare forma di crudeltà che talvolta ci è capitato di vedere è l'utilizzo di "stringhe identificative" per le stringhe tradotte, al posto di un normale valore "di default":

if foo > bar:
    # invece del normale:
    emit_warning(_('Your foo is getting too big.'))
    # qualcosa come:
    emit_warning(_('Warning 104'))

Questo metodo produce naturalmente dei cataloghi .po del tipo:

msgid "Warning 104"
msgstr ""

L'idea qui è di usare i cataloghi .po come resource file multilingua esterni. Voi compilate il primo catalogo fornendo una "traduzione" (ovvero una spiegazione della sigla!) nella vostra lingua, e gli altri traduttori compilano i loro a partire dal vostro. È inutile dire che questa è una pessima idea, che mescola il peggio dei due mondi. La stringa "identificativa" funziona un po' come una costante, ma non lo è: quindi per esempio non può essere messa insieme alle altre in un unico modulo, magari in ordine alfabetico per facilitarne la ricerca. Se poi deve essere interpolata, tende ad assumere un aspetto bizzarro (qualcosa come _('Warning 104 {foo} {bar}')). Infine, non esiste più l'ancora di salvezza della "lingua di default" della stringa originale: questo significa che se gettext per qualche motivo non trova nessuna traduzione, presenterà all'utente l'incomprensibile stringa 'Warning 104'.

Dopo aver ribadito che per noi questa è una cattiva idea, c'è però almeno una possibile ragione per implementarla, e ne parliamo nel prossimo paragrafo.

Problemi con la "lingua di default".

La lingua "originale" che voi usate nel codice è pur sempre una delle lingue che possono essere richieste dall'utente (in modo esplicito, o implicito attraverso il locale). In certi scenari, questo potrebbe essere un problema.

Supponiamo di avere, nel codice originale, delle stringhe in Italiano. Naturalmente questo significa che l'Italiano non sarà disponibile tra le lingue tradotte: concretamente, non ci sarà nessun catalogo locale/it/LC_MESSAGES/myapp.mo. Di solito questo non è un problema. Infatti se installate gettext chiedendo l'Italiano, così:

t = gettext.translation('myapp', 'locale', languages=['it'], 
                        fallback=True)
t.intall()

allora gettext non trova il catalogo italiano e visualizza le stringhe originali... in Italiano, appunto! Il paracadute è offerto dal parametro fallback=True, che probabilmente volete impostare in ogni caso. Infatti se gettext non trova nessuna delle lingue richieste e fallback=False, allora abortisce con un OSError. Non è una cosa che potete permettervi, soprattutto perché i cataloghi delle traduzioni sono pur sempre risorse esterne che possono sfuggire al controllo della vostra applicazione (qualcuno può cancellarli per sbaglio, per dire).

Lo stesso paracadute si apre anche quando istallate con gettext.install, così:

os.environ['LANG'] = locale.getdefaultlocale()[0] # supponiamo sia "it"
gettext.install('myapp', 'locale')

perché, ricordiamo, gettext.install installa automaticamente con fallback=True.

Il problema si pone invece quando volete prevedere dei meccanismi di fallback più complessi:

t = gettext.translation('myapp', 'locale', languages=['fr', 'it', 'en'], 
                        fallback=True)
t.intall()

Questo non funzionerà come sperate: voi chiedete il Francese, dove non c'è allora l'Italiano, e come ultima risorsa l'Inglese; ma se gettext non trova il Francese salterà subito all'Inglese, perché in effetti non trova nessun catalogo italiano. Tuttavia l'Italiano c'è eccome, nelle stringhe originali del codice! Ma questo gettext non può saperlo. Ora, va detto che questo è uno scenario un po' assurdo: voi sapete già che il codice originale ha le stringhe in Italiano, e quindi sapete che l'Italiano sarà sempre disponibile come fallback prima di dover ricorre all'Inglese. La sequenza ['fr', 'it', 'en'] non ha senso, se il codice originale ha le stringhe in Italiano: tutto ciò che viene dopo 'it' non dovrebbe importare. A rigore, non importa neppure 'it': tanto voi sapete già che il catalogo italiano non esiste, ma avete cura di imporre fallback=True. Quello che volete, in questo caso, si potrebbe ottenere in modo più corretto (e più semplice) così:

t = gettext.translation('myapp', 'locale', languages=['fr'], 
                        fallback=True)

Chiedete il Francese; se non è disponibile, il fallback restituirà l'Italiano (la lingua delle stringhe originali). Non ha senso pensare ad altri fallback dopo l'Italiano, perché per definizione tutte le stringhe hanno una versione italiana (quella del codice originale, ovvio).

Tuttavia, potreste essere in uno scenario convoluto in cui il componente che fa la richiesta di traduzione arriva da una terza parte, e non sa che le stringhe originarie sono in Italiano. Dal suo punto di vista, una sequenza come ['fr', 'it', 'en'] potrebbe aver senso. E sarebbe sgradevole dare l'impressione che non sia disponibile la lingua italiana solo perché non si trova il catalogo della traduzione italiana. Una soluzione è quella di implementare il pattern delle "stringhe-identificativo" visto nel paragrafo precedente: in pratica il codice sorgente è language agnostic e invece delle stringhe c'è soltanto un identificativo. A questo punto, siete costretti a produrre un catalogo anche per l'Italiano. Come sapete, questa soluzione non ci convince.

Un'altra soluzione è fornire comunque un catalogo anche per l'Italiano: un catalogo che "traduce" ogni stringa con la stessa identica stringa. Non avete bisogno di farvelo a mano: per questo compito esiste msgen tra le utility di GNU Gettext:

> xgettext -d myapp main.py
> msgen myapp.po > locale/it/LC_MESSAGES/myapp.po

Conoscere la lingua usata a runtime.

Va detto che gettext.GNUTranslations non conserva l'elenco delle lingue di cui sta fornendo la traduzione: chiama gettext.find in fase di inizializzazione per trovare i cataloghi richiesti e li carica, ma una volta usate queste informazioni per costruire il suo dizionario interno, non se ne preoccupa più.

Ricordiamo che c'è una differenza tra "lingua/e richiesta/e" e "lingua/e fornita/e". Di conseguenza non potete semplicemente affidarvi a una variabile mysettings.LANGUAGE o alla lettura di variabili d'ambiente o al locale predefinito: tutte queste cose esprimono solo il desiderio di avere una certa lingua. Non vi resta che usare voi stessi gettext.find per capire esattamente come stanno le cose. Ricordate anche che se gettext.find restituisce None o una lista vuota, vuol dire che la lingua usata è quella del codice originale... e a questo punto non c'è modo di conoscerla se non ispezionando il codice.

Traduzioni, GUI e design MCV.

Un avvertimento da non sottovalutare: l'aggiunta di traduzioni potrebbe non essere solo un fattore cosmetico per il vostro codice. Soprattutto nel caso di GUI (programmi a interfaccia grafica) potreste dover modificare l'architettura in modo piuttosto significativo. Una buona notizia: la presenza di stringhe tradotte potrebbe spingervi a scrivere codice migliore.

Ci riferiamo a tutte quelle circostenze in cui preleviamo dall'utente una stringa, e poi per prigrizia ne utilizziamo il valore "così com'è" per calcoli successivi. Finché la stringa non è tradotta, spesso ce la caviamo comunque. Ma se la stringa è tradotta, il valore di ritorno potrebbe essere qualsiasi cosa, e di solito questo manda all'aria i nostri piani.

Per esempio, supponiamo di presentare all'utente una scelta tra alcuni valori in una lista (il codice che segue è per wxPython, ma si capisce senza problemi):

choices = ['mela', 'pera', 'banana']
ctrl = wx.ListBox(parent, choices=choices)

Questo disegna una lista con tre scelte. Quando l'utente fa clic su una voce della lista, noi potremmo voler discriminare in base alla scelta:

selected = ctrl.GetString(ctrl.GetSelection())
if selected == 'mela':
    foo()
elif selected == 'pera':
    bar()
(...)

Questo è cattivo design: ma tutto sommato, poco importa. Se però introduciamo nel nostro progetto le traduzioni, allora le stringhe tra cui scegliere diventeranno:

choices = [_('mela'), _('pera'), _('banana')]

Adesso, a seconda della lingua l'utente farà clic su una stringa completamente inaspettata (potrebbe essere "apple", "apfel", manzana"...) e così la nostra logica di selezione non tiene più. Possiamo pensare di recuperare il numero di riga selezionata, invece della stringa: ma questo vale solo per il caso semplice, senza tener conto che la lista potrebbe essere riordinata o modificata a runtime in vari modi.

Problemi di questo tipo appaiono continuamente, quando prendiamo la scorciatoia di prelevare direttamente dall'interfaccia (la "view") e poi da questa passare alla logica "di business". La soluzione è implementare un'architettura MCV che separa ciò che vede l'utente dagli oggetti reali da manipolare per gestire la logica dell'applicazione.

Ora, MCV potrebbe apparire come una sovrastruttura gigantesca e molto intricata, ma in realtà spesso si tratta di implementarlo "dal basso", widget per widget. Come sempre in questi casi, prima di escogitare soluzioni fai-da-te è meglio verificare che il framework già non disponga di strumenti utili. Per esempio, per questi casi semplici wxPython ha il concetto di client data: a ciascuna riga della lista è possibile associare un oggetto qualsiasi, nascosto. Potete approfittarne per "agganciare" degli indici di qualche tipo, che mantenete stabili (il model) mentre le stringhe della lista (la view) variano con la traduzione; wxPython provvede al meccanismo (il controller) per passare direttamente dalla selezione dell'utente all'indice corrispondente, senza interessarsi alla stringa di rappresentazione:

# la "view": valori tradotti per l'utente
choices = [_('mela'), _('pera'), _('banana')] 
# il "model": una lista di valori stabili
values = ['mela', 'pera', 'banana']

ctrl = wx.ListBox(parent)
for ch, val in zip(choices, values):
    # "val" resta nascosto: e' il "client data" della riga
    ctrl.Append(ch, val) 

(...)

# recuperiamo direttamente il "client data" della selezione
selected = ctrl.GetClientData(ctrl.GetSelection())
# adesso la selezione avviene rispetto al "model", non alla "view"
if selected == 'mela':
    foo()
elif selected == 'pera':
    bar()
(...)

Questo adesso funziona qualunque sia la "view": e non solo riguardo alla traduzione delle stringhe della lista, ma anche rispetto per esempio al riordino, inserimenti modifiche e cancellazioni, etc.: il client data resta sempre attaccato alla sua riga, qualsiasi cosa succeda. Naturalmente per casi più complessi esistono poi strumenti più raffinati, ma il concetto resta identico.

Lingua, locale, layout.

Ci sarebbe da scrivere un libro: e in effetti si trovano in giro saggi, guidelines, discussioni... e non tutto ciò che trovate è concorde e ugualmente affidabile. Ci limitiamo ad alcune osservazioni basilari, più che altro per segnalare che, in effetti, i problemi esistono e sono parecchi.

In primo luogo, ricordiamo ancora che "lingua" (i18n) e "locale" (l10n) sono concetti diversi, anche se parzialmente sovrapposti. Potete impostare la lingua ma non il locale, o viceversa, o entrambi o nessuno. Idealmente, la "lingua di default" dovrebbe andare d'accordo con il "locale di default": ovvero, un utente in Italia dovrebbe visualizzare il testo in Italiano insieme alle convenzioni per il locale italiano (date, numeri...). In pratica però le cose possono complicarsi in vari modi. Primo, il locale di default vale per il computer, non per l'utente che lo sta usando: un utente egiziano che usa un computer italiano non vedrà il locale "giusto" per lui se chiamate semplicemente setlocale(LC_ALL, ''). In teoria potreste far scegliere anche il locale all'utente. In pratica però questo non è un disturbo che in genere le applicazioni si prendono: se il locale non piace all'utente, è compito suo re-impostarlo a livello di sistema operativo, in modo che poi tutte le applicazioni locale-aware facciano la cosa giusta per lui. Quindi, se volete essere locale-aware, potete limitarvi a chiamare setlocale(LC_ALL, '') senza troppi problemi.

Secondo, la lingua scelta potrebbe non essere quella suggerita dal locale scelto. È vero che, se all'utente non piace la lingua predefinita, potrebbe cambiare locale nel sistema operativo. Tuttavia, per l'utente di solito un locale straniero è più sopportabile di una lingua straniera. Se il vostro programma offre traduzioni alternative, allora dovrebbe anche offrire un modo di scegliere una lingua disponibile (anche se magari non di cambiarla a runtime). Impostare la lingua di default del locale va bene solo come scelta di default, appunto: ma l'utente dovrebbe poter cambiare lingua. Questo significa naturalmente che l'utente potrebbe vedere il programma nella sua lingua, ma con un locale diverso dal suo: sta a voi decidere se per la vostra applicazione questo è inaccettabile, ed eventualmente cambiare anche il locale.

Poi c'è da considerare lo spinoso problema delle lingue per cui non è sufficiente una "traduzione". Qui lo scoglio maggiore è il supporto per le lingue che si scrivono da destra a sinistra (RTL, right to left). Il problema è troppo vasto per essere discusso qui: basterà dire che Unicode rappresenta l'ordine logico dei caratteri (la sequenza temporale in cui vanno letti), e non l'ordine visivo (RTL o LTR). Esiste però un Unicode Bidirectional Algorithm che descrive il modo in cui andrebbe trattato il testo per essere bidirectional-aware (badate: è un pochino più complesso che fare txt = txt[::-1]). Python non ha il supporto per il testo bidirezionale nella libreria standard. Esiste Python-bidi, un'implementazione pure-Python dell'algoritmo Unicode. Chi scrive non ha sufficiente competenza per dire fino a che punto l'implementazione di Python-bidi sia completa e corretta: in genere si trova sempre qualche corner-case insidioso. Altrimenti, l'implementazione di riferimento è GNU Fribidi, scritta in C e quindi facile da chiamare con ctypes: ne esistono a nostra conoscenza almeno due wrapper Python: Pyfribidi e Python-fribidi.

A parte il problema della bidirezionalità, potreste aver bisogno di ricorrre a soluzioni ad-hoc per lingue specifiche, come l'Arabo (che unisce i caratteri con legature in base al contesto, vedi Python Arabic Reshaper) o il Giapponese (che ha diversi sistemi di scrittura, vedi Jaconv). Ma più in generale, per tutte le sottigliezze Unicode non supportate nativamente da Python, il vostro primo riferimento dovrebbero essere le librerie ICU, per cui esiste il binding Python PyICU.

Infine, e potrebbe essere l'ostacolo maggiore, il supporto per le lingue bidirezionali si estende a tutto il layout della GUI. Se la lingua è RTL, non solo il testo andrebbe allineato a destra, ma proprio tutto il layout dovrebbe essere realizzato "a specchio". Potete dare un'occhiata a queste linee guida di Google per farvi un'idea delle dimensioni del problema. Come sempre, prima di lanciarvi in soluzioni artigianali pasticciate, verificate il supporto per la bidirezionalità nel framework che volete usare. Per esempio nel campo delle web app molti front-end supportano soluzioni CSS, javascript, etc.

Altre considerazioni sul cambio di lingua a runtime.

Cambiare lingua a runtime, per esempio in risposta a un input dell'utente, è relativamente semplice dal punto di vista di gettext, ma potrebbe essere complicato da eseguire nell'architettura complessiva della vostra applicazione. Abbiamo visto qualche possibile trappola parlando di deferred translation; aggiungiamo qui alcune considerazioni ulteriori.

Prima di tutto, se lasciate all'utente la possibilità di scegliere una lingua, allora dovete accertarvi che il catalogo corrispondente esista davvero: se l'utente chiede una lingua e ne vede un'altra (magari una lingua di fallback, o quella della stringa originale) potrebbe pensare a un baco. Usate gettext.find per scoprire se il catalogo esiste, ed eventualmente comunicate il problema all'utente; oppure lasciategli scegliere solo tra un elenco di traduzioni che siete sicuri che esistono.

Se il vostro programma ha un'interfaccia testuale (CLI), in genere le cose sono facili: l'utente fa la sua scelta, e da quel momento in poi l'output del programma compare nella nuova lingua.

Se invece il programma ha un'interfaccia grafica (GUI), l'approccio deve essere studiato meglio. Se vi limitate a cambiare la lingua, anche dopo aver superato lo scoglio della valutazione delle variabili dal lato di gettext, resta il fatto che dal lato della GUI l'interfaccia non si aggiorna da sola. Di solito i singoli elementi si aggiornano man mano che vengono ri-disegnati dal gui framework: per esempio, un menu comparirà nella nuova lingua la prossima volta che l'utente lo seleziona; ma l'etichetta di un pulsante probabilmente resterà nella vecchia lingua per molto tempo ancora. Dovete pensare a una strategia per forzare l'aggiornamento di tutti gli elementi: per esempio nascondere temporaneamente tutta l'interfaccia per un istante (mostrando eventualmente il cursore "di attesa" nel frattempo), inviare a tutti gli elementi un segnale di aggiornamento, e quindi far riapparire la finestra.

Tutto sommato, in genere potete risparmiarvi il disturbo: se l'utente vuole cambiare lingua, trascrivete la sua decisione in un file di configurazione e informatelo che la modifica avrà effetto dal prossimo riavvio dell'applicazione. Di solito è più che sufficiente.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment