mirror of
https://github.com/spoopyghosty0/frameport.git
synced 2026-10-06 10:00:36 +02:00
100 lines
3.9 KiB
Python
100 lines
3.9 KiB
Python
#!/usr/bin/env python3
|
|
"""Collect every translatable text into src/frameport/locales/template.json (see src/frameport/i18n.py).
|
|
|
|
python scripts/i18n_extract.py # rewrite the template
|
|
python scripts/i18n_extract.py --check # exit 1 if the template is out of date (CI / tests)
|
|
|
|
Sources: tr("…") and tr_n("…", "…", n) calls in src/frameport (literal arguments), the GUI help texts, and every
|
|
patch's title and description. A translation file (locales/<language>.json) copies the template and fills in the
|
|
values: a string for tr() texts, a list [one, other] for tr_n() texts.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import ast
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
SRC = ROOT / "src" / "frameport"
|
|
TEMPLATE = SRC / "locales" / "template.json"
|
|
sys.path.insert(0, str(ROOT / "src"))
|
|
|
|
|
|
def from_code() -> tuple[dict, list[str]]:
|
|
"""(texts, problems): tr()/tr_n() literals, and calls whose text isn't a literal (can't be extracted)."""
|
|
texts: dict = {}
|
|
problems = []
|
|
for path in sorted(SRC.rglob("*.py")):
|
|
if path.name == "i18n.py" and path.parent == SRC:
|
|
continue
|
|
tree = ast.parse(path.read_text(encoding="utf-8"))
|
|
for node in ast.walk(tree):
|
|
if not (isinstance(node, ast.Call) and isinstance(node.func, ast.Name) and node.func.id in ("tr", "tr_n")):
|
|
continue
|
|
args = node.args[:1] if node.func.id == "tr" else node.args[:2]
|
|
if all(isinstance(a, ast.Constant) and isinstance(a.value, str) for a in args) and args:
|
|
if node.func.id == "tr":
|
|
texts.setdefault(args[0].value, "")
|
|
else:
|
|
texts[args[0].value] = ["", ""]
|
|
elif any(isinstance(a, ast.JoinedStr) for a in args):
|
|
problems.append(f"{path.relative_to(ROOT)}:{node.lineno}: f-string inside {node.func.id}() "
|
|
"(use a template and .format())")
|
|
return texts, problems
|
|
|
|
|
|
def from_data() -> dict:
|
|
"""Help texts and patch titles/descriptions (defined as data, translated where they're shown)."""
|
|
from frameport.patches import base
|
|
from frameport.patches.settings import GROUPS, UI
|
|
from frameport.ui.help import HELP
|
|
|
|
texts = {dict.__getitem__(HELP, k): "" for k in HELP}
|
|
for p in base.all_patches():
|
|
for t in (p.title, p.description, p.summary):
|
|
if t:
|
|
texts[t] = ""
|
|
for _, title in GROUPS: # the Game settings dialog
|
|
texts[title] = ""
|
|
for ui in UI.values():
|
|
for t in (ui.get("label"), ui.get("help")):
|
|
if t:
|
|
texts[t] = ""
|
|
if ui["control"][0] == "choice":
|
|
for _, label in ui["control"][1]:
|
|
if any(c.isalpha() for c in label):
|
|
texts[label] = ""
|
|
return texts
|
|
|
|
|
|
def build() -> tuple[dict, list[str]]:
|
|
code, problems = from_code()
|
|
texts = {**from_data(), **code}
|
|
return dict(sorted(texts.items(), key=lambda kv: kv[0].lower())), problems
|
|
|
|
|
|
def main() -> int:
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("--check", action="store_true", help="only check that the template is up to date")
|
|
args = ap.parse_args()
|
|
texts, problems = build()
|
|
for p in problems:
|
|
print(p, file=sys.stderr)
|
|
text = json.dumps(texts, ensure_ascii=False, indent=1) + "\n"
|
|
if args.check:
|
|
current = TEMPLATE.read_text(encoding="utf-8") if TEMPLATE.exists() else ""
|
|
if current != text:
|
|
print("locales/template.json is out of date: run python scripts/i18n_extract.py", file=sys.stderr)
|
|
return 1
|
|
return 1 if problems else 0
|
|
TEMPLATE.parent.mkdir(parents=True, exist_ok=True)
|
|
TEMPLATE.write_text(text, encoding="utf-8")
|
|
print(f"{len(texts)} texts -> {TEMPLATE.relative_to(ROOT)}")
|
|
return 1 if problems else 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|