1use indexmap::IndexMap;
4use rustauth_core::api::ApiRequest;
5use rustauth_core::context::AuthContext;
6use serde::{Deserialize, Serialize};
7use std::fmt;
8use std::future::Future;
9use std::pin::Pin;
10use std::sync::Arc;
11
12pub type LocaleResolver = Arc<dyn Fn(&AuthContext, &ApiRequest) -> Option<String> + Send + Sync>;
14
15pub type AsyncLocaleResolver = Arc<
17 dyn for<'a> Fn(
18 &'a AuthContext,
19 &'a ApiRequest,
20 ) -> Pin<Box<dyn Future<Output = Option<String>> + Send + 'a>>
21 + Send
22 + Sync,
23>;
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
27#[serde(rename_all = "lowercase")]
28pub enum LocaleDetectionStrategy {
29 Header,
30 Cookie,
31 Session,
32 Callback,
33}
34
35pub type TranslationDictionary = IndexMap<String, String>;
37
38pub trait TranslationKey {
40 fn into_translation_key(self) -> String;
41}
42
43impl TranslationKey for String {
44 fn into_translation_key(self) -> String {
45 self
46 }
47}
48
49impl TranslationKey for &str {
50 fn into_translation_key(self) -> String {
51 self.to_owned()
52 }
53}
54
55impl TranslationKey for &String {
56 fn into_translation_key(self) -> String {
57 self.clone()
58 }
59}
60
61mod error_code_translation {
62 use super::TranslationKey;
63 use rustauth_core::api::ApiErrorCode;
64 use rustauth_core::auth::email_password::AuthFlowErrorCode;
65 use rustauth_core::error_codes::ErrorCode;
66 use rustauth_core::plugin::PluginErrorCode;
67
68 pub trait Sealed {}
69
70 impl Sealed for ApiErrorCode {}
71 impl Sealed for AuthFlowErrorCode {}
72 impl Sealed for PluginErrorCode {}
73
74 impl<T> TranslationKey for T
75 where
76 T: ErrorCode + Sealed,
77 {
78 fn into_translation_key(self) -> String {
79 self.as_str().to_owned()
80 }
81 }
82}
83
84pub fn translation_dictionary<K, V, I>(entries: I) -> TranslationDictionary
99where
100 K: TranslationKey,
101 V: Into<String>,
102 I: IntoIterator<Item = (K, V)>,
103{
104 entries
105 .into_iter()
106 .map(|(key, value)| (key.into_translation_key(), value.into()))
107 .collect()
108}
109
110#[non_exhaustive]
112#[derive(Clone)]
113pub struct I18nOptions {
114 pub translations: IndexMap<String, TranslationDictionary>,
116 pub default_locale: Option<String>,
118 pub detection: Vec<LocaleDetectionStrategy>,
120 pub locale_cookie: String,
122 pub user_locale_field: String,
124 pub get_locale: Option<LocaleResolver>,
126 pub get_locale_async: Option<AsyncLocaleResolver>,
128 pub resolve_user_locale: Option<LocaleResolver>,
130}
131
132impl Default for I18nOptions {
133 fn default() -> Self {
134 Self {
135 translations: IndexMap::new(),
136 default_locale: None,
137 detection: vec![LocaleDetectionStrategy::Header],
138 locale_cookie: "locale".to_owned(),
139 user_locale_field: "locale".to_owned(),
140 get_locale: None,
141 get_locale_async: None,
142 resolve_user_locale: None,
143 }
144 }
145}
146
147impl I18nOptions {
148 pub fn new() -> Self {
166 Self::default()
167 }
168
169 pub fn locale<K, V, I>(mut self, code: impl Into<String>, entries: I) -> Self
171 where
172 K: TranslationKey,
173 V: Into<String>,
174 I: IntoIterator<Item = (K, V)>,
175 {
176 self.translations
177 .insert(code.into(), translation_dictionary(entries));
178 self
179 }
180
181 pub fn from_translations(translations: IndexMap<String, TranslationDictionary>) -> Self {
183 Self {
184 translations,
185 ..Self::default()
186 }
187 }
188
189 pub fn default_locale(mut self, locale: impl Into<String>) -> Self {
191 self.default_locale = Some(locale.into());
192 self
193 }
194
195 pub fn detection<I>(mut self, detection: I) -> Self
197 where
198 I: IntoIterator<Item = LocaleDetectionStrategy>,
199 {
200 self.detection = detection.into_iter().collect();
201 self
202 }
203
204 pub fn locale_cookie(mut self, name: impl Into<String>) -> Self {
206 self.locale_cookie = name.into();
207 self
208 }
209
210 pub fn user_locale_field(mut self, field: impl Into<String>) -> Self {
212 self.user_locale_field = field.into();
213 self
214 }
215
216 pub fn get_locale(mut self, resolver: LocaleResolver) -> Self {
218 self.get_locale = Some(resolver);
219 self
220 }
221
222 pub fn get_locale_async(mut self, resolver: AsyncLocaleResolver) -> Self {
224 self.get_locale_async = Some(resolver);
225 self
226 }
227
228 pub fn resolve_user_locale(mut self, resolver: LocaleResolver) -> Self {
230 self.resolve_user_locale = Some(resolver);
231 self
232 }
233}
234
235impl fmt::Debug for I18nOptions {
236 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
237 formatter
238 .debug_struct("I18nOptions")
239 .field("translations", &self.translations)
240 .field("default_locale", &self.default_locale)
241 .field("detection", &self.detection)
242 .field("locale_cookie", &self.locale_cookie)
243 .field("user_locale_field", &self.user_locale_field)
244 .field(
245 "get_locale",
246 &self.get_locale.as_ref().map(|_| "<locale-resolver>"),
247 )
248 .field(
249 "get_locale_async",
250 &self
251 .get_locale_async
252 .as_ref()
253 .map(|_| "<async-locale-resolver>"),
254 )
255 .field(
256 "resolve_user_locale",
257 &self
258 .resolve_user_locale
259 .as_ref()
260 .map(|_| "<locale-resolver>"),
261 )
262 .finish()
263 }
264}