tina-core 0.0.2

Tina platform
Documentation
//! 国际化
pub mod message;

use std::borrow::Cow;
use std::hash::{Hash, Hasher};
use std::sync::{Arc, RwLock};

use crate::tina::data::AppResult;
use crate::tina::util::not_empty::INotEmpty;
use crate::tina::util::string_joiner::StringJoiner;
use crate::{app_error_from_none_static, app_system_error};
use fluent_bundle::bundle::FluentBundle;
use fluent_bundle::{FluentArgs, FluentResource};
use intl_memoizer::concurrent::IntlLangMemoizer;

use indexmap::IndexMap;
use once_cell::sync::Lazy;
use std::ops::Deref;
use std::path::Path;
use unic_langid::LanguageIdentifier;
use yaml_rust::{Yaml, YamlLoader};

#[derive(RustEmbed)]
#[folder = "$CARGO_MANIFEST_DIR/resources/i18n/"]
struct SystemMessageAsset;

static GLOBAL_RESOURCE_BUNDLES: Lazy<ResourceBundleContainer> = Lazy::new(|| {
    let bundle = ResourceBundleContainer::new();
    if let Err(err) = bundle.add_embedded_yaml_dir::<SystemMessageAsset>("system_message") {
        panic!("{}", err);
    }
    bundle
});

static GLOBAL_DEFAULT_LOCALE: Lazy<Arc<RwLock<LocaleKey>>> = Lazy::new(|| Arc::new(RwLock::new(LocaleKey("zh-CN".to_string()))));

/// 国际化的Key
pub struct LocaleKey(pub String);

impl Hash for LocaleKey {
    fn hash<H: Hasher>(&self, state: &mut H) {
        self.0.replace('-', "_").to_lowercase().hash(state)
    }
}

impl PartialEq for LocaleKey {
    fn eq(&self, other: &Self) -> bool {
        self.0.replace('-', "_").to_lowercase() == other.0.replace('-', "_").to_lowercase()
    }
}

impl Eq for LocaleKey {}

/// 国际化资源容器
pub(crate) struct ResourceBundleContainer(Arc<RwLock<IndexMap<LocaleKey, FluentBundle<FluentResource, IntlLangMemoizer>>>>);
impl ResourceBundleContainer {
    /// 初始化
    pub fn new() -> ResourceBundleContainer {
        ResourceBundleContainer(Arc::new(RwLock::new(IndexMap::new())))
    }
    /// 添加国际化资源
    pub fn add<'a>(&self, locale: &str, resource: IndexMap<&'a str, &'a str>) -> AppResult<()> {
        let langid_en: LanguageIdentifier = match locale.parse() {
            Ok(l) => l,
            Err(err) => return Err(app_system_error!("parse locale faield: {}", err)),
        };
        let locale = LocaleKey(locale.to_string());
        let resources = ResourceBundle::init_resource(resource);
        let mut lock = self.0.write().map_err(app_error_from_none_static!())?;
        match lock.get_mut(&locale) {
            None => {
                let mut bundle = FluentBundle::new_concurrent(vec![langid_en]);
                bundle.set_use_isolating(false);
                for resource in resources.into_iter() {
                    bundle.add_resource_overriding(resource);
                }
                lock.insert(locale, bundle);
            }
            Some(bundle) => {
                for resource in resources.into_iter() {
                    bundle.add_resource_overriding(resource);
                }
            }
        }
        Ok(())
    }
    /// 加载内嵌的国际化资源目录
    pub fn add_embedded_yaml_dir<Dir: rust_embed::RustEmbed>(&self, prefix: &str) -> AppResult<()> {
        for file_path in Dir::iter() {
            let path = Path::new(file_path.as_ref());
            let name = path.file_name().expect("get file name failed").to_string_lossy();
            let name = name.as_ref();
            let lower_name = name.to_lowercase();
            if name.starts_with(prefix) && (lower_name.ends_with(".yaml") || lower_name.ends_with(".yml")) {
                let content = Dir::get(file_path.as_ref())
                    .ok_or_else(|| app_system_error!("read file content as yaml failed: {}", path.to_string_lossy()))?;
                let yamls = YamlLoader::load_from_str(String::from_utf8_lossy(content.data.as_ref()).as_ref())
                    .map_err(|_| app_system_error!("read file content as yaml failed: {}", path.to_string_lossy()))?;
                for yaml in yamls.iter() {
                    if let Some(hash) = yaml.as_hash() {
                        for (key, value) in hash.iter() {
                            if let Some(locale) = key.as_str() {
                                if locale.not_empty() {
                                    let value = self.analyze_yaml(value)?;
                                    let value: IndexMap<&str, &str> = value.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect();
                                    self.add(locale, value)?;
                                }
                            }
                        }
                    }
                }
            }
        }
        Ok(())
    }
    fn analyze_yaml(&self, yaml: &Yaml) -> AppResult<IndexMap<String, String>> {
        let prefix = "";
        let mut map: IndexMap<String, String> = IndexMap::new();
        add_to_map(yaml, prefix, &mut map)?;
        Ok(map)
    }
}

