Skip to content

feat: i18n Phase 1-4b wiring + rebase compile fixes#2239

Open
gordonlu wants to merge 18 commits into
Hmbown:mainfrom
gordonlu:feat/i18n-phase1-composer-config-statusline
Open

feat: i18n Phase 1-4b wiring + rebase compile fixes#2239
gordonlu wants to merge 18 commits into
Hmbown:mainfrom
gordonlu:feat/i18n-phase1-composer-config-statusline

Conversation

@gordonlu
Copy link
Copy Markdown
Contributor

@gordonlu gordonlu commented May 27, 2026

Rebases and supersedes PR #812.

Wires the Phase 1–4b MessageId translations into the actual UI layer across 47 files. Fixes 109 compile errors caused by the upstream rebase (E0004 non-exhaustive patterns, E0061 argument count mismatches).

Changes

  • localization.rs: +1059 lines — 55 new MessageId variants for ConfigScope, AppMode, VimMode, Welcome, ContextInspector
  • 46 files wired: approval menu, agent cards, sidebar, history, context inspector, config views, onboarding/welcome, composer, header, status picker, pending input preview, tool card family labels
  • ** → **: renamed 1-arg version to avoid ambiguity with the new 2-arg — 109 call sites updated across 19 command files
  • Bug fixes:
    • default arm now propagates (was hardcoded )
    • test assertion corrected (was checking for nonexistent character)
    • Unused / imports removed to satisfy CI

Test status

3438 passed, 0 failures (2 pre-existing environment-specific tests do not apply in CI)

Greptile Summary

This PR wires the Phase 1–4b i18n MessageId translations into 46 UI files, renames the 1-arg CommandResult::error to error_msg and introduces a locale-aware 2-arg error, and fixes 109 compile errors from the upstream rebase (non-exhaustive match arms, argument-count mismatches).

  • Locale threading: AppMode::label, VimMode::label, ElevationOption::label/description, and all tool-card family labels now accept a Locale parameter; HookContext, analytics events, and StructuredState::capture() calls are all correctly pinned to Locale::En.
  • New message IDs: 55 variants added across ConfigScope, AppMode, VimMode, Welcome, ContextInspector, approval/elevation, queue commands, and tool-family labels — all with English strings wired end-to-end.
  • Bug fixes bundled: AppMode::label default arm now propagates Locale::En in model-facing contexts; the qa_pty viewport assertion is broadened to accept both old title-case and new uppercase mode labels.

Confidence Score: 4/5

Safe to merge after addressing two missed wiring spots: a model-facing relay instruction that embeds the user's locale instead of English, and a FanoutCounts message ID that was registered but never connected to the rendering code.

Two call sites were left partially wired. In build_relay_instruction the mode label is built with app.ui_locale rather than Locale::En, so the relay text sent to the model will contain a translated mode name once non-English locales are added — diverging from every other model-facing site in the same PR. In FanoutCard::render_lines the FanoutCounts message ID was declared and translated but the rendering code still uses a hardcoded format string, leaving fanout stats unlocalized and the new variant dead. Everything else — hook context, analytics, engine state capture, header, approval, context inspector, queue commands — is correctly wired.

crates/tui/src/commands/mod.rs (build_relay_instruction locale) and crates/tui/src/tui/widgets/agent_card.rs (FanoutCard stats row)

Important Files Changed

