Skip to content

Commit 80a88fb

Browse files
use icu4x for number formatting
1 parent b2a8f7d commit 80a88fb

File tree

6 files changed

+118
-19
lines changed

6 files changed

+118
-19
lines changed

fluent-bundle/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,12 @@ include = [
2424
]
2525

2626
[dependencies]
27+
fixed_decimal = { version = "0.5.1", features = ["ryu"] }
2728
fluent-langneg.workspace = true
2829
fluent-syntax.workspace = true
30+
icu_decimal = "1"
31+
icu_locid = "1"
32+
icu_provider = { version = "1", features = ["sync"] }
2933
intl_pluralrules.workspace = true
3034
rustc-hash.workspace = true
3135
unic-langid.workspace = true

fluent-bundle/src/bundle.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
//! internationalization formatters, functions, scopeironmental variables and are expected to be used
55
//! together.
66
7+
use icu_provider::AnyProvider;
78
use rustc_hash::FxHashMap;
89
use std::borrow::Borrow;
910
use std::borrow::Cow;
@@ -25,6 +26,8 @@ use crate::resolver::{ResolveValue, Scope, WriteValue};
2526
use crate::resource::FluentResource;
2627
use crate::types::FluentValue;
2728

29+
pub type IcuDataProvider = Box<dyn AnyProvider>;
30+
2831
/// A collection of localization messages for a single locale, which are meant
2932
/// to be used together in a single view, widget or any other UI abstraction.
3033
///
@@ -141,6 +144,7 @@ pub struct FluentBundle<R, M> {
141144
pub(crate) use_isolating: bool,
142145
pub(crate) transform: Option<fn(&str) -> Cow<str>>,
143146
pub(crate) formatter: Option<fn(&FluentValue, &M) -> Option<String>>,
147+
pub(crate) icu_data_provider: Option<IcuDataProvider>,
144148
}
145149

146150
impl<R, M> FluentBundle<R, M> {
@@ -641,6 +645,7 @@ impl<R> FluentBundle<R, IntlLangMemoizer> {
641645
use_isolating: true,
642646
transform: None,
643647
formatter: None,
648+
icu_data_provider: None,
644649
}
645650
}
646651
}

fluent-bundle/src/concurrent.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ impl<R> FluentBundle<R> {
3939
use_isolating: true,
4040
transform: None,
4141
formatter: None,
42+
icu_data_provider: None,
4243
}
4344
}
4445
}

fluent-bundle/src/types/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ impl<'source> FluentValue<'source> {
229229
}
230230
match self {
231231
FluentValue::String(s) => w.write_str(s),
232-
FluentValue::Number(n) => w.write_str(&n.as_string()),
232+
FluentValue::Number(n) => w.write_str(&n.as_string(scope.bundle)),
233233
FluentValue::Custom(s) => w.write_str(&scope.bundle.intls.stringify_value(&**s)),
234234
FluentValue::Error => Ok(()),
235235
FluentValue::None => Ok(()),
@@ -251,7 +251,7 @@ impl<'source> FluentValue<'source> {
251251
}
252252
match self {
253253
FluentValue::String(s) => s.clone(),
254-
FluentValue::Number(n) => n.as_string(),
254+
FluentValue::Number(n) => n.as_string(scope.bundle),
255255
FluentValue::Custom(s) => scope.bundle.intls.stringify_value(&**s),
256256
FluentValue::Error => "".into(),
257257
FluentValue::None => "".into(),

fluent-bundle/src/types/number.rs

Lines changed: 103 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,19 @@ use std::convert::TryInto;
33
use std::default::Default;
44
use std::str::FromStr;
55

6+
use fixed_decimal::{DoublePrecision, FixedDecimal};
7+
use icu_decimal::options::{FixedDecimalFormatterOptions, GroupingStrategy};
8+
use icu_decimal::provider::DecimalSymbolsV1Marker;
9+
use icu_decimal::{DecimalError, FixedDecimalFormatter};
10+
use icu_locid::{LanguageIdentifier as IcuLanguageIdentifier, ParserError};
11+
use icu_provider::prelude::*;
12+
use intl_memoizer::Memoizable;
613
use intl_pluralrules::operands::PluralOperands;
14+
use unic_langid::LanguageIdentifier;
715

816
use crate::args::FluentArgs;
17+
use crate::bundle::{FluentBundle, IcuDataProvider};
18+
use crate::memoizer::MemoizerKind;
919
use crate::types::FluentValue;
1020

1121
#[derive(Debug, Default, Copy, Clone, Hash, PartialEq, Eq)]
@@ -145,22 +155,38 @@ impl FluentNumber {
145155
Self { value, options }
146156
}
147157

148-
pub fn as_string(&self) -> Cow<'static, str> {
149-
let mut val = self.value.to_string();
158+
pub fn as_string<R, M: MemoizerKind>(&self, bundle: &FluentBundle<R, M>) -> Cow<'static, str> {
159+
let fixed_decimal = self.as_fixed_decimal();
160+
let options = FormatterOptions {
161+
use_grouping: self.options.use_grouping,
162+
};
163+
if let Some(data_provider) = &bundle.icu_data_provider {
164+
let formatted = bundle
165+
.intls
166+
.with_try_get_threadsafe::<NumberFormatter, _, _>(
167+
(options,),
168+
data_provider,
169+
|formatter| formatter.0.format_to_string(&fixed_decimal),
170+
)
171+
.unwrap();
172+
return formatted.into();
173+
}
174+
175+
fixed_decimal.to_string().into()
176+
}
177+
178+
fn as_fixed_decimal(&self) -> FixedDecimal {
179+
let mut fixed_decimal =
180+
FixedDecimal::try_from_f64(self.value, DoublePrecision::Floating).unwrap();
181+
150182
if let Some(minfd) = self.options.minimum_fraction_digits {
151-
if let Some(pos) = val.find('.') {
152-
let frac_num = val.len() - pos - 1;
153-
let missing = if frac_num > minfd {
154-
0
155-
} else {
156-
minfd - frac_num
157-
};
158-
val = format!("{}{}", val, "0".repeat(missing));
159-
} else {
160-
val = format!("{}.{}", val, "0".repeat(minfd));
161-
}
183+
fixed_decimal.pad_end(-(minfd as i16));
162184
}
163-
val.into()
185+
fixed_decimal
186+
}
187+
188+
pub fn as_string_basic(&self) -> String {
189+
self.as_fixed_decimal().to_string()
164190
}
165191
}
166192

