update to latest fluent-rs and add basic locale-aware decimals

- git version pinned at the moment until the concurrency fix
lands in 0.10.2
- currently float values are hard-coded at 2 decimal places;
we should switch to using NUMBER() in the future
This commit is contained in:
Damien Elmes 2020-02-20 10:42:16 +10:00
parent 598226a5c0
commit 370bb38b8b
5 changed files with 112 additions and 16 deletions

View file

@ -299,6 +299,6 @@ message TranslateStringIn {
message TranslateArgValue {
oneof value {
string str = 1;
string number = 2;
double number = 2;
}
}

View file

@ -335,7 +335,7 @@ class RustBackend:
if isinstance(v, str):
args[k] = pb.TranslateArgValue(str=v)
else:
args[k] = pb.TranslateArgValue(number=str(v))
args[k] = pb.TranslateArgValue(number=v)
return self._run_command(
pb.BackendInput(

View file

@ -30,8 +30,10 @@ serde_tuple = "0.4.0"
coarsetime = "=0.1.11"
utime = "0.2.1"
serde-aux = "0.6.1"
unic-langid = { version = "0.7.0", features = ["macros"] }
fluent = "0.9.1"
unic-langid = { version = "0.8.0", features = ["macros"] }
fluent = { git = "https://github.com/projectfluent/fluent-rs#6a711ca1" }
intl-memoizer = { git = "https://github.com/projectfluent/fluent-rs#6a711ca1" }
num-format = "0.4.0"
[target.'cfg(target_vendor="apple")'.dependencies]
rusqlite = { version = "0.21.0", features = ["trace"] }

View file

@ -404,7 +404,7 @@ fn translate_arg_to_fluent_val(arg: &pb::TranslateArgValue) -> FluentValue {
match &arg.value {
Some(val) => match val {
V::Str(s) => FluentValue::String(s.into()),
V::Number(s) => FluentValue::Number(s.into()),
V::Number(f) => FluentValue::Number(f.into()),
},
None => FluentValue::String("".into()),
}

View file

@ -1,8 +1,10 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use fluent::{FluentArgs, FluentBundle, FluentResource};
use fluent::{FluentArgs, FluentBundle, FluentResource, FluentValue};
use intl_memoizer::IntlLangMemoizer;
use log::{error, warn};
use num_format::Locale;
use std::borrow::Cow;
use std::fs;
use std::path::{Path, PathBuf};
@ -33,13 +35,13 @@ pub use tr_strs;
/// Otherwise, try the language alone (eg en).
/// If neither folder exists, return None.
fn lang_folder(lang: LanguageIdentifier, ftl_folder: &Path) -> Option<PathBuf> {
if let Some(region) = lang.get_region() {
let path = ftl_folder.join(format!("{}_{}", lang.get_language(), region));
if let Some(region) = lang.region() {
let path = ftl_folder.join(format!("{}_{}", lang.language(), region));
if fs::metadata(&path).is_ok() {
return Some(path);
}
}
let path = ftl_folder.join(lang.get_language());
let path = ftl_folder.join(lang.language());
if fs::metadata(&path).is_ok() {
Some(path)
} else {
@ -142,7 +144,7 @@ impl I18n {
}
struct I18nInner {
// all preferred languages of the user, used for date/time processing
// all preferred languages of the user, used for determine number format
langs: Vec<LanguageIdentifier>,
// the available ftl folder subset of the user's preferred languages
available_ftl_folders: Vec<PathBuf>,
@ -167,6 +169,18 @@ pub struct I18nCategory {
bundles: Vec<FluentBundle<FluentResource>>,
}
fn set_bundle_formatter_for_langs<T>(bundle: &mut FluentBundle<T>, langs: &[LanguageIdentifier]) {
let num_formatter = NumberFormatter::new(langs);
let formatter = move |val: &FluentValue, _intls: &Mutex<IntlLangMemoizer>| -> Option<String> {
match val {
FluentValue::Number(n) => Some(num_formatter.format(n.value)),
_ => None,
}
};
bundle.set_formatter(Some(formatter));
}
impl I18nCategory {
pub fn new(langs: &[LanguageIdentifier], preferred: &[PathBuf], group: StringsGroup) -> Self {
let mut bundles = Vec::with_capacity(preferred.len() + 1);
@ -176,6 +190,7 @@ impl I18nCategory {
if cfg!(test) {
bundle.set_use_isolating(false);
}
set_bundle_formatter_for_langs(&mut bundle, langs);
bundles.push(bundle);
} else {
error!("Failed to create bundle for {:?} {:?}", ftl_folder, group);
@ -187,6 +202,7 @@ impl I18nCategory {
if cfg!(test) {
fallback_bundle.set_use_isolating(false);
}
set_bundle_formatter_for_langs(&mut fallback_bundle, langs);
bundles.push(fallback_bundle);
@ -230,11 +246,76 @@ impl I18nCategory {
}
}
fn first_available_num_format_locale(langs: &[LanguageIdentifier]) -> Option<Locale> {
for lang in langs {
if let Some(locale) = num_format_locale(lang) {
return Some(locale);
}
}
None
}
// try to locate a num_format locale for a given language identifier
fn num_format_locale(lang: &LanguageIdentifier) -> Option<Locale> {
// region provided?
if let Some(region) = lang.region() {
let code = format!("{}_{}", lang.language(), region);
if let Ok(locale) = Locale::from_name(code) {
return Some(locale);
}
}
// try the language alone
Locale::from_name(lang.language()).ok()
}
struct NumberFormatter {
decimal_separator: &'static str,
}
impl NumberFormatter {
fn new(langs: &[LanguageIdentifier]) -> Self {
if let Some(locale) = first_available_num_format_locale(langs) {
Self {
decimal_separator: locale.decimal(),
}
} else {
// fallback on English defaults
Self {
decimal_separator: ".",
}
}
}
fn format(&self, num: f64) -> String {
// is it an integer?
if (num - num.round()).abs() < std::f64::EPSILON {
num.to_string()
} else {
// currently we hard-code to 2 decimal places
let mut formatted = format!("{:.2}", num);
if self.decimal_separator != "." {
formatted = formatted.replace(".", self.decimal_separator)
}
formatted
}
}
}
#[cfg(test)]
mod test {
use crate::i18n::StringsGroup;
use crate::i18n::{tr_args, I18n};
use crate::i18n::{NumberFormatter, StringsGroup};
use std::path::PathBuf;
use unic_langid::langid;
#[test]
fn numbers() {
let fmter = NumberFormatter::new(&[]);
assert_eq!(&fmter.format(1.0), "1");
assert_eq!(&fmter.format(1.007), "1.01");
let fmter = NumberFormatter::new(&[langid!("pl-PL")]);
assert_eq!(&fmter.format(1.007), "1,01");
}
#[test]
fn i18n() {
@ -248,8 +329,8 @@ mod test {
);
assert_eq!(
cat.trn("two-args-key", tr_args!["one"=>1, "two"=>"2"]),
"two args: 1 and 2"
cat.trn("two-args-key", tr_args!["one"=>1.1, "two"=>"2"]),
"two args: 1.10 and 2"
);
// commented out to avoid scary warning during unit tests
@ -258,13 +339,17 @@ mod test {
// "two args: testing error reporting and {$two}"
// );
assert_eq!(cat.trn("plural", tr_args!["hats"=>1]), "You have 1 hat.");
assert_eq!(cat.trn("plural", tr_args!["hats"=>1.0]), "You have 1 hat.");
assert_eq!(
cat.trn("plural", tr_args!["hats"=>1.1]),
"You have 1.10 hats."
);
assert_eq!(cat.trn("plural", tr_args!["hats"=>3]), "You have 3 hats.");
// Other language
// Another language
let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
d.push("tests/support");
let i18n = I18n::new(&["ja_JP"], d);
let i18n = I18n::new(&["ja_JP"], &d);
let cat = i18n.get(StringsGroup::Test);
assert_eq!(cat.tr("valid-key"), "キー");
assert_eq!(cat.tr("only-in-english"), "not translated");
@ -277,5 +362,14 @@ mod test {
cat.trn("two-args-key", tr_args!["one"=>1, "two"=>"2"]),
"1と2"
);
// Decimal separator
let i18n = I18n::new(&["pl-PL"], &d);
let cat = i18n.get(StringsGroup::Test);
// falls back on English, but with Polish separators
assert_eq!(
cat.trn("two-args-key", tr_args!["one"=>1, "two"=>2.07]),
"two args: 1 and 2,07"
);
}
}