i18n phase 3: Italian catalog
- babel.cfg and the catalog sources: bal/locale/bal.pot (424 texts) and
bal/locale/it_IT/LC_MESSAGES/bal.po, fully translated (43 entries taken
from Electrum's it_IT catalog, the rest following the owner's glossary
and review: "locktime" kept in English, "transazione senza
Will-Executor" for the backup transaction).
- build_zip.py compiles each bal.po into bal.mo inside the zip (Babel
required); .po/.pot are not shipped and *.mo is ignored by git.
- tests/test_translations.py: catalogs compile, {} fields and $tokens
match, no address/e-mail/URL added by a translation (patterns from
electrum-locale).
- AGENTS.md: how to update the catalogs or add a language.
See CHANGELOG entry 60 and PLAN_I18N.md.
This commit is contained in:
55
build_zip.py
55
build_zip.py
@@ -9,7 +9,12 @@ Electrum loads external plugins from a ``.zip`` using Python's ``zipimport``.
|
||||
* uses standard DEFLATE compression (well supported by ``zipimport``);
|
||||
* emits entries in a deterministic, sorted order so the archive is
|
||||
reproducible (stable SHA-256);
|
||||
* skips ``__pycache__`` directories and compiled ``*.pyc``/``*.pyo`` files.
|
||||
* skips ``__pycache__`` directories and compiled ``*.pyc``/``*.pyo`` files;
|
||||
* compiles each translation catalog ``bal/locale/<lang>/LC_MESSAGES/bal.po``
|
||||
into ``bal.mo`` inside the archive (the ``.mo`` files are not kept in
|
||||
git, and the ``.po``/``.pot`` sources are not shipped). This needs Babel
|
||||
(``pip install babel``); without it the build stops, so a release can
|
||||
never ship without its translations by mistake.
|
||||
|
||||
The archive keeps the top-level ``bal/`` directory so that the package is
|
||||
importable as ``bal`` (and Electrum derives ``dirname='bal'`` from the path of
|
||||
@@ -23,14 +28,36 @@ Prints the resulting size and SHA-256 so the download can be integrity-checked.
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import io
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import zipfile
|
||||
|
||||
SRC_ROOT = "bal"
|
||||
DEFAULT_OUT = "bal-electrum-plugin.zip"
|
||||
|
||||
|
||||
def compile_catalog(po_path: str) -> bytes:
|
||||
"""Return the compiled ``.mo`` bytes of the gettext catalog ``po_path``.
|
||||
|
||||
Fuzzy (unreviewed) entries are left out, as ``msgfmt`` does.
|
||||
"""
|
||||
try:
|
||||
from babel.messages.mofile import write_mo
|
||||
from babel.messages.pofile import read_po
|
||||
except ImportError:
|
||||
raise SystemExit(
|
||||
"ERROR: Babel is needed to compile the translations "
|
||||
"(pip install babel)"
|
||||
) from None
|
||||
with open(po_path, "rb") as f:
|
||||
catalog = read_po(f)
|
||||
buf = io.BytesIO()
|
||||
write_mo(buf, catalog, use_fuzzy=False)
|
||||
return buf.getvalue()
|
||||
|
||||
|
||||
def build(out_path: str) -> None:
|
||||
if os.path.exists(out_path):
|
||||
os.remove(out_path)
|
||||
@@ -40,17 +67,34 @@ def build(out_path: str) -> None:
|
||||
# prune cache dirs in place so os.walk does not descend into them
|
||||
dirnames[:] = [d for d in dirnames if d != "__pycache__"]
|
||||
for fn in filenames:
|
||||
if fn.endswith((".pyc", ".pyo")):
|
||||
# .mo files are rebuilt from the .po sources below; the .po/.pot
|
||||
# sources themselves are not shipped.
|
||||
if fn.endswith((".pyc", ".pyo", ".po", ".pot", ".mo")):
|
||||
continue
|
||||
files.append(os.path.join(dirpath, fn))
|
||||
catalogs = {} # archive path of the .mo -> source .po
|
||||
for dirpath, _dirnames, filenames in os.walk(os.path.join(SRC_ROOT, "locale")):
|
||||
for fn in filenames:
|
||||
if fn.endswith(".po"):
|
||||
po = os.path.join(dirpath, fn)
|
||||
catalogs[po[: -len(".po")] + ".mo"] = po
|
||||
files.sort()
|
||||
|
||||
with zipfile.ZipFile(
|
||||
out_path, "w", compression=zipfile.ZIP_DEFLATED, compresslevel=6
|
||||
) as z:
|
||||
for f in files:
|
||||
entries = [(f, None) for f in files] + [
|
||||
(mo, po) for mo, po in catalogs.items()
|
||||
]
|
||||
for f, po in sorted(entries):
|
||||
arc = f.replace(os.sep, "/") # forward slashes inside the archive
|
||||
z.write(f, arc)
|
||||
if po is None:
|
||||
z.write(f, arc)
|
||||
else:
|
||||
# Same timestamp rule as z.write(): the source file's mtime.
|
||||
info = zipfile.ZipInfo(arc, time.localtime(os.path.getmtime(po))[:6])
|
||||
info.compress_type = zipfile.ZIP_DEFLATED
|
||||
z.writestr(info, compile_catalog(po))
|
||||
|
||||
# Integrity + summary
|
||||
with zipfile.ZipFile(out_path) as z:
|
||||
@@ -63,7 +107,8 @@ def build(out_path: str) -> None:
|
||||
|
||||
data = open(out_path, "rb").read()
|
||||
print(f"built : {out_path}")
|
||||
print(f"files : {len(files)}")
|
||||
print(f"files : {len(files) + len(catalogs)}")
|
||||
print(f"langs : {', '.join(sorted(os.path.basename(os.path.dirname(os.path.dirname(m))) for m in catalogs)) or '-'}")
|
||||
print(f"size : {len(data)} bytes")
|
||||
print(f"sha256: {hashlib.sha256(data).hexdigest()}")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user