refactor: make the 40 interpolated UI strings format strings, and assert the argument order
Phase 4, third step, of docs/material-design-conformance.md. `Text("Add chapter to
${uiState.artifact.name}")` becomes a resource holding `Add chapter to %1$s` and a call
passing the expression. 49 call sites. Literals in composables go 76 -> 39;
`stringResource` goes 374 -> 424.
**A silent bug in the previous commit's extractor, found by this one.** Imports were
tested with `statement in source`, and the generated accessors are named after their
strings -- so `import mantra.composeapp.generated.resources.translate` is a *prefix* of
`...resources.translate_into_which_dialect`. The substring test decided the import was
already there, and the compiler reported "Unresolved reference 'translate'" in a file
whose imports looked complete. Both extractors now match whole lines, and the helper
carries the explanation.
**Four filters, each earned by something the dry run got wrong.**
*A template that is only interpolation has nothing to translate.* `Text("$name")` would
have become a resource holding `%1$s` -- longer, slower, and no more localisable than the
code it replaced.
*A leading or trailing space means it is being glued to a neighbour.* " \\u00b7 %1$s" is a
separator. The test has to be on the format string rather than on the literal halves: a
template opening with an interpolation leaves the first part empty and the second starting
with the separating space, which makes "%1$s Key packages" look like a fragment when it is
a whole label.
*`\\uXXXX` and `\\"` are Kotlin syntax, not XML.* Left alone they would have shipped as the
six visible characters of the escape. They are decoded into the resource, which is UTF-8
and can hold `·` directly. `\\n` is **not** decoded, because
StringCatalogueJvmTest shows Compose Resources processes that one and a real newline in an
XML value would be reflowed by the parser.
*A term of a `+` concatenation is still not a string.* Same rule as the plain extractor.
**Three copy problems surfaced only here, because interpolated strings had never been
checked.** `m3-title-case.py` excludes anything containing `$` -- an interpolation is not a
literal -- so `"$count Key Packages"` had been invisible to every pass so far, as had
`"replying To ${…}"`. And a third instance of the old product name, in
`"...once they're on Torch."`. All three fixed. Worth noting as a gap in the checker rather
than a one-off: title case inside a template is still unchecked, and there are 83
concatenation fragments left where it could hide.
**Two new assertions, on the two things a compiler cannot see.** Argument *order* is
decided by where each `${…}` sat, and a transposition compiles and reads plausibly --
"Recovered 3 of 12" against "Recovered 12 of 3" -- so a two-argument and a three-argument
string are asserted end to end. The three-argument one doubles as the check that `·`
was decoded rather than passed through.
**What is deliberately left.** 83 literals that are terms of a `+` concatenation.
Reassembling `"a " + x + " b"` into one format string means deciding what the whole
sentence is, and several are pluralisations -- `(if (n == 2) "event" else "events")` --
which want a real plural resource rather than a format argument, and that is an API choice
rather than a rewrite. `m3-extract-formatted.py --remaining` lists them.
**Tests.** 949 pass, 600 jvm over 73 classes and 349 android over 44, up from 947/598/349.
`:composeApp:compileDebugKotlinAndroid` builds; the debug apk installs and runs on
emulator-5554 through onboarding, the message list and a chat room with its text intact.
`m3-audit.sh --check` exits 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -608,35 +608,51 @@ neither an outline nor a checkmark, and adding one is component work.
|
||||
|
||||
### Phase 4 — text the system can translate
|
||||
|
||||
**Why here.** It is the last phase that touches every file, and doing it after
|
||||
Phase 2 means one pass over each file instead of two. It must precede Phase 6:
|
||||
RTL is a breakpoint concern too, and there is no point testing a mirrored layout
|
||||
against 334 English literals.
|
||||
**Why here.** It is the last phase that touches every file, and doing it after phase 2
|
||||
means one pass over each file instead of two. It must precede phase 6: RTL is a breakpoint
|
||||
concern too, and there is no point testing a mirrored layout against 334 English literals.
|
||||
|
||||
**Work.**
|
||||
**Built.** Three commits.
|
||||
|
||||
1. **Externalise all 334 strings** into `composeResources/values/strings.xml`,
|
||||
organised by screen. Clear out the inherited Phoenix wallet strings that
|
||||
nothing references, and settle `app_name`.
|
||||
2. **Sentence case, everywhere.** "Edit profile", "Create profile", "New chat",
|
||||
"Sign in", "Leave group", "Key package management", "Publish new key
|
||||
package". Roughly 45 strings. Product names stay capitalised — which requires
|
||||
settling on one: Torch, Mantra or Machankura.
|
||||
3. **Rewrite the destructive confirmations** to state consequences plainly.
|
||||
"Delete group" and "Leave group" currently offer a label and nothing else;
|
||||
the style guide wants the outcome and whether it can be undone.
|
||||
4. **Alt text for meaningful images** — profile avatars, QR codes, artifact
|
||||
images — following the Phase 3 triage rule.
|
||||
5. **Spell out abbreviations** in user-facing text. Protocol terms that are
|
||||
genuinely the domain (npub, NIP-05, relay) stay; incidental shortenings go.
|
||||
6. Verify RTL by mirroring: the codebase has no `left`/`right` modifiers, so
|
||||
this should be confirmation rather than repair.
|
||||
1. **Sentence case, 100 occurrences across 60 strings.** Two passes, and the second is
|
||||
the instructive one: the first pattern required every word after the first to be
|
||||
capitalised, so anything with an article survived — "Invite a Friend" was still on the
|
||||
app's first screen after the audit reported zero — and it read one line at a time, so a
|
||||
`Text(` whose literal sat on the next line was invisible.
|
||||
|
||||
**Done when** the audit reports fewer than ten string literals in composables
|
||||
(test data and previews), and no user-facing string uses title case.
|
||||
Sample data is deliberately left in title case: "Steve Biko", "To Kill a Mockingbird".
|
||||
Those are a person and a book.
|
||||
|
||||
**Risk:** low, high volume. Sentence-casing is the part most likely to draw
|
||||
disagreement — settle the product name first, in one decision.
|
||||
2. **251 plain strings into the catalogue**, 315 call sites. The extractor took four
|
||||
attempts and each failure is recorded in `docs/scripts/m3-extract-strings.py`: a bare
|
||||
`text = "…"` is not a Compose string (it rewrote a data class), a regex over quote
|
||||
pairs is not a Kotlin lexer (it lifted `"chunk"` out of `"${if (n == 1) "chunk" …}"`),
|
||||
and a fragment of a `+` concatenation is not a translatable unit.
|
||||
|
||||
3. **40 interpolated strings as format strings**, 49 call sites — `${expr}` to `%1$s`,
|
||||
passed as arguments.
|
||||
|
||||
**Compose Resources is not aapt, and a test caught that before a device did.** The first
|
||||
extraction escaped apostrophes as `\'` and doubled `%`, which is what android's resource
|
||||
compiler requires. Compose Resources does neither: `getString` returned `Don\'t sign`,
|
||||
backslash included, across 30-odd strings. Escape handling is *partial* rather than
|
||||
absent — `\n` **is** processed — so there is no family rule to lean on.
|
||||
`StringCatalogueJvmTest` asserts each escape it depends on.
|
||||
|
||||
**Done:** literals in composables 334 → 39, `stringResource` 0 → 424, title case 0. Plus
|
||||
1101 inherited phoenix strings deleted (nothing referenced them), the product settled on
|
||||
one name, and the two destructive actions now state their consequences — read out of the
|
||||
repository rather than guessed, because "Delete group" with no qualifier invites the
|
||||
belief that the messages are gone from the relays, which is the opposite of true.
|
||||
|
||||
**Left for later, and why it is not a script's job.** 83 literals that are terms of a `+`
|
||||
concatenation. Reassembling `"a " + x + " b"` into one format string means deciding what
|
||||
the whole sentence is, and several of them are pluralisations —
|
||||
`(if (n == 2) "event" else "events")` — which want a real plural resource rather than a
|
||||
format argument. `m3-extract-formatted.py --remaining` lists them.
|
||||
|
||||
`UserAgent.APP_NAME` still says "Torch". It goes on the wire to relay operators, so it is
|
||||
a network identity question rather than a content one.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user