gettext - Cataloghi di Messaggi

Scopo API per catalogo di messaggi per internazionalizzazione.
Versione Python 2.1.3 e successive

Il modulo gettext fornisce una implementazione in puro Python compatibile con la libreria GNU gettext per traduzione di messaggi e gestione di catalogo. Gli strumenti disponibili con la distribuzione del sorgente Python consente di estrarre messaggi dalla propria sorgente, costruire un catalogo di messaggi contenente le traduzioni, ed usarlo per stampare un messaggio appropriato per l'utente in fase di esecuzione.

I cataloghi di messaggi possono essere usati per fornire interfacce internazionalizzate per il proprio programma, mostrando messaggi nella lingua appropriata per l'utente. Essi possono anche essere usati per altre personalizzazioni di messaggi, inclusa lo "skinning" di una interfaccia per diversi wrappers o partner.

Nota: Sebbene la documentazione della libreria standard dice che tutto quello che serve è incluso in Python, ho scoperto (Doug Hellman - n.d.t.) che pygettext.py si rifiuta di estrarre messaggi racchiusi in una chiamata a ungettext, anche quando si usano quelle che sembrerebbero le appropriate opzioni di riga di comando. Quindi ho finito per installare gli strumenti GNU gettext dalla sorgente, ed usare invece xgettext

Sguardo di Insieme del Flusso di Lavoro di Traduzione

Il processo per impostare ed usare le traduzioni consta di cinque passi:

  1. Marcatura nel proprio codice delle stringhe letterali che contengono i messaggi da tradurre

    Si inizia con l'identificare i messaggi all'interno del proprio sorgente che occorre tradurre, marcando le stringhe letterali in modo che il programma di estrazione le possa trovare.

  2. Estrazione dei messaggi

    Dopo avere identificato le stringhe traducibili nel proprio sorgente, si usa xgettext per estrarre le stringhe e creare un file con estensione .pot, o modello di traduzione. Il modello è un file di testo con una copia di tutte le stringhe che sono state identificate ed i segnaposto per le loro traduzioni.

  3. Traduzione dei messaggi.

    Passare una copia del file .pot al traduttore, modificando l'estensione in .po. Il file .po è un file sorgente modificabile usato come input per la fase di compilazione. Il traduttore dovrebbe aggiornare i dati di intestazione nel file e fornire le traduzioni per tutte le stringhe.

  4. "Compilare" il catalogo messaggi dalla traduzione.

    Quando il traduttore restituisce il file .po completato, si compila il file di testo nel formato binario del catalogo usando msgfmt. Il formato binario viene usato dal codice che cerca nel catalogo in fase di esecuzione.

  5. Caricare ed attivare l'appropriato catalogo di messaggi in fase di esecuzione

    Il passo finale è quello di aggiungere poche righe di codice alla propria applicazione per configurare e caricare il catalogo di messaggi ed installare la funzione di traduzione. Ci sono un paio di metodi per fare questo, con associati compromessi, ognuno dei quali viene trattato di seguito.

Si analizzano ora questi passi più dettagliatamente, partendo con le modifiche che occorre fare al proprio codice.

Creare Cataloghi di Messaggi dal Codice Sorgente

gettext funziona cercando le stringhe letterali incorporate nel proprio programma in un database di traduzioni ed estraendo la stringa tradotta appropriata. Ci sono diverse varianti delle funzionI per accedere al catalogo, a seconda che si lavori con stringhe Unicode o meno. Il modello tradizionale consiste nel legare la funzione di ricerca che si vuole usare al nome _ così da non infestare il proprio codice con tante chiamate a funzioni con nomi più lunghi.

Il programma di estrazione dei messaggi xgettext cerca i messaggi incorporati in chiamate alle funzioni di ricerca del catalogo. Comprende diversi linguaggi sorgente, ed usa un appropriato analizzatore per ognuno. Se si usano degli alias per le funzioni di ricerca od occorre aggiungere funzioni supplementari, si possono passare a xgettext i nomi dei simboli aggiuntivi da considerare quando si estraggono i messaggi.

Ecco un semplice script con un singolo messaggio pronto per essere tradotto.

import gettext

# Imposta l'accesso al catalogo di messaggi
t = gettext.translation('gettext_example', 'locale', fallback=True)
_ = t.ugettext

print _("Questo messaggio si trova nello script.")

In questo caso viene usata la versione Unicode della funzione di ricerca ugettext. Il testo "Questo messaggio si trova nello script." è il messaggio da sostituire dal catalogo. E' stata abilitata la modalità fallback, in modo che se lo script viene eseguito senza un catalogo di messaggi viene stampato il messaggio in linea

$ python gettext_example.py
Questo messaggio si trova nello script.

Il passo successivo è l'estrazione del(i) messaggio(i) e la creazione del file .pot, usando pygettext.py

$ xgettext -d gettext_example -o gettext_example.pot gettext_example.py

Il file in uscita (gettext_example.pot) assomiglia a questo:

# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR , YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2010-04-12 21:11+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME \n"
"Language-Team: LANGUAGE \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=CHARSET\n"
"Content-Transfer-Encoding: 8bit\n"

#: gettext_example.py:11
msgid "Questo messaggio si trova nello script."
msgstr ""

I cataloghi di messaggi sono installati in directory organizzate per dominio e linguaggio. In genere il dominio è un valore univoco, tipo il nome della propria applicazione. In questo caso è stato usato gettext_example. Il valore del linguaggio viene fornito dall'ambiente dell'utente in fase di esecuzione, attraverso una delle variabili di ambiente LANGUAGE, LC_ALL, LC_MESSAGES o LANG, a seconda della configurazione e piattaforma. Il linguaggio è impostato a it_IT (en_US nell'originale - n.d.t.) e sarà quello usato in tutti gli esempi che seguono.

Ora che si ha un modello il prossimo passo è creare la struttura di directory necessaria e copiare il modello al posto giusto. Si utilizzerà la directory locale all'interno della cartella dei sorgenti di questo sito come radice della directory del catalogo dei messaggi; tipicamente, si vorrà utilizzare una directory accessibile all'intero sistema. Il percorso completo della sorgente in input del catalogo è //LC_MESSAGES/.po, e l'effettivo catalogo ha l'estensione .mo.

Per questa configurazione, occorre copiare gettext_example.pot in locale/it_IT/LC_MESSAGES/gettext_example.po, quindi modificare i valori nell'intestazione ad aggiungere i messaggi alternativi. Il risultato è circa questo:

# Messaggi da gettext_example.py
# Copyright (C) 2015
# Roberto Pauletto , 2015.
#
msgid ""
msgstr ""
"Project-Id-Version: PyMOT\n"
"Report-Msgid-Bugs-To: Roberto Pauletto \n"
"POT-Creation-Date: 2015-10-20 21:11+0200\n"
"PO-Revision-Date: 2015-10-20 21:11+0200\n"
"Last-Translator: Roberto Pauletto \n"
"Language-Team: Italian \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"

#: gettext_example.py:11
msgid "Questo messaggio si trova nello script."
msgstr "Questo messaggio si trova nel catalogo it_IT"

Il catalogo viene costruito dal file .po utilizzando msgformat

$ cd locale/it_IT/LC_MESSAGES/; msgfmt -o gettext_example.mo gettext_example.po

Ora quando viene eseguito lo script, viene stampato il messaggio presente nel catalogo in luogo della stringa in linea nel codice:

$ python gettext_example.py

Questo messaggio si trova nel catalogo it_IT

Cercare il Catalogo Messaggi in Fase di Esecuzione

Come sopra descritto, la directory locale contiene i cataloghi di messaggi organizzati in base alla lingua con i nomi dei cataloghi attribuiti per il dominio del programma. I diversi sistemi operativi definiscono i loro propri valori predefiiniti, tuttavia gettext non conosce tutti quei valori. Per la maggior parte delle volte è più sicuro attribuire esplicitamente un valore a localedir piuttosto che dover dipendere dal fatto che il valore predefinito sia valido.

La porzione lingua nel percorso viene presa da una delle diverse variabili di ambiente che possono essere usate per configurare le caratteristiche di localizzazione (LANGUAGE, LC_ALL, LC_MESSAGES e LANG). La prima variabile che viene trovata impostata viene utilizzata. Lingue multiple possono essere selezionate separandone i valori tramite i due punti (:). Per trovare i cataloghi corrispondenti alla lingua utilizziamo questo script:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import gettext

catalogs = gettext.find('gettext_example', 'locale', all=True)
print 'Cataloghi:', catalogs

Si può ora illustrare come questo funziona creando un paio di cataloghi di messaggi ed eseguendo qualche esperimento.

$ cd locale/fr_FR/LC_MESSAGES/; msgfmt -o gettext_example.mo gettext_example.po
$ cd locale/es_ES/LC_MESSAGES/; msgfmt -o gettext_example.mo gettext_example.po
$ LANGUAGE=fr_FR
$ python gettext_find.py

Cataloghi: ['locale/fr_FR/LC_MESSAGES/gettext_example.mo']
$ LANGUAGE=es_ES
$ python gettext_find.py

Cataloghi: ['locale/es_ES/LC_MESSAGES/gettext_example.mo']
LANGUAGE=es_ES:fr_FR
$ python gettext_find.py
Cataloghi: ['locale/es_ES/LC_MESSAGES/gettext_example.mo', 'locale/fr_FR/LC_MESSAGES/gettext_example.mo']

Sebbene find() mostri l'elenco completo dei cataloghi, solo il primo nella sequenza viene effettivamente caricato per la ricerca dei messaggi.

$ LANGUAGE=fr_FR
$ python gettext_example.py

Questo messaggio si trova nel catalogo fr_FR
$ LANGUAGE=fr_FR:es_ES
$ python gettext_example.py

Questo messaggio si trova nel catalogo fr_FR
$ LANGUAGE=es_ES:fr_FR
$ python gettext_example.py

Questo messaggio si trova nel catalogo es_ES

Valori Plurali

Mentre una semplice sostituzione di messaggi può gestire la maggior parte delle proprie esigenze di traduzione, gettext tratta la gestione dei plurali come caso speciale. A seconda della lingua, la differenza tra le forme singolari e plurali di un messaggio potrebbero variare solo nella parte finale di una singola parola, oppure l'intera struttura della frase potrebbe essere diversa. Potrebbero anche esserci diverse forme a seconda del livello di pluralità. Per facilitare (e rendrere possibile) la gestione dei plurali, si ha un insieme a se stante di funzioni per ottenere la forma plurale di un messaggio.

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from gettext import translation
import sys

t = translation('gettext_plural', 'locale', fallback=True)
num = int(sys.argv[1])
msg = t.ungettext('%(num)d significa singolare.', '%(num)d significa plurale.', num)

# Ci si deve comunque occupare di aggiungere i  valori  al messaggio
print msg % {'num':num}
$ xgettext -L Python -d gettext_plural -o gettext_plural.pot gettext_plural.py

Visto che ci sono forme alternative da tradurre, le sostituizioni sono elencate in un array. Utilizzare un array consente traduzioni per lingue con forme multiple di plurali (il polacco, ad esempio) ha diverse forme che indicano le relative quantità.

# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR , YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2015-10-24 21:33+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME \n"
"Language-Team: LANGUAGE \n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=CHARSET\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"

#: gettext_plural.py:9
#, python-format
msgid "%(num)d significa singolare."
msgid_plural "%(num)d significa plurale."
msgstr[0] ""
msgstr[1] ""

Oltre al riempimento delle stringhe di traduzione, si deve anche descrivere il modo nel quale sono formati i plurali in modo che la libreria sappia come muoversi nell'indice dell'array per qualsiasi valore passato. La riga: "Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" include due valori da rimpiazzare manualmente, nplurals è un intero che indica la dimensione dell'array (il numero di transazioni utilizzate) e plural è una espressione in linguaggio C per convertire la quantità ricevuta in un indice nell'array quando si cerca la traduzione. La stringa letterale n viene sostituita con la quantità passata in ungettext().

Ad esempio, l'italiano comprende due forme di plurale. Una quantità di 0 viene trattata come plurale ("0 banane"). L'elemento Plural-Forms dovrebbe essere tipo questo:

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

La traduzione della forma singolare andrebbe in posizione 0 e quella plurale in posizione 1.

# SOME DESCRIPTIVE TITLE.
# Copyright (C) 2015
# This file is distributed under the same license as the PACKAGE package.
# Roberto Pauletto , 2015.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PyMOTW2\n"
"Report-Msgid-Bugs-To: Roberto Pauletto \n"
"POT-Creation-Date: 2015-10-24 21:33+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Roberto Pauletto \n"
"Language-Team: Italian \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;"

#: gettext_plural.py:9
#, python-format
msgid "%(num)d significa singolare."
msgid_plural "%(num)d significa plurale."
msgstr[0] "In it_IT, %(num)d è singolare."
msgstr[1] "In it_IT, %(num)d è plurale."

Se si esegue qualche volta lo script di prova dopo che il catalogo è stato compilato, si può notare come diversi valori di N sono convertiti come indici per le stringhe di traduzione.

$ cd locale/it_IT/LC_MESSAGES/; msgfmt -o gettext_plural.mo gettext_plural.po
$ python gettext_plural.py 0

In it_IT, 0 è plurale.
$ python gettext_plural.py 1

In it_IT, 1 è singolare.
$ python gettext_plural.py 2

In it_IT, 2 è plurale.

Localizzazione in Applicazione contro Modulo

Il perimetro del proprio sforzo di traduzione definisce come si installa e si usano le funzioni di gettext nel proprio codice.

Localizzazione a Livello di Applicazione

Per traduzioni a livello di applicazione, potrebbe essere accettabile installare una funzione tipo unggettext() globalmente utilizzando lo spazio dei nomi __builtins__ visto che si ha il controllo sul livello più alto del codice dell'applicazione

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import gettext
gettext.install('gettext_example', 'locale', unicode=True, names=['ngettext'])

print _('Questo messaggio è nello script.')

La funzione install() lega gettext() al nome _() nello spazio dei nomi __builtins__. Aggiunge anche ngettext() ed altre funzioni elencate in names. Se unicode è vero, le versioni Unicode delle funzioni sono utilizzate in luogo delle versioni ASCII predefinite.

Localizzazione a Livello di Modulo

Per una libreria, o modulo singolo, non è una buona idea modificare __builtins__ in quanto non si conosce quali conflitti si possono introdurre con valore globale di applicazione. Si possono importare o riagganciare il nomi delle funzioni di traduzione a mano all'inizio del proprio modulo.

import gettext
t = gettext.translation('gettext_example', 'locale', fallback=True)
_ = t.ugettext
ngettext = t.ungettext

print _('Questo messaggio è nello script.')

Vedere anche:

gettext
La documentazione della libreria standard per questo modulo
locale
Altri strumenti di localizzazione
GNU gettext
I formati del catalogo messaggi, API, ecc. per questo modulo sono tutti basati sul pacchetto gettext originale da GNU. I formati del file catalogo sono compatibili, e gli script da riga di comando hanno opzioni simili (se non identiche). Il manuale GNU gettext (in inglese - n.d.t.) ha una descrizione dettagliata sui formati di file e descrive le versioni GNU degli strumenti per lavorarci.
Internazionalizzare Python
Un documento di Martin von Löwis (in inglese - n.d.t.) riguardo alle tecniche per l'internazionalizzazione delle applicazioni Python.
Internazionalizzare Django
Un'altra buona fonte di informazione (in inglese - n.d.t.) sull'uso di gettext, inclusi esempi reali.