לדלג לתוכן

למה מחרוזות-t

ארבע דרכים להכניס ערך להודעה הניתנת לתרגום, מושוות על אותה הודעה בדיוק. בכל ארבעתן מצייני המקום בעלי שם, וכולן מאפשרות למתרגם לסדר אותם מחדש; הן נבדלות במה שקורה כשתרגום שגוי, בכמה מהתוכנית שלכם הקטלוג יכול להגיע, ובמה שעולה לאמץ אותן.

הטבלאות באות תחילה, כדי שתוכלו למצוא את השורה שמעניינת אתכם ולקרוא רק את הסעיף שמאחוריה.

שלושה צדדים נוגעים בכל הודעה מתורגמת

קטלוג הוא קובץ התרגומים — .po כל עוד בני אדם עורכים אותו, ומהודר ל-.mo כדי שהאפליקציה תטען אותו (מדריך המבוא עובר על שניהם). שלושה צדדים נוגעים בכל הודעה: המפתח כותב את מחרוזת המקור, מתרגם עורך את הקטלוג — לעיתים קרובות בפלטפורמה חיצונית, הרחק מכל סקירת קוד — והאפליקציה מרנדרת את השניים יחד בזמן ריצה. כל סגנון פורמט להלן עונה על אותה שאלה בדרך אחרת: כמה משפת הפורמט נתון לשליטת הקטלוג? בדוגמאות, _ הוא השם המקובל לפונקציית התרגום, ו-tr הוא שמה של הספרייה הזו.

זו מול זו

כשמתרגם טועה. קטלוג עובר דרך ידיים רבות, ורוב מה שמשתבש בו הוא מקרי:

%(name)s .format() flufl.i18n $name t"…"
תרגום משמיט מציין מקום — מה מרונדר? הערך נעלם בשקט הערך נעלם בשקט הערך נעלם בשקט הודעת המקור, עם אזהרה (כברירת מחדל)
תרגום מוסיף מציין מקום לא מוכר — מה מרונדר? חריגה חריגה מציין המקום נשאר גלוי כטקסט הודעת המקור, עם אזהרה (כברירת מחדל)
תרגום מעצב מחדש מציין מקום — מה מרונדר? מה שהקטלוג ביקש, או חריגה אם אות הטיפוס כבר אינה מתאימה לערך מה שהקטלוג ביקש לא ניתן לביטוי במחרוזות-$ הודעת המקור, עם אזהרה
האם מצייני המקום נבדקים בזמן הרינדור? לא לא לא כן (ראו להלן)

איזו סמכות יש לקטלוג. תרגום הוא נתון שמגיע מחוץ למאגר שלכם, וכל סגנון מוסר לו כמות שונה של כוח:

%(name)s .format() flufl.i18n $name t"…"
מאין מגיעים הערכים? מיפוי מפורש ארגומנטים מפורשים המשתנים הלוקליים והגלובליים של הקורא, בתוספת extras אופציונלי הערכים שנלכדו בתוך מחרוזת ה-t
האם הקטלוג יכול לשנות את אופן הפורמט של ערך? כן כן לא לא
האם הקטלוג יכול להגיע לתוך אובייקטים (גישה לתכונות)? לא כן כן, עם שמות מנוקדים לא
היכן שוכנת "השפה הנוכחית"? היכן שהאפליקציה מניחה אותה היכן שהאפליקציה מניחה אותה מחסנית של קודי שפה על אובייקט האפליקציה המשותף ContextVar, לכל משימה או בקשה

מה עולה ההטמעה. כל מה שלמעלה הוא בחינם אם שרשרת הכלים מתאימה; כאן היא עשויה לא להתאים:

%(name)s .format() flufl.i18n $name t"…"
גרסת Python מזערית כל אחת כל אחת 3.10 3.14
בשלות ספרייה סטנדרטית ספרייה סטנדרטית גרסה יציבה אלפא
משתמשת בקטלוגי 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 היא אלפא ודורשת Python 3.14 ומעלה, מפני שמחרוזות-t הן תחביר חדש ב-3.14 — אין back-port ולא יכול להיות כזה. המפרט שלה הוא החלק היציב; ה-API של Python עוד עשוי לזוז לפני 1.0.

