1use fluent::{FluentArgs, FluentResource, bundle::FluentBundle};
4use intl_memoizer::concurrent::IntlLangMemoizer;
5use std::{fs, io::ErrorKind};
6use unic_langid::LanguageIdentifier;
7use zino_core::{
8 LazyLock, SharedString,
9 application::{Agent, Application},
10 bail,
11 error::Error,
12 extension::TomlTableExt,
13 state::State,
14 warn,
15};
16
17pub fn translate(
19 locale: &LanguageIdentifier,
20 message: &str,
21 args: Option<FluentArgs<'_>>,
22) -> Result<SharedString, Error> {
23 let bundle = LOCALIZATION
24 .iter()
25 .find_map(|(lang_id, bundle)| (lang_id == locale).then_some(bundle))
26 .or_else(|| {
27 let lang = locale.language;
28 LOCALIZATION
29 .iter()
30 .find_map(|(lang_id, bundle)| (lang_id.language == lang).then_some(bundle))
31 })
32 .or(*DEFAULT_BUNDLE)
33 .ok_or_else(|| warn!("localization bundle does not exits"))?;
34 let pattern = bundle
35 .get_message(message)
36 .ok_or_else(|| warn!("fail to get the localization message for `{}`", message))?
37 .value()
38 .ok_or_else(|| {
39 warn!(
40 "fail to retrieve an option of the pattern for `{}`",
41 message
42 )
43 })?;
44
45 let mut errors = vec![];
46 if let Some(args) = args {
47 let mut value = String::new();
48 bundle.write_pattern(&mut value, pattern, Some(&args), &mut errors)?;
49 if errors.is_empty() {
50 Ok(value.into())
51 } else {
52 bail!("{:?}", errors);
53 }
54 } else {
55 let value = bundle.format_pattern(pattern, None, &mut errors);
56 if errors.is_empty() {
57 Ok(value)
58 } else {
59 bail!("{:?}", errors);
60 }
61 }
62}
63
64type Translation = FluentBundle<FluentResource, IntlLangMemoizer>;
66
67static LOCALIZATION: LazyLock<Vec<(LanguageIdentifier, Translation)>> = LazyLock::new(|| {
69 let mut locales = Vec::new();
70 let locale_dir = Agent::config_dir().join("locale");
71 match fs::read_dir(locale_dir) {
72 Ok(entries) => {
73 let files = entries.filter_map(|entry| entry.ok());
74 for file in files {
75 let locale_file = file.path();
76 let ftl_string = fs::read_to_string(&locale_file).unwrap_or_else(|err| {
77 let locale_file = locale_file.display();
78 panic!("fail to read `{locale_file}`: {err}");
79 });
80 let resource =
81 FluentResource::try_new(ftl_string).expect("fail to parse an FTL string");
82 if let Some(locale) = file
83 .file_name()
84 .to_str()
85 .map(|s| s.trim_end_matches(".ftl"))
86 {
87 let lang = locale
88 .parse::<LanguageIdentifier>()
89 .unwrap_or_else(|_| panic!("fail to language identifier `{locale}`"));
90
91 let mut bundle = FluentBundle::new_concurrent(vec![lang.clone()]);
92 bundle.set_use_isolating(false);
93 bundle
94 .add_resource(resource)
95 .expect("fail to add FTL resources to the bundle");
96 locales.push((lang, bundle));
97 }
98 }
99 }
100 Err(err) => {
101 if err.kind() != ErrorKind::NotFound {
102 tracing::error!("{err}");
103 }
104 }
105 }
106 locales
107});
108
109static DEFAULT_BUNDLE: LazyLock<Option<&'static Translation>> = LazyLock::new(|| {
111 let default_locale = LazyLock::force(&DEFAULT_LOCALE);
112 LOCALIZATION
113 .iter()
114 .find_map(|(lang_id, bundle)| (lang_id == default_locale).then_some(bundle))
115});
116
117pub(crate) static DEFAULT_LOCALE: LazyLock<&'static str> = LazyLock::new(|| {
119 if let Some(i18n) = State::shared().get_config("i18n") {
120 i18n.get_str("default-locale").unwrap_or("en-US")
121 } else {
122 "en-US"
123 }
124});
125
126pub(crate) static SUPPORTED_LOCALES: LazyLock<Vec<&'static str>> = LazyLock::new(|| {
128 LOCALIZATION
129 .iter()
130 .map(|(key, _)| {
131 let language: &'static str = key.to_string().leak();
132 language
133 })
134 .collect::<Vec<_>>()
135});