glossa_codegen/generator/
output_match.rs

1use std::io::{self, Write};
2
3use glossa_shared::{
4  ToCompactString, fmt_compact,
5  tap::{Pipe, Tap},
6};
7
8use crate::{
9  MiniStr,
10  generator::{Generator, MapType},
11};
12
13impl<'h> Generator<'h> {
14  /// Generates individual match functions per locale
15  ///   => `const fn map(map_name: &[u8], key: &[u8]) -> &'static str`
16  pub fn output_match_fn(&self, non_dsl: MapType) -> io::Result<()> {
17    const HEADER: &str = r##"const fn map(map_name: &[u8], key: &[u8]) -> &'static str {
18    match (map_name, key) {
19    "##;
20    let new_header = || self.new_fn_header(HEADER);
21
22    // Process non-DSL maps only (DSL MapType not supported)
23    non_dsl
24      .get_non_dsl_maps(self)?
25      .iter()
26      // .filter(|(_, data)| !data.is_empty())
27      .map(|(lang, map_entry)| {
28        let match_fn_string = map_entry
29          .iter()
30          .fold(
31            new_header(), //
32            |mut acc, ((map_name, data_k), data_v)| {
33              // Build match arms for each entry
34              [
35                "(",
36                key_as_bytes(map_name).as_str(),
37                ", ",
38                key_as_bytes(data_k).as_str(),
39                r#") => r#####"#,
40                "\"", // "
41                data_v.as_str(),
42                "\"",                            // "
43                r###########"#####,"###########, // ###,
44                "\n",
45              ]
46              .iter()
47              .for_each(|s| acc.push_str(s));
48              acc
49            },
50          )
51          .tap_mut(|buf| buf.push_str("    _ => \"\",\n}}"));
52        (lang, match_fn_string)
53      })
54      .try_for_each(|(lang, s)| {
55        // Write generated content to module files
56        self
57          .create_rs_mod_file(lang)?
58          .write_all(s.as_bytes())
59      })
60  }
61
62  /// Generates a consolidated match function containing all localization
63  /// mappings
64  ///
65  /// Generates:
66  ///
67  /// ```ignore
68  /// const fn map(lang: &[u8], map_name: &[u8], key: &[u8])
69  ///   -> &'static str {
70  ///   match (lang, map_name, key) {...}
71  /// }
72  /// ```
73  ///
74  /// ## Parameter
75  ///
76  /// - `non_dsl`
77  ///   - Specifies the type of mapping to process.
78  ///   - Note: Does not support DSL MapType
79  ///
80  /// ## Example
81  ///
82  /// ```ignore
83  /// use glossa_codegen::{L10nResources, Generator, generator::MapType};
84  ///
85  /// const L10N_DIR: &str = "../../locales/";
86  ///
87  /// let data = L10nResources::new(L10N_DIR)
88  ///   .with_include_languages(["en-GB", "de", "es", "pt",
89  /// "zh-pinyin"]);
90  ///
91  /// let function_data = Generator::default()
92  ///   .with_resources(data)
93  ///   .output_match_fn_all_in_one(MapType::Regular)?;
94  ///
95  /// assert_eq!(function_data.trim(), r#######"
96  ///   pub(crate) const fn map(lang: &[u8], map_name: &[u8], key: &[u8]) ->
97  ///   &'static str {
98  ///     match (lang, map_name, key) {
99  ///     (b"de", b"error", b"text-not-found") => r#####"Kein lokalisierter Text
100  /// gefunden"#####,
101  ///     (b"en-GB", b"error", b"text-not-found") => r#####"No
102  /// localised text found"#####,
103  ///     (b"es", b"error", b"text-not-found") =>
104  /// r#####"No se encontró texto localizado"#####,
105  ///     (b"pt", b"error",
106  /// b"text-not-found") => r#####"Nenhum texto localizado encontrado"#####,
107  ///     (b"zh-Latn-CN", b"error", b"text-not-found") => r#####"MeiYou ZhaoDao
108  /// BenDiHua WenBen"#####,
109  ///     _ => "",
110  /// }}
111  ///     "#######.trim());
112  /// ```
113  pub fn output_match_fn_all_in_one(
114    &'h self,
115    non_dsl: MapType,
116  ) -> io::Result<String> {
117    const S_HEADER: &str = r##"const fn map(lang: &[u8], map_name: &[u8], key: &[u8])
118    -> &'static str {
119    match (lang, map_name, key) {
120    "##;
121
122    let new_header = || self.new_fn_header(S_HEADER);
123
124    non_dsl
125      .get_non_dsl_maps(self)?
126      .iter()
127      // .filter(|(_, data)| !data.is_empty())
128      .flat_map(|(lang, map_entry)| {
129        map_entry
130          .iter()
131          .map(move |e| (lang, e))
132      })
133      .fold(
134        new_header(), //
135        |mut acc, (lang, ((name, key), value))| {
136          // Build match arms for each localization entry
137          [
138            "(b\"",
139            lang
140              .to_compact_string()
141              .as_str(),
142            "\", ",
143            key_as_bytes(name).as_str(),
144            ", ",
145            key_as_bytes(key).as_str(),
146            ") => r#####",
147            "\"",
148            value.as_str(),
149            "\"",
150            "#####,",
151            "\n",
152          ]
153          .iter()
154          .for_each(|s| acc.push_str(s));
155          acc
156        },
157      )
158      .tap_mut(|buf| buf.push_str("    _ => \"\",\n}}"))
159      .pipe(Ok)
160  }
161
162  /// Generates a function:
163  ///   `const fn map(language: &[u8]) -> &'static str { match language {...} }`
164  ///
165  /// Note: This function is for performance optimization.
166  /// **Only** invoke it to generate a new function when both `map_name` and
167  /// `key` are guaranteed to be unique.
168  /// Otherwise, use [`Self::output_match_fn_all_in_one`].
169  pub fn output_match_fn_all_in_one_by_language(
170    &'h self,
171    non_dsl: MapType,
172  ) -> io::Result<String> {
173    const S_HEADER: &str = r##"const fn map(language: &[u8]) -> &'static str {
174    match language {
175    "##;
176
177    let new_header = || self.new_fn_header(S_HEADER);
178
179    non_dsl
180      .get_non_dsl_maps(self)?
181      .iter()
182      .flat_map(|(lang, map_entry)| {
183        map_entry
184          .values()
185          .map(move |v| (lang, v))
186      })
187      .fold(
188        new_header(), //
189        |mut acc, (lang, value)| {
190          [
191            "b\"",
192            lang
193              .to_compact_string()
194              .as_str(),
195            "\" => r#####",
196            "\"",
197            value.as_str(),
198            "\"",
199            "#####,",
200            "\n",
201          ]
202          .iter()
203          .for_each(|s| acc.push_str(s));
204          acc
205        },
206      )
207      .tap_mut(|buf| buf.push_str("    _ => \"\",\n}}"))
208      .pipe(Ok)
209  }
210
211  /// Generates a function:
212  ///   `const fn map(language: &[u8], key: &[u8]) -> &'static str { match
213  /// (language, key) {...} }`
214  ///
215  /// # Note
216  ///
217  /// You can invoke this function to generate a new function **only** when
218  /// `map_name` is unique.
219  ///
220  /// **Example**:
221  ///
222  /// - `en/yes-no { yes: "Yes", no: "No"}`
223  /// - `de/yes-no { yes: "Ja", no: "Nein" }`
224  ///
225  /// Here, `map_name` is unique (per language), so it can be omitted:
226  ///
227  /// ```ignore
228  /// match (language, key) {
229  ///   (b"en", b"yes") => r#####"Yes"#####,
230  ///   (b"en", b"no") => r#####"No"#####,
231  ///   (b"de", b"yes") => r#####"Ja"#####,
232  ///   (b"de", b"no") => r#####"Nein"#####,
233  /// }
234  /// ```
235  ///
236  /// If `map_names` are not unique, use [`Self::output_match_fn_all_in_one`]
237  /// instead.
238  ///
239  /// For example, adding a new map: `en/yes-no2 { yes: "YES", no: "NO"}`
240  /// would create conflicting keys ("yes", "no") if `map_name` is omitted.
241  pub fn output_match_fn_all_in_one_without_map_name(
242    &'h self,
243    non_dsl: MapType,
244  ) -> io::Result<String> {
245    const S_HEADER: &str = r##"const fn map(language: &[u8], key: &[u8])
246    -> &'static str {
247    match (language, key) {
248    "##;
249
250    let new_header = || self.new_fn_header(S_HEADER);
251
252    non_dsl
253      .get_non_dsl_maps(self)?
254      .iter()
255      .flat_map(|(lang, map_entry)| {
256        map_entry
257          .iter()
258          .map(move |((_name, key), value)| (lang, key, value))
259      })
260      .fold(
261        new_header(), //
262        |mut acc, (lang, key, value)| {
263          [
264            "(b\"",
265            lang
266              .to_compact_string()
267              .as_str(),
268            "\", ",
269            key_as_bytes(key).as_str(),
270            ") => r#####",
271            "\"",
272            value.as_str(),
273            "\"",
274            "#####,",
275            "\n",
276          ]
277          .iter()
278          .for_each(|s| acc.push_str(s));
279          acc
280        },
281      )
282      .tap_mut(|buf| buf.push_str("    _ => \"\",\n}}"))
283      .pipe(Ok)
284  }
285
286  /// Creates header for generated functions
287  pub fn new_fn_header(&self, header: &str) -> String {
288    let vis_fn = self.get_visibility().as_str();
289    String::with_capacity(8192).tap_mut(|buf| {
290      [vis_fn, " ", header]
291        .iter()
292        .for_each(|s| buf.push_str(s));
293    })
294  }
295
296  /// Generates individual match functions per locale
297  /// => `const fn map(key: &[u8]) -> &'static str`
298  ///
299  /// Compared to `output_match_fn`, omits map_name. If you're unsure which one
300  /// to use, then use [output_match_fn()](Self::output_match_fn)
301  pub fn output_match_fn_without_map_name(
302    &'h self,
303    non_dsl: MapType,
304  ) -> io::Result<()> {
305    const HEADER: &str = r##"const fn map(key: &[u8]) -> &'static str {
306    match key {
307    "##;
308    let new_header = || self.new_fn_header(HEADER);
309
310    // Process non-DSL maps only (DSL MapType not supported)
311    non_dsl
312      .get_non_dsl_maps(self)?
313      .iter()
314      // .filter(|(_, data)| !data.is_empty())
315      .map(|(lang, map_entry)| {
316        let match_fn_string = map_entry
317          .iter()
318          .fold(
319            new_header(), //
320            |mut acc, ((_, data_k), data_v)| {
321              // Build match arms for each entry
322              [
323                key_as_bytes(data_k).as_str(),
324                r#" => r#####"#,
325                "\"", // "
326                data_v.as_str(),
327                "\"",                            // "
328                r###########"#####,"###########, // ###,
329                "\n",
330              ]
331              .iter()
332              .for_each(|s| acc.push_str(s));
333              acc
334            },
335          )
336          .tap_mut(|buf| buf.push_str("    _ => \"\",\n}}"));
337        (lang, match_fn_string)
338      })
339      .try_for_each(|(lang, s)| {
340        // Write generated content to module files
341        self
342          .create_rs_mod_file(lang)?
343          .write_all(s.as_bytes())
344      })
345  }
346}
347
348/// Helper function to format keys as byte string literals
349pub(crate) fn key_as_bytes(key: &str) -> MiniStr {
350  use fmt_compact as fmt;
351
352  match key.is_ascii() {
353    true => fmt!("b{key:?}"),
354    _ => fmt!("{:?}", key.as_bytes()),
355  }
356}
357
358#[cfg(test)]
359mod tests {
360  use glossa_shared::display::puts;
361
362  use super::*;
363  use crate::{
364    AnyResult,
365    generator::dbg_generator::{en_generator, new_generator},
366  };
367
368  #[ignore]
369  #[test]
370  fn test_output_match_fn() -> AnyResult<()> {
371    new_generator().output_match_fn(MapType::Regular)?;
372    Ok(())
373  }
374
375  #[ignore]
376  #[test]
377  fn test_output_aio_match_fn() -> AnyResult<()> {
378    en_generator()
379      .output_match_fn_all_in_one(MapType::Regular)?
380      .pipe_ref(puts)
381      .pipe(Ok)
382  }
383
384  const fn map(language: &[u8], map_name: &[u8], key: &[u8]) -> &'static str {
385    match (language, map_name, key) {
386      (b"de", b"error", b"text-not-found") => {
387        r###"Kein lokalisierter Text gefunden"###
388      }
389      (b"el", b"error", b"text-not-found") => {
390        r###"Δεν βρέθηκε κανένα τοπικό κείμενο"###
391      }
392      (b"en", b"error", b"text-not-found") => r###"No localized text found"###,
393      (b"en", b"test", [240, 159, 145, 139, 240, 159, 140, 144]) => {
394        r###"hello world"###
395      }
396      (b"en", b"test", b"hello") => r###"world"###,
397      (b"en-GB", b"error", b"text-not-found") => r###"No localised text found"###,
398      (b"zh", b"error", b"text-not-found") => r###"未找到本地化文本"###,
399      (b"zh", b"test", b"quote") => r###"""no"''""###,
400      (b"zh-Hant", b"error", b"text-not-found") => r###"沒有找到本地化文本"###,
401      _ => "",
402    }
403  }
404
405  #[ignore]
406  #[test]
407  fn test_get_match_map() {
408    const S: &str = map(b"de", b"error", "text-not-found".as_bytes());
409    assert!(!S.is_empty());
410    println!("{S}");
411  }
412
413  #[ignore]
414  #[test]
415  fn doc_test_all_in_one_match_fn() -> io::Result<()> {
416    use crate::L10nResources;
417    const L10N_DIR: &str = "../../locales/";
418
419    let data = L10nResources::new(L10N_DIR).with_include_languages([
420      "en-GB",
421      "de",
422      "es",
423      "pt",
424      "zh-pinyin",
425    ]);
426
427    let function_data = Generator::default()
428      .with_resources(data)
429      .output_match_fn_all_in_one(MapType::Regular)?;
430
431    assert_eq!(function_data, r#######"
432    pub(crate) const fn map(lang: &[u8], map_name: &[u8], key: &[u8]) ->
433  &'static str {
434    match (lang, map_name, key) {
435    (b"de", b"error", b"text-not-found") => r#####"Kein lokalisierter Text gefunden"#####,
436(b"en-GB", b"error", b"text-not-found") => r#####"No localised text found"#####,
437(b"es", b"error", b"text-not-found") => r#####"No se encontró texto localizado"#####,
438(b"pt", b"error", b"text-not-found") => r#####"Nenhum texto localizado encontrado"#####,
439(b"zh-Latn-CN", b"error", b"text-not-found") => r#####"MeiYou ZhaoDao BenDiHua WenBen"#####,
440    _ => "",
441}}
442    "#######.trim());
443
444    println!("{function_data}");
445    Ok(())
446  }
447}