English copy — the PILOT: privatebin, paperless-ngx, romm (R-560 slice 5)
gates / gates (push) Failing after 2s

89 of 1 032 strings. No Hungarian byte moved; no image, pin, catalog_since or
compose line changed. EN_MISSING_CEILING 1032 -> 943 in this commit.

Chosen for SHAPE: privatebin exercises the plain case (description, tagline, lists);
paperless-ngx adds select options, a placeholder and a customer-facing folder label;
romm carries the catalog's only optional_config block, whose group has no id of its
own and is matched by `match_group` — the Hungarian group name it translates. All
three run on the demo box, so the English pages can be fetched rather than reasoned
about.

Two gate defects fixed while translating, each found by its own decoy rather than by
reading: coverage was counted only for the apps NAMED on the command line, so
`check-copy-i18n.py privatebin` reported 47 more missing strings than the same tree
unscoped and either number could have been made to "pass"; and the ASCII-Hungarian
stems matched as bare substrings, so „ird be" convicted "the third best" and „angol"
convicted "Angola". Both now have their own case in the decoy suite.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-09-20 14:34:22 +02:00
parent 84e058463b
commit e81d41e527
6 changed files with 285 additions and 24 deletions
+50 -16
View File
@@ -126,8 +126,22 @@ def case_copy(name, clone, edits, expect_rc, must_contain=(), extra_args=()):
try:
for relpath, fn in edits:
edit(clone, relpath, fn)
args = list(extra_args)
if "--expect-missing" not in args:
# MEASURE the clone's current coverage and hand it back as the ceiling. The ratchet is
# not what these cases test — they test the freeze, the structure and the language —
# and hard-coding a number here would make every case fail the day a batch lands.
# The ratchet has its own two cases below, in both directions.
probe = sh([sys.executable, os.path.join(ROOT, "scripts", "check-copy-i18n.py"),
"--root", clone, "--expect-missing", "-1"], cwd=clone)
m = re.search(r"(\d+) strings have no English", probe.stdout + probe.stderr)
if not m:
fails.append("%s: could not measure the clone's coverage — the case is broken, "
"not the gate" % name)
return ""
args += ["--expect-missing", m.group(1)]
r = sh([sys.executable, os.path.join(ROOT, "scripts", "check-copy-i18n.py"),
"--root", clone] + list(extra_args), cwd=clone)
"--root", clone] + args, cwd=clone)
out = r.stdout + r.stderr
if r.returncode == expect_rc and all(m in out for m in must_contain):
print(" ok %-52s rc=%d (expected %d)" % (name, r.returncode, expect_rc))
@@ -267,8 +281,14 @@ i18n:
description: "The address this app answers on"
"""
def strip_en(t):
"""Remove an existing English block (and its comment header) — a case must behave the
same before and after that app's batch lands, or the suite rots on a future push."""
t = re.sub(r"\n# --- English copy.*\Z", "\n", t, flags=re.S)
return re.sub(r"\n^i18n:\n.*\Z", "\n", t, flags=re.S | re.M)
def add_en(block=GENUINE_EN):
return lambda t: t.rstrip("\n") + "\n" + block
return lambda t: strip_en(t).rstrip("\n") + "\n" + block
def en_with(old_, new_):
return add_en(GENUINE_EN.replace(old_, new_))
@@ -294,22 +314,18 @@ i18n:
expect_rc=1, must_contain=("no Hungarian twin", "NOSUCHFIELD"))
case_copy("FACT: an accented Hungarian letter left in the English", clone,
[(PB, en_with("Share sensitive text safely", "Érzékeny text sharing"))],
expect_rc=1, must_contain=("accented Hungarian letter",),
extra_args=("--expect-missing", str(TOTAL - PB_EN)))
expect_rc=1, must_contain=("accented Hungarian letter",))
case_copy("FACT: ASCII-only Hungarian left in the English (no accent to find)", clone,
[(PB, en_with(' description: "The address this app answers on"',
' description: "Aldomain for the app"'))],
expect_rc=1, must_contain=("ASCII-only Hungarian", "aldomain"),
extra_args=("--expect-missing", str(TOTAL - PB_EN)))
expect_rc=1, must_contain=("ASCII-only Hungarian", "aldomain"))
case_copy("FACT: the product begs (\"please\")", clone,
[(PB, en_with("Type your text and select Send", "Please type your text and select Send"))],
expect_rc=1, must_contain=("does not beg",),
extra_args=("--expect-missing", str(TOTAL - PB_EN)))
expect_rc=1, must_contain=("does not beg",))
case_copy("FACT: an English retrieval promise the Hungarian never made", clone,
[(PB, en_with("Password protection for extra safety",
"Deleted notes can still be restored later"))],
expect_rc=1, must_contain=("retrieval promise",),
extra_args=("--expect-missing", str(TOTAL - PB_EN)))
expect_rc=1, must_contain=("retrieval promise",))
# A credential is a LOGIN, not prose: gokapi's default_creds carries admin / adminadmin.
GK = "templates/gokapi/.felhom.yml"
case_copy("FACT: a credential token rewritten in translation", clone,
@@ -319,21 +335,39 @@ i18n:
app_info:
default_creds: "Sign in: administrator / hunter2"
""")],
expect_rc=1, must_contain=("credential token",),
extra_args=("--expect-missing", str(TOTAL - 1)))
expect_rc=1, must_contain=("credential token",))
case_copy("FACT: an i18n block for a language the controller does not render", clone,
[(PB, lambda t: t.rstrip("\n") + "\ni18n:\n de:\n description: \"Verschluesselte Notizen\"\n")],
expect_rc=1, must_contain=("renders en only",))
case_copy("FACT: an English list with a different number of steps", clone,
[(PB, en_with(" - 'Share the link you get - the encryption key is inside the URL'\n", ""))],
expect_rc=1, must_contain=("a list is replaced",),
extra_args=("--expect-missing", str(TOTAL - PB_EN + 1)))
expect_rc=1, must_contain=("a list is replaced",))
# THE RATCHET — the one thing --expect-missing does not test for the cases above, so it is
# tested here explicitly, in BOTH directions. A ceiling that only convicts upwards can be
# left behind by a push that translated more than it recorded.
case_copy("FACT: ratchet — fewer strings translated than the ceiling records", clone,
[(PB, add_en())], expect_rc=1,
must_contain=("English coverage", "ABOVE"),
extra_args=("--expect-missing", "0"))
case_copy("FACT: ratchet — more translated than the ceiling records", clone,
[(PB, add_en())], expect_rc=1,
must_contain=("English coverage", "BELOW"),
extra_args=("--expect-missing", "999999"))
# THE GENUINE ARTICLE — must pass.
case_copy("GENUINE: a correct English block on privatebin", clone,
[(PB, add_en())], expect_rc=0,
must_contain=("copy-i18n: OK", "privatebin 14/14"),
extra_args=("--expect-missing", str(TOTAL - PB_EN)))
must_contain=("copy-i18n: OK", "privatebin 14/14"))
# The ratchet is a fact about the CATALOG, not about how the gate was invoked. Naming one
# app must not change the count — the first version of this gate counted coverage only for
# the apps in scope, so `check-copy-i18n.py privatebin` reported 47 more missing strings
# than the same tree unscoped, and either number could have been made to "pass".
case_copy("GENUINE: naming an app does not change the coverage count", clone,
[(PB, add_en())], expect_rc=0,
must_contain=("copy-i18n: OK",),
extra_args=("privatebin", "--expect-missing", str(TOTAL - PB_EN)))
# THE DECOYS — the LABEL moves, the FACT does not. Each must pass.
case_copy("DECOY: Hungarian rewritten inside a YAML COMMENT", clone,