Merge PR #2896 from gordonlu: localize status picker surface

Part of the i18n localization batch. Locale strings + MessageId wiring; no logic changes.
This commit is contained in:
Hunter Bown
2026-06-09 20:09:38 -07:00
committed by GitHub
3 changed files with 116 additions and 20 deletions
+66
View File
@@ -228,6 +228,14 @@ pub enum MessageId {
HistoryHintAccept,
HistoryHintRestore,
HistoryNoMatches,
// StatusPicker — `/statusline` multi-select footer-item picker.
StatusPickerTitle,
StatusPickerInstruction,
StatusPickerActionToggle,
StatusPickerActionAll,
StatusPickerActionNone,
StatusPickerActionSave,
StatusPickerActionCancel,
ConfigTitle,
ConfigModalTitle,
ConfigSearchPlaceholder,
@@ -586,6 +594,13 @@ pub const ALL_MESSAGE_IDS: &[MessageId] = &[
MessageId::HistoryHintAccept,
MessageId::HistoryHintRestore,
MessageId::HistoryNoMatches,
MessageId::StatusPickerTitle,
MessageId::StatusPickerInstruction,
MessageId::StatusPickerActionToggle,
MessageId::StatusPickerActionAll,
MessageId::StatusPickerActionNone,
MessageId::StatusPickerActionSave,
MessageId::StatusPickerActionCancel,
MessageId::ConfigTitle,
MessageId::ConfigModalTitle,
MessageId::ConfigSearchPlaceholder,
@@ -1113,6 +1128,13 @@ fn english(id: MessageId) -> &'static str {
MessageId::HistoryHintAccept => "Enter accept",
MessageId::HistoryHintRestore => "Esc restore",
MessageId::HistoryNoMatches => " No matches",
MessageId::StatusPickerTitle => " Status line ",
MessageId::StatusPickerInstruction => "Pick the chips you want in the footer:",
MessageId::StatusPickerActionToggle => "toggle ",
MessageId::StatusPickerActionAll => "all ",
MessageId::StatusPickerActionNone => "none ",
MessageId::StatusPickerActionSave => "save ",
MessageId::StatusPickerActionCancel => "cancel ",
MessageId::ConfigTitle => "Session Configuration",
MessageId::ConfigModalTitle => " Config ",
MessageId::ConfigSearchPlaceholder => "type to filter",
@@ -1606,6 +1628,15 @@ fn vietnamese(id: MessageId) -> Option<&'static str> {
MessageId::HistoryHintAccept => "Enter để chấp nhận",
MessageId::HistoryHintRestore => "Esc để khôi phục",
MessageId::HistoryNoMatches => " Không tìm thấy kết quả",
MessageId::StatusPickerTitle => " Dòng trạng thái ",
MessageId::StatusPickerInstruction => {
"Chọn các thành phần bạn muốn hiển thị ở cuối màn hình:"
}
MessageId::StatusPickerActionToggle => "bật/tắt ",
MessageId::StatusPickerActionAll => "tất cả ",
MessageId::StatusPickerActionNone => "không ",
MessageId::StatusPickerActionSave => "lưu ",
MessageId::StatusPickerActionCancel => "huỷ ",
MessageId::ConfigTitle => "Cấu hình phiên làm việc",
MessageId::ConfigModalTitle => " Cấu hình ",
MessageId::ConfigSearchPlaceholder => "Nhập để lọc kết quả",
@@ -2196,6 +2227,13 @@ fn traditional_chinese(id: MessageId) -> Option<&'static str> {
MessageId::CtxInspCacheTip => {
"提示:穩定前綴區塊符合 DeepSeek V4 前綴快取條件。易變工作集的更改僅會破壞快取尾部。"
}
MessageId::StatusPickerTitle => " 狀態列 ",
MessageId::StatusPickerInstruction => "選擇要在底部顯示的項目:",
MessageId::StatusPickerActionToggle => "切換 ",
MessageId::StatusPickerActionAll => "全部 ",
MessageId::StatusPickerActionNone => "",
MessageId::StatusPickerActionSave => "儲存 ",
MessageId::StatusPickerActionCancel => "取消 ",
other => chinese_simplified(other)?,
})
}
@@ -2209,6 +2247,13 @@ fn japanese(id: MessageId) -> Option<&'static str> {
MessageId::HistoryHintAccept => "Enter 確定",
MessageId::HistoryHintRestore => "Esc 復元",
MessageId::HistoryNoMatches => " 一致なし",
MessageId::StatusPickerTitle => " ステータス行 ",
MessageId::StatusPickerInstruction => "フッターに表示する項目を選択:",
MessageId::StatusPickerActionToggle => "切替 ",
MessageId::StatusPickerActionAll => "すべて ",
MessageId::StatusPickerActionNone => "なし ",
MessageId::StatusPickerActionSave => "保存 ",
MessageId::StatusPickerActionCancel => "キャンセル ",
MessageId::ConfigTitle => "セッション設定",
MessageId::ConfigModalTitle => " 設定 ",
MessageId::ConfigSearchPlaceholder => "入力して絞り込み",
@@ -2696,6 +2741,13 @@ fn chinese_simplified(id: MessageId) -> Option<&'static str> {
MessageId::HistoryHintAccept => "Enter 接受",
MessageId::HistoryHintRestore => "Esc 还原",
MessageId::HistoryNoMatches => " 无匹配",
MessageId::StatusPickerTitle => " 状态行 ",
MessageId::StatusPickerInstruction => "选择要在底部显示的项目:",
MessageId::StatusPickerActionToggle => "切换 ",
MessageId::StatusPickerActionAll => "全部 ",
MessageId::StatusPickerActionNone => "",
MessageId::StatusPickerActionSave => "保存 ",
MessageId::StatusPickerActionCancel => "取消 ",
MessageId::ConfigTitle => "会话配置",
MessageId::ConfigModalTitle => " 配置 ",
MessageId::ConfigSearchPlaceholder => "输入以筛选",
@@ -3125,6 +3177,13 @@ fn portuguese_brazil(id: MessageId) -> Option<&'static str> {
MessageId::HistoryHintAccept => "Enter aceita",
MessageId::HistoryHintRestore => "Esc restaura",
MessageId::HistoryNoMatches => " Sem resultados",
MessageId::StatusPickerTitle => " Linha de status ",
MessageId::StatusPickerInstruction => "Escolha os itens que deseja no rodapé:",
MessageId::StatusPickerActionToggle => "alternar ",
MessageId::StatusPickerActionAll => "todos ",
MessageId::StatusPickerActionNone => "nenhum ",
MessageId::StatusPickerActionSave => "salvar ",
MessageId::StatusPickerActionCancel => "cancelar ",
MessageId::ConfigTitle => "Configuração da sessão",
MessageId::ConfigModalTitle => " Config ",
MessageId::ConfigSearchPlaceholder => "digite para filtrar",
@@ -3636,6 +3695,13 @@ fn spanish_latin_america(id: MessageId) -> Option<&'static str> {
MessageId::HistoryHintAccept => "Enter aceptar",
MessageId::HistoryHintRestore => "Esc restaurar",
MessageId::HistoryNoMatches => " Sin resultados",
MessageId::StatusPickerTitle => " Línea de estado ",
MessageId::StatusPickerInstruction => "Elige los elementos que quieres en el pie:",
MessageId::StatusPickerActionToggle => "alternar ",
MessageId::StatusPickerActionAll => "todos ",
MessageId::StatusPickerActionNone => "ninguno ",
MessageId::StatusPickerActionSave => "guardar ",
MessageId::StatusPickerActionCancel => "cancelar ",
MessageId::ConfigTitle => "Configuración de la sesión",
MessageId::ConfigModalTitle => " Config ",
MessageId::ConfigSearchPlaceholder => "escribe para filtrar",
+1
View File
@@ -6266,6 +6266,7 @@ async fn apply_command_result(
.push(crate::tui::views::status_picker::StatusPickerView::new(
&app.status_items,
app.api_provider,
app.ui_locale,
));
}
}
+49 -20
View File
@@ -19,7 +19,7 @@ use ratatui::{
};
use crate::config::{ApiProvider, StatusItem};
use crate::localization::truncate_to_width;
use crate::localization::{Locale, MessageId, tr, truncate_to_width};
use crate::palette;
use crate::tui::views::{ModalKind, ModalView, ViewAction, ViewEvent};
use unicode_width::UnicodeWidthStr;
@@ -39,11 +39,12 @@ pub struct StatusPickerView {
cursor: usize,
/// Snapshot of `app.status_items` at open time so Esc reverts cleanly.
original: Vec<StatusItem>,
locale: Locale,
}
impl StatusPickerView {
#[must_use]
pub fn new(active: &[StatusItem], provider: ApiProvider) -> Self {
pub fn new(active: &[StatusItem], provider: ApiProvider, locale: Locale) -> Self {
let rows: Vec<StatusItem> = StatusItem::all()
.iter()
.filter(|item| item.is_available_for(provider))
@@ -55,6 +56,7 @@ impl StatusPickerView {
selected,
cursor: 0,
original: active.to_vec(),
locale,
}
}
@@ -185,22 +187,22 @@ impl ModalView for StatusPickerView {
let block = Block::default()
.title(Line::from(Span::styled(
" Status line ",
tr(self.locale, MessageId::StatusPickerTitle),
Style::default()
.fg(palette::DEEPSEEK_SKY)
.add_modifier(Modifier::BOLD),
)))
.title_bottom(Line::from(vec![
Span::styled(" Space ", Style::default().fg(palette::TEXT_MUTED)),
Span::raw("toggle "),
Span::raw(tr(self.locale, MessageId::StatusPickerActionToggle)),
Span::styled(" a ", Style::default().fg(palette::TEXT_MUTED)),
Span::raw("all "),
Span::raw(tr(self.locale, MessageId::StatusPickerActionAll)),
Span::styled(" n ", Style::default().fg(palette::TEXT_MUTED)),
Span::raw("none "),
Span::raw(tr(self.locale, MessageId::StatusPickerActionNone)),
Span::styled(" Enter ", Style::default().fg(palette::TEXT_MUTED)),
Span::raw("save "),
Span::raw(tr(self.locale, MessageId::StatusPickerActionSave)),
Span::styled(" Esc ", Style::default().fg(palette::TEXT_MUTED)),
Span::raw("cancel "),
Span::raw(tr(self.locale, MessageId::StatusPickerActionCancel)),
]))
.borders(Borders::ALL)
.border_style(Style::default().fg(palette::BORDER_COLOR))
@@ -215,7 +217,7 @@ impl ModalView for StatusPickerView {
let mut lines: Vec<Line> = Vec::with_capacity(visible_rows + 2);
lines.push(Line::from(Span::styled(
"Pick the chips you want in the footer:",
tr(self.locale, MessageId::StatusPickerInstruction),
Style::default().fg(palette::TEXT_MUTED),
)));
lines.push(Line::from(""));
@@ -297,19 +299,19 @@ fn status_row_text(pointer: &str, mark: &str, item: &StatusItem, width: usize) -
#[cfg(test)]
mod tests {
use super::*;
use crate::localization::Locale;
#[test]
fn opens_with_active_items_pre_selected() {
let active = StatusItem::default_footer();
let view = StatusPickerView::new(&active, ApiProvider::Deepseek);
let view = StatusPickerView::new(&active, ApiProvider::Deepseek, Locale::En);
assert_eq!(view.current_selection(), active);
}
#[test]
fn space_toggles_current_row_and_emits_live_preview() {
let active = StatusItem::default_footer();
let mut view = StatusPickerView::new(&active, ApiProvider::Deepseek);
// Cursor starts at row 0 = StatusItem::Mode (currently checked).
let mut view = StatusPickerView::new(&active, ApiProvider::Deepseek, Locale::En);
let action = view.handle_key(KeyEvent::new(KeyCode::Char(' '), KeyModifiers::NONE));
match action {
ViewAction::Emit(ViewEvent::StatusItemsUpdated { items, final_save }) => {
@@ -323,7 +325,7 @@ mod tests {
#[test]
fn enter_emits_final_save() {
let active = StatusItem::default_footer();
let mut view = StatusPickerView::new(&active, ApiProvider::Deepseek);
let mut view = StatusPickerView::new(&active, ApiProvider::Deepseek, Locale::En);
let action = view.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
match action {
ViewAction::EmitAndClose(ViewEvent::StatusItemsUpdated { final_save, .. }) => {
@@ -336,8 +338,7 @@ mod tests {
#[test]
fn esc_reverts_to_snapshot() {
let active = StatusItem::default_footer();
let mut view = StatusPickerView::new(&active, ApiProvider::Deepseek);
// Toggle a few items off so the working set diverges from snapshot.
let mut view = StatusPickerView::new(&active, ApiProvider::Deepseek, Locale::En);
view.handle_key(KeyEvent::new(KeyCode::Char(' '), KeyModifiers::NONE));
view.move_down();
view.handle_key(KeyEvent::new(KeyCode::Char(' '), KeyModifiers::NONE));
@@ -354,7 +355,7 @@ mod tests {
#[test]
fn select_all_and_select_none_keys_work() {
let active: Vec<StatusItem> = Vec::new();
let mut view = StatusPickerView::new(&active, ApiProvider::Deepseek);
let mut view = StatusPickerView::new(&active, ApiProvider::Deepseek, Locale::En);
let action = view.handle_key(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE));
match action {
ViewAction::Emit(ViewEvent::StatusItemsUpdated { items, .. }) => {
@@ -374,7 +375,7 @@ mod tests {
#[test]
fn arrow_keys_wrap_cursor_at_edges() {
let active = StatusItem::default_footer();
let mut view = StatusPickerView::new(&active, ApiProvider::Deepseek);
let mut view = StatusPickerView::new(&active, ApiProvider::Deepseek, Locale::En);
assert_eq!(view.cursor, 0);
view.handle_key(KeyEvent::new(KeyCode::Up, KeyModifiers::NONE));
assert_eq!(view.cursor, StatusItem::all().len() - 1);
@@ -404,10 +405,38 @@ mod tests {
#[test]
fn balance_excluded_for_non_deepseek_provider() {
let active = StatusItem::default_footer();
let view = StatusPickerView::new(&active, ApiProvider::Openrouter);
// Balance should not appear as a row for non-DeepSeek providers.
let view = StatusPickerView::new(&active, ApiProvider::Openrouter, Locale::En);
assert!(!view.rows.contains(&StatusItem::Balance));
// Mode should still be present.
assert!(view.rows.contains(&StatusItem::Mode));
}
#[test]
fn status_picker_displays_localized_title_for_zh_hans() {
assert_eq!(tr(Locale::ZhHans, MessageId::StatusPickerTitle), " 状态行 ");
}
#[test]
fn status_picker_no_english_leak_in_non_en_locales() {
for locale in [
Locale::Ja,
Locale::ZhHans,
Locale::ZhHant,
Locale::PtBr,
Locale::Es419,
Locale::Vi,
] {
let title = tr(locale, MessageId::StatusPickerTitle);
assert!(
!title.contains("Status"),
"{} leaks English in title: {title}",
locale.tag()
);
let instruction = tr(locale, MessageId::StatusPickerInstruction);
assert!(
!instruction.contains("footer"),
"{} leaks English in instruction: {instruction}",
locale.tag()
);
}
}
}