Filename Overview
crates/tui/src/localization.rs Adds 55 new MessageId variants and their English translations for Phase 1–4b; includes FanoutCounts which is defined but not yet wired in agent_card.rs
crates/tui/src/commands/mod.rs Renames error() to error_msg() and adds locale-aware error(); build_relay_instruction still passes app.ui_locale to mode.label() for model-facing text instead of Locale::En
crates/tui/src/tui/widgets/agent_card.rs Wires locale into AgentLifecycle labels, card headers, and render_lines; FanoutCard stats row still uses a hardcoded format string instead of FanoutCounts message ID
crates/tui/src/tui/app.rs AppMode::label and VimMode::label now take a Locale; HookContext and StructuredState calls correctly use Locale::En; status_message uses ui_locale (user-facing, correct)
crates/tui/src/core/engine.rs All three StructuredState::capture() call sites correctly updated to mode.label(Locale::En), keeping model-facing context locale-stable
crates/tui/src/tui/ui.rs Analytics/telemetry events and ElevationView construction updated to pass Locale::En and ui_locale respectively; context inspector and pending preview wired correctly
crates/tui/src/commands/queue.rs Queue commands wired with locale-aware strings; list_queue strips {n} from CmdEditingQueuedDraft leaving double-space and extraneous hint text in list context (pre-existing flag)
crates/tui/src/tui/context_inspector.rs Full locale wiring for build_context_inspector_text; all test assertions updated to use Locale::En and match new English strings correctly
crates/tui/src/tui/history.rs Locale threaded through ToolCell::render and SubAgentCell dispatch; legacy no-locale entry points fall back to Locale::En preserving backward compatibility
crates/tui/src/tui/approval.rs ElevationOption::label and description now locale-aware; ElevationView stores locale and passes it to the widget; all tests updated to Locale::En
crates/tui/src/tui/widgets/header.rs HeaderData gains a locale field; mode_name now delegates to AppMode::label(locale); tests updated, string expectations changed from title-case to uppercase to match new labels
crates/tui/tests/qa_pty.rs Viewport assertion broadened to accept both old title-case and new uppercase mode labels, maintaining backward compatibility during any transition

Comments Outside Diff (3)

  1. crates/tui/src/commands/queue.rs, line 51-52 (link)

    P1 Unresolved {n} placeholder in list context

    CmdEditingQueuedDraft translates to "Editing queued message {n} (press Enter to re-queue/send)", but in list_queue the {n} placeholder is never replaced. When a user runs /queue list with an active draft the output will literally contain "Editing queued message {n} (press Enter to re-queue/send):". The original string before this PR was a separate "Editing queued message:" without any placeholder, so these two sites need distinct message IDs — or the list site needs to call .replace("{n}", "") at minimum.

    Fix in Codex Fix in Claude Code Fix in Cursor

  2. crates/tui/src/commands/mod.rs, line 911-914 (link)

    P2 #[allow(dead_code)] on a function with 109+ callers

    error_msg is called at over 100 sites introduced in this very PR, so the compiler will not flag it as dead code. The annotation is a no-op right now but may mislead future readers into thinking the function is intended for eventual removal. Consider dropping the attribute, or — if error_msg is genuinely transitional — leave a // TODO: comment explaining the migration plan.

    Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

    Fix in Codex Fix in Claude Code Fix in Cursor

  3. crates/tui/src/tui/widgets/agent_card.rs, line 318-328 (link)

    P1 FanoutCounts message ID wired but never used

    MessageId::FanoutCounts was added to localization.rs with the English translation "{done} done · {running} running · {failed} failed · {pending} pending", but FanoutCard::render_lines still builds this string with a hardcoded format literal rather than calling tr(locale, MessageId::FanoutCounts). The new message ID is effectively dead code, and the fanout stats row will always display in English regardless of the active locale — the only part of the fanout card that was not actually wired in this PR.

    Fix in Codex Fix in Claude Code Fix in Cursor

Fix All in Codex Fix All in Claude Code Fix All in Cursor

Reviews (2): Last reviewed commit: "fix: queue draft header {n} leak, hook/a..." | Re-trigger Greptile

Greptile also left 1 inline comment on this PR.

gordonlu and others added 13 commits May 14, 2026 14:07
Hmbown#1599)