fn add_to_map(yaml: &Yaml, prefix: &str, map: &mut IndexMap<String, String>) -> AppResult<()> {
    let key = prefix.to_string();
    let value = yaml;
    match value {
        Yaml::Real(v) => {
            if key.not_empty() {
                map.insert(key, v.to_string());
            }
        }
        Yaml::Integer(v) => {
            if key.not_empty() {
                map.insert(key, v.to_string());
            }
        }
        Yaml::String(v) => {
            if key.not_empty() {
                map.insert(key, v.to_string());
            }
        }
        Yaml::Boolean(v) => {
            if key.not_empty() {
                map.insert(key, v.to_string());
            }
        }
        Yaml::Array(v) => {
            if !key.not_empty() {
                return Ok(());
            }
            for (idx, item) in v.iter().enumerate() {
                add_to_map(item, format!("{key}[{idx}]").as_str(), map)?;
            }
        }
        Yaml::Hash(h) => {
            for (k, v) in h.iter() {
                let k = match k {
                    Yaml::Real(k) => k.to_string(),
                    Yaml::Integer(k) => k.to_string(),
                    Yaml::String(k) => k.to_string(),
                    Yaml::Boolean(k) => k.to_string(),
                    Yaml::Array(k) => {
                        return Err(app_system_error!("unsupported yaml key: {:?}", k));
                    }
                    Yaml::Hash(k) => {
                        return Err(app_system_error!("unsupported yaml key: {:?}", k));
                    }
                    Yaml::Alias(k) => k.to_string(),
                    Yaml::Null => {
                        return Err(app_system_error!("unsupported yaml key: {:?}", k));
                    }
                    Yaml::BadValue => {
                        return Err(app_system_error!("unsupported yaml key: {:?}", k));
                    }
                };
                if !k.not_empty() {
                    continue;
                }
                let mut sj2 = StringJoiner::new(None, None, Some("."));
                sj2.push_str(key.as_str());
                sj2.push_string(k);
                let k = sj2.to_string();
                add_to_map(v, k.as_str(), map)?;
            }
        }
        Yaml::Alias(v) => {
            if key.not_empty() {
                map.insert(key, v.to_string());
            }
        }
        Yaml::Null => {
            if key.not_empty() {
                map.insert(key, "".to_string());
            }
        }
        Yaml::BadValue => {
            if key.not_empty() {
                map.insert(key, "".to_string());
            }
        }
    }
    Ok(())
}

impl Deref for ResourceBundleContainer {
    type Target = Arc<RwLock<IndexMap<LocaleKey, FluentBundle<FluentResource, IntlLangMemoizer>>>>;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

/// 国际化工具
pub struct ResourceBundle;

impl ResourceBundle {
    /// 获取系统默认的locale
    pub fn get_default_locale() -> String {
        match GLOBAL_DEFAULT_LOCALE.read() {
            Ok(l) => l.0.to_string(),
            Err(err) => {
                tracing::error!("{}", err);
                "zh-CN".to_string()
            }
        }
    }

