Vai al contenuto

Guida

Questa pagina è il riferimento a runtime: tutto ciò che il codice applicativo fa con questa libreria una volta che i cataloghi esistono. Se non hai ancora visto il ciclo completo — marcare, estrarre, tradurre, compilare, eseguire — il tutorial lo percorre una volta in cinque minuti; la creazione e la validazione dei cataloghi sono coperte in Estrazione, e come un team tiene in moto il ciclo — cicli di aggiornamento, CI, piattaforme di traduzione — è In produzione.

Quale entry point dovrei usare?

Il pacchetto esporta diversi modi di tradurre un messaggio perché le applicazioni legano una lingua in diversi modi. Scegli in base a come il tuo programma decide in che lingua si trova:

La tua situazione Usa
Una lingua per l'intero processo — una CLI, un'app desktop, uno script Translator, chiamato come _
Una lingua per richiesta o per task asincrono — un'applicazione web use_translations() attorno al lavoro, poi tr()
Un messaggio definito al momento dell'import — l'etichetta di un form, un enum, una costante lazy_gettext() o lazy_pgettext()
Un conteggio decide la formulazione ngettext() / npgettext(), in una qualunque delle forme sopra
Rendere un pattern senza coinvolgere nessun catalogo compile_template()

Tutto quel che segue sono quei cinque casi, in quest'ordine.

Legare un catalogo

La forma raccomandata rispecchia l'uso a classi di gettext: lega una volta un normale oggetto di traduzione e usa il processore chiamabile come _.

import gettext

from gettext_tstrings import Translator

translations = gettext.translation("messages", localedir="locales", languages=["ja"])
_ = Translator(translations)

name = "Ada"
print(_(t"Hello {name}"))  # こんにちは Ada

n = 3
print(_.ngettext(t"One file", t"{n} files", n))  # picks the right plural form for n

filename = "report.txt"
print(_.pgettext("button", t"Open {filename}"))  # "button" disambiguates homonyms

Le funzioni a livello di modulo seguono i nomi della libreria standard e la sua convenzione di chiamata con soli argomenti posizionali:

from gettext_tstrings import gettext, ngettext, npgettext, pgettext

gettext(t"Hello {name}", translations=translations)
ngettext(t"One file", t"{n} files", n, translations=translations)
pgettext("button", t"Open {filename}", translations=translations)
npgettext("inbox", t"One message", t"{n} messages", n, translations=translations)

tr e ntr sono alias esatti di gettext e ngettext.

Lingua per richiesta

Un framework web sceglie una lingua per ogni richiesta. Lega le traduzioni della richiesta al contesto corrente e ogni chiamata a livello di modulo si risolve in quella lingua, in sicurezza anche tra richieste concorrenti:

from gettext_tstrings import tr, use_translations


def handle(request):
    name = request.user.display_name
    translations = load_translations(request.locale)
    with use_translations(translations):
        return render(tr(t"Hello {name}"))

set_translations(translations) lega senza un blocco with, per i framework che gestiscono da soli il ciclo di vita della richiesta; get_translations() legge il binding corrente. Un argomento esplicito translations= vince sempre sul contesto, e un contesto non legato ripiega sulle funzioni gettext installate globalmente dalla libreria standard. Esempi svolti per Flask e per un middleware ASGI sono nella pagina In produzione.

Traduzione differita

Una t-string cattura i suoi valori subito, il che è sbagliato per una stringa definita al momento dell'import — l'etichetta di un form, il valore di un enum, una costante di modulo — che deve essere resa in qualunque lingua sia attiva quando viene usata.

from gettext_tstrings import lazy_gettext, lazy_pgettext, use_translations

SAVE = lazy_gettext(t"Save changes")  # defined once, at import
OPEN = lazy_pgettext("button", t"Open file")

with use_translations(japanese):
    assert str(SAVE) == "変更を保存"  # rendered here, in this language

Una LazyString si rende attraverso str(), format() e le f-string, e risulta uguale al suo testo reso nei confronti.

