mirror of
https://github.com/ankitects/anki.git
synced 2025-09-22 07:52:24 -04:00
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:
parent
598226a5c0
commit
370bb38b8b
5 changed files with 112 additions and 16 deletions
|
@ -299,6 +299,6 @@ message TranslateStringIn {
|
||||||
message TranslateArgValue {
|
message TranslateArgValue {
|
||||||
oneof value {
|
oneof value {
|
||||||
string str = 1;
|
string str = 1;
|
||||||
string number = 2;
|
double number = 2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -335,7 +335,7 @@ class RustBackend:
|
||||||
if isinstance(v, str):
|
if isinstance(v, str):
|
||||||
args[k] = pb.TranslateArgValue(str=v)
|
args[k] = pb.TranslateArgValue(str=v)
|
||||||
else:
|
else:
|
||||||
args[k] = pb.TranslateArgValue(number=str(v))
|
args[k] = pb.TranslateArgValue(number=v)
|
||||||
|
|
||||||
return self._run_command(
|
return self._run_command(
|
||||||
pb.BackendInput(
|
pb.BackendInput(
|
||||||
|
|
|
@ -30,8 +30,10 @@ serde_tuple = "0.4.0"
|
||||||
coarsetime = "=0.1.11"
|
coarsetime = "=0.1.11"
|
||||||
utime = "0.2.1"
|
utime = "0.2.1"
|
||||||
serde-aux = "0.6.1"
|
serde-aux = "0.6.1"
|
||||||
unic-langid = { version = "0.7.0", features = ["macros"] }
|
unic-langid = { version = "0.8.0", features = ["macros"] }
|
||||||
fluent = "0.9.1"
|
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]
|
[target.'cfg(target_vendor="apple")'.dependencies]
|
||||||
rusqlite = { version = "0.21.0", features = ["trace"] }
|
rusqlite = { version = "0.21.0", features = ["trace"] }
|
||||||
|
|
|
@ -404,7 +404,7 @@ fn translate_arg_to_fluent_val(arg: &pb::TranslateArgValue) -> FluentValue {
|
||||||
match &arg.value {
|
match &arg.value {
|
||||||
Some(val) => match val {
|
Some(val) => match val {
|
||||||
V::Str(s) => FluentValue::String(s.into()),
|
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()),
|
None => FluentValue::String("".into()),
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
// Copyright: Ankitects Pty Ltd and contributors
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
// 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 log::{error, warn};
|
||||||
|
use num_format::Locale;
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
@ -33,13 +35,13 @@ pub use tr_strs;
|
||||||
/// 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: LanguageIdentifier, ftl_folder: &Path) -> Option<PathBuf> {
|
fn lang_folder(lang: LanguageIdentifier, ftl_folder: &Path) -> Option<PathBuf> {
|
||||||
if let Some(region) = lang.get_region() {
|
if let Some(region) = lang.region() {
|
||||||
let path = ftl_folder.join(format!("{}_{}", lang.get_language(), region));
|
let path = ftl_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.get_language());
|
let path = ftl_folder.join(lang.language());
|
||||||
if fs::metadata(&path).is_ok() {
|
if fs::metadata(&path).is_ok() {
|
||||||
Some(path)
|
Some(path)
|
||||||
} else {
|
} else {
|
||||||
|
@ -142,7 +144,7 @@ impl I18n {
|
||||||
}
|
}
|
||||||
|
|
||||||
struct I18nInner {
|
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>,
|
langs: Vec<LanguageIdentifier>,
|
||||||
// the available ftl folder subset of the user's preferred languages
|
// the available ftl folder subset of the user's preferred languages
|
||||||
available_ftl_folders: Vec<PathBuf>,
|
available_ftl_folders: Vec<PathBuf>,
|
||||||
|
@ -167,6 +169,18 @@ pub struct I18nCategory {
|
||||||
bundles: Vec<FluentBundle<FluentResource>>,
|
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 {
|
impl I18nCategory {
|
||||||
pub fn new(langs: &[LanguageIdentifier], preferred: &[PathBuf], group: StringsGroup) -> Self {
|
pub fn new(langs: &[LanguageIdentifier], preferred: &[PathBuf], group: StringsGroup) -> Self {
|
||||||
let mut bundles = Vec::with_capacity(preferred.len() + 1);
|
let mut bundles = Vec::with_capacity(preferred.len() + 1);
|
||||||
|
@ -176,6 +190,7 @@ impl I18nCategory {
|
||||||
if cfg!(test) {
|
if cfg!(test) {
|
||||||
bundle.set_use_isolating(false);
|
bundle.set_use_isolating(false);
|
||||||
}
|
}
|
||||||
|
set_bundle_formatter_for_langs(&mut bundle, langs);
|
||||||
bundles.push(bundle);
|
bundles.push(bundle);
|
||||||
} else {
|
} else {
|
||||||
error!("Failed to create bundle for {:?} {:?}", ftl_folder, group);
|
error!("Failed to create bundle for {:?} {:?}", ftl_folder, group);
|
||||||
|
@ -187,6 +202,7 @@ impl I18nCategory {
|
||||||
if cfg!(test) {
|
if cfg!(test) {
|
||||||
fallback_bundle.set_use_isolating(false);
|
fallback_bundle.set_use_isolating(false);
|
||||||
}
|
}
|
||||||
|
set_bundle_formatter_for_langs(&mut fallback_bundle, langs);
|
||||||
|
|
||||||
bundles.push(fallback_bundle);
|
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)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use crate::i18n::StringsGroup;
|
|
||||||
use crate::i18n::{tr_args, I18n};
|
use crate::i18n::{tr_args, I18n};
|
||||||
|
use crate::i18n::{NumberFormatter, StringsGroup};
|
||||||
use std::path::PathBuf;
|
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]
|
#[test]
|
||||||
fn i18n() {
|
fn i18n() {
|
||||||
|
@ -248,8 +329,8 @@ mod test {
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
cat.trn("two-args-key", tr_args!["one"=>1, "two"=>"2"]),
|
cat.trn("two-args-key", tr_args!["one"=>1.1, "two"=>"2"]),
|
||||||
"two args: 1 and 2"
|
"two args: 1.10 and 2"
|
||||||
);
|
);
|
||||||
|
|
||||||
// commented out to avoid scary warning during unit tests
|
// commented out to avoid scary warning during unit tests
|
||||||
|
@ -258,13 +339,17 @@ mod test {
|
||||||
// "two args: testing error reporting and {$two}"
|
// "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.");
|
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"));
|
let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||||
d.push("tests/support");
|
d.push("tests/support");
|
||||||
let i18n = I18n::new(&["ja_JP"], d);
|
let i18n = I18n::new(&["ja_JP"], &d);
|
||||||
let cat = i18n.get(StringsGroup::Test);
|
let cat = i18n.get(StringsGroup::Test);
|
||||||
assert_eq!(cat.tr("valid-key"), "キー");
|
assert_eq!(cat.tr("valid-key"), "キー");
|
||||||
assert_eq!(cat.tr("only-in-english"), "not translated");
|
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"]),
|
cat.trn("two-args-key", tr_args!["one"=>1, "two"=>"2"]),
|
||||||
"1と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"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue