glossa_codegen/generator/
output_match.rs

1use std::io::{self, Write};
2
3use anyhow::bail;
4use glossa_shared::{
5  ToCompactString, fmt_compact,
6  tap::{Pipe, Tap},
7};
8// use compact_str::{ToCompactString, fmt_compact};
9use itertools::Itertools;
10use lang_id::{LangID, RawID};
11
12use crate::{
13  AnyResult, MiniStr,
14  generator::{Generator, MapType},
15};
16
17impl<'h> Generator<'h> {
18  /// Generates a consolidated match function containing all localization
19  /// mappings
20  ///
21  /// Generates:
22  ///
23  /// ```ignore
24  /// const fn map(lang: &[u8], map_name: &[u8], key: &[u8])
25  ///   -> &'static str {
26  ///   match (lang, map_name, key) {...}
27  /// }
28  /// ```
29  ///
30  /// ## Parameter
31  ///
32  /// - `non_dsl`
33  ///   - Specifies the type of mapping to process.
34  ///   - Note: Does not support DSL MapType
35  ///
36  /// ## Example
37  ///
38  /// ```ignore
39  /// use glossa_codegen::{L10nResources, Generator, generator::MapType};
40  ///
41  /// const L10N_DIR: &str = "../../locales/";
42  ///
43  /// let data = L10nResources::new(L10N_DIR)
44  ///   .with_include_languages(["en-GB", "de", "es", "pt",
45  /// "zh-pinyin"]);
46  ///
47  /// let function_data = Generator::default()
48  ///   .with_resources(data)
49  ///   .output_match_fn_all_in_one(MapType::Regular)?;
50  ///
51  /// assert_eq!(function_data.trim(), r#######"
52  ///   pub(crate) const fn map(lang: &[u8], map_name: &[u8], key: &[u8]) ->
53  ///   &'static str {
54  ///     match (lang, map_name, key) {
55  ///     (b"de", b"error", b"text-not-found") => r#####"Kein lokalisierter Text
56  /// gefunden"#####,
57  ///     (b"en-GB", b"error", b"text-not-found") => r#####"No
58  /// localised text found"#####,
59  ///     (b"es", b"error", b"text-not-found") =>
60  /// r#####"No se encontró texto localizado"#####,
61  ///     (b"pt", b"error",
62  /// b"text-not-found") => r#####"Nenhum texto localizado encontrado"#####,
63  ///     (b"zh-Latn-CN", b"error", b"text-not-found") => r#####"MeiYou ZhaoDao
64  /// BenDiHua WenBen"#####,
65  ///     _ => "",
66  /// }}
67  ///     "#######.trim());
68  /// ```
69  pub fn output_match_fn_all_in_one(
70    &'h self,
71    non_dsl: MapType,
72  ) -> io::Result<String> {
73    const S_HEADER: &str = r##"const fn map(lang: &[u8], map_name: &[u8], key: &[u8])
74    -> &'static str {
75    match (lang, map_name, key) {
76    "##;
77
78    let new_header = || self.new_match_fn_header(S_HEADER);
79
80    non_dsl
81      .get_non_dsl_maps(self)?
82      .iter()
83      // .filter(|(_, data)| !data.is_empty())
84      .flat_map(|(lang, map_entry)| {
85        map_entry
86          .iter()
87          .map(move |e| (lang, e))
88      })
89      .fold(
90        new_header(), //
91        |mut acc, (lang, ((name, key), value))| {
92          // Build match arms for each localization entry
93          [
94            "(b\"",
95            lang
96              .to_compact_string()
97              .as_str(),
98            "\", ",
99            key_as_bytes(name).as_str(),
100            ", ",
101            key_as_bytes(key).as_str(),
102            ") => r#####",
103            "\"",
104            value.as_str(),
105            "\"",
106            "#####,",
107            "\n",
108          ]
109          .map(|s| acc.push_str(s));
110          acc
111        },
112      )
113      .tap_mut(|buf| buf.push_str("    _ => \"\",\n}}"))
114      .pipe(Ok)
115  }
116
117  /// Generates a function:
118  ///   `const fn map(language: &[u8]) -> &'static str { match language {...} }`
119  ///
120  /// Note: This function is for performance optimization.
121  /// **Only** invoke it to generate a new function when both `map_name` and
122  /// `key` are guaranteed to be unique.
123  /// Otherwise, use [`Self::output_match_fn_all_in_one`].
124  pub fn output_match_fn_all_in_one_by_language(
125    &'h self,
126    non_dsl: MapType,
127  ) -> io::Result<String> {
128    const S_HEADER: &str = r##"const fn map(language: &[u8]) -> &'static str {
129    match language {
130    "##;
131
132    let new_header = || self.new_match_fn_header(S_HEADER);
133
134    non_dsl
135      .get_non_dsl_maps(self)?
136      .iter()
137      .flat_map(|(lang, map_entry)| {
138        map_entry
139          .iter()
140          .map(move |(_ks, v)| (lang, v))
141      })
142      .fold(
143        new_header(), //
144        |mut acc, (lang, value)| {
145          [
146            "b\"",
147            lang
148              .to_compact_string()
149              .as_str(),
150            "\" => r#####",
151            "\"",
152            value.as_str(),
153            "\"",
154            "#####,",
155            "\n",
156          ]
157          .map(|s| acc.push_str(s));
158          acc
159        },
160      )
161      .tap_mut(|buf| buf.push_str("    _ => \"\",\n}}"))
162      .pipe(Ok)
163  }
164
165  /// Generates a function:
166  ///   `const fn map(language: &[u8], key: &[u8]) -> &'static str { match
167  /// (language, key) {...} }`
168  ///
169  /// # Note
170  ///
171  /// You can invoke this function to generate a new function **only** when
172  /// `map_name` is unique.
173  ///
174  /// **Example**:
175  ///
176  /// - `en/yes-no { yes: "Yes", no: "No"}`
177  /// - `de/yes-no { yes: "Ja", no: "Nein" }`
178  ///
179  /// Here, `map_name` is unique (per language), so it can be omitted:
180  ///
181  /// ```ignore
182  /// match (language, key) {
183  ///   (b"en", b"yes") => r#####"Yes"#####,
184  ///   (b"en", b"no") => r#####"No"#####,
185  ///   (b"de", b"yes") => r#####"Ja"#####,
186  ///   (b"de", b"no") => r#####"Nein"#####,
187  /// }
188  /// ```
189  ///
190  /// If `map_names` are not unique, use [`Self::output_match_fn_all_in_one`]
191  /// instead.
192  ///
193  /// For example, adding a new map: `en/yes-no2 { yes: "YES", no: "NO"}`
194  /// would create conflicting keys ("yes", "no") if `map_name` is omitted.
195  pub fn output_match_fn_all_in_one_by_language_and_key(
196    &'h self,
197    non_dsl: MapType,
198  ) -> io::Result<String> {
199    const S_HEADER: &str = r##"const fn map(language: &[u8], key: &[u8])
200    -> &'static str {
201    match (language, key) {
202    "##;
203
204    let new_header = || self.new_match_fn_header(S_HEADER);
205
206    non_dsl
207      .get_non_dsl_maps(self)?
208      .iter()
209      .flat_map(|(lang, map_entry)| {
210        map_entry
211          .iter()
212          .map(move |((_name, key), value)| (lang, key, value))
213      })
214      .fold(
215        new_header(), //
216        |mut acc, (lang, key, value)| {
217          [
218            "(b\"",
219            lang
220              .to_compact_string()
221              .as_str(),
222            "\", ",
223            key_as_bytes(key).as_str(),
224            ") => r#####",
225            "\"",
226            value.as_str(),
227            "\"",
228            "#####,",
229            "\n",
230          ]
231          .map(|s| acc.push_str(s));
232          acc
233        },
234      )
235      .tap_mut(|buf| buf.push_str("    _ => \"\",\n}}"))
236      .pipe(Ok)
237  }
238
239  /// Generates a function listing all available locales
240  ///
241  /// ## Parameters
242  ///
243  /// - `map_type`
244  ///   - Mapping type to process
245  /// - `const_lang_id`
246  ///   - Whether to generate lang_id constants
247  ///
248  /// ## Example
249  ///
250  /// ```ignore
251  /// let function_data = new_generator()
252  ///   .with_visibility(crate::Visibility::Pub)
253  ///   .output_locales_fn(
254  ///     MapType::Regular,
255  ///     false,
256  /// )?;
257  /// ```
258  ///
259  /// function_data:
260  ///
261  /// ```ignore
262  /// pub const fn all_locales() -> [lang_id::LangID; 107] {
263  /// use lang_id::consts::*;
264  /// [
265  ///   lang_id_af(),
266  ///   lang_id_am(),
267  ///   lang_id_ar(),
268  ///   lang_id_az(),
269  ///   lang_id_be(),
270  ///   lang_id_bg(),
271  ///   lang_id_bn(),
272  ///   lang_id_bs(),
273  ///   lang_id_ca(),
274  ///   lang_id_ceb(),
275  ///   lang_id_co(),
276  ///   ...
277  /// ]}
278  /// ```
279  pub fn output_locales_fn(
280    &'h self,
281    map_type: MapType,
282    const_lang_id: bool,
283  ) -> AnyResult<String> {
284    let raw_locales = self.collect_raw_locales(map_type)?;
285    let locales_len = raw_locales.len();
286    let new_header = || self.new_locales_fn_header(&locales_len, &const_lang_id);
287
288    if !const_lang_id {
289      return new_header()
290        .tap_mut(|buf| {
291          let push_str = |s| buf.push_str(s);
292          [&format!("{raw_locales:#?}"), "}\n"].map(push_str);
293        })
294        .pipe(Ok);
295    }
296
297    // Process constant lang IDs
298    raw_locales
299      .iter()
300      .map(try_conv_const_id)
301      .try_fold(
302        new_header(), //
303        |mut acc, fn_name| {
304          let push_str = |s| acc.push_str(s);
305          ["\n    ", &fn_name?, ","].map(push_str);
306          Ok::<_, anyhow::Error>(acc)
307        },
308      )?
309      .tap_mut(|buf| buf.push_str("  ]\n}"))
310      .pipe(Ok)
311  }
312
313  fn collect_raw_locales(&'h self, map_type: MapType) -> io::Result<Vec<MiniStr>> {
314    match map_type.is_dsl() {
315      true => match self.get_or_init_dsl_maps() {
316        x if x.is_empty() => "// Error: Empty DSL Map"
317          .pipe(io::Error::other)
318          .pipe(Err),
319        data => data
320          .iter()
321          .map(|(id, _)| id.to_compact_string())
322          .collect_vec()
323          .pipe(Ok),
324      },
325      _ => map_type
326        .get_non_dsl_maps(self)?
327        .iter()
328        .map(|(id, _)| id.to_compact_string())
329        .collect_vec()
330        .pipe(Ok),
331    }
332  }
333  fn new_locales_fn_header(
334    &'h self,
335    locales_len: &usize,
336    const_lang_id: &bool,
337    // this: &Generator<'_, 'h>,
338  ) -> String {
339    // Generate appropriate header based on const_lang_id flag
340    let ret_type = {
341      match *const_lang_id {
342        true => fmt_compact!(
343          r#"[super::lang_id::LangID; {locales_len}] {{
344  #[allow(unused_imports)]
345  use super::lang_id::RawID;
346  use super::lang_id::consts::*;
347  ["#
348        ),
349        _ => fmt_compact!("[&'static str; {locales_len}] {{\n  "),
350      }
351    };
352
353    let s_header = format!("const fn all_locales() -> {ret_type}",);
354    self.new_match_fn_header(&s_header)
355  }
356
357  /// Creates header for generated match functions
358  fn new_match_fn_header(&'h self, header: &str) -> String {
359    let vis_fn = self.get_visibility().as_str();
360    String::with_capacity(8192).tap_mut(|buf| {
361      [vis_fn, " ", header].map(|s| buf.push_str(s));
362    })
363  }
364
365  /// Generates individual match functions per locale
366  ///   => `const fn map(map_name: &[u8], key: &[u8]) -> &'static str`
367  pub fn output_match_fn(&'h self, non_dsl: MapType) -> io::Result<()> {
368    const HEADER: &str = r##"const fn map(map_name: &[u8], key: &[u8]) -> &'static str {
369    match (map_name, key) {
370    "##;
371    let new_header = || self.new_match_fn_header(HEADER);
372
373    // Process non-DSL maps only (DSL MapType not supported)
374    non_dsl
375      .get_non_dsl_maps(self)?
376      .iter()
377      // .filter(|(_, data)| !data.is_empty())
378      .map(|(lang, map_entry)| {
379        let match_fn_string = map_entry
380          .iter()
381          .fold(
382            new_header(), //
383            |mut acc, ((map_name, data_k), data_v)| {
384              // Build match arms for each entry
385              [
386                "(",
387                key_as_bytes(map_name).as_str(),
388                ", ",
389                key_as_bytes(data_k).as_str(),
390                r#") => r#####"#,
391                "\"", // "
392                data_v.as_str(),
393                "\"",                            // "
394                r###########"#####,"###########, // ###,
395                "\n",
396              ]
397              .map(|s| acc.push_str(s));
398              acc
399            },
400          )
401          .tap_mut(|buf| buf.push_str("    _ => \"\",\n}}"));
402        (lang, match_fn_string)
403      })
404      .try_for_each(|(lang, s)| {
405        // Write generated content to module files
406        self
407          .create_rs_mod_file(lang)?
408          .write_all(s.as_bytes())
409      })
410  }
411
412  /// Generates individual match functions per locale
413  /// => `const fn map(key: &[u8]) -> &'static str`
414  ///
415  /// Compared to `output_match_fn`, omits map_name. If you're unsure which one
416  /// to use, then use [output_match_fn()](Self::output_match_fn)
417  pub fn output_match_fn_by_key(&'h self, non_dsl: MapType) -> io::Result<()> {
418    const HEADER: &str = r##"const fn map(key: &[u8]) -> &'static str {
419    match key {
420    "##;
421    let new_header = || self.new_match_fn_header(HEADER);
422
423    // Process non-DSL maps only (DSL MapType not supported)
424    non_dsl
425      .get_non_dsl_maps(self)?
426      .iter()
427      // .filter(|(_, data)| !data.is_empty())
428      .map(|(lang, map_entry)| {
429        let match_fn_string = map_entry
430          .iter()
431          .fold(
432            new_header(), //
433            |mut acc, ((_, data_k), data_v)| {
434              // Build match arms for each entry
435              [
436                key_as_bytes(data_k).as_str(),
437                r#" => r#####"#,
438                "\"", // "
439                data_v.as_str(),
440                "\"",                            // "
441                r###########"#####,"###########, // ###,
442                "\n",
443              ]
444              .map(|s| acc.push_str(s));
445              acc
446            },
447          )
448          .tap_mut(|buf| buf.push_str("    _ => \"\",\n}}"));
449        (lang, match_fn_string)
450      })
451      .try_for_each(|(lang, s)| {
452        // Write generated content to module files
453        self
454          .create_rs_mod_file(lang)?
455          .write_all(s.as_bytes())
456      })
457  }
458}
459
460fn try_conv_const_id(id: &MiniStr) -> Result<MiniStr, anyhow::Error> {
461  use lang_id::matches::{get_fn_name, match_id};
462
463  match match_id(id.as_bytes())
464    .to_compact_string()
465    .as_str()
466  {
467    x if x == id => id
468      .as_bytes()
469      .pipe(get_fn_name)
470      .to_compact_string()
471      .pipe(Ok),
472    _ => {
473      let id = id.parse::<LangID>()?;
474      if id.variants().count() >= 1 {
475        bail!("This ID ({id}) contains variants and cannot be converted to const.")
476      }
477      RawID::try_from_str(
478        id.language.as_str(),
479        id.script
480          .map(|x| x.to_compact_string())
481          .unwrap_or_default()
482          .as_str(),
483        id.region
484          .map(|x| x.to_compact_string())
485          .unwrap_or_default()
486          .as_str(),
487      )?
488      .to_compact_string()
489      .pipe(Ok)
490    }
491  }
492}
493
494/// Helper function to format keys as byte string literals
495fn key_as_bytes(key: &str) -> MiniStr {
496  use fmt_compact as fmt;
497
498  match key.is_ascii() {
499    true => fmt!("b{key:?}"),
500    _ => fmt!("{:?}", key.as_bytes()),
501  }
502}
503
504#[cfg(test)]
505mod tests {
506  use super::*;
507  use crate::{
508    AnyResult,
509    generator::dbg_generator::{en_generator, new_generator},
510  };
511
512  #[ignore]
513  #[test]
514  fn test_output_match_fn() -> AnyResult<()> {
515    new_generator().output_match_fn(MapType::Regular)?;
516    Ok(())
517  }
518
519  #[ignore]
520  #[test]
521  fn test_output_aio_match_fn() -> AnyResult<()> {
522    en_generator()
523      .output_match_fn_all_in_one(MapType::Regular)?
524      .tap(|s| println!("{s}"));
525    Ok(())
526  }
527
528  const fn map(language: &[u8], map_name: &[u8], key: &[u8]) -> &'static str {
529    match (language, map_name, key) {
530      (b"de", b"error", b"text-not-found") => {
531        r###"Kein lokalisierter Text gefunden"###
532      }
533      (b"el", b"error", b"text-not-found") => {
534        r###"Δεν βρέθηκε κανένα τοπικό κείμενο"###
535      }
536      (b"en", b"error", b"text-not-found") => r###"No localized text found"###,
537      (b"en", b"test", [240, 159, 145, 139, 240, 159, 140, 144]) => {
538        r###"hello world"###
539      }
540      (b"en", b"test", b"hello") => r###"world"###,
541      (b"en-GB", b"error", b"text-not-found") => r###"No localised text found"###,
542      (b"zh", b"error", b"text-not-found") => r###"未找到本地化文本"###,
543      (b"zh", b"test", b"quote") => r###"""no"''""###,
544      (b"zh-Hant", b"error", b"text-not-found") => r###"沒有找到本地化文本"###,
545      _ => "",
546    }
547  }
548
549  #[ignore]
550  #[test]
551  fn test_get_match_map() {
552    const S: &str = map(b"de", b"error", "text-not-found".as_bytes());
553    assert!(!S.is_empty());
554    println!("{S}");
555  }
556
557  #[ignore]
558  #[test]
559  fn test_list_all_locales() -> AnyResult<()> {
560    let s = new_generator()
561      .with_visibility(crate::Visibility::Pub)
562      .output_locales_fn(
563        MapType::Regular, //
564        // false,
565        true,
566      )?;
567    println!("{s}");
568
569    Ok(())
570  }
571
572  #[ignore]
573  #[test]
574  fn doc_test_all_in_one_match_fn() -> io::Result<()> {
575    use crate::L10nResources;
576    const L10N_DIR: &str = "../../locales/";
577
578    let data = L10nResources::new(L10N_DIR).with_include_languages([
579      "en-GB",
580      "de",
581      "es",
582      "pt",
583      "zh-pinyin",
584    ]);
585
586    let function_data = Generator::default()
587      .with_resources(data)
588      .output_match_fn_all_in_one(MapType::Regular)?;
589
590    assert_eq!(function_data, r#######"
591    pub(crate) const fn map(lang: &[u8], map_name: &[u8], key: &[u8]) ->
592  &'static str {
593    match (lang, map_name, key) {
594    (b"de", b"error", b"text-not-found") => r#####"Kein lokalisierter Text gefunden"#####,
595(b"en-GB", b"error", b"text-not-found") => r#####"No localised text found"#####,
596(b"es", b"error", b"text-not-found") => r#####"No se encontró texto localizado"#####,
597(b"pt", b"error", b"text-not-found") => r#####"Nenhum texto localizado encontrado"#####,
598(b"zh-Latn-CN", b"error", b"text-not-found") => r#####"MeiYou ZhaoDao BenDiHua WenBen"#####,
599    _ => "",
600}}
601    "#######.trim());
602
603    println!("{function_data}");
604    Ok(())
605  }
606}