1#![forbid(unsafe_code)]
48
49pub mod catalog;
50pub mod format;
51pub mod locale;
52pub mod parser;
53pub mod plural;
54pub mod regional;
55mod system;
56
57pub use catalog::TranslationCatalog;
59#[cfg(feature = "number")]
60pub use format::LocalizedArgument;
61pub use format::LocalizedDisplay;
62#[cfg(feature = "list")]
63pub use format::LocalizedList;
64#[cfg(feature = "number")]
65pub use format::number::{Currency, format_currency, format_number, format_percent};
66#[cfg(feature = "number")]
67pub use format::unit::{Feet, Kilometer, Length, LengthUnit, Meter, Mile};
68pub use locale::{Locale, locales};
69pub use plural::{PluralCategory, select_plural, valid_categories};
70
71#[must_use]
73pub fn layout_direction(locale: &Locale) -> waterui_core::layout::LayoutDirection {
74 if icu_locale::LocaleDirectionality::new_extended().is_right_to_left(locale.id()) {
75 waterui_core::layout::LayoutDirection::RightToLeft
76 } else {
77 waterui_core::layout::LayoutDirection::LeftToRight
78 }
79}
80
81#[must_use]
83pub fn layout_direction_computed(
84 env: &waterui_core::Environment,
85) -> nami::Computed<waterui_core::layout::LayoutDirection> {
86 use nami::SignalExt;
87
88 locale_binding(env)
89 .map(|locale| layout_direction(&locale))
90 .computed()
91}
92
93pub fn start_system_locale_listener() {
99 system::start_system_listener();
100}
101
102#[doc(hidden)]
103pub fn shutdown_current_thread_runtime_locale_state() {
105 system::shutdown_current_thread_runtime_locale_state();
106}
107
108#[must_use]
110pub fn locale_binding(env: &waterui_core::Environment) -> nami::Binding<Locale> {
111 if let Some(locale) = env.get::<nami::Binding<Locale>>() {
112 return locale.clone();
113 }
114
115 if let Some(context) = env.get::<regional::RegionalContext>().cloned() {
116 return nami::Binding::custom(nami::Container::new(context.locale().clone()));
117 }
118
119 if let Some(locale) = env.get::<Locale>().cloned() {
120 return nami::Binding::custom(nami::Container::new(locale));
121 }
122
123 system::runtime_locale_binding()
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129 use format::date::{DateStyle, SimpleDate, format_date};
130 use format::unit::{Gram, Kilogram, Mass};
131 use waterui_core::Environment;
132
133 #[test]
138 fn locale_binding_uses_static_locale_from_environment() {
139 let mut env = Environment::new();
140 env.insert(locales::EN_GB);
141
142 let locale = locale_binding(&env).get();
143 assert_eq!(locale.language.as_str(), "en");
144 assert_eq!(
145 locale
146 .region
147 .as_ref()
148 .map(icu_locale::subtags::Region::as_str),
149 Some("GB")
150 );
151 }
152
153 #[test]
154 fn locale_binding_prefers_an_explicit_binding_over_a_static_locale() {
155 let binding = nami::Binding::container(locales::ZH_CN);
156 let mut env = Environment::new();
157 env.insert(locales::EN_GB);
158 env.insert(binding.clone());
159
160 let resolved = locale_binding(&env);
161 assert_eq!(resolved.get().language.as_str(), "zh");
162
163 binding.set(locales::EN_US);
165 assert_eq!(resolved.get().language.as_str(), "en");
166 }
167
168 #[test]
169 fn locale_direction_uses_unicode_script_data() {
170 assert_eq!(
171 layout_direction(&locales::EN),
172 waterui_core::layout::LayoutDirection::LeftToRight
173 );
174 assert_eq!(
175 layout_direction(&locales::AR),
176 waterui_core::layout::LayoutDirection::RightToLeft
177 );
178 let hebrew = "he".parse().expect("Hebrew locale must parse");
179 assert_eq!(
180 layout_direction(&hebrew),
181 waterui_core::layout::LayoutDirection::RightToLeft
182 );
183 }
184
185 #[test]
190 fn test_plural_selection_english() {
191 crate::system::reset_runtime_locale_state_for_tests();
192 assert_eq!(select_plural(&locales::EN, &1), PluralCategory::One);
194 assert_eq!(select_plural(&locales::EN, &0), PluralCategory::Other);
195 assert_eq!(select_plural(&locales::EN, &2), PluralCategory::Other);
196 assert_eq!(select_plural(&locales::EN, &100), PluralCategory::Other);
197 }
198
199 #[test]
200 fn test_plural_selection_english_fractional() {
201 assert_eq!(select_plural(&locales::EN, &1.2), PluralCategory::Other);
203 assert_eq!(select_plural(&locales::EN, &0.5), PluralCategory::Other);
204 }
205
206 #[test]
207 fn test_plural_selection_chinese() {
208 assert_eq!(select_plural(&locales::ZH_CN, &0), PluralCategory::Other);
210 assert_eq!(select_plural(&locales::ZH_CN, &1), PluralCategory::Other);
211 assert_eq!(select_plural(&locales::ZH_CN, &100), PluralCategory::Other);
212 }
213
214 #[test]
215 fn test_plural_selection_japanese() {
216 assert_eq!(select_plural(&locales::JA, &0), PluralCategory::Other);
218 assert_eq!(select_plural(&locales::JA, &1), PluralCategory::Other);
219 assert_eq!(select_plural(&locales::JA, &100), PluralCategory::Other);
220 }
221
222 #[test]
223 fn test_plural_selection_korean() {
224 assert_eq!(select_plural(&locales::KO, &0), PluralCategory::Other);
226 assert_eq!(select_plural(&locales::KO, &1), PluralCategory::Other);
227 assert_eq!(select_plural(&locales::KO, &100), PluralCategory::Other);
228 }
229
230 #[test]
231 fn test_plural_selection_german() {
232 assert_eq!(select_plural(&locales::DE, &1), PluralCategory::One);
234 assert_eq!(select_plural(&locales::DE, &0), PluralCategory::Other);
235 assert_eq!(select_plural(&locales::DE, &2), PluralCategory::Other);
236 }
237
238 #[test]
239 fn test_plural_selection_french() {
240 assert_eq!(select_plural(&locales::FR, &0), PluralCategory::One);
242 assert_eq!(select_plural(&locales::FR, &1), PluralCategory::One);
243 assert_eq!(select_plural(&locales::FR, &2), PluralCategory::Other);
244 }
245
246 #[test]
247 fn test_plural_selection_spanish() {
248 assert_eq!(select_plural(&locales::ES, &1), PluralCategory::One);
250 assert_eq!(select_plural(&locales::ES, &0), PluralCategory::Other);
251 assert_eq!(select_plural(&locales::ES, &2), PluralCategory::Other);
252 }
253
254 #[test]
255 fn test_plural_selection_russian() {
256 assert_eq!(select_plural(&locales::RU, &1), PluralCategory::One);
258 assert_eq!(select_plural(&locales::RU, &2), PluralCategory::Few);
259 assert_eq!(select_plural(&locales::RU, &5), PluralCategory::Many);
260 assert_eq!(select_plural(&locales::RU, &21), PluralCategory::One);
261 assert_eq!(select_plural(&locales::RU, &22), PluralCategory::Few);
262 }
263
264 #[test]
265 fn test_plural_selection_negative_uses_absolute_value() {
266 assert_eq!(select_plural(&locales::EN, &-1), PluralCategory::One);
268 assert_eq!(select_plural(&locales::EN, &-2), PluralCategory::Other);
269 }
270
271 #[test]
276 fn test_length_conversion() {
277 let km = Length::<Kilometer>::new(1.0);
278 let m = km.to::<Meter>();
279 assert!((m.value() - 1000.0).abs() < 0.001);
280
281 let mile = Length::<Mile>::new(1.0);
282 let m2 = mile.to::<Meter>();
283 assert!((m2.value() - 1609.344).abs() < 0.001);
284 }
285
286 #[test]
287 fn test_length_addition() {
288 let km = Length::<Kilometer>::new(1.0);
289 let m = Length::<Meter>::new(500.0);
290 let total = km + m;
291 assert!((total.value() - 1.5).abs() < 0.001);
292 }
293
294 #[test]
295 fn test_localized_display_length() {
296 let distance = Length::<Meter>::new(100.0);
297
298 let en_str = distance.to_localized_string(&locales::EN);
300 assert!(en_str.contains("100"));
301 assert!(en_str.contains('m'));
302
303 let zh_str = distance.to_localized_string(&locales::ZH_CN);
305 assert!(zh_str.contains("100"));
306 assert!(zh_str.contains("米"));
307
308 let ja_str = distance.to_localized_string(&locales::JA);
310 assert!(ja_str.contains("100"));
311 assert!(ja_str.contains("メートル"));
312
313 let ko_str = distance.to_localized_string(&locales::KO);
315 assert!(ko_str.contains("100"));
316 assert!(ko_str.contains("미터"));
317 }
318
319 #[test]
324 fn test_mass_localized_display() {
325 let mass = Mass::<Kilogram>::new(5.0);
326
327 let zh_str = mass.to_localized_string(&locales::ZH_CN);
329 assert!(zh_str.contains('5'));
330 assert!(zh_str.contains("公斤"));
331
332 let ja_str = mass.to_localized_string(&locales::JA);
334 assert!(ja_str.contains('5'));
335 assert!(ja_str.contains("キログラム"));
336
337 let ko_str = mass.to_localized_string(&locales::KO);
339 assert!(ko_str.contains('5'));
340 assert!(ko_str.contains("킬로그램"));
341
342 let de_str = mass.to_localized_string(&locales::DE);
344 assert!(de_str.contains('5'));
345 assert!(de_str.contains("kg"));
346 }
347
348 #[test]
349 fn test_mass_conversion() {
350 let kg = Mass::<Kilogram>::new(1.0);
351 let g = kg.to::<Gram>();
352 assert!((g.value() - 1000.0).abs() < 0.001);
353 }
354
355 #[test]
360 fn test_date_formatting_english() {
361 let date = SimpleDate::new(2006, 3, 20);
362 let short = format_date(&locales::EN, &date, DateStyle::Short);
363 let long = format_date(&locales::EN, &date, DateStyle::Long);
364 assert!(short.contains('/'));
365 assert!(short.contains("20"));
366 assert!(long.contains("2006"));
367 assert!(long.to_lowercase().contains("mar"));
368 }
369
370 #[test]
371 fn test_date_formatting_german() {
372 let date = SimpleDate::new(2006, 3, 20);
373 let short = format_date(&locales::DE, &date, DateStyle::Short);
374 let long = format_date(&locales::DE, &date, DateStyle::Long);
375 assert!(short.contains('.'));
376 assert!(short.contains("20"));
377 assert!(long.contains("2006"));
378 assert!(long.contains("Mär") || long.to_lowercase().contains("mar"));
379 }
380
381 #[test]
382 fn test_date_formatting_french() {
383 let date = SimpleDate::new(2006, 3, 20);
384 let short = format_date(&locales::FR, &date, DateStyle::Short);
385 let long = format_date(&locales::FR, &date, DateStyle::Long);
386 assert!(short.contains("2006"));
387 assert!(short.contains('/'));
388 assert!(long.contains("2006"));
389 assert!(long.to_lowercase().contains("mar"));
390 }
391
392 #[test]
393 fn test_date_formatting_spanish() {
394 let date = SimpleDate::new(2006, 3, 20);
395 let short = format_date(&locales::ES, &date, DateStyle::Short);
396 let long = format_date(&locales::ES, &date, DateStyle::Long);
397 assert!(short.contains('/'));
398 assert!(short.contains("20"));
399 assert!(long.contains("2006"));
400 assert!(long.to_lowercase().contains("mar"));
401 }
402
403 #[test]
404 fn test_date_formatting_japanese() {
405 let date = SimpleDate::new(2006, 3, 20);
406 let short = format_date(&locales::JA, &date, DateStyle::Short);
407 let long = format_date(&locales::JA, &date, DateStyle::Long);
408 assert!(short.contains("2006"));
409 assert!(long.contains('年'));
410 assert!(long.contains('月'));
411 assert!(long.contains('日'));
412 }
413
414 #[test]
415 fn test_date_formatting_korean() {
416 let date = SimpleDate::new(2006, 3, 20);
417 let short = format_date(&locales::KO, &date, DateStyle::Short);
418 let long = format_date(&locales::KO, &date, DateStyle::Long);
419 assert!(short.contains("20"));
420 assert!(long.contains('년'));
421 assert!(long.contains('월'));
422 assert!(long.contains('일'));
423 }
424
425 #[test]
426 fn test_date_formatting_chinese() {
427 let date = SimpleDate::new(2006, 3, 20);
428 let short = format_date(&locales::ZH_CN, &date, DateStyle::Short);
429 let long = format_date(&locales::ZH_CN, &date, DateStyle::Long);
430 assert!(short.contains("2006"));
431 assert!(long.contains('年'));
432 assert!(long.contains('月'));
433 assert!(long.contains('日'));
434 }
435
436 #[test]
441 fn test_translation_file_parsing() {
442 use parser::{TranslationFile, TranslationValue};
443
444 let toml_content = r#"
445"Hello, World!" = "Hello, World!"
446"Goodbye" = "Farewell"
447"I have {#count} apple" = { one = "I have {count} apple", other = "I have {count} apples" }
448"#;
449
450 let file = TranslationFile::parse(toml_content).unwrap();
451 assert_eq!(file.len(), 3);
452
453 match file.get("Goodbye") {
455 Some(TranslationValue::Simple(s)) => assert_eq!(s, "Farewell"),
456 _ => panic!("Expected simple translation"),
457 }
458
459 match file.get("I have {#count} apple") {
461 Some(TranslationValue::Plural(forms)) => {
462 assert_eq!(forms.get(PluralCategory::One), "I have {count} apple");
463 assert_eq!(forms.get(PluralCategory::Other), "I have {count} apples");
464 }
465 _ => panic!("Expected plural translation"),
466 }
467 }
468
469 #[test]
470 fn test_valid_categories() {
471 let en_cats = plural::valid_categories(&locales::EN);
472 assert!(en_cats.contains(&PluralCategory::One));
473 assert!(en_cats.contains(&PluralCategory::Other));
474
475 let zh_cats = plural::valid_categories(&locales::ZH_CN);
476 assert!(zh_cats.contains(&PluralCategory::Other));
478 assert!(!zh_cats.contains(&PluralCategory::One));
479 }
480}