atomcode_core/i18n/
mod.rs1mod en;
2mod messages;
3mod zh_cn;
4
5pub use crate::locale::Locale;
6pub use messages::Msg;
7
8use std::borrow::Cow;
9use std::sync::RwLock;
10
11static LOCALE: RwLock<Locale> = RwLock::new(Locale::En);
12
13pub fn t(msg: Msg<'_>) -> Cow<'static, str> {
18 t_with(current_locale(), msg)
19}
20
21pub fn t_with(locale: Locale, msg: Msg<'_>) -> Cow<'static, str> {
23 match locale {
24 Locale::En => en::en(msg),
25 Locale::ZhCn => zh_cn::zh_cn(msg),
26 }
27}
28
29pub fn current_locale() -> Locale {
32 LOCALE.read().map(|g| *g).unwrap_or(Locale::En)
33}
34
35pub fn set_locale(locale: Locale) {
38 if let Ok(mut g) = LOCALE.write() {
39 *g = locale;
40 }
41}
42
43pub fn resolve_initial_locale(
47 cli_lang: Option<&str>,
48 config_lang: Option<Locale>,
49) -> Locale {
50 resolve_initial_locale_with_env(cli_lang, config_lang, &|k| std::env::var(k).ok())
51}
52
53#[doc(hidden)]
54pub fn resolve_initial_locale_with_env(
55 cli_lang: Option<&str>,
56 config_lang: Option<Locale>,
57 env: &dyn Fn(&str) -> Option<String>,
58) -> Locale {
59 if let Some(s) = cli_lang {
60 if let Ok(loc) = s.parse::<Locale>() {
61 return loc;
62 }
63 }
64 if let Some(loc) = config_lang {
65 return loc;
66 }
67 for key in ["LC_ALL", "LC_MESSAGES", "LANG"] {
68 if let Some(val) = env(key) {
69 if !val.is_empty() {
70 return classify_env_locale(&val);
71 }
72 }
73 }
74 Locale::En
75}
76
77fn classify_env_locale(value: &str) -> Locale {
78 let lower = value.to_ascii_lowercase();
79 if lower == "zh"
82 || lower.starts_with("zh_")
83 || lower.starts_with("zh-")
84 || lower.starts_with("zh.")
85 {
86 Locale::ZhCn
87 } else {
88 Locale::En
89 }
90}
91
92pub fn test_lock() -> LocaleTestGuard {
113 use std::sync::{Mutex, OnceLock};
114 static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
115 let guard = LOCK
122 .get_or_init(|| Mutex::new(()))
123 .lock()
124 .unwrap_or_else(|e| e.into_inner());
125 let original = current_locale();
126 LocaleTestGuard {
127 original,
128 _guard: guard,
129 }
130}
131
132pub struct LocaleTestGuard {
139 original: Locale,
140 _guard: std::sync::MutexGuard<'static, ()>,
141}
142
143impl Drop for LocaleTestGuard {
144 fn drop(&mut self) {
145 set_locale(self.original);
146 }
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152
153 #[test]
154 fn t_with_returns_english_for_en() {
155 let s = t_with(Locale::En, Msg::WelcomeBannerLine1);
156 assert!(s.starts_with("Welcome to AtomCode"));
157 }
158
159 #[test]
160 fn t_with_returns_chinese_for_zh_cn() {
161 let s = t_with(Locale::ZhCn, Msg::WelcomeBannerLine1);
162 assert!(s.starts_with("欢迎使用 AtomCode"));
163 }
164
165 #[test]
166 fn set_locale_flips_global() {
167 let _g = test_lock();
168 set_locale(Locale::ZhCn);
169 assert_eq!(current_locale(), Locale::ZhCn);
170 let s = t(Msg::WelcomeBannerLine1);
171 assert!(s.starts_with("欢迎使用"));
172
173 set_locale(Locale::En);
174 assert_eq!(current_locale(), Locale::En);
175 let s = t(Msg::WelcomeBannerLine1);
176 assert!(s.starts_with("Welcome to AtomCode"));
177 }
178
179 #[test]
180 fn err_unsupported_locale_includes_input() {
181 let s = t_with(Locale::En, Msg::ErrUnsupportedLocale { input: "fr" });
182 assert!(s.contains("fr"));
183 let s = t_with(Locale::ZhCn, Msg::ErrUnsupportedLocale { input: "fr" });
184 assert!(s.contains("fr"));
185 }
186
187 #[test]
188 fn cli_flag_wins_over_everything() {
189 let env = |_: &str| Some("zh_CN.UTF-8".to_string());
190 assert_eq!(
191 resolve_initial_locale_with_env(Some("en"), Some(Locale::ZhCn), &env),
192 Locale::En
193 );
194 }
195
196 #[test]
197 fn config_beats_env() {
198 let env = |_: &str| Some("zh_CN.UTF-8".to_string());
199 assert_eq!(
200 resolve_initial_locale_with_env(None, Some(Locale::En), &env),
201 Locale::En
202 );
203 }
204
205 #[test]
206 fn env_zh_cn_resolves_to_zh_cn() {
207 let env =
208 |k: &str| if k == "LANG" { Some("zh_CN.UTF-8".into()) } else { None };
209 assert_eq!(
210 resolve_initial_locale_with_env(None, None, &env),
211 Locale::ZhCn
212 );
213 }
214
215 #[test]
216 fn env_zh_tw_maps_to_zh_cn() {
217 let env = |k: &str| if k == "LANG" { Some("zh_TW".into()) } else { None };
218 assert_eq!(
219 resolve_initial_locale_with_env(None, None, &env),
220 Locale::ZhCn
221 );
222 }
223
224 #[test]
225 fn env_c_or_english_resolves_to_en() {
226 let mk = |val: &'static str| {
227 move |k: &str| {
228 if k == "LANG" {
229 Some(val.to_string())
230 } else {
231 None
232 }
233 }
234 };
235 assert_eq!(
236 resolve_initial_locale_with_env(None, None, &mk("C")),
237 Locale::En
238 );
239 assert_eq!(
240 resolve_initial_locale_with_env(None, None, &mk("en_US.UTF-8")),
241 Locale::En
242 );
243 assert_eq!(
244 resolve_initial_locale_with_env(None, None, &mk("")),
245 Locale::En
246 );
247 }
248
249 #[test]
250 fn env_no_locale_vars_resolves_to_en() {
251 let env = |_: &str| None;
252 assert_eq!(
253 resolve_initial_locale_with_env(None, None, &env),
254 Locale::En
255 );
256 }
257
258 #[test]
259 fn lc_all_overrides_lc_messages_and_lang() {
260 let env = |k: &str| match k {
261 "LC_ALL" => Some("zh_CN.UTF-8".into()),
262 "LANG" => Some("en_US.UTF-8".into()),
263 _ => None,
264 };
265 assert_eq!(
266 resolve_initial_locale_with_env(None, None, &env),
267 Locale::ZhCn
268 );
269 }
270
271 #[test]
272 fn lc_messages_overrides_lang() {
273 let env = |k: &str| match k {
274 "LC_MESSAGES" => Some("zh_CN.UTF-8".into()),
275 "LANG" => Some("en_US.UTF-8".into()),
276 _ => None,
277 };
278 assert_eq!(
279 resolve_initial_locale_with_env(None, None, &env),
280 Locale::ZhCn
281 );
282 }
283
284 #[test]
285 fn cli_flag_unparseable_falls_through() {
286 let env = |_: &str| None;
287 assert_eq!(
288 resolve_initial_locale_with_env(Some("fr"), Some(Locale::ZhCn), &env),
289 Locale::ZhCn
290 );
291 assert_eq!(
292 resolve_initial_locale_with_env(Some("fr"), None, &env),
293 Locale::En
294 );
295 }
296}