use std::{collections::HashMap, fs::File, io::Read, path::PathBuf};
use proc_macro::TokenStream;
use syn::LitStr;
#[proc_macro]
pub fn i18n(stream: TokenStream) -> TokenStream {
let locales_dir = syn::parse::<LitStr>(stream)
.map(|value| value.value())
.unwrap_or("i18n".to_string());
let translations = match read_languages_directory_to_hashmap(&locales_dir) {
Ok(translations) => translations,
Err(e) => {
return quote::quote! {
compile_error!(#e)
}
.into()
}
};
let message_id_macros: Vec<_> = translations
.into_iter()
.map(|(message_id, lang_and_translations)| {
let hashmap_inserts: Vec<proc_macro2::TokenStream> = lang_and_translations
.iter()
.map(|(lang, translation)| {
quote::quote! {
hashmap.insert(#lang.to_string(), format!(#translation $(, $args)*));
}
})
.collect();
let message_id = syn::Ident::new(&message_id, proc_macro2::Span::mixed_site());
quote::quote! {
macro_rules! #message_id {
($lang:expr $(, $args:expr)*) => ({
let mut hashmap = std::collections::HashMap::new();
#(#hashmap_inserts)*
hashmap.get($lang).unwrap_or(hashmap.get("en-us").unwrap()).to_string()
})
}
pub(crate) use #message_id;
}
})
.collect();
quote::quote! {
mod i18n {
#(#message_id_macros)*
}
}
.into()
}
fn read_languages_directory_to_hashmap(
dir: &str,
) -> Result<HashMap<String, HashMap<String, String>>, String> {
let root_dir = match std::fs::read_dir(dir) {
Ok(dir) => dir,
Err(e) => return Err(format!("Error reading languages directory : {e}")),
};
let mut languages_wrongly_sorted = HashMap::new();
for language_dir in root_dir {
let language_dir = match language_dir {
Ok(dir) => dir,
Err(e) => return Err(format!("Error reading language directory : {e}")),
};
let lang = language_dir.file_name();
let lang = match lang.to_str() {
Some(lang) => lang,
None => {
return Err(format!(
"Non Unicode name of folder {}",
lang.to_string_lossy()
))
}
};
let metadata = match language_dir.metadata() {
Ok(metadata) => metadata,
Err(e) => {
return Err(format!(
"Error getting lang directory `{lang}` metadata : {e}"
))
}
};
if !metadata.is_dir() {
continue;
}
let _ = metadata;
let language_translations = read_language_directory_to_hashmap(language_dir.path())?;
languages_wrongly_sorted.insert(lang.to_lowercase(), language_translations);
}
let translations = organize_hashmap_properly(languages_wrongly_sorted);
Ok(translations)
}
fn read_language_directory_to_hashmap(dir: PathBuf) -> Result<HashMap<String, String>, String> {
let dir = match std::fs::read_dir(dir.clone()) {
Ok(dir) => dir,
Err(e) => {
return Err(format!(
"Error reading `{}` language directory : {e}",
dir.display()
))
}
};
let mut translations = HashMap::new();
for file in dir {
let file = match file {
Ok(file) => file,
Err(e) => {
return Err(format!(
"Error reading a translation file in the lang directory : {e}"
))
}
};
let metadata = match file.metadata() {
Ok(metadata) => metadata,
Err(e) => {
return Err(format!(
"Error reading {} translation file metadata : {e}",
file.path().display()
))
}
};
if !metadata.is_file() {
continue;
}
let _ = metadata;
let file_name = file.file_name();
let Some(file_name) = file_name.to_str() else {
continue;
};
if !file_name.ends_with(".json") {
continue;
}
let file_path = file.path();
let mut file = match File::open(file.path()) {
Ok(file) => file,
Err(e) => {
return Err(format!(
"Error opening {} translation file : {e}",
file.path().display()
))
}
};
let mut file_content = String::new();
if let Err(e) = file.read_to_string(&mut file_content) {
return Err(format!(
"Error reading {} translation file {e}",
file_path.display()
));
}
drop(file);
let file_translations: HashMap<String, String> =
match miniserde::json::from_str(&file_content) {
Ok(translations) => translations,
Err(e) => {
return Err(format!(
"Error parsing {} JSON translation file : {e}",
file_path.display()
))
}
};
let file_translations = file_translations
.into_iter()
.map(delete_invalid_rust_identifiers_characters)
.collect::<Result<HashMap<String, String>, _>>()?;
for (translation_key, translation_value) in file_translations {
if let Some(..) = translations.insert(translation_key.to_string(), translation_value) {
return Err(format!("Duplicate translation key `{translation_key}`"));
}
}
}
Ok(translations)
}
fn delete_invalid_rust_identifiers_characters(
(raw_key, value): (String, String),
) -> Result<(String, String), String> {
let mut is_first = true;
let key = raw_key
.chars()
.filter_map(|c| {
if is_first {
if c.is_ascii_alphabetic() || c == '_' {
is_first = false;
Some(c.to_ascii_lowercase())
} else {
None
}
} else {
if c.is_ascii_alphanumeric() || c == '_' {
Some(c.to_ascii_lowercase())
} else {
None
}
}
})
.collect::<String>();
if key.is_empty() {
return Err(format!(
"Key `{raw_key}` doesn't contain any valid Rust identifier character"
));
}
Ok((key, value))
}
fn organize_hashmap_properly(
mut languages_wrongly_sorted: HashMap<String, HashMap<String, String>>,
) -> HashMap<String, HashMap<String, String>> {
let Some(en_translations) = languages_wrongly_sorted.remove("en-us") else {
panic!("Missing en-US translations");
};
let mut translations = HashMap::new();
for (translation_id, translation_value) in en_translations {
let mut languages = HashMap::new();
languages.insert("en-us".to_string(), translation_value);
for (lang, lang_translations) in &mut languages_wrongly_sorted {
if let Some(translation) = lang_translations.remove(&translation_id) {
languages.insert(lang.to_string(), translation);
}
}
translations.insert(translation_id, languages);
}
translations
}