1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2pub(crate) enum RuntimeLanguageSource {
3 Cli,
4 Config,
5 OsNative,
6 PosixEnv,
7 EnglishFallback,
8}
9
10impl RuntimeLanguageSource {
11 pub(crate) const fn as_str(self) -> &'static str {
12 match self {
13 Self::Cli => "cli",
14 Self::Config => "config",
15 Self::OsNative => "os_native",
16 Self::PosixEnv => "posix_env",
17 Self::EnglishFallback => "english_fallback",
18 }
19 }
20}
21
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub(crate) struct RuntimeLanguageResolution {
24 pub(crate) lang: String,
25 pub(crate) source: RuntimeLanguageSource,
26}
27
28pub(crate) fn normalize_runtime_lang(raw: &str) -> &'static str {
33 let lang = raw.trim().to_ascii_lowercase().replace('-', "_");
34 match lang.as_str() {
35 "chs" | "zh" | "zh_cn" | "zh_sg" | "zh_hans" | "zh_hans_cn" | "zh_hans_sg" => "chs",
36 "cht" | "zh_tw" | "zh_hk" | "zh_mo" | "zh_hant" | "zh_hant_tw" | "zh_hant_hk"
37 | "zh_hant_mo" => "cht",
38 "en" | "en_us" | "en_gb" | "en_au" | "en_ca" => "en",
39 _ => "en",
40 }
41}
42
43fn first_locale_candidate(locale: Option<&str>) -> Option<&str> {
44 locale?
45 .split(':')
46 .map(str::trim)
47 .find(|value| !value.is_empty())
48}
49
50pub(crate) fn runtime_lang_from_locale(locale: Option<&str>) -> &'static str {
51 let Some(locale) = first_locale_candidate(locale) else {
52 return "en";
53 };
54 let primary = locale
55 .split(['.', '@'])
56 .next()
57 .unwrap_or(locale)
58 .replace('-', "_")
59 .to_ascii_lowercase();
60 let mut subtags = primary.split('_');
61 let language = subtags.next().unwrap_or_default();
62 let next = subtags.next();
63 let (script, region_candidate) = match next {
64 Some(value) if value.len() == 4 && value.bytes().all(|byte| byte.is_ascii_alphabetic()) => {
65 (Some(value), subtags.next())
66 }
67 value => (None, value),
68 };
69 let region = region_candidate.filter(|value| {
70 (value.len() == 2 && value.bytes().all(|byte| byte.is_ascii_alphabetic()))
71 || (value.len() == 3 && value.bytes().all(|byte| byte.is_ascii_digit()))
72 });
73
74 match language {
75 "zh" => {
76 if script == Some("hant") || matches!(region, Some("tw" | "hk" | "mo")) {
77 "cht"
78 } else {
79 "chs"
80 }
81 }
82 "en" => "en",
83 _ => "en",
84 }
85}
86
87fn resolution(lang: &'static str, source: RuntimeLanguageSource) -> RuntimeLanguageResolution {
88 RuntimeLanguageResolution {
89 lang: lang.to_string(),
90 source,
91 }
92}
93
94pub(crate) fn resolve_runtime_language(
95 cli_lang: Option<&str>,
96 config_lang: Option<&str>,
97 os_locale: Option<&str>,
98 posix_locales: [Option<&str>; 4],
99) -> RuntimeLanguageResolution {
100 if let Some(raw) = cli_lang {
101 return resolution(normalize_runtime_lang(raw), RuntimeLanguageSource::Cli);
102 }
103 if let Some(raw) = config_lang {
104 return resolution(normalize_runtime_lang(raw), RuntimeLanguageSource::Config);
105 }
106 if let Some(raw) = first_locale_candidate(os_locale) {
107 return resolution(
108 runtime_lang_from_locale(Some(raw)),
109 RuntimeLanguageSource::OsNative,
110 );
111 }
112 for raw in posix_locales.into_iter().flatten() {
113 if let Some(raw) = first_locale_candidate(Some(raw)) {
114 return resolution(
115 runtime_lang_from_locale(Some(raw)),
116 RuntimeLanguageSource::PosixEnv,
117 );
118 }
119 }
120 resolution("en", RuntimeLanguageSource::EnglishFallback)
121}
122
123pub(super) fn unicode_env_value(value: Option<std::ffi::OsString>) -> Option<String> {
124 value
125 .and_then(|value| value.into_string().ok())
126 .map(|value| value.trim().to_string())
127 .filter(|value| !value.is_empty())
128}
129
130#[cfg(any(test, target_os = "macos", target_os = "windows"))]
131pub(super) fn normalize_native_locale(value: Option<String>) -> Option<String> {
132 value
133 .map(|value| value.trim().to_string())
134 .filter(|value| !value.is_empty())
135}
136
137#[cfg(any(test, target_os = "macos"))]
138pub(super) fn validated_native_locale_buffer_len(capacity: isize, out_len: isize) -> Option<usize> {
139 if capacity <= 0 || out_len < 0 || out_len > capacity {
140 return None;
141 }
142 usize::try_from(out_len).ok()
143}
144
145#[cfg(target_os = "macos")]
146mod macos_native_locale {
147 use super::validated_native_locale_buffer_len;
148 use std::ffi::c_void;
149
150 type CfIndex = isize;
151 type CfStringEncoding = u32;
152 type CfTypeRef = *const c_void;
153 type CfArrayRef = *const c_void;
154 type CfStringRef = *const c_void;
155
156 const CF_STRING_ENCODING_UTF8: CfStringEncoding = 0x0800_0100;
160
161 #[repr(C)]
162 #[derive(Clone, Copy)]
163 struct CfRange {
164 location: CfIndex,
165 length: CfIndex,
166 }
167
168 #[link(name = "CoreFoundation", kind = "framework")]
169 unsafe extern "C" {
170 fn CFArrayGetCount(array: CfArrayRef) -> CfIndex;
171 fn CFArrayGetValueAtIndex(array: CfArrayRef, index: CfIndex) -> *const c_void;
172 fn CFStringGetLength(string: CfStringRef) -> CfIndex;
173 fn CFStringGetBytes(
174 string: CfStringRef,
175 range: CfRange,
176 encoding: CfStringEncoding,
177 loss_byte: u8,
178 is_external_representation: u8,
179 buffer: *mut u8,
180 max_buffer_length: CfIndex,
181 used_buffer_length: *mut CfIndex,
182 ) -> CfIndex;
183 fn CFRelease(value: CfTypeRef);
184 fn CFLocaleCopyPreferredLanguages() -> CfArrayRef;
185 }
186
187 struct OwnedCfArray(CfArrayRef);
188
189 impl OwnedCfArray {
190 fn copy_preferred_languages() -> Option<Self> {
191 let raw = unsafe { CFLocaleCopyPreferredLanguages() };
193 (!raw.is_null()).then_some(Self(raw))
194 }
195 }
196
197 impl Drop for OwnedCfArray {
198 fn drop(&mut self) {
199 unsafe { CFRelease(self.0.cast()) };
202 }
203 }
204
205 pub(super) fn preferred_locale() -> Option<String> {
206 let languages = OwnedCfArray::copy_preferred_languages()?;
207 let count = unsafe { CFArrayGetCount(languages.0) };
209 if count <= 0 {
210 return None;
211 }
212
213 let locale = unsafe { CFArrayGetValueAtIndex(languages.0, 0) } as CfStringRef;
216 if locale.is_null() {
217 return None;
218 }
219 let string_length = unsafe { CFStringGetLength(locale) };
221 if string_length <= 0 {
222 return None;
223 }
224 let range = CfRange {
225 location: 0,
226 length: string_length,
227 };
228
229 let mut capacity = 0;
230 let measured_characters = unsafe {
233 CFStringGetBytes(
234 locale,
235 range,
236 CF_STRING_ENCODING_UTF8,
237 0,
238 0,
239 std::ptr::null_mut(),
240 0,
241 &mut capacity,
242 )
243 };
244 if measured_characters != string_length {
245 return None;
246 }
247 let buffer_capacity = validated_native_locale_buffer_len(capacity, capacity)?;
248 let mut buffer = Vec::new();
249 buffer.try_reserve_exact(buffer_capacity).ok()?;
250
251 let mut out_len = 0;
252 let written_characters = unsafe {
255 CFStringGetBytes(
256 locale,
257 range,
258 CF_STRING_ENCODING_UTF8,
259 0,
260 0,
261 buffer.as_mut_ptr(),
262 capacity,
263 &mut out_len,
264 )
265 };
266 if written_characters != string_length {
267 return None;
268 }
269 let initialized_len = validated_native_locale_buffer_len(capacity, out_len)?;
270 unsafe { buffer.set_len(initialized_len) };
273 String::from_utf8(buffer).ok()
274 }
275}
276
277#[cfg(target_os = "macos")]
278pub(super) fn os_preferred_locale() -> Option<String> {
279 normalize_native_locale(macos_native_locale::preferred_locale())
280}
281
282#[cfg(target_os = "windows")]
283fn os_preferred_locale() -> Option<String> {
284 normalize_native_locale(sys_locale::get_locale())
285}
286
287#[cfg(not(any(target_os = "macos", target_os = "windows")))]
288fn os_preferred_locale() -> Option<String> {
289 None
290}
291
292fn posix_locale_candidates() -> [Option<String>; 4] {
293 ["LC_ALL", "LC_MESSAGES", "LANGUAGE", "LANG"]
294 .map(|key| unicode_env_value(std::env::var_os(key)))
295}
296
297pub(crate) fn resolve_process_runtime_language(
298 cli_lang: Option<&str>,
299 config_lang: Option<&str>,
300) -> RuntimeLanguageResolution {
301 if cli_lang.is_some() || config_lang.is_some() {
302 return resolve_runtime_language(cli_lang, config_lang, None, [None, None, None, None]);
303 }
304
305 if let Some(os_locale) = os_preferred_locale() {
306 return resolve_runtime_language(
307 None,
308 None,
309 Some(os_locale.as_str()),
310 [None, None, None, None],
311 );
312 }
313
314 let posix = posix_locale_candidates();
315 resolve_runtime_language(
316 None,
317 None,
318 None,
319 [
320 posix[0].as_deref(),
321 posix[1].as_deref(),
322 posix[2].as_deref(),
323 posix[3].as_deref(),
324 ],
325 )
326}
327
328pub(crate) fn app_lang_from_runtime_lang(raw: &str) -> i32 {
329 match normalize_runtime_lang(raw) {
330 "chs" => 0,
331 "cht" => 1,
332 "en" => 2,
333 _ => 2,
334 }
335}