Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ICU4x number formatting #303

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ serde_json = "1.0"
thiserror = "1.0"
tokio = "1.0"
unic-langid = "0.9"
icu_locid = "1.4"
icu_provider = "1.4"
icu_decimal = "1.4"
icu = { version = "1.4", default-features = false }
fixed_decimal = "0.5"

fluent-bundle = { version = "0.15.3", path = "fluent-bundle" }
fluent-fallback = { version = "0.7.1", path = "fluent-fallback" }
Expand Down
5 changes: 5 additions & 0 deletions fluent-bundle/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,13 @@ include = [
]

[dependencies]
fixed_decimal = { workspace = true, features = ["ryu"] }
fluent-langneg.workspace = true
fluent-syntax.workspace = true
icu_decimal.workspace = true
icu_locid.workspace = true
icu_provider = { workspace = true, features = ["sync"] }
icu.workspace = true
intl_pluralrules.workspace = true
rustc-hash.workspace = true
unic-langid.workspace = true
Expand Down
10 changes: 8 additions & 2 deletions fluent-bundle/examples/custom_type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ impl FluentType for DateTime {
}
fn as_string(&self, intls: &intl_memoizer::IntlLangMemoizer) -> std::borrow::Cow<'static, str> {
intls
.with_try_get::<DateTimeFormatter, _, _>((self.options.clone(),), |dtf| {
.with_try_get::<DateTimeFormatter, _, _>((self.options.clone(),), &(), |dtf| {
dtf.format(self.epoch).into()
})
.expect("Failed to format a date.")
Expand Down Expand Up @@ -138,7 +138,13 @@ impl DateTimeFormatter {
impl Memoizable for DateTimeFormatter {
type Args = (DateTimeOptions,);
type Error = ();
fn construct(lang: LanguageIdentifier, args: Self::Args) -> Result<Self, Self::Error> {
type DataProvider = ();

fn construct(
lang: LanguageIdentifier,
args: Self::Args,
_: &Self::DataProvider,
) -> Result<Self, Self::Error> {
Self::new(lang, args.0)
}
}
Expand Down
18 changes: 16 additions & 2 deletions fluent-bundle/src/bundle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
//! internationalization formatters, functions, scopeironmental variables and are expected to be used
//! together.

use icu_provider::AnyProvider;
use rustc_hash::FxHashMap;
use std::borrow::Borrow;
use std::borrow::Cow;
Expand All @@ -25,6 +26,8 @@ use crate::resolver::{ResolveValue, Scope, WriteValue};
use crate::resource::FluentResource;
use crate::types::FluentValue;

pub type IcuDataProvider = Box<dyn AnyProvider>;

/// A collection of localization messages for a single locale, which are meant
/// to be used together in a single view, widget or any other UI abstraction.
///
Expand Down Expand Up @@ -141,6 +144,7 @@ pub struct FluentBundle<R, M> {
pub(crate) use_isolating: bool,
pub(crate) transform: Option<fn(&str) -> Cow<str>>,
pub(crate) formatter: Option<fn(&FluentValue, &M) -> Option<String>>,
pub(crate) icu_data_provider: Option<IcuDataProvider>,
}

impl<R, M> FluentBundle<R, M> {
Expand Down Expand Up @@ -548,6 +552,10 @@ impl<R, M> FluentBundle<R, M> {
}
}

pub fn set_icu_data_provider(&mut self, provider: IcuDataProvider) {
self.icu_data_provider = Some(provider);
}

/// Adds the builtin functions described in the [FTL syntax guide] to the bundle, making them
/// available in messages.
///
Expand Down Expand Up @@ -641,6 +649,7 @@ impl<R> FluentBundle<R, IntlLangMemoizer> {
use_isolating: true,
transform: None,
formatter: None,
icu_data_provider: None,
}
}
}
Expand All @@ -653,14 +662,19 @@ impl crate::memoizer::MemoizerKind for IntlLangMemoizer {
Self::new(lang)
}

fn with_try_get_threadsafe<I, R, U>(&self, args: I::Args, cb: U) -> Result<R, I::Error>
fn with_try_get_threadsafe<I, R, U>(
&self,
args: I::Args,
data_provider: &I::DataProvider,
cb: U,
) -> Result<R, I::Error>
where
Self: Sized,
I: intl_memoizer::Memoizable + Send + Sync + 'static,
I::Args: Send + Sync + 'static,
U: FnOnce(&I) -> R,
{
self.with_try_get(args, cb)
self.with_try_get(args, data_provider, cb)
}

fn stringify_value(
Expand Down
10 changes: 8 additions & 2 deletions fluent-bundle/src/concurrent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ impl<R> FluentBundle<R> {
use_isolating: true,
transform: None,
formatter: None,
icu_data_provider: None,
}
}
}
Expand All @@ -51,14 +52,19 @@ impl MemoizerKind for IntlLangMemoizer {
Self::new(lang)
}

fn with_try_get_threadsafe<I, R, U>(&self, args: I::Args, cb: U) -> Result<R, I::Error>
fn with_try_get_threadsafe<I, R, U>(
&self,
args: I::Args,
data_provider: &I::DataProvider,
cb: U,
) -> Result<R, I::Error>
where
Self: Sized,
I: Memoizable + Send + Sync + 'static,
I::Args: Send + Sync + 'static,
U: FnOnce(&I) -> R,
{
self.with_try_get(args, cb)
self.with_try_get(args, data_provider, cb)
}

fn stringify_value(&self, value: &dyn FluentType) -> std::borrow::Cow<'static, str> {
Expand Down
2 changes: 2 additions & 0 deletions fluent-bundle/src/icu4x_data/any.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// @generated
impl_any_provider ! (BakedDataProvider) ;
2 changes: 2 additions & 0 deletions fluent-bundle/src/icu4x_data/macros.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// @generated
# [doc = r" Marks a type as a data provider. You can then use macros like"] # [doc = r" `impl_core_helloworld_v1` to add implementations."] # [doc = r""] # [doc = r" ```ignore"] # [doc = r" struct MyProvider;"] # [doc = r" const _: () = {"] # [doc = r#" include!("path/to/generated/macros.rs");"#] # [doc = r" make_provider!(MyProvider);"] # [doc = r" impl_core_helloworld_v1!(MyProvider);"] # [doc = r" }"] # [doc = r" ```"] # [doc (hidden)] # [macro_export] macro_rules ! __make_provider { ($ name : ty) => { # [clippy :: msrv = "1.67"] impl $ name { # [doc (hidden)] # [allow (dead_code)] pub const MUST_USE_MAKE_PROVIDER_MACRO : () = () ; } } ; } # [doc (inline)] pub use __make_provider as make_provider ; # [macro_use] # [path = "macros/decimal_symbols_v1.rs.data"] mod decimal_symbols_v1 ; # [doc (inline)] pub use __impl_decimal_symbols_v1 as impl_decimal_symbols_v1 ;

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions fluent-bundle/src/icu4x_data/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// @generated
include ! ("macros.rs") ; macro_rules ! impl_data_provider { ($ provider : ty) => { make_provider ! ($ provider) ; impl_decimal_symbols_v1 ! ($ provider) ; } ; } # [allow (unused_macros)] macro_rules ! impl_any_provider { ($ provider : ty) => { # [clippy :: msrv = "1.67"] impl icu_provider :: AnyProvider for $ provider { fn load_any (& self , key : icu_provider :: DataKey , req : icu_provider :: DataRequest) -> Result < icu_provider :: AnyResponse , icu_provider :: DataError > { match key . hashed () { h if h == < icu::decimal :: provider :: DecimalSymbolsV1Marker as icu_provider :: KeyedDataMarker > :: KEY . hashed () => icu_provider :: DataProvider :: < icu::decimal :: provider :: DecimalSymbolsV1Marker > :: load (self , req) . map (icu_provider :: DataResponse :: wrap_into_any_response) , _ => Err (icu_provider :: DataErrorKind :: MissingDataKey . with_req (key , req)) , } } } } } # [clippy :: msrv = "1.67"] pub struct BakedDataProvider ; impl_data_provider ! (BakedDataProvider) ;
26 changes: 26 additions & 0 deletions fluent-bundle/src/icu_data_provider.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
extern crate alloc;

// to regerate the icu4x_data module, run from the root of the project:
// cargo build --examples
// rm -r fluent-bundle/src/icu4x_data
// icu4x-datagen --keys-for-bin target/debug/examples/functions --format mod --locales full -o fluent-bundle/src/icu4x_data
// for more information: https://github.com/unicode-org/icu4x/blob/79480dfbafdedf6cc810e4bfb0770f3268ff86ab/tutorials/data_management.md
include!("./icu4x_data/mod.rs");
impl_any_provider!(BakedDataProvider);

/// ICU data provider
///
/// Fluent uses ICU4X data for formatting purposes, like number formatting.
/// Use FluentIcuDataProvider to add all ICU data needed by fluent-bundle.
/// You can bring your own provider if you don't need all locales, or have one in your project already.
///
/// Example:
/// ```
/// use fluent_bundle::{FluentBundle, FluentIcuDataProvider, FluentResource};
/// use unic_langid::langid;
///
/// let langid = langid!("en-US");
/// let mut bundle: FluentBundle<FluentResource> = FluentBundle::new(vec![langid]);
/// bundle.set_icu_data_provider(Box::new(FluentIcuDataProvider));
/// ```
pub use BakedDataProvider as FluentIcuDataProvider;
2 changes: 2 additions & 0 deletions fluent-bundle/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ pub mod bundle;
pub mod concurrent;
mod entry;
mod errors;
mod icu_data_provider;
#[doc(hidden)]
pub mod memoizer;
mod message;
Expand All @@ -122,6 +123,7 @@ pub use args::FluentArgs;
/// [`FluentBundle::new_concurrent`](crate::concurrent::FluentBundle::new_concurrent).
pub type FluentBundle<R> = bundle::FluentBundle<R, intl_memoizer::IntlLangMemoizer>;
pub use errors::FluentError;
pub use icu_data_provider::FluentIcuDataProvider;
pub use message::{FluentAttribute, FluentMessage};
pub use resource::FluentResource;
#[doc(inline)]
Expand Down
7 changes: 6 additions & 1 deletion fluent-bundle/src/memoizer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,12 @@ pub trait MemoizerKind: 'static {
///
/// `U` - The callback that accepts the instance of the intl formatter, and generates
/// some kind of results `R`.
fn with_try_get_threadsafe<I, R, U>(&self, args: I::Args, callback: U) -> Result<R, I::Error>
fn with_try_get_threadsafe<I, R, U>(
&self,
args: I::Args,
data_provider: &I::DataProvider,
callback: U,
) -> Result<R, I::Error>
where
Self: Sized,
I: Memoizable + Send + Sync + 'static,
Expand Down
8 changes: 4 additions & 4 deletions fluent-bundle/src/types/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ impl<'source> FluentValue<'source> {
scope
.bundle
.intls
.with_try_get_threadsafe::<PluralRules, _, _>((r#type,), |pr| {
.with_try_get_threadsafe::<PluralRules, _, _>((r#type,), &(), |pr| {
pr.0.select(b) == Ok(cat)
})
.unwrap()
Expand All @@ -229,7 +229,7 @@ impl<'source> FluentValue<'source> {
}
match self {
FluentValue::String(s) => w.write_str(s),
FluentValue::Number(n) => w.write_str(&n.as_string()),
FluentValue::Number(n) => w.write_str(&n.as_string(scope.bundle)),
FluentValue::Custom(s) => w.write_str(&scope.bundle.intls.stringify_value(&**s)),
FluentValue::Error => Ok(()),
FluentValue::None => Ok(()),
Expand All @@ -251,7 +251,7 @@ impl<'source> FluentValue<'source> {
}
match self {
FluentValue::String(s) => s.clone(),
FluentValue::Number(n) => n.as_string(),
FluentValue::Number(n) => n.as_string(scope.bundle),
FluentValue::Custom(s) => scope.bundle.intls.stringify_value(&**s),
FluentValue::Error => "".into(),
FluentValue::None => "".into(),
Expand All @@ -273,7 +273,7 @@ impl<'source> FluentValue<'source> {
}
match self {
FluentValue::String(s) => s,
FluentValue::Number(n) => n.as_string(),
FluentValue::Number(n) => n.as_string(scope.bundle),
FluentValue::Custom(s) => scope.bundle.intls.stringify_value(s.as_ref()),
FluentValue::Error => "".into(),
FluentValue::None => "".into(),
Expand Down
Loading