mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-05 19:28:25 +00:00
The [ExtraTranslation] failure that took main red in 1ce583ec92 was not documented anywhere. amethyst/src/main/res/CLAUDE.md and the find-missing-translations skill both mention the lint rule, but only inside one narrow case (converting a <string> to <plurals>). Neither stated the general rule: removing or renaming a key in the default values/strings.xml orphans every locale entry that still declares it. Nor would running lint have caught it in practice. The only pre-push gate is pre-push-spotless.sh, which runs spotlessApply and nothing else, and :amethyst:lintFdroidBenchmark takes ~19 minutes on a warm daemon, so it is not a per-commit check. Add both halves: - amethyst/src/main/res/CLAUDE.md gains a "Renaming or removing a string key" section stating the same-commit rule and why Crowdin is not a cleanup step CI waits for. The trap is that Crowdin is *partly* reliable — it cleaned 32 of 47 locales — so the tree looks correct in whichever files you open. Retitled the file (it is no longer plural-only) and marked the existing sections as the plural-specific ones they always were. - orphan_strings_check.py scans every locale's resource names against its tree's default values/, across both Crowdin-managed resource systems: the Android res trees and the commons Compose-Multiplatform catalog. 0.17s against the full repo. pre-push-orphan-strings.sh wraps it as a PreToolUse gate on git push / create_pull_request, reusing the shell-tokenizing push-detection from pre-push-spotless.sh so "push" inside a commit message is not mistaken for the subcommand. - find-missing-translations gains a Common Mistakes entry pointing at both. Verified: clean on the current tree; exit 2 listing the orphans when route_video is reintroduced in two locales and a retired key is seeded in the Compose catalog; gate fires on git push and create_pull_request, stays quiet on a commit whose message contains "push" and on non-Bash tools.
88 lines
3.4 KiB
Python
Executable File
88 lines
3.4 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Fail if any locale declares a string resource its default values/ no longer has.
|
|
|
|
A key removed or renamed in a default `values/strings.xml` orphans every
|
|
`values-<locale>/strings.xml` entry that still declares it. In an Android res
|
|
tree that is an `[ExtraTranslation]` lint ERROR, which aborts
|
|
`:amethyst:lint<Variant>` and with it the whole `test-and-build-android` CI job.
|
|
|
|
Run directly, or via the pre-push-orphan-strings.sh hook that wraps it.
|
|
Exits 0 when clean, 2 with a report when not.
|
|
"""
|
|
|
|
import glob
|
|
import os
|
|
import re
|
|
import sys
|
|
from collections import defaultdict
|
|
|
|
# values-night, values-v29, values-sw600dp, ... are configuration qualifiers,
|
|
# not locales; only locale-qualified dirs can hold a translation.
|
|
LOCALE = re.compile(r"^values-(?:b\+[A-Za-z0-9+]+|[a-z]{2,3}(?:-r[A-Z]{2,3})?)$")
|
|
NAMED = re.compile(r'<(?:string|plurals|string-array)\s+[^>]*name="([^"]+)"')
|
|
|
|
# Both Crowdin-managed resource systems (see the find-missing-translations
|
|
# skill, "Resource trees — scan BOTH"), each with what an orphan costs there.
|
|
# Android res is the tree lint policies; the Compose-Multiplatform catalog is
|
|
# not lint-checked, but an orphan there is the same authoring mistake and
|
|
# leaves a dead translation behind.
|
|
ROOTS = (
|
|
("*/src/*/res/values", "Android lint [ExtraTranslation] error — aborts the build"),
|
|
("*/src/*/composeResources/values", "dead translation — key no longer exists in the default catalog"),
|
|
)
|
|
|
|
|
|
def names(paths):
|
|
found = set()
|
|
for path in paths:
|
|
with open(path, encoding="utf-8") as handle:
|
|
found |= set(NAMED.findall(handle.read()))
|
|
return found
|
|
|
|
|
|
def find_orphans():
|
|
orphans = defaultdict(list) # (res_root, key, consequence) -> [locale, ...]
|
|
for pattern, consequence in ROOTS:
|
|
for default_dir in sorted(glob.glob(pattern)):
|
|
res_root = os.path.dirname(default_dir)
|
|
base = names(glob.glob(os.path.join(default_dir, "*.xml")))
|
|
for locale_dir in sorted(glob.glob(os.path.join(res_root, "values-*"))):
|
|
locale = os.path.basename(locale_dir)
|
|
if not LOCALE.match(locale):
|
|
continue
|
|
extra = names(glob.glob(os.path.join(locale_dir, "*.xml"))) - base
|
|
for key in extra:
|
|
orphans[(res_root, key, consequence)].append(locale[len("values-"):])
|
|
return orphans
|
|
|
|
|
|
def main():
|
|
orphans = find_orphans()
|
|
if not orphans:
|
|
return 0
|
|
|
|
total = sum(len(v) for v in orphans.values())
|
|
out = sys.stderr
|
|
print(
|
|
f"BLOCKED: {total} orphaned translation(s) across {len(orphans)} key(s) — "
|
|
"translated in a locale, absent from that tree's default values/.",
|
|
file=out,
|
|
)
|
|
print(file=out)
|
|
for (res_root, key, consequence), locales in sorted(orphans.items()):
|
|
print(f" {res_root}: {key!r} in {len(locales)} locale(s) — {consequence}", file=out)
|
|
print(f" {' '.join(sorted(locales))}", file=out)
|
|
print(file=out)
|
|
print(
|
|
"A key removed or renamed in a default values/strings.xml must be deleted\n"
|
|
"from every values-*/strings.xml in the SAME commit. Crowdin's next sync is\n"
|
|
"not a cleanup step CI waits for — lint runs on the tree you push.\n"
|
|
"See amethyst/src/main/res/CLAUDE.md, 'Renaming or removing a string key'.",
|
|
file=out,
|
|
)
|
|
return 2
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|