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

feat: add currency formatter #190

Merged
merged 4 commits into from
Feb 12, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ icu_calendar = { version = "1.5", default-features = false }
icu_list = { version = "1.5", default-features = false }
icu_decimal = { version = "1.5", default-features = false }
icu_locid_transform = { version = "1.5", default-features = false }
icu_experimental = { version = "0.1.0", default_features = false }
tinystr = "0.8.0"

# internal use
tests_common = { path = "./tests/common", version = "0.1.0" }
4 changes: 4 additions & 0 deletions docs/book/src/06_features.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,7 @@ Allow the use of the `list` formatter.
#### `format_nums`

Allow the use of the `number` formatter.

#### `format_currency`

Allow the use of the `currency` formatter.
36 changes: 36 additions & 0 deletions docs/book/src/declare/08_formatters.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,42 @@ let num = move || 100_000;
t!(i18n, number_formatter, num);
```

## Currency (experimental)

```json
{
"currency_formatter": "{{ num, currency }}"
}
```

Will format the currency based on the locale.
The variable should be the same as [number](#number).

Enable the "format_currency" feature to use the number formatter.

### Arguments

There are two arguments at the moment for the currency formatter: `width` and `country_code`, which are based on [`icu_experimental::dimension::currency::options::Width`](https://docs.rs/icu_experimental/0.1.0/icu_experimental/dimension/currency/options/enum.Width.html) and [`icu_experimental::dimension::currency::formatter::CountryCode`](https://docs.rs/icu_experimental/0.1.0/icu_experimental/dimension/currency/formatter/struct.CurrencyCode.html).

`width` values:

- short (default)
- narrow

`country_code` value should be a [currency code](https://www.iban.com/currency-codes), such as USD or EUR. The USD is the default value.

### Example

```rust,ignore
use crate::i18n::*;

let i18n = use_i18n();

let num = move || 100_000;

