mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-05 19:28:25 +00:00
Every key whose usages are all composable stringRes/stringResource
calls, with no XML references, no bare %s/%d, only %N$s/%N$d args and
no inline markup, moves from the app's res/ to commons Compose
resources (~105,800 locale entries across 57 locales, translations
preserved byte-for-byte for Crowdin). 568 app files repointed to
Res.string via the stringRes bridge overloads.
The app keeps 1,866 keys that are genuinely Android-bound: ctx-based
call sites, Int-typed id storage (maps/whens), @string/ XML references,
and non-positional format args.
Tool fix folded in: Crowdin emits some entries with attributes before
name= (xmlns:ns0=... name="key") - the extraction regex now matches
any attribute order; the six entries the old pattern missed (zh,
nl-rBE, es x account_backup_tips{2,3}_md) are relocated, and 11 in-file
duplicates from keys that already existed in commons are removed.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011S1vbFWVVAMFDT8PTgdibV
105 lines
3.8 KiB
Python
Executable File
105 lines
3.8 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Move string keys from the Android app's res/ to commons Compose resources.
|
|
|
|
Usage: tools/strings-migrate/migrate.py key1 key2 ...
|
|
|
|
For the default locale and every values-* locale dir, each named
|
|
<string> or <plurals> element is removed from
|
|
amethyst/src/main/res/<values*>/strings.xml
|
|
and appended (before </resources>) to
|
|
commons/src/commonMain/composeResources/<values*>/strings.xml
|
|
creating the commons locale file when a translation exists that commons
|
|
doesn't carry yet. Elements are moved verbatim (raw XML slice), so
|
|
escaping, CDATA and comments inside values survive byte-for-byte and
|
|
Crowdin sees a pure move.
|
|
|
|
Fails loudly when a key's default-locale value uses non-positional format
|
|
specifiers (bare %s / %d): compose-resources only formats the positional
|
|
%1$s form, so those keys must be rewritten (in code AND all locales)
|
|
before migrating. Run from the repo root.
|
|
"""
|
|
import os
|
|
import re
|
|
import sys
|
|
|
|
APP_RES = "amethyst/src/main/res"
|
|
COMMONS_RES = "commons/src/commonMain/composeResources"
|
|
|
|
NEW_FILE_TEMPLATE = '<?xml version="1.0" encoding="utf-8"?>\n<resources>\n</resources>\n'
|
|
|
|
# %s, %d, %.2f ... without a position (%1$s). %% is a literal percent.
|
|
BARE_FORMAT = re.compile(r"%(?!%)(?!\d+\$)[-#+ 0,(]*\d*(?:\.\d+)?[a-zA-Z]")
|
|
|
|
|
|
def element_pattern(key: str) -> re.Pattern:
|
|
# `name` is not always the first attribute (Crowdin emits e.g.
|
|
# `<string xmlns:ns0="..." name="key" ns0:ignore="Typos">`), so allow
|
|
# any attributes before it.
|
|
return re.compile(
|
|
r"[ \t]*<(string|plurals)\b[^>]*?\sname=\"" + re.escape(key) + r"\"[^>]*?(?:/>|>.*?</\1>)[ \t]*\n?",
|
|
re.S,
|
|
)
|
|
|
|
|
|
def extract(path: str, key: str):
|
|
"""Remove key's element from path; return the element text or None."""
|
|
if not os.path.exists(path):
|
|
return None
|
|
src = open(path, encoding="utf-8").read()
|
|
m = element_pattern(key).search(src)
|
|
if not m:
|
|
return None
|
|
open(path, "w", encoding="utf-8").write(src[: m.start()] + src[m.end() :])
|
|
return m.group(0).strip("\n")
|
|
|
|
|
|
def append(path: str, elements: list):
|
|
if os.path.exists(path):
|
|
src = open(path, encoding="utf-8").read()
|
|
else:
|
|
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
src = NEW_FILE_TEMPLATE
|
|
close = src.rindex("</resources>")
|
|
block = "".join(" " + el.strip() + "\n" for el in elements)
|
|
open(path, "w", encoding="utf-8").write(src[:close] + block + src[close:])
|
|
|
|
|
|
def main(keys):
|
|
default_src = os.path.join(APP_RES, "values", "strings.xml")
|
|
default_text = open(default_src, encoding="utf-8").read()
|
|
|
|
for key in keys:
|
|
m = element_pattern(key).search(default_text)
|
|
if not m:
|
|
sys.exit(f"ERROR: key '{key}' not found in {default_src}")
|
|
if BARE_FORMAT.search(m.group(0)):
|
|
sys.exit(
|
|
f"ERROR: key '{key}' uses a non-positional format specifier (bare %s/%d).\n"
|
|
"compose-resources only formats %1$s-style args - rewrite the key "
|
|
"(code + every locale) before migrating."
|
|
)
|
|
|
|
locale_dirs = ["values"] + sorted(
|
|
d for d in os.listdir(APP_RES) if d.startswith("values-") and os.path.exists(os.path.join(APP_RES, d, "strings.xml"))
|
|
)
|
|
|
|
moved_total = 0
|
|
for d in locale_dirs:
|
|
src_path = os.path.join(APP_RES, d, "strings.xml")
|
|
elements = []
|
|
for key in keys:
|
|
el = extract(src_path, key)
|
|
if el is not None:
|
|
elements.append(el)
|
|
if elements:
|
|
append(os.path.join(COMMONS_RES, d, "strings.xml"), elements)
|
|
moved_total += len(elements)
|
|
print(f"{d}: moved {len(elements)}")
|
|
print(f"done: {moved_total} elements across {len(locale_dirs)} locale dirs")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
if len(sys.argv) < 2:
|
|
sys.exit(__doc__)
|
|
main(sys.argv[1:])
|