Skip to main content

trait_kit/i18n/
i18n_impl.rs

1// Copyright (c) 2026 Kirky.X
2// SPDX-License-Identifier: MIT
3//! Implementation of [`I18nFormatter`] methods.
4
5use std::cmp::Ordering;
6use std::str::FromStr;
7
8use icu::collator::Collator;
9use icu::collator::options::CollatorOptions;
10use icu::datetime::DateTimeFormatter;
11use icu::datetime::fieldsets::YMD;
12use icu::datetime::input::{Date, DateTime, Time};
13use icu::decimal::DecimalFormatter;
14use icu::decimal::input::Decimal;
15use icu::decimal::options::DecimalFormatterOptions;
16use icu::locale::Locale;
17use icu::plurals::{PluralCategory, PluralRules, PluralRulesOptions};
18use writeable::Writeable;
19
20use super::{I18nError, I18nFormatter};
21
22impl I18nFormatter {
23    /// Create a new formatter for the given BCP-47 locale tag.
24    ///
25    /// # Errors
26    /// Returns [`I18nError::InvalidLocale`] if the tag cannot be parsed,
27    /// or [`I18nError::FormatError`] if ICU4X lacks compiled data for it.
28    pub fn new(locale: &str) -> Result<Self, I18nError> {
29        let parsed = Locale::from_str(locale).map_err(|e| I18nError::InvalidLocale {
30            input: locale.to_string(),
31            reason: e.to_string(),
32        })?;
33
34        let decimal_formatter =
35            DecimalFormatter::try_new(parsed.clone().into(), DecimalFormatterOptions::default())
36                .map_err(|e| I18nError::FormatError(e.to_string()))?;
37
38        let plural_rules =
39            PluralRules::try_new(parsed.clone().into(), PluralRulesOptions::default())
40                .map_err(|e| I18nError::FormatError(e.to_string()))?;
41
42        let collator = Collator::try_new(parsed.clone().into(), CollatorOptions::default())
43            .map_err(|e| I18nError::FormatError(e.to_string()))?;
44
45        Ok(Self {
46            locale: parsed,
47            decimal_formatter,
48            plural_rules,
49            collator,
50        })
51    }
52
53    /// Format a floating-point number with locale-sensitive grouping
54    /// and decimal separators.
55    ///
56    /// # Errors
57    /// Returns [`I18nError::InvalidNumber`] for non-finite values or
58    /// if the value cannot be parsed into a fixed decimal.
59    pub fn format_number(&self, value: f64) -> Result<String, I18nError> {
60        if !value.is_finite() {
61            return Err(I18nError::InvalidNumber {
62                input: value.to_string(),
63                reason: "value is not finite (NaN or Infinity)".into(),
64            });
65        }
66        // Use fixed-point notation to avoid scientific notation (e.g. "1e-10")
67        // which Decimal::from_str cannot parse. 20 decimal places cover the
68        // full precision of f64.
69        let repr = format!("{value:.20}");
70        // Trim trailing zeros after decimal point, but keep at least one digit
71        let repr = repr.trim_end_matches('0');
72        let repr = repr.trim_end_matches('.');
73        let decimal = Decimal::from_str(repr).map_err(|e| I18nError::InvalidNumber {
74            input: repr.to_string(),
75            reason: e.to_string(),
76        })?;
77        let formatted = self.decimal_formatter.format(&decimal);
78        Ok(formatted.write_to_string().to_string())
79    }
80
81    /// Format an ISO calendar date (year / month / day) using a medium
82    /// length locale-specific pattern.
83    ///
84    /// # Errors
85    /// Returns [`I18nError::DateError`] if any component is out of range,
86    /// or [`I18nError::FormatError`] if the formatter cannot be constructed.
87    pub fn format_date(&self, year: i32, month: u8, day: u8) -> Result<String, I18nError> {
88        let date =
89            Date::try_new_iso(year, month, day).map_err(|e| I18nError::DateError(e.to_string()))?;
90        let time = Time::try_new(0, 0, 0, 0).map_err(|e| I18nError::DateError(e.to_string()))?;
91        let datetime = DateTime { date, time };
92
93        let dtf = DateTimeFormatter::try_new(self.locale.clone().into(), YMD::medium())
94            .map_err(|e| I18nError::FormatError(e.to_string()))?;
95        let formatted = dtf.format(&datetime);
96        Ok(formatted.write_to_string().to_string())
97    }
98
99    /// Return the plural category for `count` in the formatter's locale.
100    ///
101    /// # Errors
102    /// This method does not currently fail, but returns `Result` for API
103    /// consistency with the other formatting methods.
104    pub fn plural_category(&self, count: u64) -> Result<PluralCategory, I18nError> {
105        Ok(self.plural_rules.category_for(count))
106    }
107
108    /// Compare two strings using locale-sensitive collation rules.
109    ///
110    /// # Errors
111    /// This method does not currently fail, but returns `Result` for API
112    /// consistency with the other formatting methods.
113    pub fn compare(&self, a: &str, b: &str) -> Result<Ordering, I18nError> {
114        Ok(self.collator.compare(a, b))
115    }
116}