1use fluent::FluentArgs;
6use fluent::FluentResource;
7use fluent::FluentValue;
8use fluent::bundle::FluentBundle;
9use lib_humus_configuration::read_from_toml_file;
10use log::warn;
11
12use std::collections::HashMap;
13use std::error::Error;
14use std::fmt::Debug;
15use std::fmt::Display;
16use std::fs;
17use std::path::Path;
18use std::sync::Arc;
19
20use crate::language::LanguageEngineLoaderError;
21use crate::language::LanguageManifest;
22use crate::language::UnicodeLanguageIdentifier;
23use crate::language::variable_description::Variable;
24
25pub type Bundle = FluentBundle<FluentResource, intl_memoizer::concurrent::IntlLangMemoizer>;
29
30#[derive(Clone)]
34pub struct LanguageEngine {
35 language_manifest: LanguageManifest,
37 loaded_fluent_bundles: Arc<HashMap<UnicodeLanguageIdentifier, Bundle>>,
39}
40
41impl Debug for LanguageEngine {
42 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43 f.debug_struct("LanguageEngine")
44 .field("language_manifest", &self.language_manifest)
45 .field(
46 "loaded_fluent_bundles",
47 &self.loaded_fluent_bundles.keys().collect::<Vec<_>>(),
48 )
49 .finish()
50 }
51}
52
53impl LanguageEngine {
54 pub fn new_empty() -> Self {
56 let language: UnicodeLanguageIdentifier = unic_langid::langid!("und").into();
57 Self {
58 language_manifest: LanguageManifest::new_empty(),
59 loaded_fluent_bundles: Arc::new(
60 [(
61 language.clone(),
62 FluentBundle::new_concurrent(vec![language.into()]),
63 )]
64 .into_iter()
65 .collect(),
66 ),
67 }
68 }
69
70 pub fn load_from_directory(path: impl AsRef<Path>) -> Result<Self, LanguageEngineLoaderError> {
80 let base_dir = path.as_ref();
81 if !base_dir.is_dir() {
82 return Err(LanguageEngineLoaderError::BaseDirectoryDoesNotExist {
83 path: base_dir.to_path_buf(),
84 is_other: base_dir.exists(),
85 });
86 }
87 let manifest_file = base_dir.join("manifest.toml");
88 let manifest: LanguageManifest = read_from_toml_file(manifest_file)
89 .map_err(LanguageEngineLoaderError::ErrorReadingManifest)?;
90
91 match manifest.languages.get(&manifest.default_language) {
93 Some(description) if description.is_hidden => {
94 return Err(LanguageEngineLoaderError::DefaultLanguageMustNotBeHidden {
95 default_language: manifest.default_language,
96 });
97 }
98 None => {
99 return Err(LanguageEngineLoaderError::DefaultLanguageNotInManifest {
100 default_language: manifest.default_language,
101 });
102 }
103 _ => { }
104 }
105
106 let mut missing_messages: Vec<(UnicodeLanguageIdentifier, String)> = vec![];
107 let mut loaded_fluent_bundles: HashMap<UnicodeLanguageIdentifier, Bundle> = HashMap::new();
108 for (language, description) in &manifest.languages {
109 let path = base_dir.join(format!("{language}.ftl"));
110 let text = fs::read_to_string(&path).map_err(|e| {
111 LanguageEngineLoaderError::ErrorReadingLanguageFile {
112 language: language.clone(),
113 path: path.clone(),
114 io_error: e,
115 }
116 })?;
117 let resource = FluentResource::try_new(text).map_err(|(_, e)| {
118 LanguageEngineLoaderError::ErrorParsingLanguageFile {
119 language: language.clone(),
120 path: path.clone(),
121 errors: e,
122 }
123 })?;
124 let mut bundle = FluentBundle::new_concurrent(vec![language.clone().into()]);
125 bundle.add_resource(resource).map_err(|e| {
128 LanguageEngineLoaderError::ErrorAddingLanguageFile {
129 language: language.clone(),
130 path: path.clone(),
131 errors: e,
132 }
133 })?;
134
135 if !description.is_hidden {
137 for message_id in &manifest.available_messages {
138 if !bundle.has_message(message_id) {
139 missing_messages.push((language.clone(), message_id.clone()))
140 }
141 }
142 }
143
144 loaded_fluent_bundles.insert(language.clone(), bundle);
145 }
146
147 if !missing_messages.is_empty() {
148 let mut missing_messages_text: String = "".to_string();
149 for (lang, id) in missing_messages {
150 missing_messages_text = format!("{missing_messages_text}\t* in {lang}: {id:?}\n");
151 }
152 warn!("Some non-hidden languages are incomplete!");
153 warn!("Missing messages:\n{missing_messages_text}");
154 warn!("Those missing messages will cause template errors!");
155 }
156
157 Ok(Self {
158 language_manifest: manifest,
159 loaded_fluent_bundles: Arc::new(loaded_fluent_bundles),
160 })
161 }
162
163 pub fn naive_get_text(
168 &self,
169 language: Option<UnicodeLanguageIdentifier>,
170 message_id: &str,
171 args: Option<&FluentArgs>,
172 ) -> String {
173 match self.get_text_with_raw_args(language, message_id, args) {
174 Ok(text) => {
175 return text;
176 }
177 Err(e) => {
178 log::error!("{e:?}");
179 }
180 }
181 if let Some(args) = args {
182 format!("[NOT TRANSLATEABLE {message_id:?} {args:?}]")
183 } else {
184 format!("[NOT TRANSLATEABLE {message_id:?}]")
185 }
186 }
187
188 pub fn get_text_with_args(
190 &self,
191 language: Option<UnicodeLanguageIdentifier>,
192 message_id: &str,
193 args: impl IntoIterator<Item = (impl Into<String>, Variable)>,
194 ) -> Result<String, TextFunctionError> {
195 let language = language.unwrap_or_else(|| self.language_manifest.default_language.clone());
196 if let Some(message_description) =
197 self.language_manifest.message_descriptions.get(message_id)
198 {
199 let mut arg_problems: TextFunctionArgumentProblems = Default::default();
200 let mut fluent_args = FluentArgs::new();
201
202 for (name, value) in args {
203 let name = name.into();
204 if let Some(arg_description) = message_description.arguments.get(&name) {
205 if !arg_description.matches_variable(&value) {
206 arg_problems
207 .description_mismatch
208 .push((name.clone(), value));
209 fluent_args.set(name, FluentValue::Error);
210 continue;
211 }
212 fluent_args.set(name, value.into_fluent_value(arg_description));
214 } else {
215 arg_problems.too_many.push(name);
216 }
217 }
218
219 for (name, desc) in &message_description.arguments {
221 if fluent_args.get(name).is_some() {
222 continue;
223 }
224 if let Some(value) = &desc.default_value {
225 fluent_args.set(name, value.clone().into_fluent_value(desc));
226 } else if !desc.optional {
227 arg_problems.missing.push(name.to_owned());
228 }
229 }
230
231 if !arg_problems.is_empty() {
232 return Err(TextFunctionError::new(
233 &language,
234 message_id,
235 TextFunctionErrorKind::ArgumentError(Box::new(arg_problems)),
236 ));
237 }
238
239 self.get_text_with_raw_args(Some(language), message_id, Some(&fluent_args))
240 } else {
241 let mut too_many_arguments: Vec<String> = vec![];
242 for (key, _) in args {
243 too_many_arguments.push(key.into())
244 }
245 if too_many_arguments.is_empty() {
246 self.get_text_with_raw_args(Some(language), message_id, None)
247 } else {
248 Err(TextFunctionError::new(
249 &language,
250 message_id,
251 TextFunctionErrorKind::ArgumentError(Box::new(TextFunctionArgumentProblems {
252 too_many: too_many_arguments,
253 missing: vec![],
254 description_mismatch: vec![],
255 })),
256 ))
257 }
258 }
259 }
260
261 pub fn get_text(
263 &self,
264 language: Option<UnicodeLanguageIdentifier>,
265 message_id: &str,
266 ) -> Result<String, TextFunctionError> {
267 let language = language.unwrap_or_else(|| self.language_manifest.default_language.clone());
268
269 if let Some(message_description) =
271 self.language_manifest.message_descriptions.get(message_id)
272 && !message_description.arguments.is_empty()
273 {
274 let mut arg_problems: TextFunctionArgumentProblems = Default::default();
275 for (name, desc) in &message_description.arguments {
276 if !(desc.optional || desc.default_value.is_some()) {
277 arg_problems.missing.push(name.to_owned());
278 }
279 }
280 if !arg_problems.is_empty() {
281 return Err(TextFunctionError::new(
282 &language,
283 message_id,
284 TextFunctionErrorKind::ArgumentError(Box::new(arg_problems)),
285 ));
286 }
287 }
288 self.get_text_with_raw_args(Some(language), message_id, None)
289 }
290
291 pub fn get_text_with_raw_args(
295 &self,
296 language: Option<UnicodeLanguageIdentifier>,
297 message_id: &str,
298 args: Option<&FluentArgs>,
299 ) -> Result<String, TextFunctionError> {
300 let language = language.unwrap_or_else(|| self.language_manifest.default_language.clone());
301 if !self
302 .language_manifest
303 .available_messages
304 .contains(message_id)
305 {
306 return Err(TextFunctionError::new(
307 &language,
308 message_id,
309 TextFunctionErrorKind::MessageIdNotListedAvailable,
310 ));
311 }
312 let bundle = self.loaded_fluent_bundles.get(&language).ok_or_else(|| {
313 TextFunctionError::new(
314 &language,
315 message_id,
316 TextFunctionErrorKind::LanguageIsNotLoadedOrPresent,
317 )
318 })?;
319 let message = bundle.get_message(message_id).ok_or_else(|| {
320 TextFunctionError::new(
321 &language,
322 message_id,
323 TextFunctionErrorKind::MessageNotPresentInLanguage,
324 )
325 })?;
326 let pattern = message.value().ok_or_else(|| {
327 TextFunctionError::new(
328 &language,
329 message_id,
330 TextFunctionErrorKind::NoPatternForMessage,
331 )
332 })?;
333 let mut errors = vec![];
334 let out = bundle.format_pattern(pattern, args, &mut errors);
335 if !errors.is_empty() {
336 log::error!(
337 "Non fatal problems occurred while translating text {message_id:?} for language {language}: {errors:#?}"
338 );
339 }
340 Ok(out.to_string())
341 }
342
343 pub fn language_manifest(&self) -> &LanguageManifest {
345 &self.language_manifest
346 }
347}
348
349#[derive(Debug, Default)]
350pub struct TextFunctionArgumentProblems {
351 pub too_many: Vec<String>,
352 pub missing: Vec<String>,
353 pub description_mismatch: Vec<(String, Variable)>,
354}
355
356impl TextFunctionArgumentProblems {
357 pub fn is_empty(&self) -> bool {
358 self.too_many.is_empty() && self.missing.is_empty() && self.description_mismatch.is_empty()
359 }
360}
361
362impl Display for TextFunctionArgumentProblems {
363 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
364 write!(
365 f,
366 "uneccessary arguments: {:?}; missing_arguments: {:?}; description mismatches: {:?}",
367 self.too_many, self.missing, self.description_mismatch
368 )
369 }
370}
371
372#[derive(Debug)]
374pub struct TextFunctionError {
375 pub language: UnicodeLanguageIdentifier,
377 pub message_id: String,
379 pub kind: TextFunctionErrorKind,
381}
382
383impl TextFunctionError {
384 pub fn new(
386 language: &UnicodeLanguageIdentifier,
387 message_id: &str,
388 kind: TextFunctionErrorKind,
389 ) -> Self {
390 Self {
391 language: language.clone(),
392 message_id: message_id.to_owned(),
393 kind,
394 }
395 }
396}
397
398#[derive(Debug)]
399pub enum TextFunctionErrorKind {
400 MessageIdNotListedAvailable,
401 NoPatternForMessage,
402 MessageNotPresentInLanguage,
403 LanguageIsNotLoadedOrPresent,
404 ArgumentError(Box<TextFunctionArgumentProblems>),
405}
406
407impl Display for TextFunctionError {
408 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
409 let id = &self.message_id;
410 let language = &self.language;
411 match &self.kind {
412 TextFunctionErrorKind::MessageIdNotListedAvailable => write!(
413 f,
414 "The messagge id {id:?} isn't listed as an available id in the language manifest. If this message should exist please register its name in the available_ids array!"
415 ),
416 TextFunctionErrorKind::NoPatternForMessage => write!(
417 f,
418 "Unable to translate text {id:?} for language {language}, message present but it does not carry a pattern"
419 ),
420 TextFunctionErrorKind::MessageNotPresentInLanguage => write!(
421 f,
422 "Unable to translate text {id:?} for language {language}, message not present in this language"
423 ),
424 TextFunctionErrorKind::LanguageIsNotLoadedOrPresent => write!(
425 f,
426 "Unable to translate texts for language {language}, language is not loaded or not present in template."
427 ),
428 TextFunctionErrorKind::ArgumentError(e) => {
429 write!(f, "Problem with passed arguments for message {id:?}: {e}")
430 }
431 }
432 }
433}
434
435impl Error for TextFunctionError {
436 fn source(&self) -> Option<&(dyn Error + 'static)> {
437 None
438 }
439}