Deliberatamente non hashabile

Il testo di una LazyString dipende dalla lingua attiva, quindi un hash cambierebbe a ogni cambio di lingua e corromperebbe in silenzio qualunque set o dict la contenga. Chiama prima str() se ti serve una chiave.

strict si decide dove il messaggio viene scritto, non dove viene reso:

SAVE = lazy_gettext(t"Save changes", strict=True)

Una stringa differita viene resa dovunque finisca per essere usata — dentro un template, un form, una riga di log — e quel punto raramente sa se si tratta di un'esecuzione di test o della produzione. Passare strict=True alla definizione è ciò che permette di applicare la stessa scelta rumorosa in CI, tollerante in produzione anche a una stringa che non viene resa nel punto in cui è chiamata.

Le forme plurali dipendono da un conteggio a runtime, quindi rendile subito con ngettext dove il conteggio è noto.

Più lingue insieme

Una sola richiesta ha spesso bisogno di più di una lingua: una pagina resa per chi legge che accoda anche una notifica a un account impostato su un'altra, o un digest che cita ogni partecipante nella propria. I binding si annidano, e uscire dal blocco interno ripristina quello esterno.

with use_translations(reader):
    page = tr(t"Hello {name}")
    with use_translations(recipient):
        notice = tr(t"Hello {name}")  # the recipient's language
    footer = tr(t"Hello {name}")  # the reader's again

Su una lista di destinatari sono le stringhe differite a fare il lavoro: il messaggio si scrive una volta sola, all'import, e viene reso una volta per lingua.

SUBJECT = lazy_gettext(t"Your order shipped")

for user in users:
    with use_translations(load_translations(user.locale)):
        send(user.email, str(SUBJECT))

Il binding è una ContextVar, non uno stack tenuto su un oggetto condiviso, quindi richieste che si sovrappongono non possono prendere l'una la lingua dell'altra — compreso il caso in cui escano dai loro blocchi nello stesso ordine in cui vi sono entrate, che è l'intreccio sbagliato da uno stack a pila. Caricare un catalogo per lingua costa poco: gettext.translation() analizza ogni .mo una sola volta e restituisce copie che condividono il catalogo già analizzato.

Se un thread di lavoro erediti il binding dipende dalla build

Un semplice threading.Thread, o ThreadPoolExecutor.submit, parte o da una copia del contesto del chiamante o da uno vuoto, e quale dei due lo decide sys.flags.thread_inherit_context — vero per impostazione predefinita sulle build free-threaded, falso ovunque altrove. Lo stesso codice rende quindi la lingua legata su 3.14t e il catalogo globale del processo su 3.14. Passa il contesto invece di dipendere dal valore predefinito:

pool.submit(contextvars.copy_context().run, render)

asyncio.to_thread lo fa già per te.

Valori sensibili al locale

Questa libreria decide dove un valore compare in un messaggio tradotto. Non localizza il valore in sé. {amount:,.2f} è una specifica di formato Python dal comportamento fisso — una virgola ogni tre cifre e un punto prima dei decimali — e produce gli stessi caratteri in qualunque lingua sia il messaggio:

>>> f"{1234.5:,.2f}"  # the same in every locale
'1,234.50'

Il tedesco scrive quel numero 1.234,50, il francese 1 234,50, e l'hindi raggruppa 1234567 come 12,34,567 anziché 1,234,567. Numeri, valute, date, orari e unità di misura appartengono a Babel. Formatta prima il valore, poi colloca la stringa già pronta:

from babel.numbers import format_currency

total = format_currency(amount, "EUR", locale=locale)
tr(t"Your order comes to {total}")

In un messaggio con conteggio il numero svolge due compiti — seleziona la forma plurale e compare nel testo — e solo il secondo viene localizzato. Tieni il conteggio grezzo per la selezione e passa la stringa formattata per la visualizzazione:

from babel.numbers import format_decimal

