Skip to content

Commit

Permalink
Implement FluentLanguageLoader::get_lang(...) methods
Browse files Browse the repository at this point in the history
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.

This closes #59.
  • Loading branch information
bikeshedder committed Feb 10, 2022
1 parent ae222f3 commit 3f35f04
Show file tree
Hide file tree
Showing 6 changed files with 230 additions and 62 deletions.
16 changes: 16 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions i18n-embed/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ fluent-syntax = { version = "0.11", optional = true }
gettext_system = { package = "gettext", version = "0.4", optional = true }
i18n-embed-impl = { version = "0.8", path = "./i18n-embed-impl", optional = true }
intl-memoizer = "0.5"
itertools = "0.10"
lazy_static = "1.4"
locale_config = { version = "0.3", optional = true }
log = "0.4"
Expand Down
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!
180 changes: 120 additions & 60 deletions i18n-embed/src/fluent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,6 @@ use parking_lot::RwLock;
use std::{borrow::Cow, collections::HashMap, fmt::Debug, 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,32 @@ 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, S, V>(map: HashMap<S, V>) -> Option<FluentArgs<'args>>
where
S: Into<Cow<'args, str>> + Clone,
V: Into<FluentValue<'args>> + Clone,
{
if map.is_empty() {
None
} else {
let mut args = FluentArgs::with_capacity(map.len());
for (key, value) in map {
args.set(key, value);
}
Some(args)
}
}
Loading

0 comments on commit 3f35f04

Please sign in to comment.