es_fluent_lang_macro/
lib.rs1#![doc = include_str!("../README.md")]
2
3use heck::ToUpperCamelCase as _;
4use proc_macro::TokenStream;
5use proc_macro_error2::{abort, abort_call_site, proc_macro_error};
6use proc_macro2::Span;
7use quote::quote;
8use syn::{
9 Fields, ItemEnum, LitStr, Variant, parse_macro_input, parse_quote, spanned::Spanned as _,
10};
11
12mod supported_locales;
13
14fn supported_language_keys_for(
15 lang: &unic_langid::LanguageIdentifier,
16) -> impl Iterator<Item = String> {
17 use std::collections::HashSet;
18
19 let mut seen = HashSet::new();
20 let mut keys = Vec::new();
21
22 let mut push_key = |key: String| {
23 if seen.insert(key.clone()) {
24 keys.push(key);
25 }
26 };
27
28 push_key(lang.to_string());
30
31 let mut without_variants = lang.clone();
33 without_variants.clear_variants();
34 push_key(without_variants.to_string());
35
36 if without_variants.region.is_some() {
38 let mut no_region = without_variants.clone();
39 no_region.region = None;
40 push_key(no_region.to_string());
41 }
42
43 if without_variants.script.is_some() {
45 let mut no_script = without_variants.clone();
46 no_script.script = None;
47 push_key(no_script.to_string());
48 }
49
50 push_key(without_variants.language.to_string());
52
53 keys.into_iter()
54}
55
56fn is_supported_language(
57 lang: &unic_langid::LanguageIdentifier,
58 supported: &std::collections::HashSet<&'static str>,
59) -> bool {
60 supported_language_keys_for(lang).any(|key| supported.contains(key.as_str()))
61}
62
63#[proc_macro_error]
66#[proc_macro_attribute]
67pub fn es_fluent_language(attr: TokenStream, item: TokenStream) -> TokenStream {
68 if !attr.is_empty() {
69 abort_call_site!("#[es_fluent_language] does not accept any arguments");
70 }
71
72 let mut input_enum = parse_macro_input!(item as ItemEnum);
73 let enum_ident = input_enum.ident.clone();
74 let enum_span = enum_ident.span();
75
76 if !input_enum.generics.params.is_empty() {
77 abort!(
78 input_enum.generics.span(),
79 "#[es_fluent_language] does not support generic enums"
80 );
81 }
82
83 if !input_enum.variants.is_empty() {
84 abort!(
85 enum_span,
86 "#[es_fluent_language] expects an enum without variants"
87 );
88 }
89
90 let config = es_fluent_toml::I18nConfig::read_from_manifest_dir()
91 .unwrap_or_else(|err| abort!(enum_span, "failed to read i18n configuration: {}", err));
92
93 let mut languages = config
94 .available_languages()
95 .unwrap_or_else(|err| abort!(enum_span, "failed to collect available languages: {}", err));
96
97 let fallback_language = config
98 .fallback_language_identifier()
99 .unwrap_or_else(|err| abort!(enum_span, "failed to parse fallback language: {}", err));
100
101 if !languages.iter().any(|lang| lang == &fallback_language) {
102 languages.push(fallback_language.clone());
103 }
104
105 if languages.is_empty() {
106 abort!(
107 enum_span,
108 "no languages found under the configured assets directory"
109 );
110 }
111
112 let fallback_canonical = fallback_language.to_string();
113
114 let mut language_entries: Vec<(String, unic_langid::LanguageIdentifier)> = languages
115 .into_iter()
116 .map(|lang| {
117 let canonical = lang.to_string();
118 (canonical, lang)
119 })
120 .collect();
121
122 language_entries.sort_by(|a, b| a.0.cmp(&b.0));
123 language_entries.dedup_by(|a, b| a.0 == b.0);
124
125 let supported_keys: std::collections::HashSet<&'static str> =
126 supported_locales::SUPPORTED_LANGUAGE_KEYS
127 .iter()
128 .copied()
129 .collect();
130
131 let unsupported_languages: Vec<_> = language_entries
132 .iter()
133 .filter_map(|(canonical, language)| {
134 if is_supported_language(language, &supported_keys) {
135 None
136 } else {
137 Some(canonical.clone())
138 }
139 })
140 .collect();
141
142 if !unsupported_languages.is_empty() {
143 let formatted = unsupported_languages.join(", ");
144 abort!(enum_span, "unsupported languages in assets: {}.", formatted);
145 }
146
147 let mut variant_idents = Vec::with_capacity(language_entries.len());
148 let mut language_literals = Vec::with_capacity(language_entries.len());
149 let mut fallback_variant_ident = None;
150
151 input_enum
152 .attrs
153 .push(parse_quote!(#[fluent(resource = "es-fluent-lang")]));
154
155 input_enum.variants.clear();
156
157 for (canonical, _language) in &language_entries {
158 let variant_name = canonical.replace('-', "_").to_upper_camel_case();
159
160 let variant_ident = syn::Ident::new(&variant_name, Span::call_site());
161 let literal = LitStr::new(canonical, Span::call_site());
162
163 let attr = parse_quote!(#[fluent(key = #literal)]);
164 let variant = Variant {
165 attrs: vec![attr],
166 ident: variant_ident.clone(),
167 fields: Fields::Unit,
168 discriminant: None,
169 };
170
171 input_enum.variants.push(variant);
172 variant_idents.push(variant_ident.clone());
173 language_literals.push(literal);
174
175 if canonical == &fallback_canonical {
176 fallback_variant_ident = Some(variant_idents.last().unwrap().clone());
177 }
178 }
179
180 let fallback_variant_ident = match fallback_variant_ident {
181 Some(ident) => ident,
182 None => abort!(
183 enum_span,
184 "fallback language was not found among available languages"
185 ),
186 };
187
188 let language_literals_for_ref = language_literals.clone();
189 let variant_idents_for_ref = variant_idents.clone();
190
191 let expanded = quote! {
192 #input_enum
193
194 impl From<#enum_ident> for es_fluent::unic_langid::LanguageIdentifier {
195 fn from(val: #enum_ident) -> Self {
196 match val {
197 #( #enum_ident::#variant_idents => es_fluent::unic_langid::langid!(#language_literals), )*
198 }
199 }
200 }
201
202 impl From<&#enum_ident> for es_fluent::unic_langid::LanguageIdentifier {
203 fn from(val: &#enum_ident) -> Self {
204 match val {
205 #( #enum_ident::#variant_idents_for_ref => es_fluent::unic_langid::langid!(#language_literals_for_ref), )*
206 }
207 }
208 }
209
210 impl From<&es_fluent::unic_langid::LanguageIdentifier> for #enum_ident {
211 fn from(lang: &es_fluent::unic_langid::LanguageIdentifier) -> Self {
212 let lang_str = lang.to_string();
213 match lang_str.as_str() {
214 #( #language_literals_for_ref => #enum_ident::#variant_idents, )*
215 _ => panic!("Unsupported language identifier: {}", lang),
216 }
217 }
218 }
219
220 impl Default for #enum_ident {
221 fn default() -> Self {
222 #enum_ident::#fallback_variant_ident
223 }
224 }
225 };
226
227 expanded.into()
228}