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()))));
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 {
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"));
}
}