Skip to content

Commit a51968a

Browse files
jamesarichclaude
andauthored
fix(strings): unescape quotes in base resources & guard against \" regressions (#6358)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent bd25b1e commit a51968a

3 files changed

Lines changed: 87 additions & 2 deletions

File tree

.github/workflows/pull-request.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,12 @@ jobs:
114114
- uses: actions/checkout@v7
115115
- name: Validate store listing metadata lengths
116116
run: python3 scripts/check-metadata-length.py
117+
# Compose Multiplatform does not strip Android-style \" / \' escapes, so a
118+
# backslash written before a quote renders literally in the UI (PR #6357).
119+
# Guards the English source strings; locale mirrors are cleaned upstream by
120+
# the Crowdin post-export processor.
121+
- name: Check for escaped quotes in base string resources
122+
run: python3 scripts/check-string-escapes.py
117123
# Flathub/AppStream needs a <release> entry for the version being shipped; a stale
118124
# <releases> block degrades (or fails) the Flathub listing. Fails the PR that bumps
119125
# VERSION_NAME_BASE until the matching entry is added.

core/resources/src/commonMain/composeResources/values/strings.xml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -629,7 +629,7 @@
629629
<string name="firmware_too_old">Firmware update required.</string>
630630
<string name="firmware_update_almost_there">Almost there...</string>
631631
<string name="firmware_update_alpha">Alpha</string>
632-
<string name="firmware_update_archive_missing_target">No firmware for %2$s (%3$s) was found in \"%1$s\".</string>
632+
<string name="firmware_update_archive_missing_target">No firmware for %2$s (%3$s) was found in "%1$s".</string>
633633
<string name="firmware_update_available">Firmware update available</string>
634634
<string name="firmware_update_battery_low">Battery too low (%1$d%). Please charge your device before updating.</string>
635635
<string name="firmware_update_checking">Checking for updates...</string>
@@ -657,7 +657,7 @@
657657
<string name="firmware_update_flashing">Flashing device, please wait...</string>
658658
<string name="firmware_update_hang_tight">Hang tight, we are working on it...</string>
659659
<string name="firmware_update_hash_rejected">Firmware hash rejected. Device may require hash provisioning or bootloader update.</string>
660-
<string name="firmware_update_invalid_local_file_detail">The selected file \"%1$s\" does not match %2$s (%3$s). Choose the firmware file for this target.</string>
660+
<string name="firmware_update_invalid_local_file_detail">The selected file "%1$s" does not match %2$s (%3$s). Choose the firmware file for this target.</string>
661661
<string name="firmware_update_keep_device_close">Keep your device close to your phone.</string>
662662
<string name="firmware_update_latest">Update To: %1$s</string>
663663
<string name="firmware_update_local_file">Local File</string>
@@ -1849,3 +1849,4 @@
18491849
<string name="zh_CN" translatable="false">简体中文</string>
18501850
<string name="zh_TW" translatable="false">繁體中文</string>
18511851
</resources>
1852+

scripts/check-string-escapes.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
#!/usr/bin/env python3
2+
"""Guard against Android-style backslash-escaped quotes in Compose resources.
3+
4+
These strings live under ``composeResources`` and are read by the Compose
5+
Multiplatform resources library, whose build-time ``handleSpecialCharacters``
6+
only processes ``\\n``, ``\\t``, ``\\uXXXX`` and ``\\\\`` -- it does NOT strip
7+
``\\"`` or ``\\'`` the way Android's AAPT does. A backslash written before a
8+
quote or apostrophe therefore renders *literally* in the UI (see the Italian
9+
``permission_missing_31`` regression, PR #6357). The correct value is a bare
10+
``"`` / ``'`` (or ``&#34;`` / ``&#39;`` if you want it explicit).
11+
12+
Scope: the English **base** ``values/strings.xml`` only -- the source of truth
13+
authored in this repo. The per-locale ``values-*/`` files are Crowdin mirrors;
14+
their escaping is fixed upstream by the project's custom post-export processor
15+
(strips ``\\'`` and ``\\"`` on export), so editing them here would just be
16+
overwritten on the next sync. This guard catches developer-authored escapes in
17+
the source before they get propagated to every translation.
18+
19+
Exit status is non-zero if any base string contains ``\\"`` or ``\\'``, so it
20+
can gate CI.
21+
"""
22+
23+
from __future__ import annotations
24+
25+
import os
26+
import re
27+
import sys
28+
from pathlib import Path
29+
30+
# Repo root = parent of this script's directory (scripts/).
31+
REPO_ROOT = Path(__file__).resolve().parent.parent
32+
33+
# Base (source-language) resource files only: `values/`, never `values-*/`.
34+
BASE_STRINGS_GLOB = "**/composeResources/values/strings.xml"
35+
36+
# A backslash directly before a quote or apostrophe, but not one that is itself
37+
# escaped (`\\"` = literal backslash + quote, which is intentional). `\n`, `\t`,
38+
# `\uXXXX` and `\\` are meaningful CMP escapes and are deliberately left alone.
39+
ESCAPED_QUOTE = re.compile(r"(?<!\\)\\(['\"])")
40+
41+
# Running inside GitHub Actions enables ::error:: annotations on the PR.
42+
IN_GITHUB_ACTIONS = os.environ.get("GITHUB_ACTIONS") == "true"
43+
44+
45+
def main() -> int:
46+
files = sorted(REPO_ROOT.glob(BASE_STRINGS_GLOB))
47+
if not files:
48+
print(f"error: no base strings.xml found under {REPO_ROOT}", file=sys.stderr)
49+
return 2
50+
51+
violations: list[tuple[Path, int, str]] = []
52+
53+
for path in files:
54+
for lineno, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1):
55+
if ESCAPED_QUOTE.search(line):
56+
violations.append((path, lineno, line.strip()))
57+
58+
if not violations:
59+
print("No backslash-escaped quotes found in base string resources.")
60+
return 0
61+
62+
print("Backslash-escaped quotes found in base string resources:\n")
63+
for path, lineno, text in violations:
64+
rel = path.relative_to(REPO_ROOT)
65+
message = "escaped quote/apostrophe renders literally in Compose Multiplatform"
66+
print(f" - {rel}:{lineno}: {text}")
67+
if IN_GITHUB_ACTIONS:
68+
print(f"::error file={rel},line={lineno}::{message}")
69+
70+
print(
71+
"\nCompose Multiplatform does not strip \\\" or \\' (unlike Android AAPT), "
72+
"so the backslash shows up in the UI. Use a bare \" / ' (or &#34; / &#39;)."
73+
)
74+
return 1
75+
76+
77+
if __name__ == "__main__":
78+
raise SystemExit(main())

0 commit comments

Comments
 (0)