1use axum::extract::FromRequestParts;
2use http::request::Parts;
3
4use super::store::TranslationStore;
5
6#[derive(Clone, Debug)]
17pub struct Translator {
18 locale: String,
19 store: TranslationStore,
20}
21
22impl Translator {
23 pub(super) fn new(locale: String, store: TranslationStore) -> Self {
25 Self { locale, store }
26 }
27
28 pub fn t(&self, key: &str, kwargs: &[(&str, &str)]) -> String {
33 self.store
34 .translate(&self.locale, key, kwargs)
35 .unwrap_or_else(|_| key.to_string())
36 }
37
38 pub fn t_plural(&self, key: &str, count: i64, kwargs: &[(&str, &str)]) -> String {
42 self.store
43 .translate_plural(&self.locale, key, count, kwargs)
44 .unwrap_or_else(|_| key.to_string())
45 }
46
47 pub fn locale(&self) -> &str {
49 &self.locale
50 }
51
52 pub fn store(&self) -> &TranslationStore {
54 &self.store
55 }
56}
57
58impl<S: Send + Sync> FromRequestParts<S> for Translator {
59 type Rejection = crate::Error;
60
61 async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
62 parts
63 .extensions
64 .get::<Translator>()
65 .cloned()
66 .ok_or_else(|| {
67 crate::Error::internal("I18nLayer not installed").with_code("i18n:layer_missing")
68 })
69 }
70}
71
72#[cfg(test)]
73mod tests {
74 use super::*;
75 use crate::i18n::config::I18nConfig;
76 use crate::i18n::factory::I18n;
77 use axum::extract::FromRequestParts;
78 use http::{Request, StatusCode};
79
80 fn test_i18n() -> I18n {
81 let dir = tempfile::tempdir().unwrap();
82 let en_dir = dir.path().join("en");
83 std::fs::create_dir_all(&en_dir).unwrap();
84 std::fs::write(en_dir.join("common.yaml"), "greeting: Hello").unwrap();
85
86 let config = I18nConfig {
87 locales_path: dir.path().to_str().unwrap().to_string(),
88 default_locale: "en".into(),
89 ..I18nConfig::default()
90 };
91 I18n::new(&config).unwrap()
92 }
93
94 #[tokio::test]
95 async fn missing_extension_returns_internal_error() {
96 let req = Request::builder().body(()).unwrap();
97 let (mut parts, _) = req.into_parts();
98 let err = Translator::from_request_parts(&mut parts, &())
99 .await
100 .expect_err("should return Err when extension missing");
101 let resp = axum::response::IntoResponse::into_response(err);
103 assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
104 }
105
106 #[tokio::test]
107 async fn present_extension_returns_translator() {
108 let i18n = test_i18n();
109 let translator = i18n.translator("en");
110
111 let req = Request::builder().body(()).unwrap();
112 let (mut parts, _) = req.into_parts();
113 parts.extensions.insert(translator);
114
115 let extracted = Translator::from_request_parts(&mut parts, &())
116 .await
117 .expect("extraction should succeed");
118
119 assert_eq!(extracted.locale(), "en");
120 assert_eq!(extracted.t("common.greeting", &[]), "Hello");
121 assert_eq!(extracted.store().default_locale(), "en");
122 }
123}