Μετάβαση στο περιεχόμενο

Γιατί t-strings

Τέσσερις τρόποι να βάλετε μια τιμή μέσα σε ένα μεταφράσιμο μήνυμα, συγκρινόμενοι πάνω στο ίδιο μήνυμα. Και οι τέσσερις δίνουν όνομα στα σύμβολα κράτησης θέσης τους και επιτρέπουν σε έναν μεταφραστή να τα αναδιατάξει· διαφέρουν στο τι συμβαίνει όταν μια μετάφραση είναι λανθασμένη, στο πόσο μεγάλο μέρος του προγράμματός σας μπορεί να φτάσει ο κατάλογος, και στο τι κοστίζει η υιοθέτησή τους.

Οι πίνακες έρχονται πρώτοι, ώστε να βρείτε τη γραμμή που σας ενδιαφέρει και να διαβάσετε μόνο την ενότητα που βρίσκεται πίσω από αυτήν.

Τρία μέρη αγγίζουν κάθε μεταφρασμένο μήνυμα

Ένας κατάλογος είναι το αρχείο των μεταφράσεων — .po όσο το επεξεργάζονται άνθρωποι, μεταγλωττισμένο σε .mo για να το φορτώσει η εφαρμογή (η εκμάθηση περνά και από τα δύο). Τρία μέρη αγγίζουν κάθε μήνυμα: ο προγραμματιστής γράφει την πηγαία συμβολοσειρά, ένας μεταφραστής επεξεργάζεται τον κατάλογο — συχνά σε εξωτερική πλατφόρμα, μακριά από κάθε αναθεώρηση κώδικα — και η εφαρμογή αποδίδει τα δύο μαζί κατά την εκτέλεση. Κάθε στυλ μορφοποίησης παρακάτω απαντά διαφορετικά στην ίδια ερώτηση: πόσο μεγάλο μέρος της γλώσσας μορφοποίησης ελέγχει ο κατάλογος; Στα παραδείγματα, το _ είναι το καθιερωμένο όνομα της συνάρτησης μετάφρασης, και το tr το όνομα αυτής της βιβλιοθήκης.

Δίπλα δίπλα

Όταν ένας μεταφραστής κάνει λάθος. Ένας κατάλογος περνά από πολλά χέρια, και τα περισσότερα από όσα πάνε στραβά μέσα του είναι ακούσια:

%(name)s .format() flufl.i18n $name t"…"
Μια μετάφραση παραλείπει ένα σύμβολο κράτησης θέσης — τι αποδίδεται; η τιμή εξαφανίζεται σιωπηλά η τιμή εξαφανίζεται σιωπηλά η τιμή εξαφανίζεται σιωπηλά το πηγαίο μήνυμα, με μια προειδοποίηση (από προεπιλογή)
Μια μετάφραση προσθέτει ένα άγνωστο σύμβολο κράτησης θέσης — τι αποδίδεται; μια εξαίρεση μια εξαίρεση το σύμβολο κράτησης θέσης μένει ορατό ως κείμενο το πηγαίο μήνυμα, με μια προειδοποίηση (από προεπιλογή)
Μια μετάφραση αλλάζει τη μορφοποίηση ενός συμβόλου κράτησης θέσης — τι αποδίδεται; ό,τι ζήτησε ο κατάλογος, ή μια εξαίρεση αν το γράμμα τύπου δεν ταιριάζει πια στην τιμή ό,τι ζήτησε ο κατάλογος δεν εκφράζεται στις συμβολοσειρές $ το πηγαίο μήνυμα, με μια προειδοποίηση
Ελέγχονται τα σύμβολα κράτησης θέσης κατά την απόδοση; όχι όχι όχι ναι (δείτε παρακάτω)

Τι εξουσία έχει ο κατάλογος. Μια μετάφραση είναι δεδομένα που έρχονται από έξω από το αποθετήριό σας, και κάθε στυλ της παραχωρεί διαφορετικό μέγεθος δύναμης:

