atomcode_tuix/modals/
language_picker.rs1use anyhow::Result;
11use crossterm::event::{KeyCode, KeyModifiers};
12
13use super::{Modal, ModalAction};
14use crate::event_loop::{build_status, Buffer, LoopCtx};
15use crate::i18n::{self, Locale};
16use crate::render::{MenuPayload, Renderer, UiLine};
17use crate::state::UiState;
18
19pub struct LanguagePicker {
20 options: Vec<(Locale, String, String)>,
21 selected: usize,
22}
23
24impl LanguagePicker {
25 pub fn open() -> Self {
26 let current = i18n::current_locale();
27 let options = vec![
28 (Locale::En, "English".to_string(), "en".to_string()),
29 (Locale::ZhCn, "简体中文".to_string(), "zh_CN".to_string()),
30 ];
31 let selected = options
32 .iter()
33 .position(|(loc, _, _)| *loc == current)
34 .unwrap_or(0);
35 Self { options, selected }
36 }
37}
38
39impl Modal for LanguagePicker {
40 fn handle_key(
41 &mut self,
42 code: KeyCode,
43 _mods: KeyModifiers,
44 buf: &mut Buffer,
45 state: &mut UiState,
46 ctx: &mut LoopCtx,
47 renderer: &mut dyn Renderer,
48 ) -> Result<ModalAction> {
49 match code {
50 KeyCode::Up => {
51 self.selected = self.selected.saturating_sub(1);
52 self.draw(buf, state, ctx, renderer);
53 Ok(ModalAction::Continue)
54 }
55 KeyCode::Down => {
56 let max = self.options.len().saturating_sub(1);
57 if self.selected < max {
58 self.selected += 1;
59 }
60 self.draw(buf, state, ctx, renderer);
61 Ok(ModalAction::Continue)
62 }
63 KeyCode::Enter => {
64 let (locale, label, _) = &self.options[self.selected];
65 let locale = *locale;
66 let label = label.clone();
67 i18n::set_locale(locale);
72 ctx.config.language = Some(locale);
73 let config_path = atomcode_core::config::Config::default_path();
74 if let Err(e) = ctx.config.save(&config_path) {
75 eprintln!("[language] failed to save config: {e}");
77 }
78 renderer.render(UiLine::CommandOutput(
79 crate::i18n::t(crate::i18n::Msg::LanguageSwitched {
80 label: &label,
81 locale: &locale.to_string(),
82 })
83 .into_owned(),
84 ));
85 renderer.flush();
86 Ok(ModalAction::Close)
87 }
88 KeyCode::Esc => Ok(ModalAction::Close),
89 _ => Ok(ModalAction::Continue),
90 }
91 }
92
93 fn draw(&self, buf: &Buffer, state: &UiState, ctx: &LoopCtx, renderer: &mut dyn Renderer) {
94 let items: Vec<(String, String)> = self
95 .options
96 .iter()
97 .map(|(_, label, hint)| (label.clone(), hint.clone()))
98 .collect();
99 let payload = MenuPayload {
100 items,
101 selected: self.selected,
102 kind: crate::render::MenuKind::SlashCommand,
103 };
104 renderer.render(UiLine::InputPrompt {
105 buf: buf.text.clone(),
106 cursor_byte: buf.cursor,
107 menu: Some(payload),
108 status: build_status(state, ctx),
109 attachments: Vec::new(),
110 });
111 renderer.flush();
112 }
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118
119 #[test]
120 fn open_selects_current_locale() {
121 let _g = crate::i18n::test_lock();
122 crate::i18n::set_locale(Locale::ZhCn);
123 let picker = LanguagePicker::open();
124 assert_eq!(picker.selected, 1); }
126
127 #[test]
128 fn open_defaults_to_en() {
129 let _g = crate::i18n::test_lock();
130 crate::i18n::set_locale(Locale::En);
131 let picker = LanguagePicker::open();
132 assert_eq!(picker.selected, 0); }
134
135 #[test]
142 fn switch_confirmation_zh_cn_has_checkmark_label_and_locale() {
143 let _g = crate::i18n::test_lock();
144 crate::i18n::set_locale(Locale::ZhCn);
145 let msg = crate::i18n::t(crate::i18n::Msg::LanguageSwitched {
146 label: "简体中文",
147 locale: "zh_CN",
148 });
149 assert!(msg.contains("✓"), "missing checkmark: {}", msg);
150 assert!(msg.contains("简体中文"), "missing label: {}", msg);
151 assert!(msg.contains("zh_CN"), "missing locale code: {}", msg);
152 assert!(msg.contains("已切换"), "missing '已切换' verb: {}", msg);
153 assert!(msg.ends_with('\n'), "missing trailing newline: {:?}", msg);
154 }
155
156 #[test]
157 fn switch_confirmation_en_has_checkmark_label_and_locale() {
158 let _g = crate::i18n::test_lock();
159 crate::i18n::set_locale(Locale::En);
160 let msg = crate::i18n::t(crate::i18n::Msg::LanguageSwitched {
161 label: "English",
162 locale: "en",
163 });
164 assert!(msg.contains("✓"), "missing checkmark: {}", msg);
165 assert!(msg.contains("English"), "missing label: {}", msg);
166 assert!(msg.contains("(en)"), "missing locale code: {}", msg);
167 assert!(msg.to_lowercase().contains("switched"), "missing 'switched' verb: {}", msg);
168 assert!(msg.ends_with('\n'), "missing trailing newline: {:?}", msg);
169 }
170}