返回 CodeWhale
language.rs
根目录 / crates / tui / src / tui / onboarding / language.rs
1 //! Language picker for first-run onboarding (#566).
2 //!
3 //! Surfaces every locale the TUI ships translations for, plus an `auto`
4 //! option that defers to `LC_ALL` / `LANG`. Selection persists via
5 //! `Settings::save` immediately so the rest of onboarding (and every
6 //! subsequent session) reads the chosen tag.
7
8 use ratatui::style::{Modifier, Style};
9 use ratatui::text::{Line, Span};
10
11 use crate::localization::MessageId;
12 use crate::palette;
13 use crate::tui::app::App;
14
15 /// Locale options shown in the picker. Order matches the keyboard hotkeys.
16 /// Each entry is `(hotkey, settings_tag, native_name, english_label)`.
17 /// `settings_tag` is what `Settings::set("locale", …)` accepts and what
18 /// `localization::Locale` resolves on next read.
19 ///
20 /// Hotkeys run `1..=9` then `a`, `b`, … so more than nine shipped locales
21 /// stay single-keystroke selectable.
22 pub const LANGUAGE_OPTIONS: &[(char, &str, &str, &str)] = &[
23 ('1', "auto", "Auto-detect", "(LC_ALL / LANG)"),
24 ('2', "en", "English", ""),
25 ('3', "ja", "日本語", "(Japanese)"),
26 ('4', "zh-Hans", "简体中文", "(Simplified Chinese)"),
27 ('5', "zh-Hant", "繁體中文", "(Traditional Chinese)"),
28 ('6', "pt-BR", "Português (Brasil)", "(Brazilian Portuguese)"),
29 (
30 '7',
31 "es-419",
32 "Español (Latinoamérica)",
33 "(Latin American Spanish)",
34 ),
35 ('8', "vi", "Tiếng Việt", "(Vietnamese)"),
36 ('9', "ko", "한국어", "(Korean)"),
37 ('a', "ca", "Català", "(Catalan)"),
38 ('b', "de", "Deutsch", "(German)"),
39 ('c', "fr", "Français", "(French)"),
40 ('d', "id", "Bahasa Indonesia", "(Indonesian)"),
41 ('e', "hi", "हिन्दी", "(Hindi)"),
42 ('f', "ru", "Русский", "(Russian)"),
43 ('g', "uk", "Українська", "(Ukrainian)"),
44 ];
45
46 pub fn lines(app: &App) -> Vec<Line<'static>> {
47 let current_owned = app.current_locale_tag();
48 let current = current_owned.as_str();
49
50 let mut out: Vec<Line<'static>> = vec![
51 Line::from(Span::styled(
52 app.tr(MessageId::OnboardLanguageTitle).to_string(),
53 Style::default()
54 .fg(palette::WHALE_INFO)
55 .add_modifier(Modifier::BOLD),
56 )),
57 Line::from(""),
58 Line::from(Span::styled(
59 app.tr(MessageId::OnboardLanguageBlurb).to_string(),
60 Style::default().fg(palette::TEXT_MUTED),
61 )),
62 Line::from(""),
63 ];
64
65 for (hotkey, tag, native, english) in LANGUAGE_OPTIONS {
66 let is_current = current == *tag;
67 let bullet = if is_current {
68 crate::tui::glyphs::CURRENT
69 } else {
70 crate::tui::glyphs::AVAILABLE
71 };
72 let bullet_color = if is_current {
73 palette::WHALE_ACTION
74 } else {
75 palette::TEXT_MUTED
76 };
77 let mut spans: Vec<Span<'static>> = vec![
78 Span::styled(format!(" {bullet} "), Style::default().fg(bullet_color)),
79 Span::styled(
80 format!("[{hotkey}] "),
81 Style::default()
82 .fg(palette::TEXT_PRIMARY)
83 .add_modifier(Modifier::BOLD),
84 ),
85 Span::styled(
86 native.to_string(),
87 Style::default().fg(palette::TEXT_PRIMARY),
88 ),
89 ];
90 if !english.is_empty() {
91 spans.push(Span::styled(
92 format!(" {english}"),
93 Style::default().fg(palette::TEXT_MUTED),
94 ));
95 }
96 out.push(Line::from(spans));
97 }
98
99 out.push(Line::from(""));
100 out.push(Line::from(Span::styled(
101 app.tr(MessageId::OnboardLanguageFooter).to_string(),
102 Style::default().fg(palette::TEXT_MUTED),
103 )));
104
105 out
106 }
107
108 #[cfg(test)]
109 mod tests {
110 use super::*;
111 use crate::localization::Locale;
112
113 /// Every locale we ship translations for must be offered in the picker,
114 /// otherwise the footer advertises hotkeys that select nothing and users
115 /// can never reach a supported UI language (#3929).
116 #[test]
117 fn picker_offers_every_shipped_locale() {
118 let offered: Vec<&str> = LANGUAGE_OPTIONS.iter().map(|(_, tag, _, _)| *tag).collect();
119 assert!(
120 offered.contains(&"auto"),
121 "picker must keep the auto-detect entry"
122 );
123 for locale in Locale::shipped() {
124 let tag = locale.tag();
125 assert!(
126 offered.contains(&tag),
127 "shipped locale {tag} is not offered in the language picker"
128 );
129 }
130 }
131
132 /// Hotkeys must be the contiguous run `1..=9` followed by contiguous
133 /// lowercase letters `a`, `b`, … so the footer hint stays truthful and
134 /// `KeyCode::Char` lookups resolve for every option.
135 #[test]
136 fn picker_hotkeys_are_contiguous_digits_then_letters() {
137 for (idx, (hotkey, tag, _, _)) in LANGUAGE_OPTIONS.iter().enumerate() {
138 let expected = if idx < 9 {
139 char::from_digit((idx + 1) as u32, 10).expect("digit")
140 } else {
141 char::from_u32('a' as u32 + (idx - 9) as u32).expect("letter")
142 };
143 assert_eq!(
144 *hotkey, expected,
145 "option {tag} should use hotkey {expected}, not {hotkey}"
146 );
147 }
148 }
149 }
150
150 lines RUST