mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-05 19:28:25 +00:00
The one real regression predates this branch: Compose resources draw a value's
XML whitespace verbatim, where aapt collapsed it. Values wrapped over indented
lines rendered with a leading line break and eight spaces, which turns the
account-backup tips' markdown heading into a code block, and Crowdin's stray
translator edge spaces (" miejsca zniknęły", Hindi's " दि॰" suffix) showed up.
fix_escapes.py now applies aapt's rule to line-break/tab runs and trims an
edge space a translation has but its source lacks. The Crowdin workflow
already runs it after each sync, and compose_escaping_check.py now fails on raw
line breaks. 598 values across 54 files repaired.
Also:
- rememberPresentation resolves only the labels its diff type writes, instead
of all 16 on every recomposition. A test pins the list against presentationOf.
- ScheduledPostStore: a failed stat is "no file" (okio posix throws on EACCES
where File.exists() did not), symlinked stores still count, no chmod against
an injected non-system FileSystem, the cleanup log keeps its throwable.
- ClickableEmail percent-encodes '%' in the mailto: URI.
- Dropped a misleading WatchScrollToTop import, a stray blank line, and a stale
doc path; noted the Base64 pad-bit difference beside the decoder.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjQQN9CgWHVtNCSWqnKoqY
103 lines
4.1 KiB
Python
Executable File
103 lines
4.1 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Fail if the Compose resource catalog carries Android-only string escaping.
|
|
|
|
Android's aapt and Compose Multiplatform do not share escaping rules. Compose
|
|
(compose-gradle-plugin `handleSpecialCharacters`) resolves only \\uXXXX, \\n and
|
|
\\t, and collapses \\\\. It leaves \\' \\" \\? \\@ alone and renders Android's
|
|
quote-wrapping literally, so a value carried verbatim out of `res/values/` ships
|
|
a visible backslash: the login screen once read `Don\\'t have a Nostr account?`.
|
|
|
|
`tools:` attributes are the same class of mistake. They are an Android-lint
|
|
construct whose namespace is declared on the Android `<resources>` root; Compose
|
|
catalog roots do not declare it, so the prefix is unbound and the XML malformed.
|
|
|
|
**This recurs on every Crowdin sync.** Crowdin holds the Android-escaped source,
|
|
so each import reintroduces it -- twice in two days, 2,068 escaped apostrophes
|
|
across 40 locales each time, always the apostrophe-heavy regional variants
|
|
(uz-rUZ, fr-rFR, fr-rCA, tr-rTR). Like the orphan-strings desync, it arrives
|
|
through a bot-authored PR with no local session in the path, so CI is the layer
|
|
that has to catch it.
|
|
|
|
Repair with:
|
|
|
|
python3 tools/strings-migrate/fix_escapes.py --no-unwrap-quotes \\
|
|
commonsUI/src/commonMain/composeResources
|
|
|
|
`--no-unwrap-quotes` is mandatory on already-migrated files: escape conversion is
|
|
idempotent, quote-unwrapping is not, and a second unwrap strips the real display
|
|
quotes from values like `import_follows_tips`.
|
|
|
|
Raw line breaks and tabs inside a value are the whitespace half of the same
|
|
mismatch: aapt collapsed them, Compose renders them (the same repair fixes them).
|
|
|
|
Only the Compose catalog is scanned. In an Android res tree the same escaping is
|
|
correct and must be left alone.
|
|
"""
|
|
import re
|
|
import sys
|
|
from collections import defaultdict
|
|
from pathlib import Path
|
|
|
|
CATALOG = "*/src/*/composeResources/values*/strings.xml"
|
|
|
|
# A backslash escape that is not itself escaped. \n, \t, \uXXXX and \\ are fine --
|
|
# Compose resolves those itself.
|
|
ANDROID_ESCAPE = re.compile(r"(?<!\\)\\(['\"?@])")
|
|
TOOLS_ATTR = re.compile(r'tools:[\w.-]+="')
|
|
# A value whose text holds a raw line break or tab. aapt collapsed that XML layout to
|
|
# one space; Compose draws it, and a markdown value indented by it becomes a code block.
|
|
STRING_TEXT = re.compile(r"<(string|item)\b[^>]*>(.*?)</\1>", re.S)
|
|
LAYOUT_WS = re.compile(r"[\r\n\t]")
|
|
|
|
|
|
def find_violations(root: Path):
|
|
found = defaultdict(lambda: defaultdict(int))
|
|
for path in sorted(root.glob(CATALOG)):
|
|
text = path.read_text(encoding="utf-8", errors="replace")
|
|
for esc in ANDROID_ESCAPE.findall(text):
|
|
found[path][f"\\{esc}"] += 1
|
|
n = len(TOOLS_ATTR.findall(text))
|
|
if n:
|
|
found[path]["tools: attribute"] += n
|
|
n = sum(
|
|
1
|
|
for m in STRING_TEXT.finditer(text)
|
|
if "<![CDATA[" not in m.group(2) and LAYOUT_WS.search(m.group(2))
|
|
)
|
|
if n:
|
|
found[path]["raw line break/tab in a value"] += n
|
|
return found
|
|
|
|
|
|
def main() -> int:
|
|
root = Path(__file__).resolve().parents[2]
|
|
found = find_violations(root)
|
|
if not found:
|
|
return 0
|
|
|
|
out = sys.stderr
|
|
total = sum(sum(k.values()) for k in found.values())
|
|
print(
|
|
f"Android-only escaping in the Compose resource catalog: "
|
|
f"{total} occurrence(s) in {len(found)} file(s).",
|
|
file=out,
|
|
)
|
|
print("Compose does not resolve these; they render literally.\n", file=out)
|
|
for path, kinds in sorted(found.items(), key=lambda kv: -sum(kv[1].values()))[:12]:
|
|
detail = ", ".join(f"{k} x{v}" for k, v in sorted(kinds.items()))
|
|
print(f" {path.relative_to(root)}: {detail}", file=out)
|
|
if len(found) > 12:
|
|
print(f" ... and {len(found) - 12} more file(s)", file=out)
|
|
print(
|
|
"\nRepair:\n"
|
|
" python3 tools/strings-migrate/fix_escapes.py --no-unwrap-quotes \\\n"
|
|
" commonsUI/src/commonMain/composeResources\n"
|
|
"(--no-unwrap-quotes is mandatory on already-migrated files.)",
|
|
file=out,
|
|
)
|
|
return 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|