fluent_localization_bindgen/
lib.rs1use std::{
4 collections::{HashMap, HashSet},
5 sync::Arc,
6};
7
8use fluent_bundle::FluentResource;
9use fluent_localization_loader::{
10 base_path, fold_displayable, load_resources_from_folder, DEFAULT_DIR,
11};
12use fluent_syntax::ast::{Entry, Expression, InlineExpression, PatternElement};
13use proc_macro::TokenStream;
14use quote::quote;
15use syn::LitStr;
16
17const ALPHABET: [char; 26] = [
19 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S',
20 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
21];
22
23struct Node<'a> {
24 category: &'a str,
25 name: &'a str,
26 variables: HashSet<&'a str>,
27 dependencies: HashSet<&'a str>,
28 term: bool,
29}
30
31impl<'a> Node<'a> {
32 pub fn new(category: &'a str, name: &'a str, term: bool) -> Self {
33 Node {
34 category,
35 name,
36 variables: HashSet::new(),
37 dependencies: HashSet::new(),
38 term,
39 }
40 }
41}
42#[proc_macro]
43pub fn bind_localizations(_meta: TokenStream) -> TokenStream {
44 let mut base_dir = base_path();
47 base_dir.push(DEFAULT_DIR);
48
49 let resources = match load_resources_from_folder(base_dir) {
50 Ok(value) => value,
51 Err(e) => panic!("{}", fold_displayable(e.chain(), "| Caused by: ")),
52 };
53
54 let mut nodes_map: HashMap<String, Node> = resources
57 .iter()
58 .flat_map(|resource| generate_nodes_for(&resource.name, &resource.resource))
59 .map(|node| (node.name.to_string(), node))
60 .collect();
61
62 let all_terms: Vec<LitStr> = nodes_map
64 .iter()
65 .filter(|(_, node)| node.term)
66 .map(|(name, _)| syn::LitStr::new(name.as_str(), proc_macro2::Span::call_site()))
67 .collect();
68 let term_count = all_terms.len();
69 let all_messages: Vec<LitStr> = nodes_map
70 .iter()
71 .filter(|(_, node)| !node.term)
72 .map(|(name, _)| syn::LitStr::new(name.as_str(), proc_macro2::Span::call_site()))
73 .collect();
74 let message_count = all_messages.len();
75 loop {
80 let Some(todo) = nodes_map
82 .iter()
83 .filter_map(|(_, node)| node.dependencies.iter().next().map(|todo| todo.to_string()))
84 .next()
85 else {
86 break;
87 };
88
89 let Some((variables, dependencies)) = nodes_map
90 .get(todo.as_str())
91 .map(|node| (node.variables.clone(), node.dependencies.clone()))
92 else {
93 panic!(
94 "Enountered a dependency on localization node {todo} but no such node was loaded"
95 );
96 };
97
98 for (name, node) in nodes_map
99 .iter_mut()
100 .filter(|(_, node)| node.dependencies.contains(todo.as_str()))
101 {
102 if name.as_str() == todo.as_str() {
103 panic!("Cyclic localization loop detected at node {name}!");
104 }
105
106 node.dependencies.remove(todo.as_str());
107 node.variables.extend(variables.iter());
108 node.dependencies.extend(dependencies.iter());
109 }
110 }
111
112 let mut code = quote! {
115 pub const MESSAGES: [&str; #message_count] = [#(#all_messages,)*];
116 pub const TERMS: [&str; #term_count] = [#(#all_terms,)*];
117
118 pub struct LanguageLocalizer<'a> {
119 localizations: &'a fluent_localization_loader::LocalizationHolder,
120 language: &'a str,
121 }
122
123
124 impl <'a> LanguageLocalizer<'a> {
125 pub fn new(holder: &'a fluent_localization_loader::LocalizationHolder, language: &'a str) -> LanguageLocalizer<'a> {
126 LanguageLocalizer {
127 localizations: holder,
128 language,
129 }
130 }
131
132
133 pub fn validate_default_bundle_complete() -> anyhow::Result<()> {
134 tracing::debug!("Validating default bundle has all expected keys");
135 let mut base_dir = fluent_localization_loader::base_path();
136 let default_lang = fluent_localization_loader::get_default_language()?;
137
138 base_dir.push(default_lang.to_string());
139
140 let resources = fluent_localization_loader::load_resources_from_folder(base_dir)?;
141
142 let mut found_messages: std::collections::HashSet<String> = std::collections::HashSet::new();
143 let mut found_terms: std::collections::HashSet<String> = std::collections::HashSet::new();
144
145 resources.iter()
146 .flat_map(|resource| resource.resource.entries())
147 .for_each(|entry| {
148 match entry {
149 fluent_syntax::ast::Entry::Message(message) => {
150 if message.value.is_some() {
151 found_messages.insert(message.id.name.to_string());
152 }
153 }
154 fluent_syntax::ast::Entry::Term(term) => {
155 found_terms.insert(term.id.name.to_string());
156 },
157 _ => ()
158 }
159 });
160
161 let missing_messages: Vec<&str> = MESSAGES.into_iter().filter(|name| !found_messages.contains(&name.to_string())).collect();
162 let missing_terms: Vec<&str> = TERMS.into_iter().filter(|name| !found_terms.contains(&name.to_string())).collect();
163 if missing_messages.is_empty() && missing_terms.is_empty() {
164 tracing::info!("Default bundle ({default_lang}) is valid");
165 Ok(())
166 } else {
167 Err(fluent_localization_loader::LocalizationLoadingError::new(format!("The following localization keys where not found in the default language bundle: {}", fluent_localization_loader::fold_displayable(missing_messages.into_iter().map(|name| name.to_string()).chain(missing_terms.into_iter().map(|name| format!("-{name}"))), ", "))))?
168 }
169 }
170
171 pub fn localize(&self, name: &str, arguments: Option<fluent_bundle::FluentArgs<'a>>) -> String {
172 let bundle = self.localizations.get_bundle(self.language);
173 let message = bundle.get_message(name).unwrap();
175
176 let mut errors = Vec::new();
177
178 let message = bundle.format_pattern(message.value().unwrap(), arguments.as_ref(), &mut errors);
179
180
181 if errors.is_empty() {
182 message.to_string()
183 } else {
184 self.handle_errors(name, errors)
185 }
186
187 }
188
189 pub fn handle_errors(&self, name: &str, errors: Vec<fluent_bundle::FluentError>) -> String {
190 let errors = fluent_localization_loader::fold_displayable(errors.into_iter(), ", ");
191 tracing::error!("Failed to localize {name} due to following errors: {errors}");
192
193 format!("Failed to localize the \"{name}\" response.")
195 }
196
197
198
199
200 }
201 };
202
203 let start = String::from("impl <'a> LanguageLocalizer<'a> {");
207 let mut simple_block = nodes_map
208 .iter()
209 .filter(|(_, node)| node.variables.is_empty() && !node.term)
210 .map(|(name, node)| {
211 let category = sanitize(node.category);
212 let sanitized_name = sanitize(name);
213 format!(
214 "
215\tpub fn {category}_{sanitized_name}(&self) -> String {{
216\t\tself.localize(\"{name}\", None)
217\t}}"
218 )
219 })
220 .fold(start, |assembled, extra| assembled + "\n" + &extra);
221 simple_block += "\n}";
222 let compiled_simple_block = simple_block
224 .parse::<proc_macro2::TokenStream>()
225 .expect("Failed to assemble simple block token stream");
226
227 code.extend(compiled_simple_block);
228
229 let hell = nodes_map
232 .iter()
233 .filter(|(_, node)| !node.variables.is_empty() && !node.term)
234 .map(|(name, node)| {
235 let count = node.variables.len();
236 let letters = get_letters(count);
237
238 let generics = format!("<{}>", fold_displayable(letters.iter(), ", "));
239
240 let generic_definitions = letters
241 .iter()
242 .map(|letter| format!("\t{letter}: Into<fluent_bundle::FluentValue<'a>>,"))
243 .fold(String::from("where"), |assembled, extra| {
244 assembled + "\n" + &extra
245 })
246 + "\n";
247
248 let mut variables: Vec<&&str> = node.variables.iter().collect();
249 variables.sort_unstable_by_key(|value| value.to_lowercase());
250
251 let mut letter_iter = letters.iter();
252 let mut params = String::from("&self");
253 let mut handle_arguments =
254 String::from("let mut arguments = fluent_bundle::FluentArgs::new();");
255 for name in variables {
256 let sanitized_name = sanitize(name);
257 let letter = letter_iter.next().unwrap();
259
260 params += &format!(", {sanitized_name}: {letter}");
261 handle_arguments +=
262 &format!("\n\t\targuments.set(\"{name}\", {sanitized_name}.into());");
263 }
264
265 let category = node.category;
266 let sanitized_name = sanitize(name);
267 format!(
268 "
269\tpub fn {category}_{sanitized_name}{generics}({params}) -> String
270\t{generic_definitions}\t{{
271\t\t{handle_arguments}
272\t\tself.localize(\"{name}\", Some(arguments))
273\t}}"
274 )
275 })
276 .fold(
277 String::from("impl <'a> LanguageLocalizer<'a> {"),
278 |assembled, extra| assembled + "\n" + &extra,
279 )
280 + "\n}";
281
282 let compiled_hell_block = hell
284 .parse::<proc_macro2::TokenStream>()
285 .expect("Failed to assemble the token stream from hell");
286
287 code.extend(compiled_hell_block);
288
289 code.into()
290}
291
292fn sanitize(original: &str) -> String {
293 original.replace('-', "_").to_lowercase()
294}
295
296fn get_letters(amount: usize) -> Vec<char> {
297 if amount > 26 {
298 todo!("Localization strings with 26+ params, what the hell is this? are we assembling a phone book?");
299 }
300 (0..amount).map(|count| ALPHABET[count]).collect()
301}
302
303fn generate_nodes_for<'a>(parrent: &'a str, resource: &'a Arc<FluentResource>) -> Vec<Node<'a>> {
304 let mut out = Vec::new();
305
306 for entry in resource.entries() {
307 let (name, pattern, term) = match entry {
308 Entry::Message(message) => {
309 let Some(pattern) = &message.value else {
310 continue;
311 };
312 (message.id.name, pattern, false)
313 }
314 Entry::Term(term) => (term.id.name, &term.value, true),
315 _ => continue,
316 };
317
318 let mut node = Node::new(parrent, name, term);
319 process_pattern_elements(&pattern.elements, &mut node);
320 out.push(node)
321 }
322
323 out
324}
325
326fn process_pattern_elements<'a>(attributes: &'a Vec<PatternElement<&'a str>>, node: &mut Node<'a>) {
327 for attribute in attributes {
328 match attribute {
330 PatternElement::TextElement { value: _ } => (),
331 PatternElement::Placeable { expression } => {
332 process_expression(expression, node);
333 }
334 }
335 }
336}
337
338fn process_expression<'a>(expression: &'a Expression<&'a str>, node: &mut Node<'a>) {
339 match expression {
340 Expression::Select { selector, variants } => {
341 process_inline_expression(selector, node);
342 for variant in variants {
343 process_pattern_elements(&variant.value.elements, node)
344 }
345 }
346 Expression::Inline(inline) => process_inline_expression(inline, node),
347 }
348}
349
350fn process_inline_expression<'a>(expression: &'a InlineExpression<&'a str>, node: &mut Node<'a>) {
351 match expression {
352 InlineExpression::FunctionReference {
353 id: _,
354 arguments: _,
355 } => todo!(), InlineExpression::MessageReference { id, attribute: _ } => {
357 node.dependencies.insert(id.name);
358 }
359 InlineExpression::TermReference {
360 id,
361 attribute: _,
362 arguments: _,
363 } => {
364 node.dependencies.insert(id.name);
365 }
366 InlineExpression::VariableReference { id } => {
367 node.variables.insert(id.name);
368 }
369 InlineExpression::Placeable { expression } => {
370 process_expression(expression, node);
371 }
372 InlineExpression::StringLiteral { value: _ }
373 | InlineExpression::NumberLiteral { value: _ } => {}
374 }
375
376 if let InlineExpression::VariableReference { id } = expression {
377 node.variables.insert(id.name);
378 }
379}