zino_http/i18n/
mod.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
//! Internationalization and localization.

use fluent::{bundle::FluentBundle, FluentArgs, FluentResource};
use intl_memoizer::concurrent::IntlLangMemoizer;
use std::{fs, io::ErrorKind};
use unic_langid::LanguageIdentifier;
use zino_core::{
    application::{Agent, Application},
    bail,
    error::Error,
    extension::TomlTableExt,
    state::State,
    warn, LazyLock, SharedString,
};

/// Translates the localization message.
pub fn translate(
    locale: &LanguageIdentifier,
    message: &str,
    args: Option<FluentArgs<'_>>,
) -> Result<SharedString, Error> {
    let bundle = LOCALIZATION
        .iter()
        .find_map(|(lang_id, bundle)| (lang_id == locale).then_some(bundle))
        .or_else(|| {
            let lang = locale.language;
            LOCALIZATION
                .iter()
                .find_map(|(lang_id, bundle)| (lang_id.language == lang).then_some(bundle))
        })
        .or(*DEFAULT_BUNDLE)
        .ok_or_else(|| warn!("the localization bundle does not exits"))?;
    let pattern = bundle
        .get_message(message)
        .ok_or_else(|| warn!("fail to get the localization message for `{}`", message))?
        .value()
        .ok_or_else(|| {
            warn!(
                "fail to retrieve an option of the pattern for `{}`",
                message
            )
        })?;

    let mut errors = vec![];
    if let Some(args) = args {
        let mut value = String::new();
        bundle.write_pattern(&mut value, pattern, Some(&args), &mut errors)?;
        if errors.is_empty() {
            Ok(value.into())
        } else {
            bail!("{:?}", errors);
        }
    } else {
        let value = bundle.format_pattern(pattern, None, &mut errors);
        if errors.is_empty() {
            Ok(value)
        } else {
            bail!("{:?}", errors);
        }
    }
}

/// Translation type.
type Translation = FluentBundle<FluentResource, IntlLangMemoizer>;

/// Localization.
static LOCALIZATION: LazyLock<Vec<(LanguageIdentifier, Translation)>> = LazyLock::new(|| {
    let mut locales = Vec::new();
    let locale_dir = Agent::config_dir().join("locale");
    match fs::read_dir(locale_dir) {
        Ok(entries) => {
            let files = entries.filter_map(|entry| entry.ok());
            for file in files {
                let locale_file = file.path();
                let ftl_string = fs::read_to_string(&locale_file).unwrap_or_else(|err| {
                    let locale_file = locale_file.display();
                    panic!("fail to read `{locale_file}`: {err}");
                });
                let resource =
                    FluentResource::try_new(ftl_string).expect("fail to parse an FTL string");
                if let Some(locale) = file
                    .file_name()
                    .to_str()
                    .map(|s| s.trim_end_matches(".ftl"))
                {
                    let lang = locale
                        .parse::<LanguageIdentifier>()
                        .unwrap_or_else(|_| panic!("fail to language identifier `{locale}`"));

                    let mut bundle = FluentBundle::new_concurrent(vec![lang.clone()]);
                    bundle.set_use_isolating(false);
                    bundle
                        .add_resource(resource)
                        .expect("fail to add FTL resources to the bundle");
                    locales.push((lang, bundle));
                }
            }
        }
        Err(err) => {
            if err.kind() != ErrorKind::NotFound {
                tracing::error!("{err}");
            }
        }
    }
    locales
});

/// Default bundle.
static DEFAULT_BUNDLE: LazyLock<Option<&'static Translation>> = LazyLock::new(|| {
    let default_locale = LazyLock::force(&DEFAULT_LOCALE);
    LOCALIZATION
        .iter()
        .find_map(|(lang_id, bundle)| (lang_id == default_locale).then_some(bundle))
});

/// Default locale.
pub(crate) static DEFAULT_LOCALE: LazyLock<&'static str> = LazyLock::new(|| {
    if let Some(i18n) = State::shared().get_config("i18n") {
        i18n.get_str("default-locale").unwrap_or("en-US")
    } else {
        "en-US"
    }
});

/// Supported locales.
pub(crate) static SUPPORTED_LOCALES: LazyLock<Vec<&'static str>> = LazyLock::new(|| {
    LOCALIZATION
        .iter()
        .map(|(key, _)| {
            let language: &'static str = key.to_string().leak();
            language
        })
        .collect::<Vec<_>>()
});