plural rules and decimal separator should use bundle's language

Instead of providing the list of languages in preferred order, when
creating a bundle we need to specify the bundle language as the first
language, so that the correct plural rules are used. Fluent's docs
are misleading here; I will submit a PR to fix them.

The old behaviour caused:
https://forums.ankiweb.net/t/bug-in-review-intervals-for-some-languages-in-number-of-cards/5744
This commit is contained in:
Damien Elmes 2020-12-14 14:23:49 +10:00
parent 6d596c8fc9
commit 77c9db5bba
2 changed files with 51 additions and 26 deletions

View file

@ -36,15 +36,15 @@ pub use tr_strs;
/// If a fully qualified folder exists (eg, en_GB), return that. /// If a fully qualified folder exists (eg, en_GB), return that.
/// Otherwise, try the language alone (eg en). /// Otherwise, try the language alone (eg en).
/// If neither folder exists, return None. /// If neither folder exists, return None.
fn lang_folder(lang: Option<&LanguageIdentifier>, ftl_folder: &Path) -> Option<PathBuf> { fn lang_folder(lang: &Option<LanguageIdentifier>, ftl_root_folder: &Path) -> Option<PathBuf> {
if let Some(lang) = lang { if let Some(lang) = lang {
if let Some(region) = lang.region { if let Some(region) = lang.region {
let path = ftl_folder.join(format!("{}_{}", lang.language, region)); let path = ftl_root_folder.join(format!("{}_{}", lang.language, region));
if fs::metadata(&path).is_ok() { if fs::metadata(&path).is_ok() {
return Some(path); return Some(path);
} }
} }
let path = ftl_folder.join(lang.language.to_string()); let path = ftl_root_folder.join(lang.language.to_string());
if fs::metadata(&path).is_ok() { if fs::metadata(&path).is_ok() {
Some(path) Some(path)
} else { } else {
@ -52,7 +52,7 @@ fn lang_folder(lang: Option<&LanguageIdentifier>, ftl_folder: &Path) -> Option<P
} }
} else { } else {
// fallback folder // fallback folder
let path = ftl_folder.join("templates"); let path = ftl_root_folder.join("templates");
if fs::metadata(&path).is_ok() { if fs::metadata(&path).is_ok() {
Some(path) Some(path)
} else { } else {
@ -193,6 +193,12 @@ two-args-key = {$one}と{$two}
" "
} }
fn test_pl_text() -> &'static str {
"
one-arg-key = fake Polish {$one}
"
}
/// Parse resource text into an AST for inclusion in a bundle. /// Parse resource text into an AST for inclusion in a bundle.
/// Returns None if text contains errors. /// Returns None if text contains errors.
/// extra_text may contain resources loaded from the filesystem /// extra_text may contain resources loaded from the filesystem
@ -239,12 +245,11 @@ fn get_bundle(
/// Get a bundle that includes any filesystem overrides. /// Get a bundle that includes any filesystem overrides.
fn get_bundle_with_extra( fn get_bundle_with_extra(
text: &str, text: &str,
lang: Option<&LanguageIdentifier>, lang: Option<LanguageIdentifier>,
ftl_folder: &Path, ftl_root_folder: &Path,
locales: &[LanguageIdentifier],
log: &Logger, log: &Logger,
) -> Option<FluentBundle<FluentResource>> { ) -> Option<FluentBundle<FluentResource>> {
let mut extra_text = if let Some(path) = lang_folder(lang, &ftl_folder) { let mut extra_text = if let Some(path) = lang_folder(&lang, &ftl_root_folder) {
match ftl_external_text(&path) { match ftl_external_text(&path) {
Ok(text) => text, Ok(text) => text,
Err(e) => { Err(e) => {
@ -258,18 +263,28 @@ fn get_bundle_with_extra(
if cfg!(test) { if cfg!(test) {
// inject some test strings in test mode // inject some test strings in test mode
match lang { match &lang {
None => { None => {
extra_text += test_en_text(); extra_text += test_en_text();
} }
Some(lang) if lang.language == "ja" => { Some(lang) if lang.language == "ja" => {
extra_text += test_jp_text(); extra_text += test_jp_text();
} }
Some(lang) if lang.language == "pl" => {
extra_text += test_pl_text();
}
_ => {} _ => {}
} }
} }
get_bundle(text, extra_text, locales, log) let mut locales = if let Some(lang) = lang {
vec![lang]
} else {
vec![]
};
locales.push("en-US".parse().unwrap());
get_bundle(text, extra_text, &locales, log)
} }
#[derive(Clone)] #[derive(Clone)]
@ -281,18 +296,18 @@ pub struct I18n {
impl I18n { impl I18n {
pub fn new<S: AsRef<str>, P: Into<PathBuf>>( pub fn new<S: AsRef<str>, P: Into<PathBuf>>(
locale_codes: &[S], locale_codes: &[S],
ftl_folder: P, ftl_root_folder: P,
log: Logger, log: Logger,
) -> Self { ) -> Self {
let ftl_folder = ftl_folder.into(); let ftl_root_folder = ftl_root_folder.into();
let mut langs = vec![]; let mut input_langs = vec![];
let mut bundles = Vec::with_capacity(locale_codes.len() + 1); let mut bundles = Vec::with_capacity(locale_codes.len() + 1);
let mut resource_text = vec![]; let mut resource_text = vec![];
for code in locale_codes { for code in locale_codes {
let code = code.as_ref(); let code = code.as_ref();
if let Ok(lang) = code.parse::<LanguageIdentifier>() { if let Ok(lang) = code.parse::<LanguageIdentifier>() {
langs.push(lang.clone()); input_langs.push(lang.clone());
if lang.language == "en" { if lang.language == "en" {
// if English was listed, any further preferences are skipped, // if English was listed, any further preferences are skipped,
// as the template has 100% coverage, and we need to ensure // as the template has 100% coverage, and we need to ensure
@ -301,17 +316,17 @@ impl I18n {
} }
} }
} }
// add fallback date/time
langs.push("en_US".parse().unwrap());
for lang in &langs { let mut output_langs = vec![];
for lang in input_langs {
// if the language is bundled in the binary // if the language is bundled in the binary
if let Some(text) = ftl_localized_text(lang) { if let Some(text) = ftl_localized_text(&lang) {
if let Some(bundle) = if let Some(bundle) =
get_bundle_with_extra(text, Some(lang), &ftl_folder, &langs, &log) get_bundle_with_extra(text, Some(lang.clone()), &ftl_root_folder, &log)
{ {
resource_text.push(text); resource_text.push(text);
bundles.push(bundle); bundles.push(bundle);
output_langs.push(lang);
} else { } else {
error!(log, "Failed to create bundle for {:?}", lang.language) error!(log, "Failed to create bundle for {:?}", lang.language)
} }
@ -320,15 +335,17 @@ impl I18n {
// add English templates // add English templates
let template_text = ftl_template_text(); let template_text = ftl_template_text();
let template_lang = "en-US".parse().unwrap();
let template_bundle = let template_bundle =
get_bundle_with_extra(template_text, None, &ftl_folder, &langs, &log).unwrap(); get_bundle_with_extra(template_text, None, &ftl_root_folder, &log).unwrap();
resource_text.push(template_text); resource_text.push(template_text);
bundles.push(template_bundle); bundles.push(template_bundle);
output_langs.push(template_lang);
Self { Self {
inner: Arc::new(Mutex::new(I18nInner { inner: Arc::new(Mutex::new(I18nInner {
bundles, bundles,
langs, langs: output_langs,
resource_text, resource_text,
})), })),
log, log,
@ -551,10 +568,16 @@ mod test {
// Decimal separator // Decimal separator
let i18n = I18n::new(&["pl-PL"], &ftl_dir, log.clone()); let i18n = I18n::new(&["pl-PL"], &ftl_dir, log.clone());
// falls back on English, but with Polish separators // Polish will use a comma if the string is translated
assert_eq!(
i18n.tr_("one-arg-key", Some(tr_args!["one"=>2.07])),
"fake Polish 2,07"
);
// but if it falls back on English, it will use an English separator
assert_eq!( assert_eq!(
i18n.tr_("two-args-key", Some(tr_args!["one"=>1, "two"=>2.07])), i18n.tr_("two-args-key", Some(tr_args!["one"=>1, "two"=>2.07])),
"two args: 1 and 2,07" "two args: 1 and 2.07"
); );
} }
} }

View file

@ -73,9 +73,11 @@ export async function setupI18n(): Promise<I18n> {
} }
const json = await resp.json(); const json = await resp.json();
for (const resourceText of json.resources) { for (const i in json.resources) {
const bundle = new FluentBundle(json.langs); const text = json.resources[i];
const resource = new FluentResource(resourceText); const lang = json.langs[i];
const bundle = new FluentBundle([lang, "en-US"]);
const resource = new FluentResource(text);
bundle.addResource(resource); bundle.addResource(resource);
i18n.bundles.push(bundle); i18n.bundles.push(bundle);
} }