polib PO catalog read, edit and write roundtrip with MO compile: working versions (tested 2026-10-16)
Read, edit and write gettext PO catalogs with polib, compile them to MO, and audit fuzzy and untranslated entries fully offline.
passed6 passed · 0 failed2026-10-16 21:57 UTC- Status
- passed
- Last passed
- 2026-10-16 21:57 UTC
- Tests
- 6 / 6
- Language
- Python
- Runtime
- Python 3.13.16
pinned versions2
polib@1.2.0pytest@9.1.1
run it
uv venv -q .venv && uv pip install -q --python .venv/bin/python -r requirements.txt .venv/bin/python -m pytest -q
test environment
Python 3.13.16 linux 6.17.0-1022-azure x64 runner: github-actions 38972656245
The code
Exactly the files that passed. Your agent can pull them with get_recipe("polib-po-catalog-roundtrip") (install), or see them on GitHub.
requirements.txt
polib==1.2.0 pytest==9.1.1
catalog.py
import polib
PLURAL_FORMS = "nplurals=2; plural=(n > 1);"
def new_catalog(locale="fr"):
po = polib.POFile()
po.metadata = {
"Project-Id-Version": "demo",
"Language": locale,
"Content-Type": "text/plain; charset=UTF-8",
"Plural-Forms": PLURAL_FORMS,
}
return po
def add(po, msgid, msgstr="", fuzzy=False, plural=None):
# fuzzy=True is silently dropped by POEntry; the flag list is the real switch
entry = polib.POEntry(
msgid=msgid,
msgstr=msgstr,
msgid_plural=plural,
msgstr_plural={},
flags=["fuzzy"] if fuzzy else [],
)
po.append(entry)
return entry
def translate(po, msgid, msgstr):
entry = po.find(msgid)
if entry is None:
entry = add(po, msgid)
entry.msgstr = msgstr
if "fuzzy" in entry.flags:
entry.flags.remove("fuzzy")
return entry
def load(path):
return polib.pofile(str(path))
def save(po, path):
po.save(str(path))
def compile_to_mo(po, path):
po.save_as_mofile(str(path))
def missing_forms(entry, nplurals=2):
return [i for i in range(nplurals) if entry.msgstr_plural.get(i, "") == ""]
def audit(po):
return {
"total": len(po),
"translated": len(po.translated_entries()),
"fuzzy": len(po.fuzzy_entries()),
"untranslated": len(po.untranslated_entries()),
"percent": po.percent_translated(),
}
tests/test_catalog.py
import polib
from catalog import add, audit, compile_to_mo, load, missing_forms, new_catalog, save, translate
def test_edit_roundtrip_upserts_and_persists(tmp_path):
po = new_catalog("fr")
add(po, "Hello", "Bonjour")
add(po, "Bye", "")
path = tmp_path / "fr.po"
save(po, path)
reloaded = load(path)
translate(reloaded, "Bye", "Au revoir")
translate(reloaded, "New", "Nouveau")
save(reloaded, path)
again = load(path)
assert again.find("Hello").msgstr == "Bonjour"
assert again.find("Bye").msgstr == "Au revoir"
assert again.find("New").msgstr == "Nouveau"
assert audit(again)["untranslated"] == 0
def test_fuzzy_kwarg_is_ignored_only_flags_work():
ignored = polib.POEntry(msgid="x", msgstr="y", fuzzy=True)
assert ignored.fuzzy is False
assert ignored.translated() is True
marked = polib.POEntry(msgid="x", msgstr="y", flags=["fuzzy"])
assert marked.fuzzy is True
assert marked.translated() is False
def test_fuzzy_is_neither_translated_nor_untranslated(tmp_path):
po = new_catalog("fr")
add(po, "Draft", "Brouillon", fuzzy=True)
add(po, "Done", "Fait")
add(po, "Todo", "")
path = tmp_path / "fr.po"
save(po, path)
reloaded = load(path)
assert [e.msgid for e in reloaded.translated_entries()] == ["Done"]
assert [e.msgid for e in reloaded.fuzzy_entries()] == ["Draft"]
assert [e.msgid for e in reloaded.untranslated_entries()] == ["Todo"]
assert audit(reloaded)["percent"] == 33
def test_translate_clears_fuzzy_flag():
po = new_catalog("fr")
add(po, "Greeting", "Salut", fuzzy=True)
translate(po, "Greeting", "Bonjour")
entry = po.find("Greeting")
assert entry.fuzzy is False
assert entry.translated() is True
def test_partial_plural_missing_form_is_not_flagged(tmp_path):
po = new_catalog("fr")
po.append(polib.POEntry(msgid="File", msgid_plural="Files", msgstr_plural={0: "Fichier"}))
po.append(polib.POEntry(msgid="Cat", msgid_plural="Cats", msgstr_plural={0: "Chat", 1: ""}))
path = tmp_path / "fr.po"
save(po, path)
reloaded = load(path)
assert reloaded.find("File").translated() is True
assert missing_forms(reloaded.find("File"), nplurals=2) == [1]
assert reloaded.find("Cat").translated() is False
assert missing_forms(reloaded.find("Cat"), nplurals=2) == [1]
def test_mo_compile_drops_fuzzy_and_untranslated(tmp_path):
po = new_catalog("fr")
add(po, "Hello", "Bonjour")
add(po, "Draft", "Brouillon", fuzzy=True)
add(po, "Todo", "")
add(po, "File", plural="Files")
po.find("File").msgstr_plural = {0: "Fichier", 1: "Fichiers"}
mo_path = tmp_path / "fr.mo"
compile_to_mo(po, mo_path)
mo = polib.mofile(str(mo_path))
assert mo.find("Hello").msgstr == "Bonjour"
assert mo.find("File").msgstr_plural == {0: "Fichier", 1: "Fichiers"}
assert mo.find("Draft") is None
assert mo.find("Todo") is None
assert mo.metadata["Content-Type"] == "text/plain; charset=UTF-8"
pytest.ini
[pytest] pythonpath = .
Connect to ProvenPin
Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.
terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp
~/.cursor/mcp.json
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }.vscode/mcp.json
{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }~/.codeium/windsurf/mcp_config.json
{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }MCP settings
{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.