1mod code_generation;
2mod utils;
3
4use std::{collections::HashMap, fs::File, io::Read, path::PathBuf};
5
6use code_generation::generate_functions;
7use proc_macro::TokenStream;
8use syn::{parse_macro_input, DeriveInput, LitStr};
9use utils::delete_invalid_rust_identifiers_characters;
10
11#[proc_macro_attribute]
23pub fn i18n(attr_args: TokenStream, stream: TokenStream) -> TokenStream {
24 let locales_dir = syn::parse::<LitStr>(attr_args)
25 .map(|value| value.value())
26 .unwrap_or("i18n".to_string());
27
28 let base_stream: proc_macro2::TokenStream = stream.clone().into();
29
30 let struct_name = parse_macro_input!(stream as DeriveInput).ident;
31
32 let translations = match read_languages_directory_to_hashmap(&locales_dir) {
33 Ok(translations) => translations,
34 Err(e) => {
35 return quote::quote! {
36 compile_error!(#e)
37 }
38 .into()
39 }
40 };
41
42 let message_id_macros: Vec<_> = translations.into_iter().map(generate_functions).collect();
43
44 quote::quote! {
45 #base_stream
46
47 impl #struct_name {
48 #(#message_id_macros)*
49 }
50 }
51 .into()
52}
53
54fn read_languages_directory_to_hashmap(
56 dir: &str,
57) -> Result<HashMap<String, HashMap<String, Vec<TokenType>>>, String> {
58 let root_dir = match std::fs::read_dir(dir) {
59 Ok(dir) => dir,
60 Err(e) => return Err(format!("Error reading languages directory : {e}")),
61 };
62 let mut languages_wrongly_sorted = HashMap::new();
63
64 for language_dir in root_dir {
65 let language_dir =
66 language_dir.map_err(|e| format!("Error reading language directory : {e}"))?;
67 let lang = language_dir.file_name();
68 let lang = lang.to_str().ok_or(format!(
69 "Non Unicode name of folder {}",
70 lang.to_string_lossy()
71 ))?;
72 let metadata = language_dir
73 .metadata()
74 .map_err(|e| format!("Error getting lang directory `{lang}` metadata : {e}"))?;
75 if !metadata.is_dir() {
76 return Err(format!(
77 "Unexpected file `{lang}` in the languages directory, exiting."
78 ));
79 }
80 let _ = metadata;
81 let language_translations = read_language_directory_to_hashmap(language_dir.path())?;
82 languages_wrongly_sorted.insert(lang.to_lowercase(), language_translations);
83 }
84
85 let translations = organize_hashmap_properly(languages_wrongly_sorted);
86
87 Ok(translations)
88}
89
90fn read_language_directory_to_hashmap(
92 dir: PathBuf,
93) -> Result<HashMap<String, Vec<TokenType>>, String> {
94 let dir = std::fs::read_dir(dir.clone())
95 .map_err(|e| format!("Error reading `{}` language directory : {e}", dir.display()))?;
96 let mut translations = HashMap::new();
97
98 for file in dir {
99 let file = file
100 .map_err(|e| format!("Error reading a translation file in the lang directory : {e}"))?;
101 let metadata = file.metadata().map_err(|e| {
102 format!(
103 "Error reading {} translation file metadata : {e}",
104 file.path().display()
105 )
106 })?;
107 if !metadata.is_file() {
108 let language_hashmap = read_language_directory_to_hashmap(file.path())?;
109 for (file_name, tokens) in language_hashmap.into_iter() {
110 if translations.insert(file_name.clone(), tokens).is_some() {
111 return Err(format!("Dupplicate translation key `{file_name}`"));
112 }
113 }
114 }
115 let _ = metadata;
116 let file_name = file.file_name();
117 let file_name = file_name.to_str().ok_or(format!(
118 "Non Unicode name of file {}",
119 file_name.to_string_lossy()
120 ))?;
121 let file_name = file_name
122 .split_once('.')
123 .map(|name| name.0)
124 .unwrap_or(file_name);
125 let file_path = file.path();
126 let mut file = File::open(file.path()).map_err(|e| {
127 format!(
128 "Error opening {} translation file : {e}",
129 file.path().display()
130 )
131 })?;
132 let mut file_content = String::new();
133 file.read_to_string(&mut file_content)
134 .map_err(|e| format!("Error reading {} translation file {e}", file_path.display()))?;
135 drop(file);
136
137 let file_name = delete_invalid_rust_identifiers_characters(file_name)?;
138
139 let tokens = parse_arguments(&file_content)?;
140
141 if translations.insert(file_name.clone(), tokens).is_some() {
142 return Err(format!("Duplicate translation key `{file_name}`"));
143 }
144 }
145
146 Ok(translations)
147}
148
149fn organize_hashmap_properly(
153 mut languages_wrongly_sorted: HashMap<String, HashMap<String, Vec<TokenType>>>,
154) -> HashMap<String, HashMap<String, Vec<TokenType>>> {
155 let Some(en_translations) = languages_wrongly_sorted.remove("en-us") else {
156 panic!("Missing en-US translations");
157 };
158
159 let mut translations = HashMap::new();
160
161 for (translation_id, translation_value) in en_translations {
162 let mut languages = HashMap::new();
163 languages.insert("en-us".to_string(), translation_value);
164 for (lang, lang_translations) in &mut languages_wrongly_sorted {
165 if let Some(translation) = lang_translations.remove(&translation_id) {
166 languages.insert(lang.to_string(), translation);
167 }
168 }
169 translations.insert(translation_id, languages);
170 }
171
172 translations
173}
174
175#[derive(Clone)]
176enum TokenType {
177 String(String),
178 Argument(String),
179}
180
181fn parse_arguments(translation: &str) -> Result<Vec<TokenType>, String> {
182 let mut tokens = Vec::new();
183
184 let mut iter = translation.char_indices().into_iter().peekable();
185
186 let mut last_string_token = String::new();
187
188 loop {
189 let Some((i, c)) = iter.next() else {
190 break;
191 };
192 match c {
193 '$' => {
194 if !last_string_token.is_empty() {
195 tokens.push(TokenType::String(std::mem::take(&mut last_string_token)));
196 }
197
198 let mut argument_name = String::new();
199 loop {
200 let Some((i, next_token)) = iter.peek() else {
201 break;
202 };
203 let (i, next_token) = (*i, *next_token);
204 if argument_name.is_empty() {
205 if !next_token.is_ascii_alphabetic() && next_token != '_' {
206 return Err(format!("Expected alphabetic ASCII character or underscore, found {next_token} at index {i}"));
207 }
208 } else {
209 if !next_token.is_ascii_alphanumeric() && next_token != '_' {
210 break;
211 }
212 }
213 iter.next();
214 argument_name.push(next_token.to_ascii_lowercase());
215 }
216
217 if argument_name.is_empty() {
218 return Err(format!("Expected name of argument at index {i}, got EOF"));
219 }
220 tokens.push(TokenType::Argument(argument_name));
221 }
222 '\\' => {
223 let Some((_, c_to_escape)) = iter.next() else {
224 return Err(format!(
225 "Expected character to escape at index {i}, got EOF"
226 ));
227 };
228 if c_to_escape != '$' {
229 last_string_token.push('\\');
230 }
231 last_string_token.push(c_to_escape);
232 }
233 c => last_string_token.push(c),
234 }
235 }
236
237 if !last_string_token.is_empty() {
238 tokens.push(TokenType::String(last_string_token));
239 }
240
241 Ok(tokens)
242}