This is an automated email from the ASF dual-hosted git repository.

jimjag pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/openoffice-devtools.git


The following commit(s) were added to refs/heads/main by this push:
     new 7a4cb47  translation-scripts: add update_translations.py to update 
every language without Pootle
7a4cb47 is described below

commit 7a4cb47771fa34cac9be1d846a47fbac2fa4001d
Author: Jim Jagielski <[email protected]>
AuthorDate: Thu Nov 12 10:16:10 2026 -0500

    translation-scripts: add update_translations.py to update every language 
without Pootle
    
    rekey now finds the template the tree is keyed from, so it can be rerun, 
and the
    tools split SDF lines at newlines only: some translations contain 
U+2028/U+2029.
---
 translation-scripts/integrate/README.md            |  36 ++
 translation-scripts/integrate/sdf_integrate.py     |  99 ++++--
 .../integrate/update_translations.py               | 370 +++++++++++++++++++++
 3 files changed, 478 insertions(+), 27 deletions(-)

diff --git a/translation-scripts/integrate/README.md 
b/translation-scripts/integrate/README.md
index 03b38bd..c6d7af5 100644
--- a/translation-scripts/integrate/README.md
+++ b/translation-scripts/integrate/README.md
@@ -11,6 +11,42 @@ so a whole-file replacement loses them (15,441 entries for 
`am` in 2026).
 Needs Python 3.7+, translate-toolkit (`po2oo`) and `gsicheck` from a built
 OpenOffice tree. No other dependencies.
 
+## All languages in one command
+
+`update_translations.py` does what the Pootle server's template update and an
+integration round do together, for every tree language or for `--langs de,fr`:
+
+    update_translations.py --aoo ~/src/openoffice            # report only
+    update_translations.py --aoo ~/src/openoffice --apply    # write the tree 
files
+
+- English template: `../scripts/en-US.sdf`, or `--extract` to make a fresh one
+  from `--aoo` (Linux, configured and built; `localize -e` fails on macOS).
+- PO files: cloned from apache/openoffice-translation into the work directory,
+  only the languages asked for, or `--po-repo` for an existing checkout.
+- translate-toolkit: 2.2.5, the server's version, installed into a venv in the
+  work directory unless this Python already has it.
+- `gsicheck`: from `main/solver` in `--aoo`, or `--solver`.
+
+Per language it re-keys the tree file (the steps below), makes templates, runs
+`fix_msgctxt.py`, `pomerge` and `po2oo`, and gates the merge on `gsicheck`. The
+tree is matched against every committed version of `../scripts/en-US.sdf` and
+the 2022 one in openoffice-translation; if it is already keyed to the current
+template, nothing moves.
+
+`--mode add-only` (the default) fills only entries the tree has no translation
+for. `--mode update` also replaces tree text with newer PO text. Use it with
+`--history` and a few older openoffice-translation revisions, so that text from
+a Pootle rollback is not taken; this needs the repository's full history. In
+2026 the revisions `ebbd469a1c a5e2ff9195 a962caea70 4b986fdc1b 3a07d1c27e
+60e3e58b9e c1358ae8ce` held back 1,086 `uk` and 789 `pl` entries.
+
+Each run gets its own directory under `--workdir` with `report.md` (entries
+moved, updated, added and left out, and translated entries before and after),
+`langs/<lang>/` with the intermediate files and a log, `pot/`, and with
+`--export-po` the updated PO files that Pootle would offer translators. Nothing
+is committed. Languages that have PO files but no tree file are listed, not
+added. A second run on the result reports no changes.
+
 ## Steps, for one language
 
 1. Convert the PO files with the English SDF they were made from:
diff --git a/translation-scripts/integrate/sdf_integrate.py 
b/translation-scripts/integrate/sdf_integrate.py
index d7ce227..df525c0 100755
--- a/translation-scripts/integrate/sdf_integrate.py
+++ b/translation-scripts/integrate/sdf_integrate.py
@@ -48,13 +48,22 @@ def key(cols):
     return (cols[0], cols[1], cols[3], cols[4], cols[5], cols[6], cols[7])
 
 
