Multilingual support via Fluent + Frontend improvements + Rewrite of ArcReactor
This commit is contained in:
parent
adec595cfa
commit
412acb7e2d
41 changed files with 2436 additions and 723 deletions
176
crates/jarvis-core/src/i18n.rs
Normal file
176
crates/jarvis-core/src/i18n.rs
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
use fluent_bundle::{FluentBundle, FluentResource, FluentArgs, FluentValue};
|
||||
use fluent_bundle::concurrent::FluentBundle as ConcurrentFluentBundle;
|
||||
use once_cell::sync::OnceCell;
|
||||
use parking_lot::RwLock;
|
||||
use std::collections::HashMap;
|
||||
use unic_langid::LanguageIdentifier;
|
||||
|
||||
// locale files embedded at compile time
|
||||
const LOCALE_RU: &str = include_str!("i18n/locales/ru.ftl");
|
||||
const LOCALE_EN: &str = include_str!("i18n/locales/en.ftl");
|
||||
const LOCALE_UA: &str = include_str!("i18n/locales/ua.ftl");
|
||||
|
||||
pub const SUPPORTED_LANGUAGES: &[&str] = &["ru", "en", "ua"];
|
||||
pub const DEFAULT_LANGUAGE: &str = "ru";
|
||||
|
||||
// use concurrent bundle (thread-safe)
|
||||
type Bundle = ConcurrentFluentBundle<FluentResource>;
|
||||
|
||||
static BUNDLES: OnceCell<HashMap<String, Bundle>> = OnceCell::new();
|
||||
static CURRENT_LANG: OnceCell<RwLock<String>> = OnceCell::new();
|
||||
|
||||
// Initialize i18n system
|
||||
pub fn init(lang: &str) {
|
||||
let bundles = create_bundles();
|
||||
BUNDLES.set(bundles).ok();
|
||||
|
||||
let lang = if SUPPORTED_LANGUAGES.contains(&lang) { lang } else { DEFAULT_LANGUAGE };
|
||||
CURRENT_LANG.set(RwLock::new(lang.to_string())).ok();
|
||||
|
||||
info!("i18n initialized with language: {}", lang);
|
||||
}
|
||||
|
||||
fn create_bundles() -> HashMap<String, Bundle> {
|
||||
let mut bundles = HashMap::new();
|
||||
|
||||
bundles.insert("ru".to_string(), create_bundle("ru", LOCALE_RU));
|
||||
bundles.insert("en".to_string(), create_bundle("en", LOCALE_EN));
|
||||
bundles.insert("ua".to_string(), create_bundle("ua", LOCALE_UA));
|
||||
|
||||
bundles
|
||||
}
|
||||
|
||||
fn create_bundle(lang: &str, source: &str) -> Bundle {
|
||||
let langid: LanguageIdentifier = lang.parse().expect("Invalid language identifier");
|
||||
let mut bundle = ConcurrentFluentBundle::new_concurrent(vec![langid]);
|
||||
|
||||
let resource = FluentResource::try_new(source.to_string())
|
||||
.expect("Failed to parse FTL resource");
|
||||
|
||||
bundle.add_resource(resource).expect("Failed to add resource");
|
||||
bundle
|
||||
}
|
||||
|
||||
// Set current language
|
||||
pub fn set_language(lang: &str) {
|
||||
if let Some(current) = CURRENT_LANG.get() {
|
||||
let lang = if SUPPORTED_LANGUAGES.contains(&lang) { lang } else { DEFAULT_LANGUAGE };
|
||||
*current.write() = lang.to_string();
|
||||
info!("Language changed to: {}", lang);
|
||||
}
|
||||
}
|
||||
|
||||
// Get current language
|
||||
pub fn get_language() -> String {
|
||||
CURRENT_LANG.get()
|
||||
.map(|l| l.read().clone())
|
||||
.unwrap_or_else(|| DEFAULT_LANGUAGE.to_string())
|
||||
}
|
||||
|
||||
// Translate a key
|
||||
pub fn t(key: &str) -> String {
|
||||
t_with_args(key, None)
|
||||
}
|
||||
|
||||
// Translate a key with arguments
|
||||
pub fn t_with_args(key: &str, args: Option<&FluentArgs>) -> String {
|
||||
let lang = get_language();
|
||||
|
||||
let bundles = match BUNDLES.get() {
|
||||
Some(b) => b,
|
||||
None => return key.to_string(),
|
||||
};
|
||||
|
||||
let bundle = match bundles.get(&lang) {
|
||||
Some(b) => b,
|
||||
None => bundles.get(DEFAULT_LANGUAGE).unwrap(),
|
||||
};
|
||||
|
||||
let msg = match bundle.get_message(key) {
|
||||
Some(m) => m,
|
||||
None => return key.to_string(),
|
||||
};
|
||||
|
||||
let pattern = match msg.value() {
|
||||
Some(p) => p,
|
||||
None => return key.to_string(),
|
||||
};
|
||||
|
||||
let mut errors = vec![];
|
||||
let result = bundle.format_pattern(pattern, args, &mut errors);
|
||||
|
||||
if !errors.is_empty() {
|
||||
warn!("i18n errors for key '{}': {:?}", key, errors);
|
||||
}
|
||||
|
||||
result.to_string()
|
||||
}
|
||||
|
||||
// Translate with a single argument
|
||||
pub fn t_arg(key: &str, arg_name: &str, arg_value: &str) -> String {
|
||||
let mut args = FluentArgs::new();
|
||||
args.set(arg_name, FluentValue::from(arg_value));
|
||||
t_with_args(key, Some(&args))
|
||||
}
|
||||
|
||||
// Translate with numeric argument
|
||||
pub fn t_count(key: &str, count: i64) -> String {
|
||||
let mut args = FluentArgs::new();
|
||||
args.set("count", FluentValue::from(count));
|
||||
t_with_args(key, Some(&args))
|
||||
}
|
||||
|
||||
// Get all translations for current language (for frontend)
|
||||
pub fn get_all_translations() -> HashMap<String, String> {
|
||||
let lang = get_language();
|
||||
get_translations_for(&lang)
|
||||
}
|
||||
|
||||
/// Get all translations for a specific language
|
||||
pub fn get_translations_for(lang: &str) -> HashMap<String, String> {
|
||||
let mut result = HashMap::new();
|
||||
|
||||
let bundles = match BUNDLES.get() {
|
||||
Some(b) => b,
|
||||
None => return result,
|
||||
};
|
||||
|
||||
let bundle = match bundles.get(lang) {
|
||||
Some(b) => b,
|
||||
None => match bundles.get(DEFAULT_LANGUAGE) {
|
||||
Some(b) => b,
|
||||
None => return result,
|
||||
},
|
||||
};
|
||||
|
||||
// get source for this language and extract all keys
|
||||
let source = match lang {
|
||||
"ru" => LOCALE_RU,
|
||||
"en" => LOCALE_EN,
|
||||
"ua" => LOCALE_UA,
|
||||
_ => LOCALE_RU,
|
||||
};
|
||||
|
||||
// parse keys from FTL source (lines that have "=" and don't start with "#" or "-")
|
||||
for line in source.lines() {
|
||||
let line = line.trim();
|
||||
if line.is_empty() || line.starts_with('#') || line.starts_with('-') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(key) = line.split('=').next() {
|
||||
let key = key.trim();
|
||||
if !key.is_empty() && !key.contains(' ') {
|
||||
if let Some(msg) = bundle.get_message(key) {
|
||||
if let Some(pattern) = msg.value() {
|
||||
let mut errors = vec![];
|
||||
let value = bundle.format_pattern(pattern, None, &mut errors);
|
||||
result.insert(key.to_string(), value.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue