feat(strings): the app's words for every game (M20) #58

Merged
whitlocktech merged 5 commits from feat/app-vocabulary into edge 2026-10-23 01:50:51 -05:00
Member

Implements M20 from docs/android/PLAN.md (planned in docs#372). It has to land in edge before the edge → main release, so the first build with Rust screens doesn't greet a Rust community with UO's words.

Commits, in the plan's order

  1. refactor(strings): split by owner. strings.xml holds the core strings. strings_uo.xml (189 strings) and strings_rust.xml (136) hold the strings only that game's screens use. Android merges the three into one R.string, so no key or Kotlin changes. Each string was classified by the screens that reference it, traced in code. All 578 elements moved byte for byte, which I checked by comparing name → element before and after. Two traces from the plan:

    • error_shard_offline and error_feature_unavailable are the shared StateViews texts, so they are core.
    • push_stream_server_status is UO: all seven legacy streams, server.status included, are declared only by module-uo (LEGACY_STREAM_IDS.uo).
  2. feat(strings): core rewording. 14 strings: connect_*, login_subtitle, error_shard_offline ("The game server is offline right now."), error_feature_unavailable, notifications_*, push_channel_*, push_service_text.

  3. test(strings): the guard. StringVocabularyTest checks every values* directory, so translations are checked too. It fails, naming the key and the word, when the core file holds either game's word or a game's file holds the other game's word. The word list is checkTerms.mjs's, word for word. Three more tests:

    • a fourth strings* file with no owner is refused;
    • the three default files must exist and hold strings;
    • the matcher is checked against known hits and misses.

    Run against the old core file, it names exactly the 14 strings. build.gradle.kts now declares src/main/res as a Test input. Without it, a text-only edit left testDebugUnitTest UP-TO-DATE and the guard did not run again.

  4. fix(nav): gate Moderation and Support queue on Capability.SHARD (org lead's decision during this work). Both call /admin/shard/*, which is module-uo's mount. Before this, a moderator on a Rust-only site saw a UO form ("Serial (0x…)") where every action failed. Covered by a new MenuCapabilityGatingTest case.

  5. feat(rust): weapon names. weaponName() (RustFormat.kt) prints frame.weaponName from Module-Rust#60. Against an older module it drops the .entity / .weapon / .deployed ending before prefabName(), the same way the website's format.js weapon() does. So "with ak47u entity" becomes "with Assault Rifle", or "with ak47u" from an older module.

Verified

  • ./gradlew testDebugUnitTest lintDebug assembleDebug (the CI gate) passes locally: 758 unit tests, 0 failures.
  • Walked on the s22_ultra emulator against both demos: rgdemo (UO only, core :3300, sidecar down) and rgdemorust (Rust only, core :3000). I signed in with role-named accounts.
    • First run: "Connect to your community", with the hint https://your-community.example.com.
    • Sign-in: "Sign in with your account on this site."
    • Drawer:
      • UO, signed out: Shard, Rules, Atlas, Leaderboards and Market show.
      • UO, moderator: also My characters, vendors and houses, Moderation and Support queue.
      • Rust, moderator: Rust servers, My Rust account, Dashboard and Content. No Moderation or Support queue.
    • Notifications: both sites say "Choose what this site notifies you about…" and "This site hasn't set up push notifications yet."
    • Server down:
      • UO: a broadcast with the sidecar down shows "The shard is offline — the action couldn't be delivered." That is UO text on a UO screen, as planned.
      • Rust: with the site in maintenance, signed out, News shows "The game server is offline right now."
    • Rust feed: "with Thompson", "with Bolt Action Rifle", "with Assault Rifle", "with MP5A4", "with LR-300 Assault Rifle".

Found, not fixed (needs a decision)

The app maps every 503 to ErrorKind.SHARD_OFFLINE. Core's siteMode middleware also answers 503 in maintenance mode. So a signed-out reader of News during maintenance is told "The game server is offline right now". Before this PR it said "The shard is offline right now", so it was wrong before too. The fix is a separate maintenance error kind, read from the 503 body's mode: "maintenance". That is outside M20's scope.

Not in this PR

  • Code identifiers and comments that say "shard" (ui/shard, Shard*), by the plan's decision.

  • website#221's leftovers, which belong to website/.

  • AI-assisted: written with Claude Code (Claude Opus 5.5). Commits carry the Co-Authored-By trailer.

🤖 Generated with Claude Code

https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY

Implements **M20** from `docs/android/PLAN.md` (planned in docs#372). It has to land in `edge` before the `edge` → `main` release, so the first build with Rust screens doesn't greet a Rust community with UO's words. ## Commits, in the plan's order 1. **`refactor(strings)`: split by owner.** `strings.xml` holds the core strings. `strings_uo.xml` (189 strings) and `strings_rust.xml` (136) hold the strings only that game's screens use. Android merges the three into one `R.string`, so no key or Kotlin changes. Each string was classified by the screens that reference it, traced in code. All 578 elements moved byte for byte, which I checked by comparing name → element before and after. Two traces from the plan: - `error_shard_offline` and `error_feature_unavailable` are the shared `StateViews` texts, so they are **core**. - `push_stream_server_status` is **UO**: all seven legacy streams, `server.status` included, are declared only by module-uo (`LEGACY_STREAM_IDS.uo`). 2. **`feat(strings)`: core rewording.** 14 strings: `connect_*`, `login_subtitle`, `error_shard_offline` ("The game server is offline right now."), `error_feature_unavailable`, `notifications_*`, `push_channel_*`, `push_service_text`. 3. **`test(strings)`: the guard.** `StringVocabularyTest` checks every `values*` directory, so translations are checked too. It fails, naming the key and the word, when the core file holds either game's word or a game's file holds the other game's word. The word list is `checkTerms.mjs`'s, word for word. Three more tests: - a fourth `strings*` file with no owner is refused; - the three default files must exist and hold strings; - the matcher is checked against known hits and misses. Run against the old core file, it names exactly the 14 strings. `build.gradle.kts` now declares `src/main/res` as a Test input. Without it, a text-only edit left `testDebugUnitTest` UP-TO-DATE and the guard did not run again. 4. **`fix(nav)`: gate Moderation and Support queue on `Capability.SHARD`** (org lead's decision during this work). Both call `/admin/shard/*`, which is module-uo's mount. Before this, a moderator on a Rust-only site saw a UO form ("Serial (0x…)") where every action failed. Covered by a new `MenuCapabilityGatingTest` case. 5. **`feat(rust)`: weapon names.** `weaponName()` (`RustFormat.kt`) prints `frame.weaponName` from Module-Rust#60. Against an older module it drops the `.entity` / `.weapon` / `.deployed` ending before `prefabName()`, the same way the website's `format.js weapon()` does. So "with ak47u entity" becomes "with Assault Rifle", or "with ak47u" from an older module. ## Verified - `./gradlew testDebugUnitTest lintDebug assembleDebug` (the CI gate) passes locally: 758 unit tests, 0 failures. - **Walked on the s22_ultra emulator** against both demos: rgdemo (UO only, core :3300, sidecar down) and rgdemorust (Rust only, core :3000). I signed in with role-named accounts. - **First run:** "Connect to your community", with the hint `https://your-community.example.com`. - **Sign-in:** "Sign in with your account on this site." - **Drawer:** - UO, signed out: Shard, Rules, Atlas, Leaderboards and Market show. - UO, moderator: also My characters, vendors and houses, Moderation and Support queue. - Rust, moderator: Rust servers, My Rust account, Dashboard and Content. **No Moderation or Support queue.** - **Notifications:** both sites say "Choose what this site notifies you about…" and "This site hasn't set up push notifications yet." - **Server down:** - UO: a broadcast with the sidecar down shows "The shard is offline — the action couldn't be delivered." That is UO text on a UO screen, as planned. - Rust: with the site in maintenance, signed out, News shows "The game server is offline right now." - **Rust feed:** "with Thompson", "with Bolt Action Rifle", "with Assault Rifle", "with MP5A4", "with LR-300 Assault Rifle". ## Found, not fixed (needs a decision) The app maps **every** 503 to `ErrorKind.SHARD_OFFLINE`. Core's `siteMode` middleware also answers 503 in maintenance mode. So a signed-out reader of News during maintenance is told "The game server is offline right now". Before this PR it said "The shard is offline right now", so it was wrong before too. The fix is a separate maintenance error kind, read from the 503 body's `mode: "maintenance"`. That is outside M20's scope. ## Not in this PR - Code identifiers and comments that say "shard" (`ui/shard`, `Shard*`), by the plan's decision. - website#221's leftovers, which belong to `website/`. - [x] AI-assisted: written with Claude Code (Claude Opus 5.5). Commits carry the Co-Authored-By trailer. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
wtclaude added 5 commits 2026-10-23 01:48:14 -05:00
PLAN.md M20 step 1. strings.xml keeps the core strings; strings_uo.xml and
strings_rust.xml take the strings only that game's screens use. Android merges
the three into one R.string, so no key changes and no Kotlin changes.

Each string was classified by the screens that reference it, traced in code,
not by its key: a string any deployment's screen can show stays core.
error_shard_offline and error_feature_unavailable are the shared error-view
texts, so they stay core. All seven legacy push streams, server.status
included, are declared only by module-uo, so their titles are UO.

No text changes: all 578 elements are moved byte for byte (checked by
comparing name → element before and after).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
PLAN.md M20 step 2. The first screen a Rust community's player sees said
"Connect to your shard", and the next said "Sign in with your shard account".
The core strings now use core's words, as the website and runicgateway.com do:

- connect_*: "your community", "a Runic Gateway site"
- login_subtitle: "Sign in with your account on this site."
- error_shard_offline: "The game server is offline right now." It is the
  shared 503 text that both games' screens show.
- error_feature_unavailable and notifications_*: "This site …"
- push_channel_* and push_service_text: "Notifications from your community"

Two comments in the file said "shard" too, and now say "site". Keys are
unchanged. The UO screens keep their words.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
PLAN.md M20 step 3. StringVocabularyTest reads strings.xml, strings_uo.xml
and strings_rust.xml in every values* directory, so translations are checked
too. It fails, naming the key and the word, when the core file holds either
game's word, the UO file holds a Rust word, or the Rust file holds a UO word.
The word list is runicgateway.com's checkTerms.mjs list, word for word. The
app never shows Discord's "guild ID", so that allowlist entry isn't carried
over.

Three more tests keep the guard honest:
- a fourth strings file with no owner is refused, because Android would read
  it and the guard would skip it;
- the three default files must exist and hold strings, so a wrong path can't
  pass on nothing;
- the matcher is checked against known hits and misses.

Run against the core file as it was before the rewording, it names exactly
the 14 strings that commit changed.

build.gradle.kts declares src/main/res as an input of the Test tasks. A
reworded string changes no class, so without that input Gradle left the test
task UP-TO-DATE and the guard did not run again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
Found by the M20 trace, and fixed in this PR by the org lead's decision.
Both rows call /admin/shard/*, module-uo's admin mount, but they carried no
capability. So a moderator on a Rust-only site saw "Moderation" in the drawer
and opened a UO form ("Serial (0x…)", kick or ban by UO account) where every
action failed.

Both rows now declare Capability.SHARD, as the player rows do, so they are UO
screens and their strings belong in strings_uo.xml, where step 1 put them. A
new MenuCapabilityGatingTest case covers it: a moderator on a Rust-only
backend sees neither row but keeps the Dashboard, and on a UO backend sees
both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
feat(rust): print the weapon name the module sends in the feed
All checks were successful
PR Checks / android-build (pull_request) Successful in 14m33s
c41aff9b2e
PLAN.md M20 step 4. Module-Rust#60 gives every event frame a weaponName: the
game's own name for a ranged weapon, or the prefab name as words. The feed
printed the prefab name as words itself, which read "with ak47u entity".

weaponName() in RustFormat.kt is the Kotlin half of module-rust's format.js
weapon(). It prints frame.weaponName. Against a module too old to send it,
it drops the .entity / .weapon / .deployed ending before prefabName(), as
the website does, so an older module's kill reads "with ak47u".

Tests: a kill with weaponName, the same kill from an older module, and the
fallback's edges (blank name, each ending, an ending only at the end, no
weapon).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
whitlocktech merged commit 478206e0bd into edge 2026-10-23 01:50:51 -05:00
whitlocktech deleted branch feat/app-vocabulary 2026-10-23 01:50:52 -05:00
Sign in to join this conversation.
No description provided.