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