    /// 添加国际化资源
    pub fn add_bundle<'a>(locale: &str, map: IndexMap<&'a str, &'a str>) -> AppResult<()> {
        GLOBAL_RESOURCE_BUNDLES.add(locale, map)
    }

    /// 加载内嵌的国际化资源目录
    pub fn add_embedded_yaml_dir<Dir: rust_embed::RustEmbed>(prefix: &str) -> AppResult<()> {
        GLOBAL_RESOURCE_BUNDLES.add_embedded_yaml_dir::<Dir>(prefix)
    }

    pub(crate) fn init_resource<'a>(map: IndexMap<&'a str, &'a str>) -> Vec<FluentResource> {
        let resources: Vec<FluentResource> = map
            .iter()
            .map(|(key, value)| {
                let mut str = key.replace('.', "-");
                str.push_str(" = ");
                str.push_str(value.to_owned());
                match FluentResource::try_new(str.to_string()) {
                    Ok(v) => v,
                    Err((_, err)) => {
                        panic!("Failed to parse i18n resources: {}. {:?}", str, err);
                    }
                }
            })
            .collect();
        resources
    }

    /// 读取国际化语言翻译
    pub fn get_string(locale: &str, key: &str) -> String {
        Self::get_string_by_key_args(locale, key, vec![])
    }

    /// 读取国际化语言翻译
    pub fn get_string_by_key_args(locale: &str, key: &str, args: Vec<(&str, &str)>) -> String {
        let lock = match GLOBAL_RESOURCE_BUNDLES.read() {
            Ok(l) => l,
            Err(err) => {
                tracing::error!("{}", err);
                return "".to_string();
            }
        };
        let locale = LocaleKey(locale.to_string());
        let key = match key.contains('.') {
            true => Cow::Owned(key.replace('.', "-")),
            false => Cow::Borrowed(key),
        };
        let key = key.as_ref();
        let get_message = |bundle: &FluentBundle<FluentResource, IntlLangMemoizer>| {
            if bundle.has_message(key) {
                match bundle.get_message(key) {
                    None => String::from(key),
                    Some(msg) => match msg.value() {
                        None => String::from(key),
                        Some(pattern) => {
                            let mut errors = vec![];
                            let mut fluent_args = FluentArgs::new();
                            for (key, value) in args.iter() {
                                fluent_args.set(String::from(*key), String::from(*value));
                            }
                            let value: Cow<str> = bundle.format_pattern(pattern, Some(&fluent_args), &mut errors);
                            return Some(value.to_string());
                        }
                    },
                };
            }
            None
        };
        if let Some(bundle) = lock.get(&locale) {
            if let Some(message) = get_message(bundle) {
                return message;
            }
        }
        let default_locale = Self::get_default_locale();
        if let Some(bundle) = lock.get(&LocaleKey(default_locale)) {
            if let Some(message) = get_message(bundle) {
                return message;
            }
        }
        String::from(key)
    }
}

#[cfg(test)]
#[allow(unused)]
mod test {
    use crate::tina::i18n::{ResourceBundle, ResourceBundleContainer};
    use indexmap::IndexMap;

    #[test]
    #[ignore]
    fn test_add_bundle() {
        let mut map = IndexMap::new();
        map.insert("hello-teddy", "你好, Teddy! {$name}");
        map.insert("system_user_display_name", "系统用户(已修改)");
        ResourceBundle::add_bundle("zh-CN", map);

        println!("{}", ResourceBundle::get_string_by_key_args("zh-CN", "hello-teddy2", vec![("name", "嘿嘿")]));
        println!("{}", ResourceBundle::get_string_by_key_args("en-US", "hello-teddy2", vec![("name", "嘿嘿")]));
        println!("{}", ResourceBundle::get_string_by_key_args("zh-CN", "system_user_display_name", vec![]));
        println!("{}", ResourceBundle::get_string_by_key_args("en-US", "system_user_display_name", vec![]));
        println!("{}", ResourceBundle::get_string("zh-CN", "unkown_user_display_name"));
        println!("{}", ResourceBundle::get_string("zh-CN", "message.http_status_200"));
    }
}