+def read_lines(path):
+    """Lines with their endings, split at \n only.
+
+    str.splitlines() also splits at U+2028, U+2029 and other separators, and
+    some translations contain them."""
+    with open(path, encoding="utf-8", newline="") as f:
+        parts = f.read().split("\n")
+    return [p + "\n" for p in parts[:-1]] + ([parts[-1]] if parts[-1] else [])
+
+
 def load(path, lang):
     entries = {}
-    with open(path, encoding="utf-8") as f:
-        for line in f:
-            cols = line.rstrip("\r\n").split("\t")
-            if len(cols) == 15 and cols[9] == lang:
-                entries[key(cols)] = cols
+    for line in read_lines(path):
+        cols = line.rstrip("\r\n").split("\t")
+        if len(cols) == 15 and cols[9] == lang:
+            entries[key(cols)] = cols
     return entries
 
 
@@ -96,16 +105,28 @@ def combine(lang, template, base_path, newer_path, 
history_paths):
     return "".join(out), stats
 
 
-def merge(lang, tree_path, new_path, template, skip=(), add_only=False):
-    """Return (text, changed keys, added keys)."""
+def history_values(lang, paths):
+    values = collections.defaultdict(set)
+    for path in paths:
+        for k, cols in load(path, lang).items():
+            for i in TEXT_COLUMNS:
+                values[(k, i)].add(cols[i])
+    return values
+
+
+def merge(lang, tree_path, new_path, template, skip=(), add_only=False, 
history=()):
+    """Return (text, changed keys, added keys).
+
+    A translation in the tree is not replaced by text found in a history SDF
+    for the same key, which is how a Pootle rollback shows up."""
     new = load(new_path, lang)
     if not new:
         sys.exit("no %s lines in %s; po2oo --language must use the tree's code 
(pt-BR, not pt_BR)"
                  % (lang, new_path))
     en = load(template, "en-US")
     skip = set(skip)
-    with open(tree_path, encoding="utf-8", newline="") as f:
-        lines = f.read().splitlines(keepends=True)
+    old = history_values(lang, history)
+    lines = read_lines(tree_path)
 
     seen, changed, last_of_file = set(), [], {}
     timestamp, eol = None, "\n"
@@ -122,7 +143,7 @@ def merge(lang, tree_path, new_path, template, skip=(), 
add_only=False):
             if n and k not in skip:
                 updated = list(cols)
                 for i in TEXT_COLUMNS:
-                    if add_only and is_translation(cols[i], e[i] if e else 
None):
+                    if is_translation(cols[i], e[i] if e else None) and 
(add_only or n[i] in old[(k, i)]):
                         continue
                     if n[i] != cols[i] and is_translation(n[i], e[i] if e else 
None):
                         updated[i] = n[i]
@@ -159,13 +180,27 @@ def with_key(cols, k):
     return row
 
 
