rosetta_build/
gen.rs

1//! Code generation
2//!
3//! # Generated code
4//! The generated code consists of a single enum (called by default `Lang`),
5//! which expose pub(crate)lic method for each of the translation keys. These
6//! methods returns a `&'static str` where possible, otherwise a `String`.
7//!
8//! # Usage
9//! The code generator is contained within the [`CodeGenerator`] struct.
10//! Calling [`generate`](CodeGenerator::generate) will produce a [TokenStream]
11//! with the generated code. Internal methods used to generate the output are not exposed.
12
13use std::{
14    collections::{HashMap, HashSet},
15    iter::FromIterator,
16};
17
18use convert_case::{Case, Casing};
19use proc_macro2::{Ident, Span, TokenStream};
20use quote::quote;
21
22use crate::{
23    builder::{LanguageId, RosettaConfig},
24    parser::{FormattedKey, SimpleKey, TranslationData, TranslationKey},
25};
26
27/// Type storing state and configuration for the code generator
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub(crate) struct CodeGenerator<'a> {
30    keys: &'a HashMap<String, TranslationKey>,
31    languages: Vec<&'a LanguageId>,
32    fallback: &'a LanguageId,
33    name: Ident,
34}
35
36impl<'a> CodeGenerator<'a> {
37    /// Initialize a new [`CodeGenerator`]
38    pub(crate) fn new(data: &'a TranslationData, config: &'a RosettaConfig) -> Self {
39        let name = Ident::new(&config.name, Span::call_site());
40
41        CodeGenerator {
42            keys: &data.keys,
43            languages: config.languages(),
44            fallback: &config.fallback.0,
45            name,
46        }
47    }
48
49    /// Generate code as a [`TokenStream`]
50    pub(crate) fn generate(&self) -> TokenStream {
51        // Transform as PascalCase strings
52        let languages: Vec<_> = self
53            .languages
54            .iter()
55            .map(|lang| lang.value().to_case(Case::Pascal))
56            .collect();
57
58        let name = &self.name;
59        let fields = languages
60            .iter()
61            .map(|lang| Ident::new(lang, Span::call_site()));
62
63        let language_impl = self.impl_language();
64        let methods = self.keys.iter().map(|(key, value)| match value {
65            TranslationKey::Simple(inner) => self.method_simple(key, inner),
66            TranslationKey::Formatted(inner) => self.method_formatted(key, inner),
67        });
68
69        quote! {
70            /// Language type generated by the [rosetta](https://github.com/baptiste0928/rosetta) i18n library.
71            #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
72            pub enum #name {
73                #(#fields),*
74            }
75
76            impl #name {
77                #(#methods)*
78            }
79
80            #language_impl
81        }
82    }
83
84    /// Generate method for [`TranslationKey::Simple`]
85    fn method_simple(&self, key: &str, data: &SimpleKey) -> TokenStream {
86        let name = Ident::new(&key.to_case(Case::Snake), Span::call_site());
87        let fallback = &data.fallback;
88        let arms = data
89            .others
90            .iter()
91            .map(|(language, value)| self.match_arm_simple(language, value));
92
93        quote! {
94            #[allow(clippy::all)]
95            pub fn #name(&self) -> &'static str {
96                match self {
97                    #(#arms,)*
98                    _ => #fallback
99                }
100            }
101        }
102    }
103
104    /// Generate match arm for [`TranslationKey::Simple`]
105    fn match_arm_simple(&self, language: &LanguageId, value: &str) -> TokenStream {
106        let name = &self.name;
107        let lang = Ident::new(&language.value().to_case(Case::Pascal), Span::call_site());
108
109        quote! { #name::#lang => #value }
110    }
111
112    /// Generate method for [`TranslationKey::Formatted`]
113    fn method_formatted(&self, key: &str, data: &FormattedKey) -> TokenStream {
114        let name = Ident::new(&key.to_case(Case::Snake), Span::call_site());
115
116        // Sort parameters alphabetically to have consistent ordering
117        let mut sorted = Vec::from_iter(&data.parameters);
118        sorted.sort_by_key(|s| s.to_lowercase());
119        let params = sorted
120            .iter()
121            .map(|param| Ident::new(param, Span::call_site()))
122            .map(|param| quote!(#param: impl ::std::fmt::Display));
123
124        let arms = data
125            .others
126            .iter()
127            .map(|(language, value)| self.match_arm_formatted(language, value, &data.parameters));
128        let fallback = self.format_formatted(&data.fallback, &data.parameters);
129
130        quote! {
131            #[allow(clippy::all)]
132            pub fn #name(&self, #(#params),*) -> ::std::string::String {
133                match self {
134                    #(#arms,)*
135                    _ => #fallback
136                }
137            }
138        }
139    }
140
141    /// Generate match arm for [`TranslationKey::Formatted`]
142    fn match_arm_formatted(
143        &self,
144        language: &LanguageId,
145        value: &str,
146        parameters: &HashSet<String>,
147    ) -> TokenStream {
148        let name = &self.name;
149        let format_value = self.format_formatted(value, parameters);
150        let lang = Ident::new(&language.value().to_case(Case::Pascal), Span::call_site());
151
152        quote! { #name::#lang => #format_value }
153    }
154
155    /// Generate `format!` for [`TranslationKey::Formatted`]
156    fn format_formatted(&self, value: &str, parameters: &HashSet<String>) -> TokenStream {
157        let params = parameters
158            .iter()
159            .map(|param| Ident::new(param, Span::call_site()))
160            .map(|param| quote!(#param = #param));
161
162        quote!(format!(#value, #(#params),*))
163    }
164
165    /// Generate implementation for `rosetta_i18n::Language` trait.
166    fn impl_language(&self) -> TokenStream {
167        let name = &self.name;
168        let fallback = Ident::new(
169            &self.fallback.value().to_case(Case::Pascal),
170            Span::call_site(),
171        );
172
173        let language_id_idents = self.languages.iter().map(|lang| lang.value()).map(|lang| {
174            (
175                lang,
176                Ident::new(&lang.to_case(Case::Pascal), Span::call_site()),
177            )
178        });
179
180        let from_language_id_arms = language_id_idents
181            .clone()
182            .map(|(lang, ident)| quote!(#lang => ::core::option::Option::Some(Self::#ident)));
183
184        let to_language_id_arms = language_id_idents
185            .map(|(lang, ident)| quote!(Self::#ident => ::rosetta_i18n::LanguageId::new(#lang)));
186
187        quote! {
188            impl ::rosetta_i18n::Language for #name {
189                fn from_language_id(language_id: &::rosetta_i18n::LanguageId) -> ::core::option::Option<Self> {
190                    match language_id.value() {
191                        #(#from_language_id_arms,)*
192                        _ => ::core::option::Option::None
193                    }
194                }
195
196                fn language_id(&self) -> ::rosetta_i18n::LanguageId {
197                    match self {
198                        #(#to_language_id_arms,)*
199                    }
200                }
201
202                fn fallback() -> Self {
203                    Self::#fallback
204                }
205            }
206        }
207    }
208}