%(name)s .format() flufl.i18n $name t"…"
Από πού έρχονται οι τιμές; από μια ρητή αντιστοίχιση από ρητά ορίσματα από τις τοπικές και καθολικές μεταβλητές του καλούντος, συν ένα προαιρετικό extras από τις τιμές που αιχμαλωτίστηκαν μέσα στο t-string
Μπορεί ο κατάλογος να αλλάξει πώς μορφοποιείται μια τιμή; ναι ναι όχι όχι
Μπορεί ο κατάλογος να φτάσει μέσα σε αντικείμενα (πρόσβαση σε ιδιότητες); όχι ναι ναι, με ονόματα με τελείες όχι
Πού ζει «η τρέχουσα γλώσσα»; όπου την βάλει η εφαρμογή όπου την βάλει η εφαρμογή σε μια στοίβα κωδικών γλώσσας πάνω στο κοινό αντικείμενο εφαρμογής σε ένα ContextVar, ανά εργασία ή αίτημα

Τι κοστίζει η ενσωμάτωση. Όλα τα παραπάνω είναι δωρεάν αν ταιριάζουν τα εργαλεία· εδώ είναι που μπορεί να μην ταιριάζουν:

%(name)s .format() flufl.i18n $name t"…"
Ελάχιστη έκδοση Python οποιαδήποτε οποιαδήποτε 3.10 3.14
Ωριμότητα τυπική βιβλιοθήκη τυπική βιβλιοθήκη σταθερή έκδοση alpha
Χρησιμοποιεί συνηθισμένους καταλόγους PO/MO; ναι ναι ναι ναι
Χρειάζεται προσαρμοσμένο εξαγωγέα πηγαίου κώδικα; όχι όχι όχι ναι, προς το παρόν
Ποια σημαία PO συμπεραίνει το Babel, για να επικυρώνουν τα υπάρχοντα εργαλεία; python-format python-brace-format καμία python-brace-format

Για τον έλεγχο κατά την απόδοση: τα μηνύματα ενικού ελέγχονται για ακριβή αντιστοιχία συμβόλων κράτησης θέσης. Τα μηνύματα πληθυντικού ελέγχονται επίσης, με τον κανόνα ένωσης/τομής που επιτρέπει στις μορφές πληθυντικού μιας γλώσσας-στόχου να διαφέρουν από της πηγαίας· ο αυστηρότερος έλεγχος ανά μορφή τρέχει όταν μεταγλωττίζονται οι κατάλογοι (Εξαγωγή).

Η γραμμή για τη σημαία μορφής αφορά την επικύρωση με επίγνωση συμβόλων κράτησης θέσης, όχι τη συμβατότητα καταλόγων. Το «καμία» σημαίνει ότι τα τυπικά εργαλεία gettext εξακολουθούν να διαβάζουν και να μεταγλωττίζουν το μήνυμα, αλλά το msgfmt --check-format δεν έχει γραμματική συμβόλων $ να εφαρμόσει.

Συμβατότητα και ωριμότητα

Οι δύο πρώτες γραμμές του τελευταίου πίνακα είναι εκείνες που κρίνουν την υιοθέτηση, οπότε αξίζει να διατυπωθούν καθαρά και όχι ως κελιά.

Το %-format και το .format() είναι ενσωματωμένα στην Python και δεν χρειάζονται καμία απολύτως εξάρτηση. Το flufl.i18n είναι ένα ώριμο πακέτο, κυκλοφορημένο και σε χρήση στην παραγωγή, που τρέχει σε Python 3.10 και νεότερες. Το gettext-tstrings είναι alpha και απαιτεί Python 3.14 ή νεότερη, επειδή τα t-strings είναι νέα σύνταξη στην 3.14 — δεν υπάρχει back-port και δεν μπορεί να υπάρξει. Η προδιαγραφή του είναι το σταθερό του μέρος· το Python API μπορεί ακόμη να αλλάξει πριν από την 1.0.

Αυτό που δεν κοστίζει κανένα από αυτά είναι η συμβατότητα καταλόγων. Και τα τέσσερα παράγουν συνηθισμένα αρχεία POT/PO/MO που κάθε επεξεργαστής PO, κάθε πλατφόρμα μετάφρασης και κάθε εργαλείο GNU gettext ήδη διαβάζει, οπότε η επιλογή που ακολουθεί είναι αναστρέψιμη με τρόπο που δεν θα ήταν μια αλλαγή μορφότυπου καταλόγων. Η Μετάβαση καλύπτει τη μετακίνηση ενός υπάρχοντος έργου.

Οι ενότητες παρακάτω δείχνουν κάθε συμβιβασμό με λεπτομέρεια, μία μέθοδο τη φορά.

