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 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 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}