shown = format_decimal(n, locale=locale)
_.ngettext(t"One file", t"{shown} files", n)

Formattare prima della chiamata è anche ciò che tiene una specifica di formato fuori dal catalogo: quello che un traduttore vede è un pezzo di testo già pronto, non un numero più le istruzioni per renderlo.

Che cosa succede quando un catalogo è sbagliato

Se i segnaposto di una traduzione non corrispondono alla sorgente — un campo mancante, sconosciuto o riformattato che è sfuggito alla validazione, da un MO modificato a mano, un catalogo di terze parti o una pipeline che salta il checker — il comportamento predefinito è rendere il messaggio sorgente invece di sollevare un'eccezione. Questo rispecchia il contratto di gettext stesso: un catalogo danneggiato non rompe mai l'applicazione.

Con Hello {name} tradotto come こんにちは {nombre}, il rendering riesce e un avviso va al logger gettext_tstrings:

WARNING gettext_tstrings: invalid translation for msgid 'Hello {name}'; using
source text: translation does not match the source placeholders: {name} is
missing; {nombre} is not in the source message
>>> _(t"Hello {name}")
'Hello Ada'

L'avviso scatta una volta per messaggio e pattern, non una volta per rendering, così una voce di catalogo danneggiata non inonda un log.

Scegli di fallire rumorosamente per i test e la CI:

strict = Translator(translations, strict=True)
tr(t"Hello {name}", translations=translations, strict=True)

La stessa ricerca allora solleva un'eccezione, portando la stessa frase senza la metà "using source text":

>>> strict(t"Hello {name}")
Traceback (most recent call last):
  ...
gettext_tstrings.errors.InvalidTranslationError: translation does not match the
source placeholders: {name} is missing; {nombre} is not in the source message

Questi messaggi sono scritti per chi può intervenire, e per un problema di catalogo è più spesso un traduttore che un programmatore — perciò, dove un segnaposto sembra presente ma non lo è, il messaggio spiega il perché invece di ripetere che manca. Graffe a larghezza intera, un {{name}} doppio, uno spazio unificatore invisibile, una lettera cirillica tra lettere latine: ogni caso ha la sua formulazione, elencata con esempi in Per i traduttori. Quella pagina è scritta per essere consegnata a chi modifica il .po.

Rendere un pattern senza un catalogo

compile_template espone lo stesso meccanismo un livello più in basso: trasforma una t-string nel suo msgid più un insieme di valori legati, e rende qualunque pattern tu gli passi.

from gettext_tstrings import compile_template

name = "Ada"
compiled = compile_template(t"Hello {name}")

compiled.msgid  # "Hello {name}"
compiled.placeholders  # ("name",)
compiled.render("こんにちは {name}")  # "こんにちは Ada"

render valida con le stesse regole e solleva sempre su una mancata corrispondenza. Qui non esiste una modalità permissiva: la permissività esiste perché una ricerca in un catalogo possa degradare al testo sorgente, e un pattern che hai passato tu stesso non ha nulla da cui degradare.

Sicurezza e ambito

Questo è valido:

tr(t"Hello {name}")

Questi sono rifiutati di proposito:

tr(t"Hello {user.name}")  # attribute access
tr(t"Hello {display_name()}")  # a call

Calcola prima un valore significativo:

name = user.display_name()
tr(t"Hello {name}")

La restrizione produce chiavi di catalogo stabili, dà ai traduttori nomi utili e impedisce a una stringa tradotta di diventare un linguaggio di espressioni.

La garanzia è limitata a struttura e formattazione: una traduzione non viene mai valutata, e non può mai aggiungere accesso agli attributi, chiamate, conversioni o specifiche di formato. Due cose restano responsabilità del chiamante, esattamente come con il gettext della stdlib — l'escaping dell'output reso per la sua destinazione (HTML, shell, terminale), e l'integrità del catalogo, dato che un catalogo ostile può ripetere un segnaposto per amplificare la dimensione dell'output, cosa inerente a qualunque i18n basata su segnaposto.