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