@@ -250,8 +276,59 @@ from_num!(i8 i16 i32 i64 i128 isize);
250276
from_num!(u8 u16 u32 u64 u128 usize);
251277
from_num!(f32 f64);
252278

279+
pub type NumberFormatProvider = Box<dyn DataProvider<DecimalSymbolsV1Marker>>;
280+
281+
#[derive(Debug, Eq, PartialEq, Clone, Default, Hash)]
282+
struct FormatterOptions {
283+
use_grouping: bool,
284+
}
285+
286+
struct NumberFormatter(FixedDecimalFormatter);
287+
288+
#[derive(Debug)]
289+
#[allow(dead_code)]
290+
enum NumberFormatterError {
291+
ParserError(ParserError),
292+
DecimalError(DecimalError),
293+
}
294+
295+
impl Memoizable for NumberFormatter {
296+
type Args = (FormatterOptions,);
297+
type Error = NumberFormatterError;
298+
type DataProvider = IcuDataProvider;
299+
300+
fn construct(
301+
lang: LanguageIdentifier,
302+
args: Self::Args,
303+
data_provider: &Self::DataProvider,
304+
) -> Result<Self, Self::Error> {
305+
let locale = to_icu_lang_id(lang).map_err(NumberFormatterError::ParserError)?;
306+
307+
let mut options: FixedDecimalFormatterOptions = Default::default();
308+
options.grouping_strategy = match args.0.use_grouping {
309+
true => GroupingStrategy::Always,
310+
false => GroupingStrategy::Auto,
311+
};
312+
313+
let inner = FixedDecimalFormatter::try_new_with_any_provider(
314+
data_provider,
315+
&locale.into(),
316+
options,
317+
)
318+
.map_err(NumberFormatterError::DecimalError)?;
319+
Ok(NumberFormatter(inner))
320+
}
321+
}
322+
323+
fn to_icu_lang_id(lang: LanguageIdentifier) -> Result<IcuLanguageIdentifier, ParserError> {
324+
return IcuLanguageIdentifier::try_from_locale_bytes(lang.to_string().as_bytes());
325+
}
326+
253327
#[cfg(test)]
254328
mod tests {
329+
use super::to_icu_lang_id;
330+
use unic_langid::langid;
331+
255332
use crate::types::FluentValue;
256333

257334
#[test]
@@ -261,4 +338,16 @@ mod tests {
261338
let z: FluentValue = y.into();
262339
assert_eq!(z, FluentValue::try_number("1"));
263340
}
341+
342+
#[test]
343+
fn lang_to_icu() {
344+
assert_eq!(
345+
to_icu_lang_id(langid!("en-US")).unwrap(),
346+
icu_locid::langid!("en-US")
347+
);
348+
assert_eq!(
349+
to_icu_lang_id(langid!("pl")).unwrap(),
350+
icu_locid::langid!("pl")
351+
);
352+
}
264353
}

fluent-bundle/tests/types_test.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -123,18 +123,18 @@ fn fluent_number_style() {
123123
assert!(!fno.use_grouping);
124124

125125
let num = FluentNumber::new(0.2, FluentNumberOptions::default());
126-
assert_eq!(num.as_string(), "0.2");
126+
assert_eq!(num.as_string_basic(), "0.2");
127127

128128
let opts = FluentNumberOptions {
129129
minimum_fraction_digits: Some(3),
130130
..Default::default()
131131
};
132132

133133
let num = FluentNumber::new(0.2, opts.clone());
134-
assert_eq!(num.as_string(), "0.200");
134+
assert_eq!(num.as_string_basic(), "0.200");
135135

136136
let num = FluentNumber::new(2.0, opts);
137-
assert_eq!(num.as_string(), "2.000");
137+
assert_eq!(num.as_string_basic(), "2.000");
138138
}
139139

140140
#[test]

0 commit comments

Comments
 (0)