%-format

_("Hello %(name)s") % {"name": name}

Τι μπορεί να πάει στραβά: ένα κατεστραμμένο σύμβολο κράτησης θέσης γίνεται εξαίρεση κατά την εκτέλεση, εκτός αν το πιάσει πρώτα η επικύρωση του καταλόγου.

Η συμβολοσειρά του καταλόγου κουβαλά σύνταξη printf, μαζί με ένα τελικό γράμμα τύπου — το s στο %(name)s — που εύκολα παραβλέπεται και εύκολα καταστρέφεται:

>>> "Hello %(name)" % {"name": "Ada"}  # the trailing "s" was deleted
Traceback (most recent call last):
  ...
ValueError: incomplete format

Μια επεξεργασία ενός χαρακτήρα σε έναν επεξεργαστή PO γίνεται εξαίρεση κατά την εκτέλεση, εκτός αν την πιάσει πρώτα η επικύρωση του καταλόγου. Το GNU msgfmt --check-format όντως πιάνει αυτήν εδώ, αλλά μόνο για μηνύματα με τη σημαία python-format, και μόνο εφόσον ο κατάλογος περνά πράγματι μέσα από το msgfmt στον δρόμο του προς την εφαρμογή σας.

str.format

_("Hello {name}").format(name=name)

Αφαιρεί το τελικό γράμμα τύπου διατηρώντας ένα επώνυμο, ελεύθερα αναδιατάξιμο σύμβολο κράτησης θέσης. Αυτό που μπορεί να πάει στραβά μετακινείται στην άλλη πλευρά της συναλλαγής: η μετάφραση αποκτά εξουσία πάνω στα αντικείμενά σας.

Το str.format είναι μια μικρή γλώσσα εκφράσεων, και το να το καλέσετε πάνω σε μια συμβολοσειρά σημαίνει να της παραχωρήσετε το δικαίωμα να τη χρησιμοποιήσει:

>>> "{name.__class__.__mro__}".format(name="Ada")
"(<class 'str'>, <class 'object'>)"
>>> settings.api_key = "sk-live-…"
>>> "{conf.api_key}".format(conf=settings)
'sk-live-…'

Τώρα αντικαταστήστε αυτές τις κυριολεκτικές συμβολοσειρές με ό,τι επιστρέφει η _(). Αν μια μετάφραση του Hello {name} γυρίσει πίσω ως {conf.api_key}, η απόδοσή της τυπώνει το κλειδί API σας — ο κατάλογος, όχι ο κώδικάς σας, αποφάσισε τι διαβάστηκε. Ένας κατάλογος δεν είναι κώδικας, αλλά ταξιδεύει σαν δεδομένα: βγαίνει σε μια πλατφόρμα μετάφρασης, περνά από πολλά χέρια, επιστρέφει ως .po, μεταγλωττίζεται σε .mo, μερικές φορές έρχεται έτοιμος από τελείως έξω από το έργο σας. Το .format() δίνει σε κάθε στάδιο αυτού του ταξιδιού πρόσβαση σε ιδιότητες των αντικειμένων που περνάτε.

Συμβολοσειρές $ και flufl.i18n

from flufl.i18n import initialize

_ = initialize("example")

name = "Ada"
print(_("Hello $name"))  # Hello Ada — the value came from the caller's locals

Το string.Template της τυπικής βιβλιοθήκης παρέχει τη γλώσσα παρεμβολής $name, αλλά δεν είναι το ίδιο ένα API μετάφρασης. Το flufl.i18n συνδυάζει αυτό το στυλ με αναζήτηση σε καταλόγους gettext. Προσέξτε ότι η τιμή δεν περνιέται ποτέ ρητά: το flufl.i18n χτίζει τον χώρο ονομάτων αντικατάστασης από τα globals και τα locals του καλούντος — όποιες μεταβλητές υπάρχουν στο σημείο κλήσης είναι διαθέσιμες στο μήνυμα. Μια προαιρετική αντιστοίχιση extras υπερισχύει και των δύο. Η σύνταξη που βλέπει ο μεταφραστής δεν έχει τελικό γράμμα τύπου ούτε προδιαγραφή μορφοποίησης, και τα σύμβολα κράτησης θέσης παραμένουν ελεύθερα αναδιατάξιμα.

