whitaker_common/i18n/
loader.rs1use std::borrow::Cow;
8use std::collections::HashMap;
9use std::str::FromStr;
10
11use fluent_templates::{Loader, fluent_bundle::FluentValue};
12use thiserror::Error;
13
14use super::locales::supports_locale;
15use super::{FALLBACK_LANGUAGE, FALLBACK_LITERAL, LOADER, LanguageIdentifier};
16
17pub type Arguments<'a> = HashMap<Cow<'a, str>, FluentValue<'a>>;
19
20#[derive(Clone, Debug, Error, PartialEq, Eq)]
22pub enum I18nError {
23 #[error("message `{key}` missing for locale `{locale}`")]
25 MissingMessage { key: String, locale: String },
26}
27
28#[derive(Clone, Debug)]
34pub struct Localizer {
35 language: LanguageIdentifier,
36 language_tag: String,
37 fallback_used: bool,
38}
39
40impl Localizer {
41 #[must_use]
56 pub fn new(locale: Option<&str>) -> Self {
57 match locale {
58 Some(value) if supports_locale(value) => match LanguageIdentifier::from_str(value) {
59 Ok(identifier) => {
60 let language_tag = identifier.to_string();
61
62 Self {
63 language: identifier,
64 language_tag,
65 fallback_used: false,
66 }
67 }
68 Err(_) => Self::fallback(),
69 },
70 _ => Self::fallback(),
71 }
72 }
73
74 #[must_use]
76 pub fn language(&self) -> &LanguageIdentifier {
77 &self.language
78 }
79
80 #[must_use]
82 pub fn locale(&self) -> &str {
83 &self.language_tag
84 }
85
86 #[must_use]
88 pub fn used_fallback(&self) -> bool {
89 self.fallback_used
90 }
91
92 pub fn message(&self, key: &str) -> Result<String, I18nError> {
94 self.lookup(key, None, None)
95 }
96
97 pub fn message_with_args(&self, key: &str, args: &Arguments<'_>) -> Result<String, I18nError> {
99 self.lookup(key, None, Some(args))
100 }
101
102 pub fn attribute(&self, key: &str, attribute: &str) -> Result<String, I18nError> {
104 self.lookup(key, Some(attribute), None)
105 }
106
107 pub fn attribute_with_args(
109 &self,
110 key: &str,
111 attribute: &str,
112 args: &Arguments<'_>,
113 ) -> Result<String, I18nError> {
114 self.lookup(key, Some(attribute), Some(args))
115 }
116
117 fn lookup(
118 &self,
119 key: &str,
120 attribute: Option<&str>,
121 args: Option<&Arguments<'_>>,
122 ) -> Result<String, I18nError> {
123 let lookup_key = attribute
124 .map(|attr| format!("{key}.{attr}"))
125 .unwrap_or_else(|| key.to_string());
126
127 let maybe_value = match args {
128 Some(arguments) => {
129 let owned_arguments = promote_arguments(arguments);
130 LOADER.try_lookup_with_args(&self.language, lookup_key.as_str(), &owned_arguments)
131 }
132 None => LOADER.try_lookup(&self.language, lookup_key.as_str()),
133 };
134
135 maybe_value.ok_or_else(|| I18nError::MissingMessage {
136 key: lookup_key,
137 locale: self.language_tag.clone(),
138 })
139 }
140
141 fn fallback() -> Self {
142 Self {
143 language: FALLBACK_LANGUAGE.clone(),
144 language_tag: FALLBACK_LITERAL.to_string(),
145 fallback_used: true,
146 }
147 }
148}
149
150fn promote_arguments(
151 arguments: &Arguments<'_>,
152) -> HashMap<Cow<'static, str>, FluentValue<'static>> {
153 arguments
154 .iter()
155 .map(|(key, value)| (Cow::Owned(key.as_ref().to_string()), promote_value(value)))
156 .collect()
157}
158
159fn promote_value(value: &FluentValue<'_>) -> FluentValue<'static> {
160 match value {
161 FluentValue::String(text) => FluentValue::String(Cow::Owned(text.as_ref().to_string())),
162 FluentValue::Number(number) => FluentValue::Number(number.clone()),
163 FluentValue::Custom(custom) => FluentValue::Custom(custom.duplicate()),
164 FluentValue::None => FluentValue::None,
165 FluentValue::Error => FluentValue::Error,
166 }
167}