מה שאף אחת מהן אינה עולה הוא תאימות קטלוגים. כל הארבע מפיקות קובצי POT/PO/MO רגילים שכל עורך PO, כל פלטפורמת תרגום וכל כלי GNU gettext כבר קוראים, ולכן הבחירה שלהלן הפיכה באופן ששינוי פורמט הקטלוג לא היה. מעבר מקיים מכסה את ההעברה של פרויקט קיים.

הסעיפים שלהלן מציגים כל פשרה בפירוט, שיטה אחת בכל פעם.

פורמט %

_("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 הופכת לחריגה בזמן ריצה, אלא אם אימות הקטלוג תופס אותה קודם. msgfmt --check-format של GNU אכן תופס את המקרה הזה, אבל רק בהודעות המסומנות בדגל 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

הספרייה הסטנדרטית מספקת את שפת האינטרפולציה $name דרך string.Template, אך אינה בעצמה API לתרגום. flufl.i18n משלבת את הסגנון הזה עם חיפוש בקטלוגי gettext. שימו לב שהערך לעולם אינו מועבר: flufl.i18n בונה את מרחב השמות של ההצבה מהמשתנים הגלובליים והלוקליים של הקורא — כל משתנה שקיים בנקודת הקריאה זמין להודעה. מיפוי extras אופציונלי גובר על שניהם. בתחביר הפונה למתרגם אין אות סוג בסוף ואין מפרט פורמט, ומצייני המקום נותרים ניתנים לסידור מחדש בחופשיות.

הצבה שאינה זמינה אינה מרימה חריגה. עם name = "Ada" וללא nombre במרחב השמות של הקורא, תרגום קטלוג של Hello $nombre מרונדר בתור Hello $nombre: מציין המקום הלא-פתור נשאר גלוי. ההתנהגות המתועדת הזו משמרת את שאר ההודעה המתורגמת במקום להכשיל את הקריאה. חריגות שהורמו במהלך פתרון תכונה או המרת ערך עדיין עשויות להתפשט.

flufl.i18n מסוגלת יותר מ-string.Template חשופה בהיבט רלוונטי אחד. ה-Template המותאם שלה מקבל מצייני מקום מנוקדים כגון $settings.api_key, והמתרגם שלה פותר את הנתיבים האלה מול ערכי הקורא. מציין מקום מתורגם רשאי לנקוב בשם כל משתנה לוקלי או גלובלי זמין של הקורא, ובעזרת תחביר נקודות אף לחצות את תכונותיו. זה נוח כשהודעה זקוקה לתכונה, ובה בעת הופך את המסגרת של הקורא לחלק ממרחב שמות ההצבה של הקטלוג. ההשוואה כאן מתארת את 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(), והספרייה הסטנדרטית שומרת במטמון את הקטלוג המפוענח.

מחרוזות-t

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 הן תחביר חדש, ולכן חילוצן לקובץ .pot דורש כיום מחלץ המודע למחרוזות-t, כמו זה שהחבילה הזו מספקת עבור Babel.

המחיר של ההגבלה

מעבר לדרישת גרסת Python, המחיר של כל זה הוא כלל אחד: אינטרפולציה חייבת להיות שם פשוט.

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

זו מגבלה אמיתית, והיא בדיוק אותה מגבלה שמייצרת את ההבטחות שלמעלה. יחד עם קשירת ערכים בצד המקור ועם בדיקת מצייני מקום בזמן ריצה, היא מונעת ממחרוזות קטלוג להעריך ביטויים ושומרת על שמות מצייני מקום בעלי משמעות עבור מי שמתרגם אותם.

במחרוזת-f אי אפשר להשתמש כך כלל — עד שספרייה כלשהי רואה אותה, היא כבר מחרוזת גמורה, ולכן לתרגם אותה פירושו לתרגם פרגמנט. מחרוזות-t (PEP 750) שומרות על הטקסט הסטטי והערכים מופרדים, תוך שמירה על תחביר דמוי מחרוזת-f ועל קשירת ערכים מפורשת.

איך הגיעה Python לכאן — שני מסמכי PEP בהפרש של עשר שנים, ודיון בספרייה הסטנדרטית שנסגר ללא תשובה — מסופר, עם מקורות, בעמוד רקע.