Skip to content

Commit

Permalink
Implement FluentLanguageLoader::get_lang(...) methods (#84)
Browse files Browse the repository at this point in the history
Close #59 Implement FluentLanguageLoader::get_lang(...) methods

This adds the following methods:

- get_lang
- get_lang_args
- get_lang_args_concrete
- get_lang_args_fluent

Those methods work exactly like their non-lang counterparts
but add a lang argument that can be used to specify a list of
language codes without needing to change the global current
language setting.
  • Loading branch information
bikeshedder authored Feb 23, 2022
1 parent ae222f3 commit aa84e6c
Show file tree
Hide file tree
Showing 4 changed files with 210 additions and 63 deletions.
3 changes: 2 additions & 1 deletion i18n-embed/i18n/ftl/en-GB/test.ftl
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
hello-world = Hello World Localisation!
only-gb = only GB
different-args = this message has {$different} {$args} in different languages
only-gb-args = Hello {$userName}!
different-args = this message has {$different} {$args} in different languages
3 changes: 2 additions & 1 deletion i18n-embed/i18n/ftl/en-US/test.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ hello-world = Hello World Localization!
only-us = only US
only-ru = only RU
only-gb = only GB (US Version)
only-gb-args = Hello {$userName}! (US Version)
different-args = this message has different {$arg}s in different languages
isolation-chars = inject a { $thing } here
multi-line =
Expand All @@ -19,4 +20,4 @@ multi-line-args =
{ $argTwo }
Finished!
Finished!
178 changes: 117 additions & 61 deletions i18n-embed/src/fluent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,9 @@ use fluent::{bundle::FluentBundle, FluentArgs, FluentMessage, FluentResource, Fl
use fluent_syntax::ast::{self, Pattern};
use intl_memoizer::concurrent::IntlLangMemoizer;
use parking_lot::RwLock;
use std::{borrow::Cow, collections::HashMap, fmt::Debug, sync::Arc};
use std::{borrow::Cow, collections::HashMap, fmt::Debug, iter::FromIterator, sync::Arc};
use unic_langid::LanguageIdentifier;

lazy_static::lazy_static! {
static ref CURRENT_LANGUAGE: RwLock<LanguageIdentifier> = {
let language = LanguageIdentifier::default();
RwLock::new(language)
};
}

struct LanguageBundle {
language: LanguageIdentifier,
bundle: FluentBundle<Arc<FluentResource>, IntlLangMemoizer>,
Expand Down Expand Up @@ -59,6 +52,9 @@ impl Debug for LanguageBundle {
struct LanguageConfig {
current_language: LanguageIdentifier,
language_bundles: Vec<LanguageBundle>,
/// This maps a `LanguageIdentifier` to the index inside the
/// `language_bundles` vector.
language_map: HashMap<LanguageIdentifier, usize>,
}

/// [LanguageLoader] implemenation for the `fluent` localization
Expand All @@ -85,6 +81,7 @@ impl FluentLanguageLoader {
let config = LanguageConfig {
current_language: fallback_language.clone(),
language_bundles: Vec::new(),
language_map: HashMap::new(),
};

Self {
Expand All @@ -106,40 +103,114 @@ impl FluentLanguageLoader {

/// Get a localized message referenced by the `message_id`.
pub fn get(&self, message_id: &str) -> String {
self.get_args_concrete(message_id, HashMap::new())
self.get_args_fluent(message_id, None)
}

/// A non-generic version of [FluentLanguageLoader::get_args()].
pub fn get_args_concrete<'source>(
pub fn get_args_concrete<'args>(
&self,
message_id: &str,
args: HashMap<&'source str, FluentValue<'source>>,
args: HashMap<&'args str, FluentValue<'args>>,
) -> String {
let args_option = if args.is_empty() {
None
} else {
let mut fluent_args = FluentArgs::with_capacity(args.len());
self.get_args_fluent(message_id, hash_map_to_fluent_args(args).as_ref())
}

for (key, value) in args {
fluent_args.set(key, value);
}
/// A non-generic version of [FluentLanguageLoader::get_args()]
/// accepting [FluentArgs] instead of a [HashMap].
pub fn get_args_fluent<'args>(
&self,
message_id: &str,
args: Option<&'args FluentArgs<'args>>,
) -> String {
let language_config = self.language_config.read();
self._get(
language_config.language_bundles.iter(),
&language_config.current_language,
message_id,
args,
)
}

Some(fluent_args)
};
/// Get a localized message referenced by the `message_id`, and
/// formatted with the specified `args`.
pub fn get_args<'a, S, V>(&self, id: &str, args: HashMap<S, V>) -> String
where
S: Into<Cow<'a, str>> + Clone,
V: Into<FluentValue<'a>> + Clone,
{
self.get_args_fluent(id, hash_map_to_fluent_args(args).as_ref())
}

self.get_args_fluent(message_id, args_option.as_ref())
/// Get a localized message referenced by the `message_id`.
pub fn get_lang(&self, lang: &[&LanguageIdentifier], message_id: &str) -> String {
self.get_lang_args_fluent(lang, message_id, None)
}

/// A non-generic version of [FluentLanguageLoader::get_args()]
/// A non-generic version of [FluentLanguageLoader::get_lang_args()].
pub fn get_lang_args_concrete<'source>(
&self,
lang: &[&LanguageIdentifier],
message_id: &str,
args: HashMap<&'source str, FluentValue<'source>>,
) -> String {
self.get_lang_args_fluent(lang, message_id, hash_map_to_fluent_args(args).as_ref())
}

/// A non-generic version of [FluentLanguageLoader::get_lang_args()]
/// accepting [FluentArgs] instead of a [HashMap].
pub fn get_args_fluent<'args>(
pub fn get_lang_args_fluent<'args>(
&self,
lang: &[&LanguageIdentifier],
message_id: &str,
args: Option<&'args FluentArgs<'args>>,
) -> String {
let current_language = if lang.is_empty() {
&self.fallback_language
} else {
lang[0]
};
let fallback_language = if lang.contains(&&self.fallback_language) {
None
} else {
Some(&self.fallback_language)
};
let config_lock = self.language_config.read();
let language_bundles = lang
.iter()
.chain(fallback_language.as_ref().into_iter())
.filter_map(|id| {
config_lock
.language_map
.get(id)
.map(|idx| &config_lock.language_bundles[*idx])
});
self._get(language_bundles, current_language, message_id, args)
}

/// Get a localized message for the given language identifiers, referenced
/// by the `message_id` and formatted with the specified `args`.
pub fn get_lang_args<'a, S, V>(
&self,
lang: &[&LanguageIdentifier],
id: &str,
args: HashMap<S, V>,
) -> String
where
S: Into<Cow<'a, str>> + Clone,
V: Into<FluentValue<'a>> + Clone,
{
let fluent_args = hash_map_to_fluent_args(args);
self.get_lang_args_fluent(lang, id, fluent_args.as_ref())
}

config_lock.language_bundles.iter().filter_map(|language_bundle| {
fn _get<'a, 'args>(
&'a self,
language_bundles: impl Iterator<Item = &'a LanguageBundle>,
current_language: &LanguageIdentifier,
message_id: &str,
args: Option<&'args FluentArgs<'args>>,
) -> String {
language_bundles.filter_map(|language_bundle| {
language_bundle
.bundle
.get_message(message_id)
Expand All @@ -151,57 +222,24 @@ impl FluentLanguageLoader {
log::error!(
target:"i18n_embed::fluent",
"Failed to format a message for language \"{}\" and id \"{}\".\nErrors\n{:?}.",
&config_lock.current_language, message_id, errors
current_language, message_id, errors
)
}

value.into()
})
})
})
.next()
.unwrap_or_else(|| {
log::error!(
target:"i18n_embed::fluent",
"Unable to find localization for language \"{}\" and id \"{}\".",
config_lock.current_language,
current_language,
message_id
);
format!("No localization for id: \"{}\"", message_id)
})
}