t!(i18n, currency_formatter, num);
```

## Date

```json
Expand Down
17 changes: 16 additions & 1 deletion leptos_i18n/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ icu_list = { workspace = true, optional = true }
icu_decimal = { workspace = true, optional = true }
typed-builder = "0.20"
fixed_decimal = { workspace = true, optional = true, features = ["ryu"] }
icu_experimental = { workspace = true, optional = true, features = ["ryu"] }
writeable = "0.5"
serde = "1.0"
async-once-cell = { version = "0.5.3", optional = true }
Expand All @@ -46,6 +47,7 @@ icu_compiled_data = [
"icu_calendar?/compiled_data",
"icu_list?/compiled_data",
"icu_decimal?/compiled_data",
"icu_experimental?/compiled_data",
"leptos_i18n_macro/icu_compiled_data",
]
plurals = ["dep:icu_plurals", "dep:icu_provider", "leptos_i18n_macro/plurals"]
Expand All @@ -66,6 +68,12 @@ format_nums = [
"dep:icu_provider",
"leptos_i18n_macro/format_nums",
]
format_currency = [
"format_nums",
"dep:icu_experimental",
"dep:icu_provider",
"leptos_i18n_macro/format_currency",
]
actix = ["ssr", "leptos-use/actix"]
axum = ["ssr", "leptos-use/axum"]
hydrate = [
Expand Down Expand Up @@ -102,7 +110,13 @@ track_locale_files = ["leptos_i18n_macro/track_locale_files"]

[package.metadata."docs.rs"]
# Features needed for the doctests
features = ["plurals", "format_datetime", "format_list", "format_nums"]
features = [
"plurals",
"format_datetime",
"format_list",
"format_nums",
"format_currency",
]


[package.metadata.cargo-all-features]
Expand Down Expand Up @@ -163,4 +177,5 @@ always_include_features = [
"format_datetime",
"format_list",
"format_nums",
"format_currency",
]
7 changes: 5 additions & 2 deletions leptos_i18n/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -171,8 +171,9 @@ pub mod __private {

/// This module contain utilities to create custom ICU providers.
pub mod custom_provider {
pub use crate::macro_helpers::formatting::data_provider::IcuDataProvider;
pub use crate::macro_helpers::formatting::inner::set_icu_data_provider;
pub use crate::macro_helpers::formatting::{
data_provider::IcuDataProvider, inner::set_icu_data_provider,
};
pub use leptos_i18n_macro::IcuDataProvider;
}

Expand All @@ -189,6 +190,8 @@ pub mod reexports {
pub use icu_datetime as datetime;
#[cfg(feature = "format_nums")]
pub use icu_decimal as decimal;
#[cfg(feature = "format_currency")]
pub use icu_experimental::dimension::currency;
#[cfg(feature = "format_list")]
pub use icu_list as list;
#[cfg(feature = "plurals")]
Expand Down
91 changes: 91 additions & 0 deletions leptos_i18n/src/macro_helpers/formatting/currency.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
use super::{IntoFixedDecimal, NumberFormatterInputFn};
use crate::Locale;
use core::fmt::{self, Display};
use icu_experimental::dimension::currency::{
formatter::CurrencyCode, options::Width as CurrencyWidth,
};
use leptos::IntoView;

use serde::{Deserialize, Serialize};
use writeable::Writeable;

// TODO: this struct should be removed in version ICU4x v2
// Reference: https://docs.rs/icu_experimental/0.1.0/icu_experimental/dimension/currency/options/enum.Width.html
// Issue: https://github.com/unicode-org/icu4x/pull/6100
#[derive(Debug, Eq, PartialEq, Clone, Copy, Hash, Serialize, Deserialize)]
#[non_exhaustive]
#[doc(hidden)]
pub enum Width {
#[serde(rename = "short")]
Short,

#[serde(rename = "narrow")]
Narrow,
}

impl Default for Width {
fn default() -> Self {
Self::Short
}
}

impl From<CurrencyWidth> for Width {
fn from(value: CurrencyWidth) -> Self {
match value {
CurrencyWidth::Short => Self::Short,
CurrencyWidth::Narrow => Self::Narrow,
_ => unimplemented!(),
}
}
}

#[doc(hidden)]
pub fn format_currency_to_view<L: Locale>(
locale: L,
number: impl NumberFormatterInputFn,
width: CurrencyWidth,
currency_code: CurrencyCode,
) -> impl IntoView + Clone {
let currency_formatter = super::get_currency_formatter(locale, width);

move || {
let fixed_dec = number.to_fixed_decimal();
let currency = currency_formatter.format_fixed_decimal(&fixed_dec, currency_code);
let mut formatted_currency = String::new();
currency.write_to(&mut formatted_currency).unwrap();
formatted_currency
}
}

#[doc(hidden)]
pub fn format_currency_to_formatter<L: Locale>(
f: &mut fmt::Formatter<'_>,
locale: L,
number: impl IntoFixedDecimal,
width: CurrencyWidth,
currency_code: CurrencyCode,
) -> fmt::Result {
let currency_formatter = super::get_currency_formatter(locale, width);
let fixed_dec = number.to_fixed_decimal();
let formatted_currency = currency_formatter.format_fixed_decimal(&fixed_dec, currency_code);
formatted_currency.write_to(f)
}

/// This function is a lie.
/// The only reason it exist is for the `format` macros.
/// It does NOT return a `impl Display` struct with no allocation like the other
/// This directly return a `String` of the formatted num, because borrow issues.
#[doc(hidden)]
pub fn format_currency_to_display<L: Locale>(
locale: L,
number: impl IntoFixedDecimal,
width: CurrencyWidth,
currency_code: CurrencyCode,
) -> impl Display {
let currency_formatter = super::get_currency_formatter(locale, width);
let fixed_dec = number.to_fixed_decimal();
let currency = currency_formatter.format_fixed_decimal(&fixed_dec, currency_code);
let mut formatted_currency = String::new();
currency.write_to(&mut formatted_currency).unwrap();
formatted_currency
}
79 changes: 74 additions & 5 deletions leptos_i18n/src/macro_helpers/formatting/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
//! This module contain traits and helper functions for formatting
//! different kind of value based on a locale.

#[cfg(feature = "format_currency")]
mod currency;
#[cfg(feature = "format_datetime")]
mod date;
#[cfg(feature = "format_datetime")]
Expand All @@ -12,6 +14,8 @@ mod nums;
#[cfg(feature = "format_datetime")]
mod time;

#[cfg(feature = "format_currency")]
pub use currency::*;
#[cfg(feature = "format_datetime")]
pub use date::*;
#[cfg(feature = "format_datetime")]
Expand All @@ -33,7 +37,8 @@ pub use time::*;
feature = "format_nums",
feature = "format_datetime",
feature = "format_list",
feature = "plurals"
feature = "plurals",
feature = "format_currency",
))]
use crate::Locale;
#[cfg(feature = "format_nums")]
Expand All @@ -42,11 +47,39 @@ use icu_decimal::options::FixedDecimalFormatterOptions;
use icu_decimal::options::GroupingStrategy;
#[cfg(feature = "format_nums")]
use icu_decimal::FixedDecimalFormatter;
#[cfg(feature = "format_currency")]
use icu_experimental::dimension::currency::formatter::CurrencyFormatter;
#[cfg(feature = "format_currency")]
use icu_experimental::dimension::currency::options::CurrencyFormatterOptions;
#[cfg(feature = "format_currency")]
use icu_experimental::dimension::currency::options::Width as CurrencyWidth;

