Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add FluentMultiLanguageLoader to allow easy translations access #62

Closed
wants to merge 8 commits into from
1 change: 1 addition & 0 deletions 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
only-gb-args = Hello {$userName}!
different-args = this message has {$different} {$args} in different languages
1 change: 1 addition & 0 deletions i18n-embed/i18n/ftl/en-US/test.ftl
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
hello-world = Hello World Localization!
only-us = only US
only-ru = only RU
only-gb-args = Hello {$userName}! (US Version)
only-gb = only GB (US Version)
different-args = this message has different {$arg}s in different languages
isolation-chars = inject a { $thing } here
Expand Down
138 changes: 78 additions & 60 deletions i18n-embed/src/fluent.rs → i18n-embed/src/fluent/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,20 @@
//!
//! ⚠️ *This module requires the following crate features to be activated: `fluent-system`.*

use crate::{I18nAssets, I18nEmbedError, LanguageLoader};

pub use i18n_embed_impl::fluent_language_loader;
use std::{borrow::Cow, collections::HashMap, fmt::Debug, sync::Arc};

use fluent::{concurrent::FluentBundle, FluentArgs, FluentMessage, FluentResource, FluentValue};
use fluent_syntax::ast::{self, Pattern};
use parking_lot::RwLock;
use std::{borrow::Cow, collections::HashMap, fmt::Debug, sync::Arc};
use unic_langid::LanguageIdentifier;

pub use i18n_embed_impl::fluent_language_loader;
pub use multi::FluentMultiLanguageLoader;

use crate::{I18nAssets, I18nEmbedError, LanguageLoader};

mod multi;

lazy_static::lazy_static! {
static ref CURRENT_LANGUAGE: RwLock<LanguageIdentifier> = {
let language = LanguageIdentifier::default();
Expand Down Expand Up @@ -112,7 +116,7 @@ impl FluentLanguageLoader {
pub fn get_args_concrete<'source>(
&self,
message_id: &str,
args: HashMap<&'source str, FluentValue<'source>>,
args: HashMap<Cow<'source, str>, FluentValue<'source>>,
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a breaking change (see fl_macro failing unit test), was there are particular motivation for using Cow over &str?

) -> String {
let args_option = if args.is_empty() {
None
Expand Down Expand Up @@ -176,28 +180,7 @@ impl FluentLanguageLoader {
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);
}

let map = prepare_args_map(args);
self.get_args_concrete(id, map)
}

Expand Down Expand Up @@ -329,40 +312,10 @@ impl LanguageLoader for FluentLanguageLoader {
}

let mut language_bundles = Vec::with_capacity(language_ids.len());

for language in load_language_ids {
let (path, file) = self.language_file(&language, i18n_assets);

if let Some(file) = file {
log::debug!(target:"i18n_embed::fluent", "Loaded language file: \"{0}\" for language: \"{1}\"", path, language);

let file_string = String::from_utf8(file.to_vec())
.map_err(|err| I18nEmbedError::ErrorParsingFileUtf8(path.clone(), err))?
// TODO: Workaround for https://github.com/kellpossible/cargo-i18n/issues/57
// remove when https://github.com/projectfluent/fluent-rs/issues/213 is resolved.
.replace("\u{000D}\n", "\n");

let resource = match FluentResource::try_new(file_string) {
Ok(resource) => resource,
Err((resource, errors)) => {
errors.iter().for_each(|err| {
log::error!(target: "i18n_embed::fluent", "Error while parsing fluent language file \"{0}\": \"{1:?}\".", path, err);
});
resource
}
};

let mut resources = Vec::new();
resources.push(Arc::new(resource));
let language_bundle = LanguageBundle::new(language.clone(), resources);

language_bundles.push(language_bundle);
} else {
log::debug!(target:"i18n_embed::fluent", "Unable to find language file: \"{0}\" for language: \"{1}\"", path, language);
if language == &self.fallback_language {
return Err(I18nEmbedError::LanguageNotAvailable(path, language.clone()));
}
}
let fluent_bundle =
files_to_fluent_bundle(self, i18n_assets, language, &self.fallback_language)?;
language_bundles.push(fluent_bundle);
}

let mut config_lock = self.language_config.write();
Expand All @@ -373,3 +326,68 @@ impl LanguageLoader for FluentLanguageLoader {
Ok(())
}
}

/// Retrieve and load the file for the designated `LanguageIdentifier`
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the fallback_language variable is unused here, I think some of the original logic might have been been altered

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can see the unused variable has been removed but I haven't spent the time to see how the error logic has been altered as a result of this.

fn files_to_fluent_bundle(
loader: &dyn LanguageLoader,
i18n_assets: &dyn I18nAssets,
language: &LanguageIdentifier,
fallback_language: &LanguageIdentifier,
) -> Result<LanguageBundle, I18nEmbedError> {
let (path, file) = loader.language_file(&language, i18n_assets);

if let Some(file) = file {
log::debug!(target:"i18n_embed::fluent", "Loaded language file: \"{0}\" for language: \"{1}\"", path, language);

let file_string = String::from_utf8(file.to_vec())
.map_err(|err| I18nEmbedError::ErrorParsingFileUtf8(path.clone(), err))?
// TODO: Workaround for https://github.com/kellpossible/cargo-i18n/issues/57
// remove when https://github.com/projectfluent/fluent-rs/issues/213 is resolved.
.replace("\u{000D}\n", "\n");

let resource = match FluentResource::try_new(file_string) {
Ok(resource) => resource,
Err((resource, errors)) => {
errors.iter().for_each(|err| {
log::error!(target: "i18n_embed::fluent", "Error while parsing fluent language file \"{0}\": \"{1:?}\".", path, err);
});
resource
}
};

let mut resources = Vec::new();
resources.push(Arc::new(resource));
Ok(LanguageBundle::new(language.clone(), resources))
} else {
log::debug!(target:"i18n_embed::fluent", "Unable to find language file: \"{0}\" for language: \"{1}\"", path, language);
Err(I18nEmbedError::LanguageNotAvailable(path, language.clone()))
}
}

/// Transforms a regular hashmap into Fluent ready args map.
fn prepare_args_map<'a, S, V>(args: HashMap<S, V>) -> HashMap<Cow<'a, str>, FluentValue<'a>>
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<Cow<'a, 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.into_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);
}
map
}
Loading