fluent_static_codegen/
message.rs

1use std::{
2    collections::{BTreeMap, BTreeSet},
3    path::{Path, PathBuf},
4    rc::Rc,
5    str::FromStr,
6};
7
8use convert_case::{Case, Casing};
9use fluent_syntax::parser;
10use proc_macro2::{Ident, Literal, TokenStream as TokenStream2};
11use quote::{format_ident, quote};
12use unic_langid::LanguageIdentifier;
13
14use crate::{
15    ast::Visitor,
16    function::{FunctionCallGenerator, FunctionRegistry},
17    language::LanguageBuilder,
18    types::{FluentMessage, PublicFluentId},
19    Error,
20};
21
22pub struct MessageBundle {
23    name: String,
24    code: TokenStream2,
25}
26
27impl MessageBundle {
28    pub fn builder(bundle_name: &str) -> MessageBundleBuilder {
29        MessageBundleBuilder::new(bundle_name)
30    }
31
32    pub fn name(&self) -> &str {
33        &self.name
34    }
35
36    pub fn write_to_file(&self, path: impl AsRef<Path>) -> Result<(), std::io::Error> {
37        std::fs::write(path, self.code.to_string())
38    }
39
40    pub fn tokens(&self) -> &TokenStream2 {
41        &self.code
42    }
43}
44
45pub struct MessageBundleBuilder {
46    bundle_name: String,
47    default_language: Option<LanguageIdentifier>,
48    base_dir: Option<PathBuf>,
49    fn_call_generator: Rc<dyn FunctionCallGenerator>,
50    formatter_fn: TokenStream2,
51    language_bundles: BTreeMap<LanguageIdentifier, LanguageBuilder>,
52    language_idents: BTreeMap<LanguageIdentifier, Ident>,
53    language_bundles_code: Vec<TokenStream2>,
54}
55
56impl MessageBundleBuilder {
57    pub fn new(name: &str) -> Self {
58        Self {
59            bundle_name: name.to_string(),
60            default_language: None,
61            base_dir: None,
62            fn_call_generator: Rc::new(FunctionRegistry::default()),
63            formatter_fn: quote! {
64                ::fluent_static::formatter::format
65            },
66            language_idents: BTreeMap::new(),
67            language_bundles: BTreeMap::new(),
68            language_bundles_code: Vec::new(),
69        }
70    }
71
72    pub fn set_bundle_name(&mut self, name: &str) -> &mut Self {
73        self.bundle_name = name.to_string();
74        self
75    }
76
77    pub fn set_message_formatter_fn(
78        &mut self,
79        formatter_fn_name: &str,
80    ) -> Result<&mut Self, Error> {
81        let expr: syn::Expr = syn::parse_str(formatter_fn_name)?;
82        self.formatter_fn = quote! {
83            #expr
84        };
85        Ok(self)
86    }
87
88    pub fn set_default_language(&mut self, language_id: &str) -> Result<&mut Self, Error> {
89        self.default_language = Some(LanguageIdentifier::from_str(language_id)?);
90        Ok(self)
91    }
92
93    pub fn set_resources_dir(&mut self, base_dir: impl AsRef<Path>) -> &mut Self {
94        self.base_dir = Some(base_dir.as_ref().to_path_buf());
95        self
96    }
97
98    pub fn set_function_call_generator(
99        &mut self,
100        fn_call_gen: impl FunctionCallGenerator + 'static,
101    ) -> &mut Self {
102        self.fn_call_generator = Rc::new(fn_call_gen);
103        self
104    }
105
106    fn default_language(&self) -> &LanguageIdentifier {
107        self.default_language
108            .as_ref()
109            .or_else(|| self.language_idents.first_key_value().map(|(k, _)| k))
110            .unwrap()
111    }
112
113    pub fn add_resource(
114        &mut self,
115        lang_id: &str,
116        path: impl AsRef<Path>,
117    ) -> Result<&mut Self, crate::Error> {
118        let resource_path = if path.as_ref().is_absolute() {
119            path.as_ref().to_path_buf()
120        } else if let Some(base_dir) = self.base_dir.as_ref() {
121            base_dir.join(path)
122        } else {
123            return Err(Error::UnexpectedRelativePath(path.as_ref().to_path_buf()));
124        };
125
126        let language_id = LanguageIdentifier::from_str(lang_id)?;
127
128        let language_ident = format_ident!("Lang{}", language_id.to_string().to_case(Case::Pascal));
129
130        self.language_idents
131            .insert(language_id.clone(), language_ident);
132
133        let src =
134            std::fs::read_to_string(&resource_path).map_err(|e| Error::ResourceReadError {
135                path: resource_path.clone(),
136                source: e,
137            })?;
138
139        let ast =
140            parser::parse(src).map_err(|(_, errors)| crate::Error::FluentResourceParseError {
141                errors,
142                path: resource_path,
143            })?;
144
145        let lang_bundle = self
146            .language_bundles
147            .entry(language_id)
148            .or_insert_with_key(|lang_id| {
149                LanguageBuilder::new(lang_id, self.fn_call_generator.clone())
150            });
151
152        self.language_bundles_code
153            .push(lang_bundle.visit_resource(&ast)?);
154
155        Ok(self)
156    }
157
158    fn validate(&self) -> Result<&Self, crate::Error> {
159        let supported_languages: BTreeSet<&LanguageIdentifier> =
160            self.language_bundles.keys().collect();
161
162        if let Some(default_language) = self.default_language.as_ref() {
163            if !supported_languages.contains(default_language) {
164                return Err(Error::UnsupportedDefaultLanguage {
165                    lang: default_language.clone().to_string(),
166                });
167            }
168        }
169
170        let validation_errors: Vec<crate::error::MessageValidationErrorEntry> = self
171            .language_bundles
172            .iter()
173            .fold(BTreeMap::new(), |mut msg_fns, (lang, language_bundle)| {
174                // construct a map of message id -> to a set of language ids
175                language_bundle
176                    .registered_message_fns
177                    .iter()
178                    .for_each(|(id, _)| {
179                        msg_fns.entry(id).or_insert_with(BTreeSet::new).insert(lang);
180                    });
181                msg_fns
182            })
183            .iter()
184            .filter_map(|(id, message_languages)| {
185                // check if message is defined for all of the supported languages
186                // and if not then report
187                if message_languages.len() != supported_languages.len() {
188                    let missing_langs = supported_languages
189                        .difference(&message_languages)
190                        .map(|lang| lang.to_string())
191                        .collect();
192
193                    Some(crate::error::MessageValidationErrorEntry {
194                        message_id: id.to_string(),
195                        defined_in_languages: message_languages
196                            .into_iter()
197                            .map(|lang| lang.to_string())
198                            .collect(),
199                        undefined_in_languages: missing_langs,
200                    })
201                } else {
202                    None
203                }
204            })
205            .collect();
206
207        if !validation_errors.is_empty() {
208            Err(crate::Error::MessageBundleValidationError {
209                bundle: self.bundle_name.clone(),
210                path: None,
211                entries: validation_errors,
212            })
213        } else {
214            Ok(self)
215        }
216    }
217
218    fn generate(&self) -> Result<TokenStream2, Error> {
219        let formatted_bundle_name = self.bundle_name.to_case(Case::Pascal);
220        let bundle_ident = format_ident!("{}", &formatted_bundle_name);
221
222        let (bundle_languages_enum, bundle_languages_code) =
223            self.generate_languages_enum(&formatted_bundle_name);
224
225        let language_bundles_code = &self.language_bundles_code;
226        let message_fns = self.generate_message_fns(&bundle_languages_enum);
227        let default_language_literal = Literal::string(&self.default_language().to_string());
228        let formatter_fn_ident = &self.formatter_fn;
229
230        Ok(quote! {
231            #bundle_languages_code
232
233            #[derive(Debug, Clone)]
234            pub struct #bundle_ident {
235                language: self::#bundle_languages_enum,
236                formatter: Option<::fluent_static::formatter::FormatterFn>,
237                use_isolating: bool,
238            }
239
240            impl ::fluent_static::LanguageAware for self::#bundle_ident {
241                fn language_id(&self) -> &str {
242                    self.language.language_id()
243                }
244            }
245
246            impl ::fluent_static::MessageBundle for self::#bundle_ident {
247                fn get(language_id: &str) -> Option<Self> {
248                    self::#bundle_languages_enum::get(language_id).map(|language| Self { language, ..Default::default() })
249                }
250
251                fn default_language_id() -> &'static str {
252                    #default_language_literal
253                }
254
255                fn supported_language_ids() -> &'static [&'static str] {
256                    self::#bundle_languages_enum::language_ids()
257                }
258            }
259
260            impl ::core::default::Default for self::#bundle_ident {
261                fn default() -> Self {
262                    Self {
263                        language: self::#bundle_languages_enum::default(),
264                        formatter: None,
265                        use_isolating: true,
266                    }
267                }
268            }
269
270            impl #bundle_ident {
271                fn _write_<W: ::std::fmt::Write>(&self, value: & ::fluent_static::value::Value, out: &mut W) -> ::std::fmt::Result {
272                    if self.use_isolating {
273                        out.write_char('\u{2068}')?;
274                    };
275                    if let Some(formatter) = self.formatter.as_ref() {
276                        (formatter)(::fluent_static::LanguageAware::language_id(self), value, out)?;
277                    } else {
278                        #formatter_fn_ident(::fluent_static::LanguageAware::language_id(self), value, out)?;
279                    }
280                    if self.use_isolating {
281                        out.write_char('\u{2069}')?;
282                    };
283                    Ok(())
284                }
285
286                pub fn set_use_isolating(&mut self, value: bool) {
287                    self.use_isolating = value;
288                }
289
290                pub fn set_value_formatter(&mut self, formatter_fn: Option<::fluent_static::formatter::FormatterFn>) {
291                    self.formatter = formatter_fn;
292                }
293            }
294
295            impl #bundle_ident {
296                #(#message_fns)*
297            }
298
299            impl #bundle_ident {
300                #(#language_bundles_code)*
301            }
302        })
303    }
304
305    fn generate_languages_enum(&self, bundle_name: &str) -> (Ident, TokenStream2) {
306        let bundle_languages_enum_ident = format_ident!("{}BundleLanguage", bundle_name);
307
308        let language_idents: Vec<(Literal, &Ident)> = self
309            .language_idents
310            .iter()
311            .map(|(lang_id, ident)| (Literal::string(&lang_id.to_string()), ident))
312            .collect();
313
314        let default_lang_ident = self
315            .language_idents
316            .get(self.default_language())
317            .expect("Unable to get default language");
318
319        let language_mappings: Vec<TokenStream2> = language_idents
320            .iter()
321            .map(|(lang_id, ident)| {
322                quote! {
323                    #lang_id => Some(Self::#ident)
324                }
325            })
326            .collect();
327
328        let ident_mappings: Vec<TokenStream2> = language_idents
329            .iter()
330            .map(|(lang_id, ident)| {
331                quote! {
332                    Self::#ident => #lang_id
333                }
334            })
335            .collect();
336
337        let plural_rules_cardinal_mappings: Vec<TokenStream2> = language_idents
338            .iter()
339            .map(|(lang_id, ident)| {
340                quote! {
341                    Self::#ident => {
342                        static RULES: ::fluent_static::once_cell::sync::Lazy<::fluent_static::intl_pluralrules::PluralRules> =
343                            ::fluent_static::once_cell::sync::Lazy::new(||
344                                ::fluent_static::intl_pluralrules::PluralRules::create(
345                                    ::fluent_static::unic_langid::LanguageIdentifier::from_bytes(#lang_id.as_bytes()).unwrap(),
346                                    ::fluent_static::intl_pluralrules::PluralRuleType::CARDINAL).unwrap());
347                        &RULES
348                    }
349                }
350            })
351            .collect();
352
353        let total_langs = Literal::usize_unsuffixed(language_idents.len());
354
355        let (bundle_languages_literals, bundle_languages_enum_members): (
356            Vec<Literal>,
357            Vec<&Ident>,
358        ) = language_idents.into_iter().unzip();
359
360        (
361            bundle_languages_enum_ident.clone(),
362            quote! {
363                #[derive(Debug, Clone)]
364                pub enum #bundle_languages_enum_ident {
365                    #(#bundle_languages_enum_members),*
366                }
367
368                impl #bundle_languages_enum_ident {
369                    const LANGUAGE_IDS: [&'static str; #total_langs] = [#(#bundle_languages_literals),*];
370
371                    fn get(lang_id: &str) -> Option<Self> {
372                        match lang_id {
373                            #(#language_mappings),*,
374                            _ => None
375                        }
376                    }
377
378                    fn language_ids() -> &'static [&'static str] {
379                        &Self::LANGUAGE_IDS
380                    }
381
382                    fn plural_rules_cardinal(&self) -> &'static ::fluent_static::intl_pluralrules::PluralRules {
383                        match self {
384                            #(#plural_rules_cardinal_mappings),*
385                        }
386                    }
387
388                }
389
390                impl ::fluent_static::LanguageAware for self::#bundle_languages_enum_ident {
391                    fn language_id(&self) -> &str {
392                        match self {
393                            #(#ident_mappings),*
394                        }
395                    }
396                }
397
398                impl ::core::default::Default for self::#bundle_languages_enum_ident {
399                    fn default() -> Self {
400                        Self::#default_lang_ident
401                    }
402                }
403            },
404        )
405    }
406
407    fn generate_message_fns(&self, languages_enum: &Ident) -> Vec<TokenStream2> {
408        self.language_bundles
409            .get(self.default_language())
410            .iter()
411            .flat_map(|bundle| {
412                bundle
413                    .registered_message_fns
414                    .iter()
415                    .map(|(id, def)| self.generate_message_fn(languages_enum, id, def))
416            })
417            .collect()
418    }
419
420    fn generate_message_fn(
421        &self,
422        languages_enum: &Ident,
423        msg_fn_id: &PublicFluentId,
424        msg: &FluentMessage,
425    ) -> TokenStream2 {
426        let fn_ident = format_ident!(
427            "{}",
428            msg.id().to_string().replace('.', "_").to_case(Case::Snake)
429        );
430
431        let vars = msg.declared_vars();
432        let fn_generics = if msg.has_vars() {
433            quote! {<'a>}
434        } else {
435            quote! {}
436        };
437        let var: Vec<&Ident> = vars.iter().map(|var| &var.var_ident).collect();
438
439        let lang_selectors: Vec<TokenStream2> = self
440            .language_bundles
441            .iter()
442            .flat_map(|(lang, bundle)| {
443                bundle
444                    .registered_message_fns
445                    .get(msg_fn_id)
446                    .map(|fn_def| (lang, fn_def))
447            })
448            .map(|(lang, lang_msg)| {
449                let lang_fn_ident = lang_msg.fn_ident();
450                let fn_vars: BTreeSet<Ident> = lang_msg
451                    .vars()
452                    .into_iter()
453                    .map(|var| var.var_ident)
454                    .collect();
455                let lang = self.language_idents.get(lang).expect("Unexpected language");
456                quote! {
457                    self::#languages_enum::#lang => self.#lang_fn_ident(&mut out, #(#fn_vars),*)
458                }
459            })
460            .collect();
461
462        quote! {
463            pub fn #fn_ident #fn_generics(&self, #(#var: impl Into<::fluent_static::value::Value<'a>>),*) -> ::fluent_static::Message {
464                #(let #var = #var.into();)*
465                let mut out = String::new();
466                match self.language {
467                    #(#lang_selectors),*,
468                }.unwrap();
469
470                ::fluent_static::Message::from(out)
471            }
472        }
473    }
474
475    pub fn build(&self) -> Result<MessageBundle, Error> {
476        let generated_tokens = self.validate()?.generate()?;
477        Ok(MessageBundle {
478            name: self.bundle_name.clone(),
479            code: generated_tokens,
480        })
481    }
482}
483
484impl Default for MessageBundleBuilder {
485    fn default() -> Self {
486        Self::new("Message")
487    }
488}