My first article about gettext-tstrings explained the design: turn a Python 3.14 t-string into a stable gettext message ID, keep translations as data rather than code, and validate placeholders before a damaged catalog reaches users.
That article left a more practical question unanswered:
Can an existing gettext codebase adopt t-strings one call site at a time without replacing its catalog workflow?
I tested the current v0.1.0a8 release on Windows with a mixed old/new example, the full test suite, and the repository's microbenchmark. This is not another t-string introduction. It is a migration check with commands and limitations.
The environment
-
gettext-tstrings 0.1.0a8at commit3b65baebfc710a750b943073a3c11b6596e396e3 - CPython 3.14.6, 64-bit
- Babel 2.18.0
- Windows 11 (
10.0.26200) - AMD Ryzen 5 3600XT, 6 cores / 12 logical processors
- uv 0.11.28 with the committed lockfile
I created the environment without allowing uv to download another Python:
uv sync `
--python "C:\Users\yusuk\AppData\Local\Python\pythoncore-3.14-64\python.exe" `
--no-python-downloads `
--frozen
A deliberately mixed source file
The migration sample contains ordinary gettext calls and one t-string call:
from gettext_tstrings import tr
name = "Ada"
count = 2
old_style = _("Legacy message")
new_style = tr(t"Hello {name}")
plural = ngettext("One legacy file", "{count} legacy files", count)
The Babel mapping remains small:
[gettext_tstrings: **.py]
encoding = utf-8
I then ran the normal extraction command:
uv run --no-sync pybabel extract `
-F babel.cfg `
-o .verification/messages.pot `
examples
The same POT contained all three forms. The ordinary message remained ordinary:
#: examples/mixed_migration.py:6
msgid "Legacy message"
msgstr ""
The t-string received the library marker and Babel's standard brace-format flag:
#. gettext-tstrings
#: examples/app.py:31 examples/app.py:37 examples/mixed_migration.py:7
#, python-brace-format
msgid "Hello {name}"
msgstr ""
The legacy plural was extracted into the same catalog:
#: examples/mixed_migration.py:8
#, python-brace-format
msgid "One legacy file"
msgid_plural "{count} legacy files"
msgstr[0] ""
msgstr[1] ""
This is the migration property I wanted to verify: the extractor does not require a flag day. Existing string calls and new t-string calls can share one POT while individual call sites move gradually.
What the full suite covered on this machine
I ran:
uv run --no-sync pytest `
--cov=gettext_tstrings `
--cov-report=term-missing
The result was:
452 tests collected
449 passed, 3 skipped in 8.52s
905 statements, 310 branches
100% coverage
The three skips matter. All were toolchain tests requiring GNU gettext tools, which were not installed on this Windows machine. I am not treating a skip as proof. Runtime, extraction, checker, conformance, retention, and binding tests passed here; the GNU msgfmt integration still needs a machine with that tool installed.
That limitation is useful migration information by itself. A green Python suite does not demonstrate every external catalog compiler in your delivery pipeline. Keep the GNU check in CI if your translators or release process depend on it.
The measured runtime cost
I ran the repository's benchmarks/runtime.py in five separate processes. These are the medians:
| Path | Median ns/op |
|---|---|
| f-string | 55.7 |
gettext(str).format |
316.2 |
compiled.render |
304.1 |
compile_template |
517.5 |
tr (1 field) |
901.6 |
tr (2 fields) |
1,271.4 |
Translator (1 field) |
1,046.0 |
ngettext (1 field) |
1,758.9 |
For tr with one field, the five runs ranged from 896.0 to 960.9 ns/op.
These numbers are not universal. My earlier article included an Apple Silicon measurement around 0.4 microseconds; this Windows/AMD result is about 0.90 microseconds for the same broad operation. Different CPU, OS, Python patch release, and package version make a direct comparison misleading.
The useful conclusion is narrower: on this machine, validation and catalog rendering remained below one microsecond for the common one-field tr path. Measure it again on your own interpreter and hardware before using the number in a capacity decision.
What I would require before migrating production code
This experiment changed my migration checklist into something concrete:
- Pin a released version or commit while the package is alpha.
- Extract old and new calls into one POT and review the diff.
- Keep placeholder validation in both Babel and GNU gettext stages used by your pipeline.
- Test damaged translations in fallback and strict modes.
- Test locale binding under the concurrency model your application actually uses.
- Benchmark on the deployment interpreter instead of copying a number from a README or article.
The most valuable result was not the benchmark. It was seeing the old and new calls coexist in an ordinary catalog with an explicit marker on messages that require the stricter t-string contract.
The reproducible contract is documented in SPEC.md, and the exact release tested here is v0.1.0a8.
If you maintain a gettext-based Python project, I would be interested in the migration failure you would test first. Catalog extraction, placeholder compatibility, thread/context behavior, or something else?
Disclosure: I used an AI assistant to organize the test plan, run the documented checks on my development machine, and edit this draft. I reviewed the commands, source references, and results included above before publication.
Top comments (0)