/// Get a localized message referenced by the `message_id`, and
/// formatted with the specified `args`.
pub fn get_args<'a, S, V>(&self, id: &str, args: HashMap<S, V>) -> String
where
S: Into<Cow<'a, str>> + Clone,
V: Into<FluentValue<'a>> + Clone,
{
let mut keys: Vec<Cow<'a, str>> = Vec::new();

let mut map: HashMap<&str, FluentValue<'_>> = HashMap::with_capacity(args.len());

let mut values = Vec::new();

for (key, value) in args.into_iter() {
keys.push(key.into());
values.push(value.into());
}

for (i, key) in keys.iter().rev().enumerate() {
let value = values.pop().unwrap_or_else(|| {
panic!(
"expected a value corresponding with key \"{}\" at position {}",
key, i
)
});

map.insert(&*key, value);
}

self.get_args_concrete(id, map)
}

/// Returns true if a message with the specified `message_id` is
/// available in any of the languages currently loaded (including
/// the fallback language).
Expand Down Expand Up @@ -371,10 +409,28 @@ impl LanguageLoader for FluentLanguageLoader {
}

let mut config_lock = self.language_config.write();
config_lock.language_bundles = language_bundles;
config_lock.current_language = current_language.clone();
config_lock.language_bundles = language_bundles;
config_lock.language_map = config_lock
.language_bundles
.iter()
.enumerate()
.map(|(i, language_bundle)| (language_bundle.language.clone(), i))
.collect();
drop(config_lock);

