diff --git a/Cargo.lock b/Cargo.lock index 3d2918630..33e9389ee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2087,6 +2087,7 @@ version = "0.1.0" dependencies = [ "fluent 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", "fluent-bundle 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "fluent-locale 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", "fluent-syntax 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", "handlebars 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/Cargo.toml b/Cargo.toml index c000f38d3..3ba236b8b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ lazy_static = "1.2.0" fluent = "0.5" fluent-bundle = "0.6.0" fluent-syntax = "0.9.0" +fluent-locale = "0.4.1" rand = "0.6" regex = "1" rocket = "0.4.1" diff --git a/src/i18n.rs b/src/i18n.rs index 02995f07c..a96caf0bb 100644 --- a/src/i18n.rs +++ b/src/i18n.rs @@ -15,24 +15,51 @@ use std::io::prelude::*; use std::path::Path; use fluent_bundle::{FluentBundle, FluentResource, FluentValue}; +use fluent_locale::negotiate_languages; lazy_static! { static ref CORE_RESOURCE: FluentResource = read_from_file("./locales/core.ftl").expect("cannot find core.ftl"); static ref RESOURCES: HashMap> = build_resources(); static ref BUNDLES: HashMap> = build_bundles(); + static ref LOCALES: Vec<&'static str> = RESOURCES.iter().map(|(l, _)| &**l).collect(); + static ref FALLBACKS: HashMap> = build_fallbacks(); +} + +pub fn build_fallbacks() -> HashMap> { + LOCALES + .iter() + .map(|locale| { + ( + locale.to_string(), + negotiate_languages( + &[locale], + &LOCALES, + None, + &fluent_locale::NegotiationStrategy::Filtering, + ) + .into_iter() + .map(|x| x.to_string()) + .collect(), + ) + }) + .collect() } pub struct I18NHelper { bundles: &'static HashMap>, + fallbacks: &'static HashMap>, } impl I18NHelper { pub fn new() -> Self { - Self { bundles: &*BUNDLES } + Self { + bundles: &*BUNDLES, + fallbacks: &*FALLBACKS, + } } - pub fn lookup( + pub fn lookup_single_language( &self, lang: &str, text_id: &str, @@ -54,23 +81,41 @@ impl I18NHelper { panic!("Unknown language {}", lang) } } - pub fn lookup_with_fallback( + + // Traverse the fallback chain, + pub fn lookup( &self, lang: &str, text_id: &str, args: Option<&HashMap<&str, FluentValue>>, ) -> String { - if let Some(val) = self.lookup(lang, text_id, args) { - val - } else if lang != "en-US" { - if let Some(val) = self.lookup("en-US", text_id, args) { - val - } else { - format!("Unknown localization {}", text_id) + for l in self.fallbacks.get(lang).expect("language not found") { + if let Some(val) = self.lookup_single_language(l, text_id, args) { + return val; + } + } + if lang != "en-US" { + if let Some(val) = self.lookup_single_language("en-US", text_id, args) { + return val; } - } else { - format!("Unknown localization {}", text_id) } + format!("Unknown localization {}", text_id) + } + + // Don't fall back to English + pub fn lookup_no_english( + &self, + lang: &str, + text_id: &str, + args: Option<&HashMap<&str, FluentValue>>, + ) -> Option { + for l in self.fallbacks.get(lang).expect("language not found") { + if let Some(val) = self.lookup_single_language(l, text_id, args) { + return Some(val); + } + } + + None } } @@ -173,7 +218,7 @@ impl HelperDef for I18NHelper { .as_bool() .expect("Pontoon must be boolean"); - let response = self.lookup_with_fallback(lang, &id, args.as_ref()); + let response = self.lookup(lang, &id, args.as_ref()); if pontoon { out.write(&format!("", id)) .map_err(RenderError::with)?; @@ -264,7 +309,7 @@ impl HelperDef for TeamHelper { if lang == "en-US" { let english = team["website_data"][id].as_str().unwrap(); out.write(&english).map_err(RenderError::with)?; - } else if let Some(value) = self.i18n.lookup(lang, &fluent_id, None) { + } else if let Some(value) = self.i18n.lookup_no_english(lang, &fluent_id, None) { out.write(&value).map_err(RenderError::with)?; } else { let english = team["website_data"][id].as_str().unwrap();