glossa_codegen/generator/
locales.rs

1use std::io;
2
3use anyhow::bail;
4use glossa_shared::{
5  ToCompactString, fmt_compact,
6  tap::{Pipe, Tap},
7};
8use itertools::Itertools;
9use lang_id::{LangID, RawID};
10
11use crate::{
12  AnyResult, Generator, MiniStr,
13  generator::{MapType, to_lower_snake_case},
14};
15
16impl Generator<'_> {
17  /// Generates a function listing all available locales
18  ///
19  /// ## Parameters
20  ///
21  /// - `map_type`
22  ///   - Mapping type to process
23  /// - `const_lang_id`
24  ///   - Whether to generate lang_id constants
25  ///
26  /// ## Example
27  ///
28  /// ```ignore
29  /// let function_data = new_generator()
30  ///   .with_visibility(crate::Visibility::Pub)
31  ///   .output_locales_fn(
32  ///     MapType::Regular,
33  ///     false,
34  /// )?;
35  /// ```
36  ///
37  /// function_data:
38  ///
39  /// ```ignore
40  /// pub const fn all_locales() -> [lang_id::LangID; 107] {
41  /// use lang_id::consts::*;
42  /// [
43  ///   lang_id_af(),
44  ///   lang_id_am(),
45  ///   lang_id_ar(),
46  ///   lang_id_az(),
47  ///   lang_id_be(),
48  ///   lang_id_bg(),
49  ///   lang_id_bn(),
50  ///   lang_id_bs(),
51  ///   lang_id_ca(),
52  ///   lang_id_ceb(),
53  ///   lang_id_co(),
54  ///   ...
55  /// ]}
56  /// ```
57  pub fn output_locales_fn(
58    &self,
59    map_type: MapType,
60    const_lang_id: bool,
61  ) -> AnyResult<String> {
62    let raw_locales = self.collect_raw_locales(map_type)?;
63    let locales_len = raw_locales.len();
64    let new_header = || self.new_locales_fn_header(&locales_len, &const_lang_id);
65
66    if !const_lang_id {
67      return new_header()
68        .tap_mut(|buf| {
69          let push_str = |s| buf.push_str(s);
70          [&format!("{raw_locales:#?}"), "}\n"]
71            .into_iter()
72            .for_each(push_str);
73        })
74        .pipe(Ok);
75    }
76
77    // Process constant lang IDs
78    raw_locales
79      .iter()
80      .map(try_conv_const_id)
81      .try_fold(
82        new_header(), //
83        |mut acc, fn_name| {
84          let push_str = |s| acc.push_str(s);
85          ["\n    ", &fn_name?, ","]
86            .into_iter()
87            .for_each(push_str);
88          Ok::<_, anyhow::Error>(acc)
89        },
90      )?
91      .tap_mut(|buf| buf.push_str("  ]\n}"))
92      .pipe(Ok)
93  }
94
95  /// -> e.g., `["en", "en-GB", "zh-Hant", "zh-Latn-CN"]`
96  pub fn collect_raw_locales(&self, map_type: MapType) -> io::Result<Vec<MiniStr>> {
97    match map_type.is_dsl() {
98      true => match self.get_or_init_dsl_maps() {
99        x if x.is_empty() => "// Error: Empty DSL Map"
100          .pipe(io::Error::other)
101          .pipe(Err),
102        data => data
103          .keys()
104          .map(|id| id.to_compact_string())
105          .collect_vec()
106          .pipe(Ok),
107      },
108      _ => map_type
109        .get_non_dsl_maps(self)?
110        .keys()
111        .map(|id| id.to_compact_string())
112        .collect_vec()
113        .pipe(Ok),
114    }
115  }
116
117  pub(crate) fn new_locales_fn_header(
118    &self,
119    locales_len: &usize,
120    const_lang_id: &bool,
121    // this: &Generator<'_, 'h>,
122  ) -> String {
123    // Generate appropriate header based on const_lang_id flag
124    let ret_type = {
125      match *const_lang_id {
126        true => fmt_compact!(
127          r#"[super::lang_id::LangID; {locales_len}] {{
128  #[allow(unused_imports)]
129  use super::lang_id::RawID;
130  use super::lang_id::consts::*;
131  ["#
132        ),
133        _ => fmt_compact!("[&'static str; {locales_len}] {{\n  "),
134      }
135    };
136
137    let s_header = format!("const fn all_locales() -> {ret_type}",);
138    self.new_fn_header(&s_header)
139  }
140
141  /// Generates `cfg` features for different languages and mod file names.
142  ///
143  /// Output Sample:
144  ///
145  /// ```ignore
146  /// #[cfg(feature = "l10n-en-GB")]
147  /// mod l10n_en_gb;
148  ///
149  /// #[cfg(feature = "l10n-es")]
150  /// mod l10n_es;
151  /// ```
152  ///
153  /// You can use this in `mod.rs`.
154  ///
155  /// > Note: If you use `output_*_all_in_one`, you do not need to call this
156  /// > method.
157  ///
158  /// - The feature prefix depends on [`Self::feature_prefix`]
159  /// - The mod prefix name depends on [`Self::mod_prefix`]
160  pub fn output_mod_rs(&self, map_type: MapType) -> io::Result<String> {
161    let mod_vis = self.get_mod_visibility();
162
163    let feature_names = self.collect_cargo_feature_names(map_type)?;
164    let module_names = self.collect_rs_mod_names(map_type)?;
165    ensure_length_equal(feature_names.len(), module_names.len())?;
166
167    feature_names
168      .iter()
169      .zip(module_names)
170      .map(|(feat_name, mod_name)| {
171        fmt_compact!(
172          r###"
173#[cfg(feature = "{feat_name}")]
174{mod_vis}mod {mod_name};
175        "###
176        )
177      })
178      .collect::<String>()
179      .pipe(Ok)
180  }
181
182  /// -> e.g., `["l10n-en", "l10n-en-GB", "l10n-zh", "l10n-zh-Hant"]`
183  pub fn collect_cargo_feature_names(
184    &self,
185    map_type: MapType,
186  ) -> io::Result<Vec<MiniStr>> {
187    let feat_prefix = self.get_feature_prefix();
188
189    self
190      .collect_raw_locales(map_type)?
191      .into_iter()
192      .map(|x| fmt_compact!("{feat_prefix}{x}"))
193      .collect_vec()
194      .pipe(Ok)
195  }
196
197  /// -> e.g., `["l10n_en", "l10n_en_gb", "l10n_zh"]`
198  pub fn collect_rs_mod_names(&self, map_type: MapType) -> io::Result<Vec<MiniStr>> {
199    let mod_prefix = self.get_mod_prefix();
200
201    self
202      .collect_raw_locales(map_type)?
203      .into_iter()
204      .map(|x| fmt_compact!("{mod_prefix}{id}", id = to_lower_snake_case(x)))
205      .collect_vec()
206      .pipe(Ok)
207  }
208
209  /// Generates Cargo features for different locales.
210  ///
211  /// Output Sample:
212  ///
213  /// ```toml,ignore
214  /// l10n-en = []
215  /// l10n-zh = []
216  /// l10n-all = ["l10n-en","l10n-zh"]
217  /// ```
218  #[cfg(feature = "json")]
219  pub fn output_cargo_features(&self, map_type: MapType) -> io::Result<String> {
220    let feat_prefix = self.get_feature_prefix();
221    let all_feat_locales = self.collect_cargo_feature_names(map_type)?;
222
223    // If all elements in the array are of the same type, then TOML and JSON are the
224    // same.
225    let all_arr_json = serde_json::to_string(&all_feat_locales)?;
226
227    format!(
228      "{} = []\n{feat_prefix}all = {all_arr_json}",
229      all_feat_locales.join(" = []\n")
230    )
231    .pipe(Ok)
232  }
233}
234
235/// Validates whether the input language identifier `id` meets specific format
236/// requirements and attempts to convert it into an identifier (function name).
237///
238/// > convert: lang_id::matches::get_fn_name(id) => function name
239///
240/// If the input identifier does not meet the requirements, it
241/// attempts to construct a constant LangID using RawID.
242fn try_conv_const_id(id: &MiniStr) -> AnyResult<MiniStr> {
243  use lang_id::matches::{get_fn_name, match_id};
244
245  match match_id(id.as_bytes())
246    .to_compact_string()
247    .as_str()
248  {
249    // Since multiple keys in match_id may correspond to one value, a check is
250    // required here.
251    x if x == id => id
252      .as_bytes()
253      .pipe(get_fn_name)
254      .to_compact_string()
255      .pipe(Ok),
256    _ => {
257      let id = id.parse::<LangID>()?;
258      if id.variants().count() >= 1 {
259        bail!("This ID ({id}) contains variants and cannot be converted to const.")
260      }
261      RawID::try_from_str(
262        id.language.as_str(),
263        id.script
264          .map(|x| x.to_compact_string())
265          .unwrap_or_default()
266          .as_str(),
267        id.region
268          .map(|x| x.to_compact_string())
269          .unwrap_or_default()
270          .as_str(),
271      )?
272      .to_compact_string()
273      .pipe(Ok)
274    }
275  }
276}
277pub(crate) fn ensure_length_equal(
278  feat_names_len: usize,
279  mod_names_len: usize,
280) -> io::Result<()> {
281  (mod_names_len == feat_names_len)
282    .then_some(())
283    .ok_or_else(|| {
284      io::Error::other("feature_names and module_names are not equal in length")
285    })
286}
287
288#[cfg(test)]
289mod tests {
290  use glossa_shared::display::puts;
291
292  use super::*;
293  use crate::generator::dbg_generator::new_generator;
294
295  #[ignore]
296  #[test]
297  fn test_list_all_locales() -> AnyResult<()> {
298    new_generator()
299      .with_visibility(crate::Visibility::Pub)
300      .output_locales_fn(
301        MapType::Regular,
302        // false,
303        true,
304      )?
305      .pipe_ref(puts)
306      .pipe(Ok)
307  }
308
309  #[ignore]
310  #[test]
311  fn test_gen_mod_rs_str() -> AnyResult<()> {
312    let generator = new_generator().with_mod_visibility(crate::Visibility::Pub);
313
314    let raw_locales = generator.collect_raw_locales(MapType::Regular)?;
315    let mod_prefix = generator.get_mod_prefix();
316    let feature_prefix = generator.get_feature_prefix();
317    let mod_vis = generator.get_mod_visibility();
318
319    let fn_content = raw_locales
320      .iter()
321      .map(|id| {
322        let mod_name = to_lower_snake_case(id);
323        fmt_compact!(
324          r###"
325#[cfg(feature = "{feature_prefix}{id}")]
326{mod_vis}mod {mod_prefix}{mod_name};
327        "###
328        )
329      })
330      .collect::<String>();
331
332    println!("{fn_content}");
333    Ok(())
334  }
335
336  #[ignore]
337  #[test]
338  fn test_output_mod_rs() -> AnyResult<()> {
339    new_generator()
340      .with_mod_visibility(crate::Visibility::PubCrate)
341      .output_mod_rs(MapType::Regular)?
342      .pipe_ref(puts)
343      .pipe(Ok)
344  }
345
346  #[test]
347  #[ignore]
348  #[cfg(feature = "json")]
349  fn test_gen_cargo_features() -> AnyResult<()> {
350    use glossa_shared::display::puts;
351
352    let generator = new_generator();
353    let feat_prefix = generator.get_feature_prefix();
354
355    let all_locales = generator
356      .collect_raw_locales(MapType::Regular)?
357      .into_iter()
358      .map(|x| fmt_compact!("{feat_prefix}{x}"))
359      .collect_vec();
360
361    let all = serde_json::to_string(&all_locales)?;
362
363    format!(
364      "{} = []\n{feat_prefix}all = {all}",
365      all_locales.join(" = []\n")
366    )
367    .pipe_ref(puts)
368    .pipe(Ok)
369  }
370
371  #[ignore]
372  #[test]
373  fn test_output_cargo_features() -> AnyResult<()> {
374    new_generator()
375      .output_cargo_features(MapType::DSL)?
376      .pipe_ref(puts)
377      .pipe(Ok)
378  }
379}