Ok(())
}
}

fn hash_map_to_fluent_args<'args, K, V>(map: HashMap<K, V>) -> Option<FluentArgs<'args>>
where
K: Into<Cow<'args, str>>,
V: Into<FluentValue<'args>>,
{
if map.is_empty() {
None
} else {
Some(FluentArgs::from_iter(map))
}
}
89 changes: 89 additions & 0 deletions i18n-embed/tests/loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,95 @@ mod fluent {
msg
);
}

#[test]
fn get_lang_default_fallback() {
setup();
let ru: LanguageIdentifier = "ru".parse().unwrap();
let en_gb: LanguageIdentifier = "en-GB".parse().unwrap();
let en_us: LanguageIdentifier = "en-US".parse().unwrap();
let loader = FluentLanguageLoader::new("test", en_us);

loader
.load_languages(&Localizations, &[&ru, &en_gb])
.unwrap();

let msg = loader.get_lang(&[&ru], "only-ru");
assert_eq!("только русский", msg);

let msg = loader.get_lang(&[&ru], "only-gb");
assert_eq!("only GB (US Version)", msg);
}

#[test]
fn get_lang_args_default_fallback() {
setup();
let ru: LanguageIdentifier = "ru".parse().unwrap();
let en_gb: LanguageIdentifier = "en-GB".parse().unwrap();
let en_us: LanguageIdentifier = "en-US".parse().unwrap();
let loader = FluentLanguageLoader::new("test", en_us);

loader
.load_languages(&Localizations, &[&ru, &en_gb])
.unwrap();

let args = maplit::hashmap! {
"argOne" => "1",
"argTwo" => "2",
};

let msg = loader.get_lang_args(&[&ru], "multi-line-args", args);
assert_eq!(
"Это многострочное сообщение с параметрами.\n\n\
\u{2068}1\u{2069}\n\n\
Это многострочное сообщение с параметрами.\n\n\
\u{2068}2\u{2069}\n\n\
Законченный!",
msg
);
}

#[test]
fn get_lang_custom_fallback() {
setup();
let ru: LanguageIdentifier = "ru".parse().unwrap();
let en_gb: LanguageIdentifier = "en-GB".parse().unwrap();
let en_us: LanguageIdentifier = "en-US".parse().unwrap();
let loader = FluentLanguageLoader::new("test", en_us);

loader
.load_languages(&Localizations, &[&ru, &en_gb])
.unwrap();

let msg = loader.get_lang(&[&ru, &en_gb], "only-gb");
assert_eq!("only GB", msg);

let msg = loader.get_lang(&[&ru, &en_gb], "only-us");
assert_eq!("only US", msg);
}

#[test]
fn get_lang_args_custom_fallback() {
setup();
let ru: LanguageIdentifier = "ru".parse().unwrap();
let en_gb: LanguageIdentifier = "en-GB".parse().unwrap();
let en_us: LanguageIdentifier = "en-US".parse().unwrap();
let loader = FluentLanguageLoader::new("test", en_us);

loader
.load_languages(&Localizations, &[&ru, &en_gb])
.unwrap();

let args = maplit::hashmap! {
"userName" => "username",
};

let msg = loader.get_lang_args(&[&ru], "only-gb-args", args.clone());
assert_eq!("Hello \u{2068}username\u{2069}! (US Version)", msg);

let msg = loader.get_lang_args(&[&ru, &en_gb], "only-gb-args", args.clone());
assert_eq!("Hello \u{2068}username\u{2069}!", msg);
}
}

#[cfg(feature = "gettext-system")]
Expand Down

0 comments on commit aa84e6c

Please sign in to comment.