Μια μη διαθέσιμη αντικατάσταση δεν εγείρει εξαίρεση. Με name = "Ada" και χωρίς nombre στον χώρο ονομάτων του καλούντος, μια μετάφραση καταλόγου Hello $nombre αποδίδεται ως Hello $nombre: το ανεπίλυτο σύμβολο κράτησης θέσης μένει ορατό. Αυτή η τεκμηριωμένη συμπεριφορά διατηρεί το υπόλοιπο του μεταφρασμένου μηνύματος αντί να αποτύχει η κλήση. Εξαιρέσεις που εγείρονται κατά την επίλυση μιας ιδιότητας ή τη μετατροπή μιας τιμής μπορούν πάντως να διαδοθούν.

Το flufl.i18n είναι ικανότερο από ένα γυμνό string.Template σε ένα σχετικό σημείο. Το προσαρμοσμένο Template του δέχεται σύμβολα κράτησης θέσης με τελείες όπως $settings.api_key, και ο translator του επιλύει αυτές τις διαδρομές πάνω στις τιμές του καλούντος. Ένα μεταφρασμένο σύμβολο κράτησης θέσης μπορεί να κατονομάσει οποιαδήποτε διαθέσιμη τοπική ή καθολική μεταβλητή του καλούντος και, με τη σύνταξη με τελείες, να διασχίσει τις ιδιότητές της. Αυτό είναι βολικό όταν ένα μήνυμα χρειάζεται μια ιδιότητα, ενώ ταυτόχρονα κάνει το πλαίσιο του καλούντος μέρος του χώρου ονομάτων αντικατάστασης του καταλόγου. Η σύγκριση εδώ περιγράφει το flufl.i18n 6.0.0, όχι κάθε δυνατή χρήση του string.Template.

Απαντά επίσης σε ένα ερώτημα που τα άλλα δύο στυλ μορφοποίησης αφήνουν εξ ολοκλήρου στην εφαρμογή: ποια γλώσσα είναι η τρέχουσα, και πώς αλλάζει. Ένα αντικείμενο εφαρμογής κρατά μια στοίβα γλωσσών, οι _.push(code) και _.pop() τη μετακινούν, το with _.using(code): φωλιάζει, και μια στρατηγική βρίσκει τον κατάλογο για έναν κωδικό γλώσσας, ώστε η εφαρμογή να μη χειρίζεται ποτέ η ίδια αντικείμενα καταλόγου. Ένας διακομιστής που πρέπει να παραγάγει κείμενο σε περισσότερες από μία γλώσσες μέσα στην ίδια μονάδα εργασίας — μια σελίδα για τον αναγνώστη, μια ειδοποίηση για κάποιον του οποίου ο λογαριασμός είναι ρυθμισμένος διαφορετικά — είναι ακριβώς η περίπτωση για την οποία υπάρχει αυτό.

Η στοίβα ζει πάνω σε εκείνο το αντικείμενο εφαρμογής, το οποίο μοιράζεται ολόκληρη η διεργασία. Δύο επικαλυπτόμενα αιτήματα μοιράζονται επομένως μία στοίβα, και μπλοκ που δεν είναι αυστηρά φωλιασμένα στον χρόνο δίνουν το ένα στο άλλο λάθος γλώσσα:

async def greet(code, delay):
    with _.using(code):
        await asyncio.sleep(delay)
        return _("Hello $name")


async def main():
    return await asyncio.gather(greet("fr", 0.01), greet("ja", 0.02))
>>> asyncio.run(main())  # "fr" entered first and left first, so it read "ja" off the top
['こんにちは Ada', 'Bonjour Ada']

Αυτή η βιβλιοθήκη διατηρεί την ίδια δυνατότητα — τα δεσίματα φωλιάζουν και ξετυλίγονται με τον ίδιο τρόπο — μέσα σε ένα ContextVar αντί για μια κοινή στοίβα, οπότε η παραπάνω αλληλοδιαπλοκή επιλύεται ανά εργασία. Τα αντίστοιχα παραδείγματα βρίσκονται στο Πολλές γλώσσες ταυτόχρονα. Αυτό που δεν παρέχει είναι η αναζήτηση καταλόγου από κωδικό γλώσσας: περνάτε ένα αντικείμενο μεταφράσεων, που στη συνηθισμένη περίπτωση είναι μία κλήση gettext.translation(), και η τυπική βιβλιοθήκη κρατά σε cache τον αναλυμένο κατάλογο.

t-strings

tr(t"Hello {name}")

Ο κατάλογος εξακολουθεί να βλέπει το Hello {name} και παραμένει ένας συνηθισμένος κατάλογος PO/MO. Η διαφορά είναι τι επιτρέπεται να πει μια μετάφραση, και ποιος το ελέγχει.

Αυτή η βιβλιοθήκη επικυρώνει κάθε μετάφραση ως προς τα σύμβολα κράτησης θέσης του πηγαίου μηνύματος πριν από την απόδοση, και δέχεται γυμνά ονόματα και τίποτα άλλο. Απέναντι στο t"Hello {name}":

Μια μετάφραση που περιέχει απορρίπτεται με
{name.__class__.__mro__} placeholder {name.__class__.__mro__} must be a plain name, copied from the source message unchanged
{name!r} placeholder {name} adds formatting; write {name} on its own, because the source message decides how the value is formatted
{0} placeholder {0} must be a plain name, copied from the source message unchanged
{nombre} translation does not match the source placeholders: {name} is missing; {nombre} is not in the source message

Απορρίφθηκε δεν σημαίνει κατέρρευσε: από προεπιλογή η βιβλιοθήκη καταγράφει μια προειδοποίηση και αποδίδει το πηγαίο μήνυμα, ώστε ένας κακός κατάλογος να μη ρίχνει ποτέ την εφαρμογή — το ίδιο συμβόλαιο που τηρεί και το ίδιο το gettext.

Η μορφοποίηση μένει εκεί όπου γράφτηκε, στον κώδικα:

amount = 1234.5
tr(t"Total: {amount:,.2f}")  # msgid is "Total: {amount}"

Το :,.2f δεν φτάνει ποτέ στον κατάλογο, οπότε καμία μετάφραση δεν μπορεί να το αλλάξει, και κανένας μεταφραστής δεν χρειάζεται να το κοιτάξει. Είναι όμως μια σταθερή μορφοποίηση, όχι τοπικοποιημένη — η επιλογή ψηφίων και διαχωριστικών ανά γλώσσα είναι δουλειά του Babel, πριν από την κλήση.

Μία ακόμη διαφορά είναι τα εργαλεία: τα t-strings είναι νέα σύνταξη, οπότε η εξαγωγή τους σε ένα .pot απαιτεί προς το παρόν έναν εξαγωγέα με επίγνωση των t-strings, όπως αυτόν που αυτό το πακέτο παρέχει για το Babel.

Το κόστος του περιορισμού

Πέρα από την απαίτηση για την έκδοση της Python, το τίμημα όλων αυτών είναι ένας κανόνας: μια παρεμβολή πρέπει να είναι ένα απλό όνομα.

tr(t"Hello {user.name}")  # raises InvalidTemplateError at the call site
name = user.name  # compute it first
tr(t"Hello {name}")

Αυτός είναι ένας πραγματικός περιορισμός, και είναι ο ίδιος περιορισμός που παράγει τις εγγυήσεις παραπάνω. Μαζί με το δέσιμο τιμών στην πλευρά της πηγής και τον έλεγχο συμβόλων κράτησης θέσης κατά την εκτέλεση, εμποδίζει τις συμβολοσειρές του καταλόγου να αποτιμούν εκφράσεις και κρατά τα ονόματα των συμβόλων κράτησης θέσης ουσιαστικά για το πρόσωπο που τα μεταφράζει.

Ένα f-string δεν μπορεί να χρησιμοποιηθεί έτσι καθόλου — μέχρι να το δει οποιαδήποτε βιβλιοθήκη είναι ήδη μια τελειωμένη συμβολοσειρά, οπότε το να το μεταφράσετε σημαίνει να μεταφράσετε ένα απόσπασμα. Τα t-strings (PEP 750) κρατούν το στατικό κείμενο και τις τιμές χωριστά, διατηρώντας σύνταξη σαν των f-strings και ρητό δέσιμο τιμών.

Πώς έφτασε η Python ώς εδώ — δύο PEP με δέκα χρόνια απόσταση, και η συζήτηση για την τυπική βιβλιοθήκη που έκλεισε χωρίς απάντηση — αφηγείται με τις πηγές του στο Ιστορικό.