-def rekey(lang, tree_path, old_template, template):
+def rekey(lang, tree_path, old_templates, template):
     """Move tree entries whose key changed to the key their English has now.
 
-    A move needs the English to be the same under both keys: first the same
-    key without the help lid (oldref), else the only key in the same file with
-    that English.  Return (text, moves, stats)."""
-    en_old, en_new = load(old_template, "en-US"), load(template, "en-US")
+    The tree is taken to be keyed from whichever of the old templates, or the
+    current one, holds most of its keys; if that is the current template,
+    nothing moves.  A move needs the English to be the same under both keys:
+    first the same key without the help lid (oldref), else the only key in the
+    same file with that English.  Return (text, moves, stats)."""
+    en_new = load(template, "en-US")
+    own = set(key(c) for c in (l.rstrip("\r\n").split("\t") for l in 
read_lines(tree_path))
+              if len(c) == 15 and c[9] == lang)
+    best, en_old = None, en_new
+    hits = len(own & en_new.keys())
+    for path in old_templates:
+        cand = load(path, "en-US")
+        n = len(own & cand.keys())
+        if n > hits:
+            best, en_old, hits = path, cand, n
+    if best is None:
+        with open(tree_path, encoding="utf-8", newline="") as f:
+            return f.read(), {}, collections.Counter({"keyed to the current 
template": len(own)})
     by_file = collections.defaultdict(lambda: collections.defaultdict(list))
     old_by_file = collections.defaultdict(collections.Counter)
     for k, cols in en_new.items():
@@ -173,8 +208,7 @@ def rekey(lang, tree_path, old_template, template):
     for k, cols in en_old.items():
         old_by_file[k[:2]][english(cols)] += 1
 
-    with open(tree_path, encoding="utf-8", newline="") as f:
-        lines = f.read().splitlines(keepends=True)
+    lines = read_lines(tree_path)
     # Some tree files carry an en-US line paired with the translation; gsicheck
     # needs the pair to keep one key, so both lines move together.
     tree_keys = []
@@ -183,7 +217,7 @@ def rekey(lang, tree_path, old_template, template):
         ok = len(cols) == 15 and cols[9] in (lang, "en-US")
         tree_keys.append(key(cols) if ok else None)
 
-    stats = collections.Counter()
+    stats = collections.Counter({"keyed from " + best: hits})
     plan = {}
     for k in dict.fromkeys(filter(None, tree_keys)):
         old, new = en_old.get(k), en_new.get(k)
@@ -278,23 +312,31 @@ def cmd_rekey(a):
 
 def cmd_merge(a):
     skip = [tuple(k) for k in json.load(open(a.skip))] if a.skip else []
-    text, changed, added = merge(a.lang, a.tree, a.new, a.template, skip, 
a.add_only)
+    text, changed, added = merge(a.lang, a.tree, a.new, a.template, skip, 
a.add_only, a.history)
     write(a.out, text)
     print("%s: %d entries updated, %d keys added" % (a.lang, len(changed), 
len(added)))
 
 
-def cmd_gate(a):
-    baseline = run_gsicheck(a.gsicheck, a.libdir, a.tree, a.lang)
+def gate(lang, tree, new, template, out, gsicheck, libdir, rounds=4, 
add_only=False, history=()):
+    """Merge, leaving out every entry that adds a gsicheck error.
+
+    Return (changed, added, left-out keys, new errors not tied to an entry)."""
+    baseline = run_gsicheck(gsicheck, libdir, tree, lang)
     skip = []
-    for _ in range(a.rounds):
-        text, changed, added = merge(a.lang, a.tree, a.new, a.template, skip, 
a.add_only)
-        write(a.out, text)
-        new_errors = run_gsicheck(a.gsicheck, a.libdir, a.out, a.lang) - 
baseline
+    for _ in range(rounds):
+        text, changed, added = merge(lang, tree, new, template, skip, 
add_only, history)
+        write(out, text)
+        new_errors = run_gsicheck(gsicheck, libdir, out, lang) - baseline
         keyed = {uid_to_key(uid) for _, uid in new_errors if uid} - set(skip)
         if not keyed:
             break
         skip += sorted(keyed)
-    unkeyed = [msg for msg, uid in new_errors.elements() if not uid]
+    return changed, added, skip, new_errors
+
+
+def cmd_gate(a):
+    changed, added, skip, new_errors = gate(a.lang, a.tree, a.new, a.template, 
a.out, a.gsicheck, a.libdir,
+                                            a.rounds, a.add_only, a.history)
     if a.skip_out:
         with open(a.skip_out, "w", encoding="utf-8") as f:
             json.dump(skip, f, ensure_ascii=False, indent=1)
@@ -323,7 +365,8 @@ def main():
     r = sub.add_parser("rekey", help="move tree entries to the keys of a newer 
en-US.sdf")
     r.add_argument("--lang", required=True)
     r.add_argument("--tree", required=True, 
help="extras/l10n/source/<lang>/localize.sdf")
-    r.add_argument("--old-template", required=True, help="en-US.sdf the tree 
keys come from")
+    r.add_argument("--old-template", required=True, nargs="+",
+                   help="en-US.sdf files the tree keys may come from; the best 
match is used")
     r.add_argument("--template", required=True, help="en-US.sdf extracted from 
the current source")
     r.add_argument("--out", required=True)
     r.set_defaults(func=cmd_rekey)
@@ -338,6 +381,8 @@ def main():
         m.add_argument("--out", required=True)
         m.add_argument("--add-only", action="store_true",
                        help="fill only entries the tree has no translation 
for")
+        m.add_argument("--history", nargs="*", default=[],
+                       help="po2oo output from older snapshots; their text 
never replaces a tree translation")
         if name == "merge":
             m.add_argument("--skip", help="JSON list of keys to leave as they 
are")
         else:
diff --git a/translation-scripts/integrate/update_translations.py 
b/translation-scripts/integrate/update_translations.py
new file mode 100755
index 0000000..7078238
--- /dev/null
+++ b/translation-scripts/integrate/update_translations.py
@@ -0,0 +1,370 @@
+#!/usr/bin/env python3
+################################################################
+#
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing,
+#  software distributed under the License is distributed on an
+#  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+#  KIND, either express or implied.  See the License for the
+#  specific language governing permissions and limitations
+#  under the License.
+#
+################################################################
+"""Update extras/l10n/source/*/localize.sdf from the Pootle PO files, without 
Pootle.
+
+For each language this does what the Pootle server and the integration
+round do together: re-key the tree file to the current en-US.sdf, make
+templates, re-key and pomerge the PO files from apache/openoffice-translation,
+convert them with po2oo, and merge the result into the tree file under a
+gsicheck gate.  Nothing is committed; --apply copies the results into the tree.
+
+See README.md for the steps it runs and how to check the result.
+"""
+
+import argparse
+import collections
+import concurrent.futures
+import datetime
+import filecmp
+import glob
+import json
+import os
+import platform
+import shutil
+import subprocess
+import sys
+
+HERE = os.path.dirname(os.path.abspath(__file__))
+sys.path.insert(0, HERE)
+import sdf_integrate as si  # noqa: E402
+
+DEFAULT_TEMPLATE = os.path.join(HERE, os.pardir, "scripts", "en-US.sdf")
+PO_REPO_URL = "https://github.com/apache/openoffice-translation.git";
+TOOLKIT = "translate-toolkit==2.2.5"  # what the Pootle server runs
+
+
+def log_run(cmd, log, **kw):
+    log.write("$ " + " ".join(cmd) + "\n")
+    log.flush()
+    return subprocess.run(cmd, stdout=log, stderr=subprocess.STDOUT, 
**kw).returncode
+
+
+def toolkit_python(args, workdir):
+    if args.python:
+        return args.python
+    if subprocess.run([sys.executable, "-c", "import translate"], 
capture_output=True).returncode == 0:
+        return sys.executable
+    env = os.path.join(workdir, "venv")
+    py = os.path.join(env, "bin", "python")
+    if not os.path.exists(py):
+        print("installing %s into %s" % (TOOLKIT, env))
+        subprocess.run([sys.executable, "-m", "venv", env], check=True)
+        subprocess.run([py, "-m", "pip", "install", "-q", 
"--disable-pip-version-check", TOOLKIT], check=True)
+    return py
+
+
+def extract_template(aoo, out):
+    if platform.system() == "Darwin":
+        sys.exit("localize -e does not work on macOS; extract on a Linux build 
or pass --template")
+    envs = glob.glob(os.path.join(aoo, "main", "*Env.Set.sh"))
+    if len(envs) != 1:
+        sys.exit("need exactly one main/*Env.Set.sh in a configured and built 
tree, found %d" % len(envs))
+    cmd = 'cd "%s" && . "./%s" >/dev/null && localize -e -l en-US -f "%s"' % (
+        os.path.join(aoo, "main"), os.path.basename(envs[0]), out)
+    with open(out + ".log", "w") as log:
+        rc = log_run(["bash", "-c", cmd], log)
+    if rc or not os.path.getsize(out):
+        sys.exit("localize -e failed, see %s.log" % out)
+
+
+def old_templates(template, po_repo, extra, outdir):
+    """en-US.sdf files the tree may still be keyed from: every committed 
version
+    of scripts/en-US.sdf, the legacy copy in openoffice-translation, and 
extra."""
+    found = list(extra)
+    committed = os.path.realpath(DEFAULT_TEMPLATE)
+    top = subprocess.run(["git", "-C", os.path.dirname(committed), 
"rev-parse", "--show-toplevel"],
+                         capture_output=True, text=True)
+    if top.returncode == 0:
+        rel = os.path.relpath(committed, top.stdout.strip())
+        revs = subprocess.run(["git", "-C", top.stdout.strip(), "log", 
"--format=%h", "--", rel],
+                              capture_output=True, text=True).stdout.split()
+        for rev in revs:
+            path = os.path.join(outdir, "en-US.%s.sdf" % rev)
+            with open(path, "wb") as f:
+                subprocess.run(["git", "-C", top.stdout.strip(), "show", 
"%s:%s" % (rev, rel)], stdout=f)
+            if os.path.getsize(path) and not filecmp.cmp(path, template, 
shallow=False):
+                found.append(path)
+    legacy = os.path.join(po_repo, "en-US.sdf") if po_repo else None
+    if legacy and os.path.exists(legacy) and not filecmp.cmp(legacy, template, 
shallow=False):
+        found.append(legacy)
+    return found
+
+
+def get_po_repo(args, workdir, langs):
+    if args.no_po:
+        return None
+    repo = args.po_repo or os.path.join(workdir, "openoffice-translation")
+    dirs = ["PO-Files/" + x.replace("-", "_") for x in langs] if args.langs 
else ["PO-Files"]
+    if args.po_repo:
+        if not os.path.isdir(os.path.join(repo, "PO-Files")):
+            sys.exit("%s has no PO-Files directory" % repo)
+    elif not os.path.isdir(os.path.join(repo, ".git")):
+        print("cloning %s (%s) into %s" % (PO_REPO_URL, " ".join(dirs) if 
args.langs else "PO-Files", repo))
+        depth = [] if args.history else ["--depth", "1"]
+        subprocess.run(["git", "clone", "--filter=blob:none", "--sparse"] + 
depth + [PO_REPO_URL, repo], check=True)
+        subprocess.run(["git", "-C", repo, "sparse-checkout", "set"] + dirs, 
check=True)
+    elif subprocess.run(["git", "-C", repo, "sparse-checkout", "list"], 
capture_output=True).returncode == 0:
+        subprocess.run(["git", "-C", repo, "sparse-checkout", "add"] + dirs, 
check=True)
+    if args.history and subprocess.run(["git", "-C", repo, "rev-parse", 
"--is-shallow-repository"],
+                                       capture_output=True, 
text=True).stdout.strip() == "true":
+        sys.exit("--history needs the full history of %s (git fetch 
--unshallow)" % repo)
+    return repo
+
+
+def po_to_sdf(py, lang, podir, pot, template, out, log):
+    """Pootle's template update and mksdf.sh for one language: re-key, 
pomerge, po2oo."""
+    fixed, merged = out + ".fixed", out + ".merged"
+    steps = [
+        [py, os.path.join(HERE, "fix_msgctxt.py"), "--template", pot, 
"--input", podir, "--output", fixed],
+        [py, "-m", "translate.tools.pomerge", "--template=" + pot + "/", 
"--input=" + fixed + "/",
+         "--output=" + merged + "/"],
+        [py, "-m", "translate.convert.po2oo", "--template=" + template, 
"--skipsource", "--keeptimestamp",
+         "--language=" + lang, merged + "/", out],
+    ]
+    for cmd in steps:
+        if log_run(cmd, log):
+            raise RuntimeError("%s failed" % os.path.basename(cmd[1] if cmd[1] 
!= "-m" else cmd[2]))
+    return merged
+
+
+def check_rekey(tree, rekeyed, old, template):
+    """Every changed line keeps its text, timestamp and line ending, and its 
English."""
+    en_new, en_old = si.load(template, "en-US"), si.load(old, "en-US")
+    a, b = si.read_lines(tree), si.read_lines(rekeyed)
+    if len(a) != len(b):
+        return "line count changed"
+    for x, y in zip(a, b):
+        if x == y:
+            continue
+        p, q = x.rstrip("\r\n").split("\t"), y.rstrip("\r\n").split("\t")
+        same = [p[i] for i in (2, 8, 9, 10, 11, 12, 13, 14)] == [q[i] for i in 
(2, 8, 9, 10, 11, 12, 13, 14)]
+        if not same or x[len(x.rstrip("\r\n")):] != y[len(y.rstrip("\r\n")):]:
+            return "a moved line changed outside its key: %s" % p[4]
+        if si.english(en_old.get(si.key(p), [""] * 15)) != 
si.english(en_new.get(si.key(q), ["-"] * 15)):
+            return "a line moved to a key with different English: %s -> %s" % 
(p[4], q[4])
+    return None
+
+
+def usable(path, lang, en):
+    return sum(1 for k, c in si.load(path, lang).items()
+               if k in en and any(si.is_translation(c[i], en[k][i]) for i in 
si.TEXT_COLUMNS))
+
+
+def do_language(lang, ctx):
+    a = ctx
+    d = os.path.join(a.run, "langs", lang)
+    os.makedirs(d)
+    tree = os.path.join(a.l10n, lang, "localize.sdf")
+    result = {"lang": lang, "status": "ok"}
+    with open(os.path.join(d, "log.txt"), "w") as log:
+        try:
+            rekeyed = os.path.join(d, "rekeyed.sdf")
+            text, plan, stats = si.rekey(lang, tree, a.olds, a.template)
+            si.write(rekeyed, text)
+            log.write("rekey: %s\n" % dict(stats))
+            chosen = next((k[len("keyed from "):] for k in stats if 
k.startswith("keyed from ")), None)
+            result["keyed from"] = os.path.basename(chosen) if chosen else 
"current"
+            result["moved"] = len(plan)
+            if plan:
+                problem = check_rekey(tree, rekeyed, chosen, a.template)
+                if problem:
+                    raise RuntimeError("re-key check: " + problem)
+            final = rekeyed
+            podir = os.path.join(a.po_repo, "PO-Files", lang.replace("-", 
"_")) if a.po_repo else None
+            if podir and os.path.isdir(podir):
+                po2oo = os.path.join(d, "po2oo.sdf")
+                merged = po_to_sdf(a.py, lang, podir, a.pot, a.template, 
po2oo, log)
+                history = []
+                for rev in a.history:
+                    hdir = os.path.join(d, "history", rev)
+                    os.makedirs(hdir)
+                    arch = subprocess.run(["git", "-C", a.po_repo, "archive", 
rev,
+                                           "PO-Files/" + lang.replace("-", 
"_")], capture_output=True)
+                    if arch.returncode:
+                        log.write("history %s: no PO files for %s\n" % (rev, 
lang))
+                        continue
+                    subprocess.run(["tar", "-x", "-C", hdir], 
input=arch.stdout, check=True)
+                    hsdf = os.path.join(d, "history-%s.sdf" % rev)
+                    po_to_sdf(a.py, lang, os.path.join(hdir, "PO-Files", 
lang.replace("-", "_")),
+                              a.pot, a.template, hsdf, log)
+                    history.append(hsdf)
+                if si.load(po2oo, lang):
+                    final = os.path.join(d, "final.sdf")
+                    if a.gsicheck:
+                        changed, added, skip, new_errors = si.gate(
+                            lang, rekeyed, po2oo, a.template, final, 
a.gsicheck, a.libdir,
+                            add_only=a.mode == "add-only", history=history)
+                        result["left out"] = len(skip)
+                        for k in skip:
+                            log.write("left out (adds a gsicheck error): %s %s 
%s\n" % (k[1], k[3], k[4]))
+                        if new_errors:
+                            result["status"] = "new gsicheck errors not tied 
to an entry"
+                            for msg, uid in sorted(new_errors):
+                                log.write("new gsicheck error: %s %s\n" % 
(msg, uid or ""))
+                        si.write(os.path.join(d, "left-out.json"), 
json.dumps(skip, ensure_ascii=False, indent=1))
+                    else:
+                        text, changed, added = si.merge(lang, rekeyed, po2oo, 
a.template, (),
+                                                        a.mode == "add-only", 
history)
+                        si.write(final, text)
+                    result["updated"], result["added"] = len(changed), 
len(added)
+                if a.export_po:
+                    shutil.copytree(merged, os.path.join(a.run, "po", 
lang.replace("-", "_")))
+            else:
+                result["note"] = "no PO files"
+            result["before"] = usable(tree, lang, a.en)
+            result["after"] = usable(final, lang, a.en)
+            result["final"] = final
+            result["changed"] = not filecmp.cmp(final, tree, shallow=False)
+        except Exception as e:  # report and go on with the other languages
+            result["status"] = "failed: %s" % e
+            log.write("FAILED: %s\n" % e)
+    return result
+
+
+def main():
+    p = argparse.ArgumentParser(description=__doc__, 
formatter_class=argparse.RawDescriptionHelpFormatter)
+    p.add_argument("--aoo", required=True, help="AOO checkout (the directory 
with main/ and extras/)")
+    p.add_argument("--template", default=DEFAULT_TEMPLATE,
+                   help="en-US.sdf of the current source (default: 
%(default)s)")
+    p.add_argument("--extract", action="store_true",
+                   help="extract en-US.sdf from --aoo with localize -e first 
(Linux, built tree)")
+    p.add_argument("--old-template", action="append", default=[],
+                   help="more en-US.sdf files the tree may be keyed from 
(earlier versions of --template "
+                        "and openoffice-translation/en-US.sdf are added 
automatically)")
+    p.add_argument("--po-repo", help="checkout of 
apache/openoffice-translation (default: clone one into the work dir)")
+    p.add_argument("--no-po", action="store_true", help="only re-key the tree 
files")
+    p.add_argument("--langs", help="comma-separated tree language codes 
(default: every tree language)")
+    p.add_argument("--mode", choices=("add-only", "update"), 
default="add-only",
+                   help="add-only: fill entries the tree has no translation 
for (default); "
+                        "update: also replace tree text with newer PO text")
+    p.add_argument("--history", nargs="*", default=[],
+                   help="openoffice-translation revisions whose text must not 
replace a tree translation "
+                        "(update mode; guards against Pootle rollbacks)")
+    p.add_argument("--solver", help="solver platform directory with 
bin/gsicheck and lib/ (default: from --aoo)")
+    p.add_argument("--no-gsicheck", action="store_true", help="merge without 
the gsicheck gate")
+    p.add_argument("--workdir", default="aoo-l10n-work", help="where runs are 
kept (default: %(default)s)")
+    p.add_argument("--jobs", type=int, default=os.cpu_count() or 4)
+    p.add_argument("--python", help="Python with translate-toolkit (default: 
this one, else a venv in the work dir)")
+    p.add_argument("--export-po", action="store_true", help="also write the 
updated PO files per language")
+    p.add_argument("--apply", action="store_true", help="copy the results into 
the tree")
+    a = p.parse_args()
+    sys.stdout.reconfigure(line_buffering=True)
+
+    a.aoo = os.path.abspath(a.aoo)
+    a.l10n = os.path.join(a.aoo, "extras", "l10n", "source")
+    if not os.path.isdir(a.l10n):
+        sys.exit("no extras/l10n/source in %s" % a.aoo)
+    if a.history and a.mode != "update":
+        sys.exit("--history only applies to --mode update")
+    a.history = list(dict.fromkeys(a.history))
+    workdir = os.path.abspath(a.workdir)
+    a.run = os.path.join(workdir, 
datetime.datetime.now().strftime("%Y%m%d-%H%M%S"))
+    os.makedirs(a.run)
+    print("work directory:", a.run)
+
+    if a.extract:
+        a.template = os.path.join(a.run, "en-US.sdf")
+        extract_template(a.aoo, a.template)
+    a.template = os.path.abspath(a.template)
+    a.en = si.load(a.template, "en-US")
+    if not a.en:
+        sys.exit("no en-US lines in %s" % a.template)
+    print("template: %s (%d entries)" % (a.template, len(a.en)))
+
+    langs = sorted(d for d in os.listdir(a.l10n) if 
os.path.isfile(os.path.join(a.l10n, d, "localize.sdf")))
+    if a.langs:
+        wanted = a.langs.split(",")
+        missing = [x for x in wanted if x not in langs]
+        if missing:
+            sys.exit("no tree file for: %s" % " ".join(missing))
+        langs = wanted
+
+    a.po_repo = get_po_repo(a, workdir, langs)
+    if a.po_repo:
+        rev = subprocess.run(["git", "-C", a.po_repo, "log", "-1", 
"--format=%h %cs"],
+                             capture_output=True, text=True).stdout.strip()
+        print("PO files: %s at %s" % (a.po_repo, rev))
+    os.makedirs(os.path.join(a.run, "old"))
+    a.olds = old_templates(a.template, a.po_repo, [os.path.abspath(x) for x in 
a.old_template],
+                           os.path.join(a.run, "old"))
+
+    a.gsicheck = a.libdir = None
+    if not a.no_gsicheck:
+        solver = a.solver or next(iter(sorted(glob.glob(os.path.join(a.aoo, 
"main", "solver", "*", "*", "bin",
+                                                                      
"gsicheck")))), None)
+        if solver and solver.endswith("gsicheck"):
+            solver = os.path.dirname(os.path.dirname(solver))
+        if not solver or not os.access(os.path.join(solver, "bin", 
"gsicheck"), os.X_OK):
+            sys.exit("no gsicheck found; build l10ntools in --aoo, pass 
--solver, or use --no-gsicheck")
+        a.gsicheck, a.libdir = os.path.join(solver, "bin", "gsicheck"), 
os.path.join(solver, "lib")
+
+    a.py = None
+    if a.po_repo:
+        a.py = toolkit_python(a, workdir)
+        a.pot = os.path.join(a.run, "pot")
+        with open(os.path.join(a.run, "oo2po.log"), "w") as log:
+            if log_run([a.py, "-m", "translate.convert.oo2po", "--pot", 
a.template, a.pot], log):
+                sys.exit("oo2po failed, see %s/oo2po.log" % a.run)
+
+    if a.po_repo:
+        tree_po = {x.replace("-", "_") for x in langs}
+        extra = sorted(set(os.listdir(os.path.join(a.po_repo, "PO-Files"))) - 
tree_po) if not a.langs else []
+        if extra:
+            print("PO files without a tree file, not integrated: %s" % " 
".join(extra))
+
+    with concurrent.futures.ThreadPoolExecutor(a.jobs) as pool:
+        results = list(pool.map(lambda x: do_language(x, a), langs))
+
+    lines = ["| language | keyed from | moved | updated | added | left out | 
translated before | after | status |",
+             "|---|---|---|---|---|---|---|---|---|"]
+    for r in results:
+        lines.append("| %s | %s | %s | %s | %s | %s | %s | %s | %s |" % (
+            r["lang"], r.get("keyed from", ""), r.get("moved", ""), 
r.get("updated", ""), r.get("added", ""),
+            r.get("left out", ""), r.get("before", ""), r.get("after", ""), 
r.get("note", r["status"])))
+    total = collections.Counter()
+    for r in results:
+        total["before"] += r.get("before", 0)
+        total["after"] += r.get("after", 0)
+    lines.append("")
+    lines.append("Translated entries whose key is in the template, all 
languages: %d before, %d after "
+                 "(%d template entries per language)." % (total["before"], 
total["after"], len(a.en)))
+    report = "\n".join(lines) + "\n"
+    si.write(os.path.join(a.run, "report.md"), report)
+    print(report)
+
+    failed = [r["lang"] for r in results if r["status"] != "ok"]
+    changed = [r for r in results if r["status"] == "ok" and r.get("changed")]
+    if a.apply:
+        for r in changed:
+            shutil.copyfile(r["final"], os.path.join(a.l10n, r["lang"], 
"localize.sdf"))
+        print("copied %d files into %s" % (len(changed), a.l10n))
+    else:
+        print("%d languages changed; rerun with --apply, or copy 
langs/<lang>/final.sdf (rekeyed.sdf if there "
+              "is no final.sdf) over the tree file" % len(changed))
+    if failed:
+        print("FAILED: %s (see langs/<lang>/log.txt)" % " ".join(failed))
+        return 1
+    return 0
+
+
+if __name__ == "__main__":
+    sys.exit(main())

Reply via email to