use std::collections::HashMap;
use std::sync::Arc;
use std::{fs, io};
use log::{debug, error, warn};
use crate::data::Data;
use crate::env::Env;
use crate::shell::Application;
use fluent_bundle::{
FluentArgs, FluentBundle, FluentError, FluentMessage, FluentResource, FluentValue,
};
use fluent_langneg::{negotiate_languages, NegotiationStrategy};
use fluent_syntax::ast::Pattern as FluentPattern;
use unic_langid::LanguageIdentifier;
static FALLBACK_STRINGS: &str = include_str!("../resources/i18n/en-US/builtin.ftl");
#[allow(dead_code)]
pub(crate) struct L10nManager {
res_mgr: ResourceManager,
resources: Vec<String>,
current_bundle: BundleStack,
current_locale: LanguageIdentifier,
}
struct ResourceManager {
resources: HashMap<String, Arc<FluentResource>>,
locales: Vec<LanguageIdentifier>,
default_locale: LanguageIdentifier,
path_scheme: String,
}
type ArgClosure<T> = Arc<dyn Fn(&T, &Env) -> FluentValue<'static> + 'static>;
#[derive(Clone)]
struct ArgSource<T>(ArgClosure<T>);
#[derive(Debug, Clone)]
pub struct LocalizedString<T> {
pub(crate) key: &'static str,
placeholder: Option<String>,
args: Option<Vec<(&'static str, ArgSource<T>)>>,
resolved: Option<String>,
resolved_lang: Option<LanguageIdentifier>,
}
struct BundleStack(Vec<FluentBundle<Arc<FluentResource>>>);
impl BundleStack {
fn get_message(&self, id: &str) -> Option<FluentMessage> {
self.0.iter().flat_map(|b| b.get_message(id)).next()
}
fn format_pattern(
&self,
id: &str,
pattern: &FluentPattern,
args: Option<&FluentArgs>,
errors: &mut Vec<FluentError>,
) -> String {
for bundle in self.0.iter() {
if bundle.has_message(id) {
return bundle.format_pattern(pattern, args, errors).to_string();
}
}
format!("localization failed for key '{}'", id)
}
}
impl ResourceManager {
fn get_resource(&mut self, res_id: &str, locale: &str) -> Arc<FluentResource> {
let path = self
.path_scheme
.replace("{locale}", locale)
.replace("{res_id}", res_id);
if let Some(res) = self.resources.get(&path) {
res.clone()
} else {
let string = fs::read_to_string(&path).unwrap_or_else(|_| {
if (res_id, locale) == ("builtin.ftl", "en-US") {
FALLBACK_STRINGS.to_string()
} else {
error!("missing resouce {}/{}", locale, res_id);
String::new()
}
});
let res = match FluentResource::try_new(string) {
Ok(res) => Arc::new(res),
Err((res, _err)) => Arc::new(res),
};
self.resources.insert(path, res.clone());
res
}
}
fn get_bundle(&mut self, locale: &LanguageIdentifier, resource_ids: &[String]) -> BundleStack {
let resolved_locales = self.resolve_locales(locale.clone());
debug!("resolved: {}", PrintLocales(resolved_locales.as_slice()));
let mut stack = Vec::new();
for locale in &resolved_locales {
let mut bundle = FluentBundle::new(&resolved_locales);
for res_id in resource_ids {
let res = self.get_resource(&res_id, &locale.to_string());
bundle.add_resource(res).unwrap();
}
stack.push(bundle);
}
BundleStack(stack)
}
pub(crate) fn resolve_locales(&self, locale: LanguageIdentifier) -> Vec<LanguageIdentifier> {
negotiate_languages(
&[locale],
&self.locales,
Some(&self.default_locale),
NegotiationStrategy::Filtering,
)
.into_iter()
.map(|l| l.to_owned())
.collect()
}
}
impl L10nManager {
pub fn new(resources: Vec<String>, base_dir: &str) -> Self {
fn get_available_locales(base_dir: &str) -> Result<Vec<LanguageIdentifier>, io::Error> {
let mut locales = vec![];
let res_dir = fs::read_dir(base_dir)?;
for entry in res_dir {
if let Ok(entry) = entry {
let path = entry.path();
if path.is_dir() {
if let Some(name) = path.file_name() {
if let Some(name) = name.to_str() {
let langid: LanguageIdentifier =
name.parse().expect("Parsing failed.");
locales.push(langid);
}
}
}
}
}
Ok(locales)
}
let default_locale: LanguageIdentifier =
"en-US".parse().expect("failed to parse default locale");
let current_locale = Application::get_locale()
.parse()
.unwrap_or_else(|_| default_locale.clone());
let locales = get_available_locales(base_dir).unwrap_or_default();
debug!(
"available locales {}, current {}",
PrintLocales(&locales),
current_locale,
);
let mut path_scheme = base_dir.to_string();
path_scheme.push_str("/{locale}/{res_id}");
let mut res_mgr = ResourceManager {
resources: HashMap::new(),
path_scheme,
default_locale,
locales,
};
let current_bundle = res_mgr.get_bundle(¤t_locale, &resources);
L10nManager {
res_mgr,
current_bundle,
resources,
current_locale,
}
}
pub fn localize<'args>(
&'args self,
key: &str,
args: impl Into<Option<&'args FluentArgs<'args>>>,
) -> Option<String> {
let args = args.into();
let value = match self
.current_bundle
.get_message(key)
.and_then(|msg| msg.value)
{
Some(v) => v,
None => return None,
};
let mut errs = Vec::new();
let result = self
.current_bundle
.format_pattern(key, value, args, &mut errs);
for err in errs {
warn!("localization error {:?}", err);
}
const START_ISOLATE: char = '\u{2068}';
const END_ISOLATE: char = '\u{2069}';
if args.is_some() && result.chars().any(|c| c == START_ISOLATE) {
Some(
result
.chars()
.filter(|c| c != &START_ISOLATE && c != &END_ISOLATE)
.collect(),
)
} else {
Some(result)
}
}
}
impl<T> LocalizedString<T> {
pub const fn new(key: &'static str) -> Self {
LocalizedString {
key,
args: None,
placeholder: None,
resolved: None,
resolved_lang: None,
}
}
pub fn with_placeholder(mut self, placeholder: String) -> Self {
self.placeholder = Some(placeholder);
self
}
pub fn localized_str(&self) -> &str {
self.resolved
.as_ref()
.map(|s| s.as_str())
.or_else(|| self.placeholder.as_ref().map(String::as_ref))
.unwrap_or(self.key)
}
}
impl<T: Data> LocalizedString<T> {
pub fn with_arg(
mut self,
key: &'static str,
f: impl Fn(&T, &Env) -> FluentValue<'static> + 'static,
) -> Self {
self.args
.get_or_insert(Vec::new())
.push((key, ArgSource(Arc::new(f))));
self
}
pub fn resolve<'a>(&'a mut self, data: &T, env: &Env) -> bool {
if self.args.is_some()
|| self.resolved_lang.as_ref() != Some(&env.localization_manager().current_locale)
{
let args: Option<FluentArgs> = self
.args
.as_ref()
.map(|a| a.iter().map(|(k, v)| (*k, (v.0)(data, env))).collect());
self.resolved_lang = Some(env.localization_manager().current_locale.clone());
let next = env.localization_manager().localize(self.key, args.as_ref());
let result = next != self.resolved;
self.resolved = next;
result
} else {
false
}
}
}
impl<T> std::fmt::Debug for ArgSource<T> {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "Arg Resolver {:p}", self.0)
}
}
struct PrintLocales<'a, T>(&'a [T]);
impl<'a, T: std::fmt::Display> std::fmt::Display for PrintLocales<'a, T> {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "[")?;
let mut prev = false;
for l in self.0 {
if prev {
write!(f, ", ")?;
}
prev = true;
write!(f, "{}", l)?;
}
write!(f, "]")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn resolve() {
let en_us: LanguageIdentifier = "en-US".parse().unwrap();
let en_ca: LanguageIdentifier = "en-CA".parse().unwrap();
let en_gb: LanguageIdentifier = "en-GB".parse().unwrap();
let fr_fr: LanguageIdentifier = "fr-FR".parse().unwrap();
let pt_pt: LanguageIdentifier = "pt-PT".parse().unwrap();
let resmgr = ResourceManager {
resources: HashMap::new(),
locales: vec![en_us.clone(), en_ca.clone(), en_gb.clone(), fr_fr.clone()],
default_locale: en_us.clone(),
path_scheme: String::new(),
};
let en_za: LanguageIdentifier = "en-GB".parse().unwrap();
let cn_hk: LanguageIdentifier = "cn-HK".parse().unwrap();
let fr_ca: LanguageIdentifier = "fr-CA".parse().unwrap();
assert_eq!(
resmgr.resolve_locales(en_ca.clone()),
vec![en_ca.clone(), en_us.clone(), en_gb.clone()]
);
assert_eq!(
resmgr.resolve_locales(en_za.clone()),
vec![en_gb.clone(), en_us.clone(), en_ca.clone()]
);
assert_eq!(
resmgr.resolve_locales(fr_ca.clone()),
vec![fr_fr.clone(), en_us.clone()]
);
assert_eq!(
resmgr.resolve_locales(fr_fr.clone()),
vec![fr_fr.clone(), en_us.clone()]
);
assert_eq!(resmgr.resolve_locales(cn_hk), vec![en_us.clone()]);
assert_eq!(resmgr.resolve_locales(pt_pt), vec![en_us.clone()]);
}
}