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 {
|
||||
oneof value {
|
||||
string str = 1;
|
||||
string number = 2;
|
||||
double number = 2;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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"] }
|
||||
|
|
|
@ -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()),
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue