Revision control
Copy as Markdown
Other Tools
use std::borrow::Cow;
use std::fs;
use fluent_bundle::{
resolver::errors::{ReferenceKind, ResolverError},
FluentArgs, FluentBundle, FluentError, FluentResource,
};
use fluent_fallback::{
env::LocalesProvider,
generator::{BundleGenerator, FluentBundleResult},
types::{L10nKey, ResourceId},
Localization, LocalizationError,
};
use rustc_hash::FxHashSet;
use std::cell::RefCell;
use std::rc::Rc;
use unic_langid::{langid, LanguageIdentifier};
struct InnerLocales {
locales: RefCell<Vec<LanguageIdentifier>>,
}
impl InnerLocales {
pub fn insert(&self, index: usize, element: LanguageIdentifier) {
self.locales.borrow_mut().insert(index, element);
}
}
#[derive(Clone)]
struct Locales {
inner: Rc<InnerLocales>,
}
impl Locales {
pub fn new(locales: Vec<LanguageIdentifier>) -> Self {
Self {
inner: Rc::new(InnerLocales {
locales: RefCell::new(locales),
}),
}
}
pub fn insert(&mut self, index: usize, element: LanguageIdentifier) {
self.inner.insert(index, element);
}
}
impl LocalesProvider for Locales {
type Iter = <Vec<LanguageIdentifier> as IntoIterator>::IntoIter;
fn locales(&self) -> Self::Iter {
self.inner.locales.borrow().clone().into_iter()
}
}
// Due to limitation of trait, we need a nameable Iterator type. Due to the
// lack of GATs, these have to own members instead of taking slices.
struct BundleIter {
locales: <Vec<LanguageIdentifier> as IntoIterator>::IntoIter,
res_ids: FxHashSet<ResourceId>,
}
impl Iterator for BundleIter {
type Item = FluentBundleResult<FluentResource>;
fn next(&mut self) -> Option<Self::Item> {
let locale = self.locales.next()?;
let mut bundle = FluentBundle::new(vec![locale.clone()]);
bundle.set_use_isolating(false);
let mut errors = vec![];
for res_id in &self.res_ids {
let full_path = format!("./tests/resources/{}/{}", locale, res_id);
let source = fs::read_to_string(full_path).unwrap();
let res = match FluentResource::try_new(source) {
Ok(res) => res,
Err((res, err)) => {
errors.extend(err.into_iter().map(Into::into));
res
}
};
bundle.add_resource(res).unwrap();
}
if errors.is_empty() {
Some(Ok(bundle))
} else {
Some(Err((bundle, errors)))
}
}
}
impl futures::Stream for BundleIter {
type Item = FluentBundleResult<FluentResource>;
fn poll_next(
mut self: std::pin::Pin<&mut Self>,
_cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Option<Self::Item>> {
if let Some(locale) = self.locales.next() {
let mut bundle = FluentBundle::new(vec![locale.clone()]);
bundle.set_use_isolating(false);
let mut errors = vec![];
for res_id in &self.res_ids {
let full_path = format!("./tests/resources/{}/{}", locale, res_id.value);
let source = fs::read_to_string(full_path).unwrap();
let res = match FluentResource::try_new(source) {
Ok(res) => res,
Err((res, err)) => {
errors.extend(err.into_iter().map(Into::into));
res
}
};
bundle.add_resource(res).unwrap();
}
if errors.is_empty() {
Some(Ok(bundle)).into()
} else {
Some(Err((bundle, errors))).into()
}
} else {
None.into()
}
}
}
struct ResourceManager;
impl BundleGenerator for ResourceManager {
type Resource = FluentResource;
type LocalesIter = std::vec::IntoIter<LanguageIdentifier>;
type Iter = BundleIter;
type Stream = BundleIter;
fn bundles_iter(
&self,
locales: Self::LocalesIter,
res_ids: FxHashSet<ResourceId>,
) -> Self::Iter {
BundleIter { locales, res_ids }
}
fn bundles_stream(
&self,
locales: Self::LocalesIter,
res_ids: FxHashSet<ResourceId>,
) -> Self::Stream {
BundleIter { locales, res_ids }
}
}
#[test]
fn localization_format() {
let resource_ids: Vec<ResourceId> = vec!["test.ftl".into(), "test2.ftl".into()];
let locales = Locales::new(vec![langid!("pl"), langid!("en-US")]);
let res_mgr = ResourceManager;
let mut errors = vec![];
let loc = Localization::with_env(resource_ids, true, locales, res_mgr);
let bundles = loc.bundles();
let value = bundles
.format_value_sync("hello-world", None, &mut errors)
.unwrap();
assert_eq!(value, Some(Cow::Borrowed("Hello World [pl]")));
let value = bundles
.format_value_sync("missing-message", None, &mut errors)
.unwrap();
assert_eq!(value, None);
let value = bundles
.format_value_sync("hello-world-3", None, &mut errors)
.unwrap();
assert_eq!(value, Some(Cow::Borrowed("Hello World 3 [en]")));
assert_eq!(errors.len(), 4);
}
#[test]
fn localization_on_change() {
let resource_ids: Vec<ResourceId> = vec!["test.ftl".into(), "test2.ftl".into()];
let mut locales = Locales::new(vec![langid!("en-US")]);
let res_mgr = ResourceManager;
let mut errors = vec![];
let mut loc = Localization::with_env(resource_ids, true, locales.clone(), res_mgr);
let bundles = loc.bundles();
let value = bundles
.format_value_sync("hello-world", None, &mut errors)
.unwrap();
assert_eq!(value, Some(Cow::Borrowed("Hello World [en]")));
locales.insert(0, langid!("pl"));
loc.on_change();
let bundles = loc.bundles();
let value = bundles
.format_value_sync("hello-world", None, &mut errors)
.unwrap();
assert_eq!(value, Some(Cow::Borrowed("Hello World [pl]")));
}
#[test]
fn localization_format_value_missing_errors() {
let resource_ids: Vec<ResourceId> = vec!["test.ftl".into(), "test2.ftl".into()];
let locales = Locales::new(vec![langid!("pl"), langid!("en-US")]);
let res_mgr = ResourceManager;
let mut errors = vec![];
let loc = Localization::with_env(resource_ids, true, locales.clone(), res_mgr);
let bundles = loc.bundles();
let _ = bundles
.format_value_sync("missing-message", None, &mut errors)
.unwrap();
assert_eq!(
errors,
vec![
LocalizationError::MissingMessage {
id: "missing-message".to_string(),
locale: Some(langid!("pl"))
},
LocalizationError::MissingMessage {
id: "missing-message".to_string(),
locale: Some(langid!("en-US"))
},
LocalizationError::MissingMessage {
id: "missing-message".to_string(),
locale: None
},
]
);
errors.clear();
let _ = bundles
.format_value_sync("message-3", None, &mut errors)
.unwrap();
assert_eq!(
errors,
vec![
LocalizationError::MissingValue {
id: "message-3".to_string(),
locale: Some(langid!("pl"))
},
LocalizationError::MissingValue {
id: "message-3".to_string(),
locale: Some(langid!("en-US"))
},
LocalizationError::MissingValue {
id: "message-3".to_string(),
locale: None
},
]
);
}
#[test]
fn localization_format_value_sync_missing_errors() {
let resource_ids: Vec<ResourceId> = vec!["test.ftl".into(), "test2.ftl".into()];
let locales = Locales::new(vec![langid!("pl"), langid!("en-US")]);
let res_mgr = ResourceManager;
let mut errors = vec![];
let loc = Localization::with_env(resource_ids, true, locales.clone(), res_mgr);
let bundles = loc.bundles();
let _ = bundles
.format_value_sync("missing-message", None, &mut errors)
.unwrap();
assert_eq!(
errors,
vec![
LocalizationError::MissingMessage {
id: "missing-message".to_string(),
locale: Some(langid!("pl"))
},
LocalizationError::MissingMessage {
id: "missing-message".to_string(),
locale: Some(langid!("en-US"))
},
LocalizationError::MissingMessage {
id: "missing-message".to_string(),
locale: None
},
]
);
errors.clear();
let _ = bundles
.format_value_sync("message-3", None, &mut errors)
.unwrap();
assert_eq!(
errors,
vec![
LocalizationError::MissingValue {
id: "message-3".to_string(),
locale: Some(langid!("pl"))
},
LocalizationError::MissingValue {
id: "message-3".to_string(),
locale: Some(langid!("en-US"))
},
LocalizationError::MissingValue {
id: "message-3".to_string(),
locale: None
},
]
);
}
#[test]
fn localization_format_values_sync_missing_errors() {
let resource_ids: Vec<ResourceId> = vec!["test.ftl".into(), "test2.ftl".into()];
let locales = Locales::new(vec![langid!("pl"), langid!("en-US")]);
let res_mgr = ResourceManager;
let mut errors = vec![];
let loc = Localization::with_env(resource_ids, true, locales.clone(), res_mgr);
let bundles = loc.bundles();
let _ = bundles
.format_values_sync(
&["missing-message".into(), "missing-message-2".into()],
&mut errors,
)
.unwrap();
assert_eq!(
errors,
vec![
LocalizationError::MissingMessage {
id: "missing-message".to_string(),
locale: Some(langid!("pl"))
},
LocalizationError::MissingMessage {
id: "missing-message-2".to_string(),
locale: Some(langid!("pl"))
},
LocalizationError::MissingMessage {
id: "missing-message".to_string(),
locale: Some(langid!("en-US"))
},
LocalizationError::MissingMessage {
id: "missing-message-2".to_string(),
locale: Some(langid!("en-US"))
},
LocalizationError::MissingMessage {
id: "missing-message".to_string(),
locale: None
},
LocalizationError::MissingMessage {
id: "missing-message-2".to_string(),
locale: None
},
]
);
errors.clear();
let _ = bundles
.format_values_sync(&["message-3".into()], &mut errors)
.unwrap();
assert_eq!(
errors,
vec![
LocalizationError::MissingValue {
id: "message-3".to_string(),
locale: Some(langid!("pl"))
},
LocalizationError::MissingValue {
id: "message-3".to_string(),
locale: Some(langid!("en-US"))
},
LocalizationError::MissingValue {
id: "message-3".to_string(),
locale: None
},
]
);
}
#[test]
fn localization_format_messages_sync_missing_errors() {
let resource_ids: Vec<ResourceId> = vec!["test.ftl".into(), "test2.ftl".into()];
let locales = Locales::new(vec![langid!("pl"), langid!("en-US")]);
let res_mgr = ResourceManager;
let mut errors = vec![];
let loc = Localization::with_env(resource_ids, true, locales.clone(), res_mgr);
let bundles = loc.bundles();
let _ = bundles
.format_messages_sync(
&["missing-message".into(), "missing-message-2".into()],
&mut errors,
)
.unwrap();
assert_eq!(
errors,
vec![
LocalizationError::MissingMessage {
id: "missing-message".to_string(),
locale: Some(langid!("pl"))
},
LocalizationError::MissingMessage {
id: "missing-message-2".to_string(),
locale: Some(langid!("pl"))
},
LocalizationError::MissingMessage {
id: "missing-message".to_string(),
locale: Some(langid!("en-US"))
},
LocalizationError::MissingMessage {
id: "missing-message-2".to_string(),
locale: Some(langid!("en-US"))
},
LocalizationError::MissingMessage {
id: "missing-message".to_string(),
locale: None
},
LocalizationError::MissingMessage {
id: "missing-message-2".to_string(),
locale: None
},
]
);
}
#[test]
fn localization_format_missing_argument_error() {
let resource_ids: Vec<ResourceId> = vec!["test2.ftl".into()];
let locales = Locales::new(vec![langid!("en-US")]);
let res_mgr = ResourceManager;
let mut errors = vec![];
let loc = Localization::with_env(resource_ids, true, locales, res_mgr);
let bundles = loc.bundles();
let mut args = FluentArgs::new();
args.set("userName", "John");
let keys = vec![L10nKey {
id: "message-4".into(),
args: Some(args),
}];
let msgs = bundles.format_messages_sync(&keys, &mut errors).unwrap();
assert_eq!(
msgs.get(0).unwrap().as_ref().unwrap().value,
Some(Cow::Borrowed("Hello, John. [en]"))
);
assert_eq!(errors.len(), 0);
let keys = vec![L10nKey {
id: "message-4".into(),
args: None,
}];
let msgs = bundles.format_messages_sync(&keys, &mut errors).unwrap();
assert_eq!(
msgs.get(0).unwrap().as_ref().unwrap().value,
Some(Cow::Borrowed("Hello, {$userName}. [en]"))
);
assert_eq!(
errors,
vec![LocalizationError::Resolver {
id: "message-4".to_string(),
locale: langid!("en-US"),
errors: vec![FluentError::ResolverError(ResolverError::Reference(
ReferenceKind::Variable {
id: "userName".to_string(),
}
))],
},]
);
}
#[tokio::test]
async fn localization_handle_state_changes_mid_async() {
let resource_ids: Vec<ResourceId> = vec!["test.ftl".into()];
let locales = Locales::new(vec![langid!("en-US")]);
let res_mgr = ResourceManager;
let mut errors = vec![];
let mut loc = Localization::with_env(resource_ids, false, locales, res_mgr);
let bundles = loc.bundles().clone();
loc.add_resource_id("test2.ftl".to_string());
bundles.format_value("key", None, &mut errors).await;
}
#[test]
fn localization_duplicate_resources() {
let resource_ids: Vec<ResourceId> =
vec!["test.ftl".into(), "test2.ftl".into(), "test2.ftl".into()];
let locales = Locales::new(vec![langid!("pl"), langid!("en-US")]);
let res_mgr = ResourceManager;
let mut errors = vec![];
let loc = Localization::with_env(resource_ids, true, locales, res_mgr);
let bundles = loc.bundles();
let value = bundles
.format_value_sync("hello-world", None, &mut errors)
.unwrap();
assert_eq!(value, Some(Cow::Borrowed("Hello World [pl]")));
assert_eq!(errors.len(), 0, "There were no errors");
}