forked from 1b-agent/opencadstudio-upstream
Complete application localization across all supported languages
This commit is contained in:
1 parent
96c794c072
commit
d66e89ca19
78 files changed
+26043
-877
No files matched your search
+105
-21
@@ -1,9 +1,9 @@
|
||||
//! Application language selection and embedded Fluent resources.
|
||||
|
||||
use i18n_embed::fluent::{fluent_language_loader, FluentLanguageLoader};
|
||||
use i18n_embed::LanguageLoader;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use i18n_embed::DesktopLanguageRequester;
|
||||
use i18n_embed::LanguageLoader;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use i18n_embed::WebLanguageRequester;
|
||||
use rust_embed::RustEmbed;
|
||||
@@ -251,12 +251,8 @@ pub fn translate(source: impl AsRef<str>) -> Cow<'static, str> {
|
||||
}
|
||||
|
||||
/// Translate a catalog message and replace the named values used by legacy
|
||||
/// command prompts. Catalog generation protects these markers from machine
|
||||
/// translation and Fluent parsing.
|
||||
pub fn translate_args(
|
||||
source: impl AsRef<str>,
|
||||
args: &[(&str, String)],
|
||||
) -> Cow<'static, str> {
|
||||
/// command prompts. Markers preserve values through Fluent parsing.
|
||||
pub fn translate_args(source: impl AsRef<str>, args: &[(&str, String)]) -> Cow<'static, str> {
|
||||
let source = source.as_ref();
|
||||
let mut translated = locale_catalog::message_attribute(source)
|
||||
.map(|(message_id, attribute_id)| loader().get_attr(message_id, attribute_id))
|
||||
@@ -416,17 +412,18 @@ mod tests {
|
||||
{
|
||||
let loader = FluentLanguageLoader::new("opencadstudio", "en-US".parse().unwrap());
|
||||
load_language(&loader, language).expect("Fluent resources must parse");
|
||||
let attributes: BTreeSet<String> = loader.with_message_iter(
|
||||
&language.requested()[0],
|
||||
|messages| {
|
||||
let attributes: BTreeSet<String> =
|
||||
loader.with_message_iter(&language.requested()[0], |messages| {
|
||||
messages
|
||||
.filter(|message| message.id.name == "donation")
|
||||
.flat_map(|message| {
|
||||
message.attributes.iter().map(|attr| attr.id.name.to_owned())
|
||||
message
|
||||
.attributes
|
||||
.iter()
|
||||
.map(|attr| attr.id.name.to_owned())
|
||||
})
|
||||
.collect()
|
||||
},
|
||||
);
|
||||
});
|
||||
assert_eq!(
|
||||
attributes,
|
||||
BTreeSet::from(["title", "heading", "body", "decline"].map(String::from))
|
||||
@@ -438,12 +435,12 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn greek_catalog_loads_and_covers_the_source_catalog() {
|
||||
fn every_catalog_covers_and_formats_the_source_catalog() {
|
||||
let loader = FluentLanguageLoader::new("opencadstudio", "en-US".parse().unwrap());
|
||||
load_language(&loader, Language::ElGr).expect("Greek Fluent resources must parse");
|
||||
let keys = |language: &str| -> BTreeSet<(String, String)> {
|
||||
loader.with_message_iter(&language.parse().unwrap(), |messages| {
|
||||
messages
|
||||
load_language(&loader, Language::EnUs).expect("English Fluent resources must parse");
|
||||
let keys = |loader: &FluentLanguageLoader, language: Language| {
|
||||
loader.with_message_iter(&language.requested()[0], |messages| {
|
||||
let entries: Vec<_> = messages
|
||||
.flat_map(|message| {
|
||||
std::iter::once((message.id.name.to_owned(), String::new())).chain(
|
||||
message.attributes.iter().map(|attribute| {
|
||||
@@ -451,12 +448,99 @@ mod tests {
|
||||
}),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
.collect();
|
||||
let unique: BTreeSet<_> = entries.iter().cloned().collect();
|
||||
assert_eq!(
|
||||
entries.len(),
|
||||
unique.len(),
|
||||
"duplicate keys in {language:?}"
|
||||
);
|
||||
unique
|
||||
})
|
||||
};
|
||||
let source = keys("en-US");
|
||||
let source = keys(&loader, Language::EnUs);
|
||||
assert!(!source.is_empty());
|
||||
assert_eq!(keys("el-GR"), source);
|
||||
|
||||
let english = Localizations::get("en-US/opencadstudio.ftl").unwrap();
|
||||
let english = std::str::from_utf8(&english.data).unwrap();
|
||||
let variables: Vec<_> = english
|
||||
.split('$')
|
||||
.skip(1)
|
||||
.map(|tail| {
|
||||
tail.split(|ch: char| !ch.is_ascii_alphanumeric() && ch != '_' && ch != '-')
|
||||
.next()
|
||||
.unwrap()
|
||||
})
|
||||
.collect();
|
||||
let args = variables.into_iter().map(|name| (name, 2)).collect();
|
||||
let render = |loader: &FluentLanguageLoader, message: &str, attribute: &str| {
|
||||
loader
|
||||
.with_fluent_message_and_bundle(message, |fluent_message, bundle| {
|
||||
let pattern = if attribute.is_empty() {
|
||||
fluent_message.value()
|
||||
} else {
|
||||
fluent_message
|
||||
.get_attribute(attribute)
|
||||
.map(|attr| attr.value())
|
||||
};
|
||||
pattern
|
||||
.map(|pattern| {
|
||||
let mut errors = Vec::new();
|
||||
let text = bundle.format_pattern(pattern, Some(&args), &mut errors);
|
||||
assert!(errors.is_empty(), "{message}.{attribute}: {errors:?}");
|
||||
text.into_owned()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
})
|
||||
.unwrap()
|
||||
};
|
||||
let markers = |text: &str| -> BTreeSet<String> {
|
||||
text.split("__ocs_")
|
||||
.skip(1)
|
||||
.filter_map(|tail| tail.split_once("__").map(|(marker, _)| marker.to_owned()))
|
||||
.collect()
|
||||
};
|
||||
for language in Language::ALL.into_iter().filter(|l| *l != Language::System) {
|
||||
let resource =
|
||||
Localizations::get(&format!("{}/opencadstudio.ftl", language.requested()[0]))
|
||||
.unwrap();
|
||||
let text = std::str::from_utf8(&resource.data).unwrap();
|
||||
if let Err((_, errors)) = fluent_syntax::parser::parse(text) {
|
||||
panic!("Invalid Fluent syntax in {language:?}: {errors:?}");
|
||||
}
|
||||
let localized = FluentLanguageLoader::new("opencadstudio", "en-US".parse().unwrap());
|
||||
load_language(&localized, language).expect("Fluent resources must parse");
|
||||
let actual = keys(&localized, language);
|
||||
let extra: Vec<_> = actual.difference(&source).collect();
|
||||
assert!(extra.is_empty(), "extra keys in {language:?}: {extra:?}");
|
||||
let missing: Vec<_> = source.difference(&actual).collect();
|
||||
assert!(
|
||||
missing.is_empty(),
|
||||
"{} missing keys in {language:?}: {:?}",
|
||||
missing.len(),
|
||||
&missing[..missing.len().min(10)]
|
||||
);
|
||||
for (message, attribute) in &source {
|
||||
let expected = render(&loader, message, attribute);
|
||||
let translated = render(&localized, message, attribute);
|
||||
assert_eq!(
|
||||
markers(&translated),
|
||||
markers(&expected),
|
||||
"{language:?}: {message}.{attribute}"
|
||||
);
|
||||
assert_eq!(
|
||||
translated.is_empty(),
|
||||
expected.is_empty(),
|
||||
"{language:?}: {message}.{attribute}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn greek_language_setting_round_trips() {
|
||||
let loader = FluentLanguageLoader::new("opencadstudio", "en-US".parse().unwrap());
|
||||
load_language(&loader, Language::ElGr).expect("Greek Fluent resources must parse");
|
||||
assert_eq!(loader.get_attr("language", "greek"), "Ελληνικά");
|
||||
assert_eq!(serde_json::to_string(&Language::ElGr).unwrap(), "\"el-GR\"");
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user