Migration¶
If your project already uses gettext, the questions that decide whether this library is adoptable are narrow ones: does it invalidate the catalogs you have, can it coexist with the code you are not ready to change, and how much of the move has to happen at once. The answers, shortest first:
| Question | Answer |
|---|---|
Do existing .po and .mo files still work? |
Yes. Same files, same tools. |
| Can old and new calls live in one file? | Yes, and one extractor mapping covers both. |
| Does the msgid change? | Not from .format(). Yes from %-format. |
| Must the whole project move at once? | No. One call site is a valid change. |
| What about Jinja, Django templates, JavaScript? | Untouched, same catalogs. |
The rest of this page is the detail behind each of those.
From .format(): the msgid does not change¶
This is the case where migration costs almost nothing. A str.format message
and a t-string message derive the same catalog key, because the key is the
text with {name} left in it either way:
# Before
_("Hello {name}").format(name=name)
# After — the msgid is still "Hello {name}"
tr(t"Hello {name}")
So the existing translation stays attached. Starting from a catalog holding
change the call, re-extract, and update:
$ pybabel extract -F babel.cfg -o locales/messages.pot .
extracting messages from app.py (encoding="utf-8")
writing PO template file to locales/messages.pot
$ pybabel update -i locales/messages.pot -d locales
updating catalog locales/ja/LC_MESSAGES/messages.po based on locales/messages.pot
The entry that comes back differs in two lines of metadata and nothing else — a marker comment identifying it as a t-string message, and a source line number:
No fuzzy flag, no re-translation, in any language. The message renders
immediately:
$ pybabel compile -d locales
compiling catalog locales/ja/LC_MESSAGES/messages.po to locales/ja/LC_MESSAGES/messages.mo
$ python app.py
こんにちは Ada
update --check will report the catalogs as out of date
That marker comment and the moved line numbers are enough for
pybabel update --check to say a catalog needs regenerating, because it
compares the whole entry and not only the translation. Run the real
pybabel update in the same commit as the code change, and commit the
catalogs with it — the same habit the
CI gate already asks for.
From %-format: the msgid changes, so translations go fuzzy¶
Printf syntax lives inside the message, so replacing it rewrites the catalog
key. There is no way around that, and it is the honest cost of leaving
%(name)s behind:
pybabel update recognizes the new message as a close relative of the removed
one and carries the old translation across, marked fuzzy:
#. gettext-tstrings
#: app.py:4
#, fuzzy, python-brace-format, python-format
msgid "Hello {name}"
msgstr "こんにちは %(name)s"
Three things to know about that state:
- Nothing breaks at runtime. Fuzzy entries are excluded from the compiled
.mo, so the application renders the source message until a human confirms the pair — the same degradation any reworded message goes through. - CI stays green while they are fuzzy. The placeholder checker skips fuzzy
entries, exactly as
msgfmt --check-formatdoes, because an entry that cannot reach the runtime should not fail a build. The moment a translator clears the flag, the entry is checked like any other — so a%(name)sleft in a confirmed translation is caught then, which is the point at which it would otherwise start rendering. - The old
python-formatflag rides along and should be deleted with thefuzzyflag, ormsgfmt --check-formatwill keep applying printf rules to a brace-format message.
For named printf placeholders the edit is mechanical — %(name)s becomes
{name} and nothing else moves — so a large catalog is a scripted pass
followed by a translator's review, rather than a re-translation. Positional
%s is not mechanical: it has no name to carry over, and choosing one is the
point of the change.
The migration can therefore proceed at whatever pace review allows: an unconverted fuzzy entry is a visible piece of work in the catalog, not a broken build.
Old and new calls coexist¶
The extractor that reads t-strings also reads ordinary gettext calls, so one mapping covers a file in mid-migration:
from gettext_tstrings import tr
from myapp.i18n import _
name = "Ada"
print(_("Save changes"))
print(tr(t"Hello {name}"))
Both messages land in the same template, and only the t-string one carries the marker comment that turns on this library's extra checking:
#: app.py:5
msgid "Save changes"
msgstr ""
#. gettext-tstrings
#: app.py:6
#, python-brace-format
msgid "Hello {name}"
msgstr ""
It recognizes _(), the four standard gettext names, the tr() / ntr()
aliases, and the deferred lazy_gettext() / lazy_pgettext(). A helper of
your own has to be named in the mapping.
At runtime the two styles are equally independent: gettext.translation()
returns one translations object, and both _ and this library's entry points
read from it.
What does not move¶
- Template languages. Jinja2's
{% trans %}, Django's template tags, and their Babel extractors keep working unchanged and keep feeding the same PO catalogs. t-strings are Python syntax; they apply to Python source. - Your catalog files. No format change, no new file, no conversion step.
- Your translation platform. The
.pointerchange is identical, and thepython-brace-formatflag that a t-string message carries is the same flag a.format()message carries — so placeholder QA keeps working. - Non-Python code. A JavaScript or C catalog in the same project is unaffected.
A migration checklist¶
- Add the
babelextra wherepybabelruns, and change thepythonmapping inbabel.cfgto thegettext_tstringsmethod — one mapping then covers both styles, and-kkeeps working for the ordinary calls. - Convert
.format()call sites first. Re-extract, runpybabel update, and commit the catalogs with the code; expect no fuzzy entries. - Convert
%-format call sites in batches you can get reviewed, rewriting the carried-over placeholders and clearing thefuzzyandpython-formatflags. - Fix what the restriction rejects: an interpolation must be a plain name, so
t"Hello {user.name}"becomes a local variable first. This is a call-site edit, not a catalog one. - Turn on
strict = truein the extractor mapping once the sweep is done, so a message that cannot be extracted fails the build rather than disappearing from the template. - Add the runtime check from In production:
render one message per shipped language through a strict
Translator.
Steps 2 and 3 are ordinary commits. Nothing in this list needs a flag day.