es_fluent_manager_core/
embedded_localization.rs1use crate::fallback::fallback_locales;
4use crate::localization::{I18nModule, LocalizationError, Localizer};
5use fluent_bundle::{FluentArgs, FluentBundle, FluentResource, FluentValue};
6use fluent_fallback::env::LocalesProvider as _;
7use rust_embed::RustEmbed;
8use std::collections::HashMap;
9use std::sync::{Arc, RwLock};
10use unic_langid::LanguageIdentifier;
11
12pub trait EmbeddedAssets: RustEmbed + Send + Sync + 'static {
13 fn domain() -> &'static str;
14}
15
16#[derive(Debug)]
17pub struct EmbeddedModuleData {
18 pub name: &'static str,
20 pub domain: &'static str,
22 pub supported_languages: &'static [LanguageIdentifier],
24 pub namespaces: &'static [&'static str],
27}
28
29#[derive(Debug)]
30pub struct EmbeddedLocalizer<T: EmbeddedAssets> {
31 data: &'static EmbeddedModuleData,
32 current_resources: RwLock<Vec<Arc<FluentResource>>>,
33 current_lang: RwLock<Option<LanguageIdentifier>>,
34 _phantom: std::marker::PhantomData<T>,
35}
36
37impl<T: EmbeddedAssets> EmbeddedLocalizer<T> {
38 pub fn new(data: &'static EmbeddedModuleData) -> Self {
39 Self {
40 data,
41 current_resources: RwLock::new(Vec::new()),
42 current_lang: RwLock::new(None),
43 _phantom: std::marker::PhantomData,
44 }
45 }
46
47 fn load_resource_for_language(
48 &self,
49 lang: &LanguageIdentifier,
50 ) -> Result<Vec<Arc<FluentResource>>, LocalizationError> {
51 let mut resources = Vec::new();
52
53 let main_file_name = format!("{}.ftl", self.data.domain);
55 let main_file_path = format!("{}/{}", lang, main_file_name);
56
57 if let Some(file_data) = T::get(&main_file_path) {
58 let content = String::from_utf8(file_data.data.to_vec()).map_err(|e| {
59 LocalizationError::BackendError(anyhow::anyhow!(
60 "Invalid UTF-8 in embedded file '{}': {}",
61 main_file_path,
62 e
63 ))
64 })?;
65
66 let resource = FluentResource::try_new(content).map_err(|(_, errs)| {
67 LocalizationError::BackendError(anyhow::anyhow!(
68 "Failed to parse fluent resource from '{}': {:?}",
69 main_file_path,
70 errs
71 ))
72 })?;
73 resources.push(Arc::new(resource));
74 }
75
76 for ns in self.data.namespaces {
78 let ns_file_name = format!("{}.ftl", ns);
79 let ns_file_path = format!("{}/{}/{}", lang, self.data.domain, ns_file_name);
80
81 if let Some(file_data) = T::get(&ns_file_path) {
82 let content = String::from_utf8(file_data.data.to_vec()).map_err(|e| {
83 LocalizationError::BackendError(anyhow::anyhow!(
84 "Invalid UTF-8 in embedded file '{}': {}",
85 ns_file_path,
86 e
87 ))
88 })?;
89
90 let resource = FluentResource::try_new(content).map_err(|(_, errs)| {
91 LocalizationError::BackendError(anyhow::anyhow!(
92 "Failed to parse fluent resource from '{}': {:?}",
93 ns_file_path,
94 errs
95 ))
96 })?;
97 resources.push(Arc::new(resource));
98 }
99 }
100
101 if resources.is_empty() {
102 Err(LocalizationError::LanguageNotSupported(lang.clone()))
103 } else {
104 Ok(resources)
105 }
106 }
107}
108
109impl<T: EmbeddedAssets> Localizer for EmbeddedLocalizer<T> {
110 fn select_language(&self, lang: &LanguageIdentifier) -> Result<(), LocalizationError> {
111 let mut current_lang_guard = self.current_lang.write().unwrap();
112 for candidate in fallback_locales(lang).locales() {
113 if !self
114 .data
115 .supported_languages
116 .iter()
117 .any(|supported| supported == &candidate)
118 {
119 continue;
120 }
121
122 if current_lang_guard.as_ref() == Some(&candidate) {
123 return Ok(());
124 }
125
126 if let Ok(resources) = self.load_resource_for_language(&candidate) {
127 *self.current_resources.write().unwrap() = resources;
128 *current_lang_guard = Some(candidate);
129 return Ok(());
130 }
131 }
132
133 Err(LocalizationError::LanguageNotSupported(lang.clone()))
134 }
135
136 fn localize<'a>(
137 &self,
138 id: &str,
139 args: Option<&HashMap<&str, FluentValue<'a>>>,
140 ) -> Option<String> {
141 let resources = self.current_resources.read().unwrap();
142 if resources.is_empty() {
143 return None;
144 }
145
146 let lang_guard = self.current_lang.read().unwrap();
147 let lang = lang_guard
148 .as_ref()
149 .expect("Language not selected before localization");
150
151 let mut bundle = FluentBundle::new(vec![lang.clone()]);
152 for resource in resources.iter() {
153 if let Err(e) = bundle.add_resource(resource.clone()) {
154 tracing::error!("Failed to add resource to bundle: {:?}", e);
155 }
156 }
157
158 let message = bundle.get_message(id)?;
159 let pattern = message.value()?;
160
161 let fluent_args = args.map(|args| {
162 let mut fa = FluentArgs::new();
163 for (key, value) in args {
164 fa.set(*key, value.clone());
165 }
166 fa
167 });
168
169 let mut errors = Vec::new();
170 let value = bundle.format_pattern(pattern, fluent_args.as_ref(), &mut errors);
171
172 if !errors.is_empty() {
173 tracing::error!("Fluent formatting errors for id '{}': {:?}", id, errors);
174 return None;
175 }
176
177 Some(value.into_owned())
178 }
179}
180
181pub struct EmbeddedI18nModule<T: EmbeddedAssets> {
182 data: &'static EmbeddedModuleData,
183 _phantom: std::marker::PhantomData<T>,
184}
185
186impl<T: EmbeddedAssets> EmbeddedI18nModule<T> {
187 pub const fn new(data: &'static EmbeddedModuleData) -> Self {
188 Self {
189 data,
190 _phantom: std::marker::PhantomData,
191 }
192 }
193
194 pub fn discover_languages() -> Vec<LanguageIdentifier> {
195 let domain = T::domain();
196 let file_name = format!("{}.ftl", domain);
197 let mut languages = Vec::new();
198 let mut seen = std::collections::HashSet::new();
199
200 for file_path in T::iter() {
201 let file_path_str = file_path.as_ref();
202
203 if file_path_str.ends_with(&file_name) {
205 let suffix = format!("/{}", file_name);
206 if let Some(lang_part) = file_path_str.strip_suffix(&suffix)
207 && let Ok(lang_id) = lang_part.parse::<LanguageIdentifier>()
208 && seen.insert(lang_id.clone())
209 {
210 languages.push(lang_id);
211 }
212 }
213
214 if let Some(parent) = std::path::Path::new(file_path_str).parent()
216 && let Some(parent_str) = parent.to_str()
217 && parent_str.ends_with(&format!("/{}", domain))
218 && let Some(lang_part) = parent_str.strip_suffix(&format!("/{}", domain))
219 && let Ok(lang_id) = lang_part.parse::<LanguageIdentifier>()
220 && seen.insert(lang_id.clone())
221 {
222 languages.push(lang_id);
223 }
224 }
225
226 languages.sort_by_key(|a| a.to_string());
227 languages
228 }
229}
230
231impl<T: EmbeddedAssets> I18nModule for EmbeddedI18nModule<T> {
232 fn name(&self) -> &'static str {
233 self.data.name
234 }
235
236 fn create_localizer(&self) -> Box<dyn Localizer> {
237 Box::new(EmbeddedLocalizer::<T>::new(self.data))
238 }
239}