Translations 2/2: language choice in Settings, contexts, one-pass placeholders (#14, #33) #136

Merged
claude-bot merged 5 commits from i18n/part-2 into main 2026-10-30 15:36:52 +00:00
Collaborator

What

You can now choose the app's language in Settings (the phone's language, English, or any installed catalogue, each named in its own language). The change shows at once with no restart, and the choice is kept. The gettext catalogue now keeps message contexts, so qsTr("…", "comment") is translated, and a string that isn't translated falls back to its source text, never to blank. tr_args fills %1–%9 in one pass, so a value that contains %2 is never filled in again. CI's e2e job now gets the catalogues along with the binary.

Closes #33. Refs #14 (it stays open for phone QC). Plural forms and languages beyond German are deferred to #135.

Blast radius

Tier: trunk
Trunk paths touched:

  • core/src/worker*
  • core/src/tasks*
  • qt/main.cpp, qt/src/i18n.rs, qt/src/reading_model.rs
  • qml/Main.qml
  • tools/e2e/cases/language-switch.sh
  • .forgejo/workflows/ci.yml

Testing tier: maximum for .forgejo/workflows/ci.yml (the e2e artifact). The rest is standard.
Gate / rollback: ui.language defaults to "" (the phone's language). On that path start-up is unchanged: the same init(), and no second catalogue. Undo: revert this merge.

Proof

  • Local gates:

    • cargo make: fmt, clippy, the tests (utsb-core 291 passed), release, deny and audit all passed.
    • cargo make app-docker: exit 0.
    • tools/i18n/check-app.sh: the window title (a context string, "Study Bible", "window title") shows "Studienbibel" under LANGUAGE=de and "Study Bible" in the C locale.
  • Fails without the change: with the .mo parser skipping context entries again, 2 of the i18n tests fail. Restored, all 9 pass. tr_args is a new API with no base version.

  • CI:

    • Run 264 failed only in language-switch: the app listed 2 options, because the artifact held only the binary, without share/locale.
    • Since a65a67a the artifact has 2 files (binary and catalogue), as run 265's download shows.
    • Run 266 on ed7ddb4: green on changes, rust, audit, app, e2e (with language-switch) and click.
  • Phone (Plinius), build i18n/part-2@a65a67a93 and then ed7ddb4:

    • Settings shows "Phone language", English and Deutsch.
    • Tapping Deutsch makes the section "Sprache" at once (log utsb: language heading Sprache).
    • After a restart the footer reads "‹ Zurück", so the choice is kept.
    • Phone language brings English back.
    • The first build centred the option labels under the Suru style. ed7ddb4 fixes that; both builds are shown in the shots below.

    labels centred before ed7ddb4
    Deutsch chosen, live
    footer after a restart

Resources

  • CI mem-check: peak PSS 165244 kB (slow profile 168434 kB), against a budget of 176000 kB. Text was on screen after 315 ms, against a budget of 3000 ms.
  • Phone, German chosen, after scrolling Prov 6 to 8: PSS 63113 kB, of which 42440 kB is anonymous. The budget is 80 MB (docs/07).
  • Extra memory from a switch: one more parsed catalogue (tens of KB), only after the user switches. It is leaked once per language, so the leak is bounded by the languages installed.

#14 exit criteria

  • Golden tests for 5+ Bibles over the KJV, MT and German versifications: tools/golden covers BSB, Darby, KJV, TR (KJV), OSHB (MT), and GerElb1871 and GerSch (German), with the v11n tables v11n-KJV, v11n-MT and v11n-German.
  • Within budget on the Plinius: 63 MB against 80 MB, above.

Mockup

No mockup. The language choice is a Settings section of plain Controls 2 RadioButtons with a heading and a hint line, like the Navigation and Module repositories sections. It adds no page, sheet or dialog.

Review

Reviewer:

  • Fresh-context review at high on e8eb225: 0 findings.
  • Delta review at medium (360) on a65a67a, the CI artifact fix: 0 findings.
  • ed7ddb4 is a one-line QML layout fix found on the phone. It comes after the reviews.

Review-Confidence: 100/100 (audit). Every trunk file was reviewed at high, and the CI-only change got a delta review. ed7ddb4 touches only a leaf page (SettingsPage.qml); it was checked on the phone, and CI is green. 0 findings are open.

Confidence by part

  • tr_args and contexts: high. Unit tests, check-app, and fails without the change.
  • Live switch: high. The e2e case and the phone.
  • CI artifact: high. The e2e on ed7ddb4 is green.
  • Radio label alignment: medium. Checked on the phone only; no e2e checks layout.

🤖 Generated with Claude Code

## What You can now choose the app's language in Settings (the phone's language, English, or any installed catalogue, each named in its own language). The change shows at once with no restart, and the choice is kept. The gettext catalogue now keeps message contexts, so `qsTr("…", "comment")` is translated, and a string that isn't translated falls back to its source text, never to blank. `tr_args` fills `%1`–`%9` in one pass, so a value that contains `%2` is never filled in again. CI's e2e job now gets the catalogues along with the binary. Closes #33. Refs #14 (it stays open for phone QC). Plural forms and languages beyond German are deferred to #135. ## Blast radius **Tier:** trunk **Trunk paths touched:** - `core/src/worker*` - `core/src/tasks*` - `qt/main.cpp`, `qt/src/i18n.rs`, `qt/src/reading_model.rs` - `qml/Main.qml` - `tools/e2e/cases/language-switch.sh` - `.forgejo/workflows/ci.yml` **Testing tier:** maximum for `.forgejo/workflows/ci.yml` (the e2e artifact). The rest is standard. **Gate / rollback:** `ui.language` defaults to "" (the phone's language). On that path start-up is unchanged: the same `init()`, and no second catalogue. Undo: revert this merge. ## Proof - **Local gates:** - `cargo make`: fmt, clippy, the tests (utsb-core 291 passed), release, deny and audit all passed. - `cargo make app-docker`: exit 0. - `tools/i18n/check-app.sh`: the window title (a context string, `"Study Bible", "window title"`) shows "Studienbibel" under `LANGUAGE=de` and "Study Bible" in the C locale. - **Fails without the change:** with the `.mo` parser skipping context entries again, 2 of the i18n tests fail. Restored, all 9 pass. `tr_args` is a new API with no base version. - **CI:** - Run 264 failed only in `language-switch`: the app listed 2 options, because the artifact held only the binary, without `share/locale`. - Since a65a67a the artifact has 2 files (binary and catalogue), as run 265's download shows. - Run 266 on ed7ddb4: green on changes, rust, audit, app, e2e (with `language-switch`) and click. - **Phone (Plinius), build `i18n/part-2@a65a67a93` and then ed7ddb4:** - Settings shows "Phone language", English and Deutsch. - Tapping Deutsch makes the section "Sprache" at once (log `utsb: language heading Sprache`). - After a restart the footer reads "‹ Zurück", so the choice is kept. - Phone language brings English back. - The first build centred the option labels under the Suru style. ed7ddb4 fixes that; both builds are shown in the shots below. ![labels centred before ed7ddb4](https://code.austinschaefer.com/schaefera/utStudyBIble/wiki/raw/images/pr-136/before-centred.png) ![Deutsch chosen, live](https://code.austinschaefer.com/schaefera/utStudyBIble/wiki/raw/images/pr-136/deutsch-live.png) ![footer after a restart](https://code.austinschaefer.com/schaefera/utStudyBIble/wiki/raw/images/pr-136/footer-after-restart.png) ## Resources - **CI mem-check:** peak PSS 165244 kB (slow profile 168434 kB), against a budget of 176000 kB. Text was on screen after 315 ms, against a budget of 3000 ms. - **Phone, German chosen, after scrolling Prov 6 to 8:** PSS 63113 kB, of which 42440 kB is anonymous. The budget is 80 MB (docs/07). - **Extra memory from a switch:** one more parsed catalogue (tens of KB), only after the user switches. It is leaked once per language, so the leak is bounded by the languages installed. ## #14 exit criteria - **Golden tests for 5+ Bibles over the KJV, MT and German versifications:** `tools/golden` covers BSB, Darby, KJV, TR (KJV), OSHB (MT), and GerElb1871 and GerSch (German), with the v11n tables `v11n-KJV`, `v11n-MT` and `v11n-German`. - **Within budget on the Plinius:** 63 MB against 80 MB, above. ## Mockup No mockup. The language choice is a Settings section of plain Controls 2 RadioButtons with a heading and a hint line, like the Navigation and Module repositories sections. It adds no page, sheet or dialog. ## Review **Reviewer:** - Fresh-context review at high on e8eb225: 0 findings. - Delta review at medium (360) on a65a67a, the CI artifact fix: 0 findings. - ed7ddb4 is a one-line QML layout fix found on the phone. It comes after the reviews. **Review-Confidence:** 100/100 (audit). Every trunk file was reviewed at high, and the CI-only change got a delta review. ed7ddb4 touches only a leaf page (`SettingsPage.qml`); it was checked on the phone, and CI is green. 0 findings are open. ## Confidence by part - **tr_args and contexts:** high. Unit tests, check-app, and fails without the change. - **Live switch:** high. The e2e case and the phone. - **CI artifact:** high. The e2e on ed7ddb4 is green. - **Radio label alignment:** medium. Checked on the phone only; no e2e checks layout. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
i18n: choose the app's language in Settings, live; qsTr contexts; extraction of tr_args (#14, #33)
Some checks failed
CI / changes (pull_request) Successful in 8s
CI / rust (pull_request) Successful in 3m42s
CI / audit (pull_request) Successful in 10s
CI / app (pull_request) Successful in 2m21s
CI / e2e (pull_request) Failing after 10m4s
CI / click (pull_request) Has been skipped
e8eb225b41
Setting ui.language, Settings > Language, catalogue swap with QQmlEngine::retranslate,
msgctxt in the QTranslator and in xgettext, the window title as a context message,
check-app.sh and the language-switch e2e case.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude-review left a comment

Fresh-context review. Level: high. Blast radius: trunk. Reviewed commit: e8eb225b41. Range: origin/main...HEAD.

Findings: none (no high, medium or low defect with a concrete failure scenario).

What was checked, in context: the Catalog context keys (\x04) and get_in against QML's empty-string disambiguation and the C++ null; select/load/CHOSEN/LOADED locking and the path guard on tags; the "" / "en" / unknown-tag fallbacks; tr_args in one pass against the old chained .replace (same results for %%, a value holding %N, a missing value, multi-byte text); every converted call site in tasks, worker and session (same placeholders, same order); the ui.language setting through SetSetting, set_setting (render.apply ignores the key), settings() at start and ReadingModel::initialize (a single ReadingModel instance, so the default "" cannot reset a chosen language); choose() on the GUI thread and the retranslate callback registered before engine.load; the SettingsPage options order against the e2e case's 6 coordinates; the xgettext keyword specs and the perl extraction of tr_args against po/ and the pot; check-app.sh against the msgctxt entry. cargo test -p utsb-core --lib i18n passes locally (9 tests). CI on this commit was still pending at review time.

Observations, not defects: strings the worker made before a language change (notices, task row texts already sent to QML) keep the old language until the next event; the code comment and docs say so. Plurals stay with #135.

Files reviewed: core/src/i18n.rs, core/src/settings.rs, core/src/starter.rs, core/src/tasks.rs, core/src/tasks/exec.rs, core/src/tasks/progress.rs, core/src/worker.rs, core/src/worker/session.rs, core/src/worker/session/jump.rs, core/src/worker/session/switch.rs, qt/main.cpp, qt/src/i18n.rs, qt/src/reading_model.rs, qml/Main.qml, qml/pages/SettingsPage.qml, qml/pages/ReadingPane.qml (usage), tools/e2e/cases/language-switch.sh, tools/e2e/lib.sh (helpers), tools/i18n/update.sh, tools/i18n/check-app.sh, po/de.po, po/utstudybible.schaefera.pot, docs/09-quality-gates.md, docs/superpowers/plans/2026-10-21-gettext.md, docs/superpowers/plans/2026-10-30-gettext-2.md, CLAUDE.md

Fresh-context review. Level: high. Blast radius: trunk. Reviewed commit: e8eb225b4100e1f65a0c40e1563fb8cde2663baf. Range: origin/main...HEAD. Findings: none (no high, medium or low defect with a concrete failure scenario). What was checked, in context: the Catalog context keys (\x04) and get_in against QML's empty-string disambiguation and the C++ null; select/load/CHOSEN/LOADED locking and the path guard on tags; the "" / "en" / unknown-tag fallbacks; tr_args in one pass against the old chained .replace (same results for %%, a value holding %N, a missing value, multi-byte text); every converted call site in tasks, worker and session (same placeholders, same order); the ui.language setting through SetSetting, set_setting (render.apply ignores the key), settings() at start and ReadingModel::initialize (a single ReadingModel instance, so the default "" cannot reset a chosen language); choose() on the GUI thread and the retranslate callback registered before engine.load; the SettingsPage options order against the e2e case's 6 coordinates; the xgettext keyword specs and the perl extraction of tr_args against po/ and the pot; check-app.sh against the msgctxt entry. `cargo test -p utsb-core --lib i18n` passes locally (9 tests). CI on this commit was still pending at review time. Observations, not defects: strings the worker made before a language change (notices, task row texts already sent to QML) keep the old language until the next event; the code comment and docs say so. Plurals stay with #135. Files reviewed: core/src/i18n.rs, core/src/settings.rs, core/src/starter.rs, core/src/tasks.rs, core/src/tasks/exec.rs, core/src/tasks/progress.rs, core/src/worker.rs, core/src/worker/session.rs, core/src/worker/session/jump.rs, core/src/worker/session/switch.rs, qt/main.cpp, qt/src/i18n.rs, qt/src/reading_model.rs, qml/Main.qml, qml/pages/SettingsPage.qml, qml/pages/ReadingPane.qml (usage), tools/e2e/cases/language-switch.sh, tools/e2e/lib.sh (helpers), tools/i18n/update.sh, tools/i18n/check-app.sh, po/de.po, po/utstudybible.schaefera.pot, docs/09-quality-gates.md, docs/superpowers/plans/2026-10-21-gettext.md, docs/superpowers/plans/2026-10-30-gettext-2.md, CLAUDE.md
ci: the e2e job gets the app's catalogues with its binary
Some checks failed
CI / changes (pull_request) Successful in 7s
CI / rust (pull_request) Successful in 3m51s
CI / audit (pull_request) Successful in 11s
CI / app (pull_request) Successful in 2m20s
CI / e2e (pull_request) Has been cancelled
CI / click (pull_request) Has been cancelled
a65a67a93d
The language-switch case lists the installed languages from share/locale
next to the binary; the artifact held only the binary, so CI found no
German (run 264).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude-review left a comment

Delta review (medium)

Commit: a65a67a93d
Range: e8eb225..a65a67a
Files: .forgejo/workflows/ci.yml

Findings: none.

Checked: upload-artifact@v3 with two paths (build-ci/utstudybible, build-ci/share/locale) roots the artifact at their least common ancestor, build-ci, so the download into app-amd64 gives app-amd64/utstudybible and app-amd64/share/locale//LC_MESSAGES/*.mo. CMakeLists.txt puts the .mo files in ${CMAKE_BINARY_DIR}/share/locale via the ALL translations target, which the binary depends on. core/src/i18n.rs locale_dir looks at exe.parent()/share/locale, which matches that layout. Nothing else uses app-amd64: only the e2e download step and UTSB_APP=$PWD/app-amd64/utstudybible, and click does not consume it. tools/ci/e2e.sh only chmods the binary, so the artifact's old shape is not depended on.

Delta review (medium) Commit: a65a67a93d2ea290aaa04c7fd6b19c3ef61ff324 Range: e8eb225..a65a67a Files: .forgejo/workflows/ci.yml Findings: none. Checked: upload-artifact@v3 with two paths (build-ci/utstudybible, build-ci/share/locale) roots the artifact at their least common ancestor, build-ci, so the download into app-amd64 gives app-amd64/utstudybible and app-amd64/share/locale/<lang>/LC_MESSAGES/*.mo. CMakeLists.txt puts the .mo files in ${CMAKE_BINARY_DIR}/share/locale via the ALL `translations` target, which the binary depends on. core/src/i18n.rs locale_dir looks at exe.parent()/share/locale, which matches that layout. Nothing else uses app-amd64: only the e2e download step and UTSB_APP=$PWD/app-amd64/utstudybible, and click does not consume it. tools/ci/e2e.sh only chmods the binary, so the artifact's old shape is not depended on.
ui: the language options keep their labels beside the circle
All checks were successful
CI / changes (pull_request) Successful in 5s
CI / rust (pull_request) Successful in 3m46s
CI / audit (pull_request) Successful in 9s
CI / app (pull_request) Successful in 2m22s
CI / e2e (pull_request) Successful in 10m4s
CI / click (pull_request) Successful in 1m17s
ed7ddb4aee
On the phone the Suru style centres a full-width RadioButton's label.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude-bot changed title from WIP: Translations 2/2: language choice in Settings, contexts, one-pass placeholders (#14, #33) to Translations 2/2: language choice in Settings, contexts, one-pass placeholders (#14, #33) 2026-10-30 15:36:01 +00:00
claude-review left a comment

Merge gate on ed7ddb4. Reviews 359 (high, e8eb225) and 360 (medium delta, a65a67a, ci.yml) have 0 findings, so nothing is left to fix or refute. The only commit after them, ed7ddb4, is a one-line QML change in qml/pages/SettingsPage.qml (a leaf page): it drops Layout.fillWidth from the language RadioButtons, with a comment. It does not touch logic. CI on head is green on all six jobs of run 266: changes, rust, audit, app, e2e and click. The PR body's Proof has no unrun elevated, maximum or AI-behavior gate. The maximum-tier ci.yml change is backed by the failing run 264 and the green run 266, which includes the language-switch e2e case. The Review-Confidence of 100 comes from the audit. Approved.

Merge gate on ed7ddb4. Reviews 359 (high, e8eb225) and 360 (medium delta, a65a67a, ci.yml) have 0 findings, so nothing is left to fix or refute. The only commit after them, ed7ddb4, is a one-line QML change in qml/pages/SettingsPage.qml (a leaf page): it drops Layout.fillWidth from the language RadioButtons, with a comment. It does not touch logic. CI on head is green on all six jobs of run 266: changes, rust, audit, app, e2e and click. The PR body's Proof has no unrun elevated, maximum or AI-behavior gate. The maximum-tier ci.yml change is backed by the failing run 264 and the green run 266, which includes the language-switch e2e case. The Review-Confidence of 100 comes from the audit. Approved.
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
3 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
schaefera/utStudyBIble!136
No description provided.