Replace a translation delivery instead of merging into it

A re-delivery could leave a stale row behind for an utterance the new
delivery does not cover: the upsert only touches rows the delivery mentions.
--replace clears the language's draft rows first, so what the reviewer sees
is what the delivery actually says.

Confirmed rows are never deleted. A confirmed translation is an approval, and
the import skips confirmed rows anyway, so the approved text survives a
re-delivery.

2 new tests.
This commit is contained in:
Umbra Tools
2026-11-06 19:36:46 -08:00
parent c476cfea1d
commit 301ff17f3e
2 changed files with 98 additions and 7 deletions

View File

@@ -20,9 +20,18 @@ second one.
python scripts/import_translations.py --project "..." \
--delivery /tmp/delivery.json --write --confirm
# replace this language's drafts, then write it
python scripts/import_translations.py --project "..." \
--delivery /tmp/delivery.json --write --replace
A delivery is a *draft* by default. A row that says the target language does
not voice an utterance is kept, with the reason, so the gap stays visible:
deleting it would lose the alignment between the source and the translation.
``--replace`` clears the language's existing *draft* rows first, so a
re-delivery cannot leave a stale row behind for an utterance the new delivery
does not cover. Confirmed rows are never deleted: a confirmed translation is
an approval, and the import skips it.
"""
from __future__ import annotations
@@ -57,12 +66,44 @@ def _describe(payload: dict) -> dict:
}
def _clear_drafts(conn: sqlite3.Connection, code: str) -> int:
"""Delete the language's draft rows. Confirmed rows are kept.
A confirmed translation is an approval; deleting it would destroy it.
The import skips confirmed rows anyway, so they are left alone.
"""
from umbraaudio.translations import canonical_code
target = canonical_code(code) or str(code).strip().lower()
rows = conn.execute("SELECT DISTINCT language_code FROM utterance_translations").fetchall()
matching = [
str(row[0])
for row in rows
if row[0] is not None and (canonical_code(row[0]) or str(row[0]).strip().lower()) == target
]
if not matching:
return 0
placeholders = ",".join("?" for _ in matching)
cursor = conn.execute(
f"DELETE FROM utterance_translations "
f"WHERE language_code IN ({placeholders}) AND status <> ?",
(*matching, "confirmed"),
)
conn.commit()
return cursor.rowcount
def main() -> int:
parser = argparse.ArgumentParser(description="Ingest a translation delivery from UmbraTrans.")
parser.add_argument("--project", required=True, help="project directory name")
parser.add_argument("--delivery", type=Path, required=True, help="delivery payload JSON")
parser.add_argument("--write", action="store_true", help="write the rows (default: report)")
parser.add_argument("--confirm", action="store_true", help="store as confirmed, not draft")
parser.add_argument(
"--replace",
action="store_true",
help="clear the language's draft rows first (confirmed rows are kept)",
)
args = parser.parse_args()
config = load_config(args.project)
@@ -93,6 +134,10 @@ def main() -> int:
print("\nNothing written. Add --write to ingest the delivery.")
return 0
if args.replace:
removed = _clear_drafts(conn, code)
print(f"\ncleared {removed} draft row(s) for {code}")
report = import_translations(conn, project_id, payload, confirm=args.confirm)
after = coverage(conn, code)
print("\nIMPORTED")

View File

@@ -16,12 +16,20 @@ delivery:
from __future__ import annotations
import importlib.util
import sqlite3
from pathlib import Path
import pytest
from umbraaudio import translations as tr
_SCRIPT = Path(__file__).resolve().parents[1] / "scripts" / "import_translations.py"
_spec = importlib.util.spec_from_file_location("import_translations", _SCRIPT)
assert _spec is not None and _spec.loader is not None
import_translations = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(import_translations)
def _seed(conn: sqlite3.Connection) -> None:
conn.executescript(
@@ -83,7 +91,8 @@ def test_delivery_writes_one_row_per_utterance(conn: sqlite3.Connection) -> None
assert report.accepted == 3
assert report.not_voiced == 1
rows = conn.execute(
"SELECT utterance_id, text, voiced, status FROM utterance_translations ORDER BY utterance_id"
"SELECT utterance_id, text, voiced, status FROM utterance_translations "
"ORDER BY utterance_id"
).fetchall()
assert len(rows) == 3
assert rows[0]["text"] == "飞船降落了。"
@@ -106,9 +115,7 @@ def test_confirmed_rows_are_not_overwritten(conn: sqlite3.Connection) -> None:
tr.import_translations(conn, 1, _delivery(), confirm=True)
report = tr.import_translations(conn, 1, _delivery())
assert report.skipped_confirmed == 3
row = conn.execute(
"SELECT text FROM utterance_translations WHERE utterance_id=1"
).fetchone()
row = conn.execute("SELECT text FROM utterance_translations WHERE utterance_id=1").fetchone()
assert row["text"] == "飞船降落了。"
@@ -120,9 +127,10 @@ def test_coverage_finds_a_delivery_stored_under_a_locale_code(
) -> None:
"""UmbraTrans writes ``zh-Hans``; the reviewer asks about ``zh``."""
tr.import_translations(conn, 1, _delivery(target_language="zh-Hans"))
assert conn.execute(
"SELECT DISTINCT language_code FROM utterance_translations"
).fetchone()[0] == "zh"
assert (
conn.execute("SELECT DISTINCT language_code FROM utterance_translations").fetchone()[0]
== "zh"
)
report = tr.coverage(conn, "zh")
assert report["delivered"] == 3
assert report["voiced"] == 2
@@ -162,3 +170,41 @@ def test_coverage_of_an_undelivered_language_is_zero(conn: sqlite3.Connection) -
report = tr.coverage(conn, "de")
assert report["delivered"] == 0
assert report["missing"] == 5
# --- replacing a delivery -------------------------------------------------
def test_replace_clears_drafts_and_keeps_confirmed(conn: sqlite3.Connection) -> None:
"""A re-delivery must not leave a stale row behind.
A confirmed translation is an approval, so it is never deleted - and the
import skips it, so the approved text survives a re-delivery.
"""
tr.import_translations(conn, 1, _delivery(), confirm=True)
conn.execute(
"INSERT INTO utterance_translations "
"(project_id, utterance_id, language_code, language_name, text, status, origin) "
"VALUES (1, 4, 'zh', 'Chinese', '陈旧的草稿。', 'draft', 'umbratrans')"
)
conn.commit()
removed = import_translations._clear_drafts(conn, "zh")
assert removed == 1
remaining = conn.execute(
"SELECT utterance_id, status FROM utterance_translations ORDER BY utterance_id"
).fetchall()
assert [row["utterance_id"] for row in remaining] == [1, 2, 3]
assert all(row["status"] == tr.STATUS_CONFIRMED for row in remaining)
def test_replace_matches_a_locale_code(conn: sqlite3.Connection) -> None:
conn.execute(
"INSERT INTO utterance_translations "
"(project_id, utterance_id, language_code, language_name, text, status, origin) "
"VALUES (1, 1, 'zh-Hans', 'Chinese', '飞船降落了。', 'draft', 'umbratrans')"
)
conn.commit()
assert import_translations._clear_drafts(conn, "zh") == 1
assert conn.execute("SELECT COUNT(*) FROM utterance_translations").fetchone()[0] == 0