Summary
Bot command menus (!help, product menus, !privacy, thread !commands, etc.) are English-only. Translation already targets the full catalog in translate_lang.rs (ALL_LANGUAGES / !list-langs). UI copy lives in menu_locale.rs. The leftover per-group MenuLanguage enum (en / es) in menu_language.rs is unused by handlers.
Canonical implementation plan: docs/plans/2026-08-14-001-feat-localize-bot-menus-plan.md (implementation-ready). Execute that file; this issue restates the decisions so the ticket can stand alone.
Goals
- Parity with
!list-langs — every catalog code is a valid menu locale (full strings or English fallback).
- Selectable via bot text —
!set-lang / !set-lang <code>, not 30 !set-xx commands.
- No UI enum per language — adding a code to
ALL_LANGUAGES is the gate; a string bundle is optional.
- Preserve mobile menu layout —
docs/solutions/signal-mobile-menus.md.
Resolved design (do not re-open at implement time)
| Question |
Decision |
| Storage |
Per-user, snapshot-level user_menu_locales map on encrypted prefs. Not per-group. Not the MenuLanguage enum. |
| DMs + mixed groups |
Same user locale in DMs and groups. Dual-key message.source and source_number (same as translate_all). |
| Authoring |
Static English source + per-locale bundles. No runtime NEAR translation. No fluent/gettext. |
| Fallback |
Missing locale or missing key → English. Commands never fail for lack of a bundle. |
| Discovery |
Hub !help / !info list !set-lang with gloss Menu language / idioma / langue. |
| Scaling |
!set-lang (picker) and !set-lang es / español. Leave !list-langs as the translation catalog; add a cross-link. |
| Sidecar |
No explicit pref → default to that Language Thread’s language code. Explicit pref (including en) wins. |
| Command tokens |
Stay English. Translate titles, descriptions, guides, confirmations only. |
| Coverage for v1 |
English complete. Spanish is the first full non-English bundle (including reviewed !privacy). Other codes persist and render English. |
Legacy menu_language |
Keep for serde of old snapshots. Do not read for UI. Do not backfill group es onto users. Do not bump DATA_VERSION. |
| CVM |
Additive field + #[serde(default)]. In-place phala deploy --cvm-id. Never wipe volumes. |
| Pairing / two roles |
Out of scope — pairing is gone; one bot. Sidecar compact menu is !commands, not hub !help. |
Stop if the work would wipe CVM volumes, translate menus via NEAR at runtime, grow MenuLanguage per language, or localize !command tokens.
Scope
Localize (or English-fallback): hub !help / !info, sidecar !commands / !info, product menus (including auto-translate disabled), feature guides, !translation redirect, !privacy, !set-lang picker + confirmation.
Out of scope: user-message translation; remaining language bundles after Spanish; operational errors (Unknown language, NOT_THREAD_MSG, on/off confirmations); !set-lang auto; RTL markup; offline translate-from-EN script.
Implementation units (dependency order)
- U1 — Store + resolver —
user_menu_locales on GroupPreferencesSnapshot; get/set dual-key; resolve_menu_locale = explicit pref → sidecar lang → en. Tests: legacy JSON load, encrypted round-trip, DM with no group id, sidecar default vs explicit en.
- U2 —
menu_locale API — builders take locale: &str; unknown locale equals English. Prove fallback before writing Spanish.
- U3 —
!set-lang — picker (flag + autonym + code); persist; reject unknown / extra tokens; hub + !list-langs discovery; register handler (set_lang); flip handlers_setup “not present” assert.
- U4 — Wire handlers — inject store into
HelpHandler, product menus, PrivacyHandler; keep hub-vs-sidecar branching.
- U5 — Spanish bundle — all menu surfaces including
!privacy (human-check attestation scope). Layout tests: command tokens present, stacked !cmd + indent, no cmd — desc one-liners.
- U6 — Docs — retire “English-only / deferred” in
signal-mobile-menus.md; CVM volume table says per-user locales on the same group_prefs.enc.
Files to expect: group_preferences_store.rs, menu_locale.rs, translate_lang.rs (autonyms), new set_lang handler, help.rs / product_menus.rs / privacy.rs / handlers_setup.rs, the docs above.
Test plan
Acceptance criteria
References
Summary
Bot command menus (
!help, product menus,!privacy, thread!commands, etc.) are English-only. Translation already targets the full catalog intranslate_lang.rs(ALL_LANGUAGES/!list-langs). UI copy lives inmenu_locale.rs. The leftover per-groupMenuLanguageenum (en/es) inmenu_language.rsis unused by handlers.Canonical implementation plan:
docs/plans/2026-08-14-001-feat-localize-bot-menus-plan.md(implementation-ready). Execute that file; this issue restates the decisions so the ticket can stand alone.Goals
!list-langs— every catalog code is a valid menu locale (full strings or English fallback).!set-lang/!set-lang <code>, not 30!set-xxcommands.ALL_LANGUAGESis the gate; a string bundle is optional.docs/solutions/signal-mobile-menus.md.Resolved design (do not re-open at implement time)
user_menu_localesmap on encrypted prefs. Not per-group. Not theMenuLanguageenum.message.sourceandsource_number(same astranslate_all).!help/!infolist!set-langwith glossMenu language / idioma / langue.!set-lang(picker) and!set-lang es/español. Leave!list-langsas the translation catalog; add a cross-link.en) wins.!privacy). Other codes persist and render English.menu_languageesonto users. Do not bumpDATA_VERSION.#[serde(default)]. In-placephala deploy --cvm-id. Never wipe volumes.!commands, not hub!help.Stop if the work would wipe CVM volumes, translate menus via NEAR at runtime, grow
MenuLanguageper language, or localize!commandtokens.Scope
Localize (or English-fallback): hub
!help/!info, sidecar!commands/!info, product menus (including auto-translate disabled), feature guides,!translationredirect,!privacy,!set-langpicker + confirmation.Out of scope: user-message translation; remaining language bundles after Spanish; operational errors (
Unknown language,NOT_THREAD_MSG, on/off confirmations);!set-lang auto; RTL markup; offline translate-from-EN script.Implementation units (dependency order)
user_menu_localesonGroupPreferencesSnapshot; get/set dual-key;resolve_menu_locale= explicit pref → sidecar lang →en. Tests: legacy JSON load, encrypted round-trip, DM with no group id, sidecar default vs expliciten.menu_localeAPI — builders takelocale: &str; unknown locale equals English. Prove fallback before writing Spanish.!set-lang— picker (flag + autonym + code); persist; reject unknown / extra tokens; hub +!list-langsdiscovery; register handler (set_lang); fliphandlers_setup“not present” assert.HelpHandler, product menus,PrivacyHandler; keep hub-vs-sidecar branching.!privacy(human-check attestation scope). Layout tests: command tokens present, stacked!cmd+ indent, nocmd — descone-liners.signal-mobile-menus.md; CVM volume table says per-user locales on the samegroup_prefs.enc.Files to expect:
group_preferences_store.rs,menu_locale.rs,translate_lang.rs(autonyms), newset_langhandler,help.rs/product_menus.rs/privacy.rs/handlers_setup.rs, the docs above.Test plan
!set-lang es/españolpersists;!helpis Spanish; tokens like!translation-threadsunchanged!set-lang frpersists; menus stay English until afrbundle exists!set-lang klingonand!set-lang es enreject; no persistes, user B unset → each!helpmatches that sender!helpin a group (dual identity keys)essidecar →!commandsuses sidecar default (Spanish once bundled, else English)user_menu_localesstill loadsnpm run ci/pnpm run ci(notpnpm ci)Acceptance criteria
docs/plans/2026-08-14-001-feat-localize-bot-menus-plan.md+ U6 docs)ALL_LANGUAGEScodes work without a new Rust enum variant (full strings or explicit English fallback)References
docs/plans/2026-08-14-001-feat-localize-bot-menus-plan.mdcrates/signal-bot/src/commands/menu_locale.rscrates/signal-bot/src/commands/translate_lang.rscrates/signal-bot/src/menu_language.rs— deserialize leftover onlydocs/solutions/signal-mobile-menus.mddocs/two-cvm-architecture.md— CVM storage (keep intact)