pub use leptos_i18n_macro::{
t_format, t_format_display, t_format_string, td_format, td_format_display, td_format_string,
tu_format, tu_format_display, tu_format_string,
};

#[cfg(feature = "format_currency")]
fn get_currency_formatter<L: Locale>(
locale: L,
width: CurrencyWidth,
) -> &'static CurrencyFormatter {
use data_provider::IcuDataProvider;

inner::FORMATTERS.with_mut(|formatters| {
let locale = locale.as_icu_locale();
let currency_formatters = formatters.currency.entry(locale).or_default();
let currency_formatter = currency_formatters.entry(width.into()).or_insert_with(|| {
let formatter = formatters
.provider
.try_new_currency_formatter(&locale.into(), CurrencyFormatterOptions::from(width))
.expect("A CurrencyFormatter");
Box::leak(Box::new(formatter))
});
*currency_formatter
})
}

#[cfg(feature = "format_nums")]
fn get_num_formatter<L: Locale>(
locale: L,
Expand Down Expand Up @@ -177,7 +210,8 @@ pub fn get_plural_rules<L: Locale>(
feature = "format_nums",
feature = "format_datetime",
feature = "format_list",
feature = "plurals"
feature = "plurals",
feature = "format_currency",
))]
pub(crate) mod inner {
use super::*;
Expand Down Expand Up @@ -214,6 +248,11 @@ pub(crate) mod inner {
}
#[derive(Default)]
pub struct Formatters {
#[cfg(feature = "format_currency")]
pub currency: HashMap<
&'static IcuLocale,
HashMap<super::currency::Width, &'static CurrencyFormatter>,
>,
#[cfg(feature = "format_nums")]
pub num:
HashMap<&'static IcuLocale, HashMap<GroupingStrategy, &'static FixedDecimalFormatter>>,
Expand Down Expand Up @@ -253,7 +292,8 @@ pub(crate) mod inner {
feature = "format_nums",
feature = "format_datetime",
feature = "format_list",
feature = "plurals"
feature = "plurals",
feature = "format_currency",
)))]
pub(crate) mod inner {
/// Supply a custom ICU data provider
Expand All @@ -268,15 +308,17 @@ pub(crate) mod data_provider {
feature = "format_nums",
feature = "format_datetime",
feature = "format_list",
feature = "plurals"
feature = "plurals",
feature = "format_currency",
))]
use super::*;

#[cfg(any(
feature = "format_nums",
feature = "format_datetime",
feature = "format_list",
feature = "plurals"
feature = "plurals",
feature = "format_currency",
))]
use icu_provider::DataLocale;

Expand Down Expand Up @@ -345,6 +387,14 @@ pub(crate) mod data_provider {
locale: &DataLocale,
rule_type: PluralRuleType,
) -> Result<PluralRules, icu_plurals::PluralsError>;
///
/// Tries to create a new `CurrencyFormatter` with the given options
#[cfg(feature = "format_currency")]
fn try_new_currency_formatter(
&self,
locale: &DataLocale,
options: CurrencyFormatterOptions,
) -> Result<CurrencyFormatter, icu_provider::DataError>;
}

#[cfg(feature = "icu_compiled_data")]
Expand Down Expand Up @@ -424,6 +474,15 @@ pub(crate) mod data_provider {
) -> Result<PluralRules, icu_plurals::PluralsError> {
PluralRules::try_new(locale, rule_type)
}

#[cfg(feature = "format_currency")]
fn try_new_currency_formatter(
&self,
locale: &DataLocale,
options: CurrencyFormatterOptions,
) -> Result<CurrencyFormatter, icu_provider::DataError> {
CurrencyFormatter::try_new(locale, options)
}
}

#[cfg(not(feature = "icu_compiled_data"))]
Expand Down Expand Up @@ -514,5 +573,15 @@ pub(crate) mod data_provider {
) -> Result<PluralRules, icu_plurals::PluralsError> {
self.get_provider().try_new_plural_rules(locale, rule_type)
}

#[cfg(feature = "format_currency")]
fn try_new_currency_formatter(
&self,
locale: &DataLocale,
options: CurrencyFormatterOptions,
) -> Result<CurrencyFormatter, icu_provider::DataError> {
self.get_provider()
.try_new_currency_formatter(locale, options)
}
}
}
Loading
Loading