\033[>1u enables the kitty keyboard protocol with DISAMBIGUATE_ESCAPE_CODES.
On Windows Terminal, this causes Enter to emit \033[57414u, but crossterm
does not decode CSI u sequences on Windows (issue Hmbown#1599).  The raw string
[57414u appears as text input instead of sending the message.

\033[>0u advertises protocol awareness without enabling any flags. Enter
stays as \r\n and crossterm processes it normally.

Changes:
- push_keyboard_enhancement_flags: write \033[>0u instead of \033[>1u on Windows
- Gate KeyboardEnhancementFlags import behind #[cfg(not(windows))]
- Update Windows test to expect \033[>0u
- Non-Windows test unchanged (still expects \033[>1u)
- Remove unused KeyEvent, MouseButton, MouseEvent, MouseEventKind imports
  introduced by upstream during rebase
…and pending input preview (Hmbown#790)

Add 23 new MessageId variants with translations for all 4 shipped locales
(en, ja, zh-Hans, pt-BR) covering high-frequency TUI chrome:

- Composer submit/queue/steer/offline hints and queue count
- Config modal section labels
- Status line picker title, instruction, and action hints
- Pending input preview headers and edit hint

Co-Authored-By: DeepSeek V4 Flash
…wn#790)

Add 41 new MessageId variants with translations for all 4 shipped locales
(en, ja, zh-Hans, pt-BR) covering:

- Approval field labels (Type, About, Impact, Params)
- Risk badges (REVIEW, DESTRUCTIVE) and category labels
- Approval option labels and destructive confirmation footer
- Sandbox elevation title, option labels, descriptions, and impact text

Co-Authored-By: DeepSeek V4 Flash
…lized prefix (Hmbown#790)

All 111+ CommandResult::error() callers across 21 command files now use the
localized `error(msg, locale)` signature, replacing the hardcoded "Error:"
prefix with locale-aware output via CmdErrorPrefix.

Breaking helper API: parse_index, clear, parse_add, trust_add, trust_remove
now take a locale: Locale parameter threaded from their callers.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…anout summaries, and sub-agent modal (Hmbown#790)

Co-Authored-By: Deepseek V4 Flash
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces comprehensive localization (i18n) across the TUI commands, views, and widgets by replacing hardcoded English strings with localized messages retrieved via localization::tr using MessageId and the active ui_locale. The review feedback highlights several important improvements for robustness and consistency. Specifically, the reviewer points out that sequential positional placeholder replacements are fragile for internationalization and recommends named placeholders instead. Additionally, the reviewer notes that the error_msg helper documentation is misleading and contains an unnecessary #[allow(dead_code)] attribute. Finally, they advise using CommandResult::error_msg instead of CommandResult::error for usage instructions to avoid prepending an inappropriate 'Error:' prefix.

Comment on lines +120 to 127
let tmpl_transcript = tr(locale, MessageId::CtxInspectorTranscript);
let _ = writeln!(
out,
"Transcript: {} cells, {} API messages",
app.history.len(),
app.api_messages.len()
"{}",
tmpl_transcript
.replacen("{}", &app.history.len().to_string(), 1)
.replace("{}", &app.api_messages.len().to_string())
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using positional placeholders {} and replacing them sequentially with .replacen() and .replace() is fragile for internationalization (i18n). If a translator changes the order of the placeholders to fit the target language's grammar, the values will be swapped. It is highly recommended to use named placeholders (e.g., {cells} and {messages}) and replace them explicitly, which is already a common pattern in this codebase.

Comment thread crates/tui/src/commands/mod.rs Outdated
Comment on lines +90 to +98
/// Create a simple error message (English prefix)
#[allow(dead_code)]
pub fn error_msg(msg: impl Into<String>) -> Self {
Self {
message: Some(format!("Error: {}", msg.into())),
message: Some(msg.into()),
action: None,
is_error: true,
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The error_msg function does not prepend any prefix to the message, so the comment (English prefix) is misleading. Additionally, since error_msg is widely used across multiple command files, the #[allow(dead_code)] attribute is unnecessary and should be removed.

    /// Create a simple error message (no prefix)
    pub fn error_msg(msg: impl Into<String>) -> Self {
        Self {
            message: Some(msg.into()),
            action: None,
            is_error: true,
        }
    }

Comment on lines 47 to 55
let Some(raw) = arg.map(str::trim) else {
return CommandResult::error(
"Usage: /cycle <n> — n is the cycle number from /cycles".to_string(),
app.ui_locale,
);
};
if raw.is_empty() {
return CommandResult::error("Usage: /cycle <n>".to_string());
return CommandResult::error("Usage: /cycle <n>".to_string(), app.ui_locale);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using CommandResult::error for usage instructions prepends the localized Error: prefix, resulting in messages like Error: Usage: /cycle <n>. To keep usage instructions clean and prefix-free, use CommandResult::error_msg instead.

Suggested change
let Some(raw) = arg.map(str::trim) else {
return CommandResult::error(
"Usage: /cycle <n> — n is the cycle number from /cycles".to_string(),
app.ui_locale,
);
};
if raw.is_empty() {
return CommandResult::error("Usage: /cycle <n>".to_string());
return CommandResult::error("Usage: /cycle <n>".to_string(), app.ui_locale);
}
let Some(raw) = arg.map(str::trim) else {
return CommandResult::error_msg("Usage: /cycle <n> — n is the cycle number from /cycles");
};
if raw.is_empty() {
return CommandResult::error_msg("Usage: /cycle <n>");
}

Comment on lines +23 to +26
_ => CommandResult::error(
localization::tr(app.ui_locale, MessageId::CmdQueueUsage),
app.ui_locale,
),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using CommandResult::error for usage instructions prepends the localized Error: prefix, even when the usage message itself is localized. Use CommandResult::error_msg instead to display the localized usage message without the error prefix.

        _ => CommandResult::error_msg(
            localization::tr(app.ui_locale, MessageId::CmdQueueUsage),
        ),

CommandResult::action(AppAction::TaskCancel { id: id.to_string() })
}
_ => CommandResult::error("Usage: /task [add <prompt>|list|show <id>|cancel <id>]"),
_ => CommandResult::error(t(MessageId::CmdTaskUsageGeneral), app.ui_locale),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using CommandResult::error for usage instructions prepends the localized Error: prefix, even when the usage message itself is localized. Use CommandResult::error_msg instead to display the localized usage message without the error prefix.

Suggested change
_ => CommandResult::error(t(MessageId::CmdTaskUsageGeneral), app.ui_locale),
_ => CommandResult::error_msg(t(MessageId::CmdTaskUsageGeneral)),

gordonlu added 2 commits May 27, 2026 09:39
- Move ApiProvider import to cfg(test) block in core.rs, debug.rs
- Move CostCurrency import to cfg(test) block in debug.rs
- Remove dead family_label function, replace test call with family_label_locale
Comment thread crates/tui/src/tui/app.rs
let _ = writeln!(out, "Current session snapshot:");
let _ = writeln!(out, "- Workspace: {}", app.workspace.display());
let _ = writeln!(out, "- Mode: {}", app.mode.label());
let _ = writeln!(out, "- Mode: {}", app.mode.label(app.ui_locale));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 The relay instruction is sent directly to the model as a SendMessage payload. Embedding app.mode.label(app.ui_locale) means a non-English locale will send a translated mode label into the model-facing context — the same issue that was already corrected in engine.rs and ui.rs by using Locale::En. Using the user's locale here would make the relay instruction inconsistent with StructuredState::capture() calls that always pass Locale::En.

Suggested change
let _ = writeln!(out, "- Mode: {}", app.mode.label(app.ui_locale));
let _ = writeln!(out, "- Mode: {}", app.mode.label(Locale::En));

Fix in Codex Fix in Claude Code Fix in Cursor

@Hmbown
Copy link
Copy Markdown
Owner

Hmbown commented May 27, 2026

Greptile review identified two remaining wiring issues. Here are the specific fixes:

Fix 1: FanoutCounts dead code in agent_card.rs (~line 321)

-                format!(
-                    "{done} done \u{00B7} {running} running \u{00B7} {failed} failed \u{00B7} {pending} pending"
-                ),
+                self.app.tr(locale, MessageId::FanoutCounts),

The FanoutCounts message ID was defined in localization.rs but never used — this wires it in.

Fix 2: Relay instruction locale in commands/mod.rs (line 830)

-    let _ = writeln!(out, "- Mode: {}", app.mode.label(app.ui_locale));
+    let _ = writeln!(out, "- Mode: {}", app.mode.label(Locale::En));

Relay instructions go to the model — they must use Locale::En for consistency with StructuredState::capture() and engine.rs.

After these two fixes, the PR is ready to merge. 3438 tests still pass.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants