use anyhow::anyhow;
use regex::Regex;
use std::{collections::HashMap, io::Write};
fn get_subfiles(path: impl AsRef<std::path::Path>) -> Vec<String> {
let mut subfolders = vec![];
if let Ok(mut entries) = std::fs::read_dir(path) {
while let Some(Ok(entry)) = entries.next() {
if let Ok(file_type) = entry.file_type() {
if file_type.is_file() {
subfolders.push(entry.file_name().to_string_lossy().to_string());
}
}
}
}
subfolders
}
pub trait StrintExt {
fn snake_to_camel(&self) -> String;
fn camel_to_snake(&self) -> String;
}
impl StrintExt for String {
fn snake_to_camel(&self) -> String {
self[..].snake_to_camel()
}
fn camel_to_snake(&self) -> String {
self[..].camel_to_snake()
}
}
impl StrintExt for str {
fn snake_to_camel(&self) -> String {
self.split('_')
.map(|word| {
word.chars()
.next()
.unwrap()
.to_uppercase()
.collect::<String>()
+ &word[1..]
})
.collect::<Vec<_>>()
.join("")
}
fn camel_to_snake(&self) -> String {
self.chars()
.enumerate()
.map(|(i, c)| match i != 0 && c.is_uppercase() {
true => format!("_{c}"),
false => format!("{c}"),
})
.collect::<Vec<_>>()
.join("")
.to_lowercase()
}
}
pub struct I18nItem {
pub content: HashMap<String, String>,
pub args: HashMap<String, String>,
}
impl I18nItem {
fn parse_args(content: &str) -> HashMap<String, String> {
let mut args = HashMap::new();
let re = Regex::new(r"\{([^}:]+):([^}]+)\}").unwrap();
for cap in re.captures_iter(content) {
let key = cap[1].to_string();
let value = cap[2].to_string();
args.insert(key, value);
}
args
}
pub fn new() -> Self {
let content = HashMap::new();
let args = HashMap::new();
Self { content, args }
}
pub fn apply(&mut self, locale: String, content: String) -> anyhow::Result<()> {
let args = Self::parse_args(&content);
if self.content.is_empty() {
self.args = args;
} else {
if self.args != args {
Err(anyhow!("args mismatch"))?;
}
}
self.content.insert(locale, content);
Ok(())
}
}
pub fn generate_rust(in_path: &str, out_file: &str) -> anyhow::Result<()> {
let root = {
let mut root = faml::FamlExpr::new();
for in_file in get_subfiles(in_path) {
let mut in_file_tmp = std::path::PathBuf::new();
in_file_tmp.push(in_path);
in_file_tmp.push(in_file);
let faml_str = std::fs::read_to_string(in_file_tmp)?;
let in_root = faml::FamlExpr::from_str(&faml_str)?;
root.apply(in_root)?;
}
root.evaluate()?
.as_map()
.ok_or_else(|| anyhow::anyhow!("root is not a map"))?
};
let mut code = "// Automatically generated by fa18n\n".to_string();
code.push_str("// Do not edit this file manually");
for (group_name , group) in root {
let group_name = group_name.snake_to_camel();
let mut locales: Vec<String> = vec![];
let mut ret_items = HashMap::new();
let group = group
.as_map()
.ok_or_else(|| anyhow!("group is not a map"))?;
for (locale , items) in group {
let locale = locale.snake_to_camel();
if let Err(p) = locales.binary_search_by(|p| p.cmp(&locale)) {
locales.insert(p, locale.clone());
}
let items = items
.as_map()
.ok_or_else(|| anyhow!("items is not a map"))?;
for (key, value) in items {
let key = key.snake_to_camel();
ret_items
.entry(key)
.or_insert_with(I18nItem::new)
.apply(locale.clone(), value.as_str())?;
}
}
if !code.is_empty() {
code.push_str("\n\n\n");
}
code.push_str("#[derive(Clone, Copy)]\n");
code.push_str(&format!("pub enum {group_name}Locale {{\n"));
for locale in locales {
code.push_str(&format!(" {locale},\n"));
}
code.push_str("}\n\n");
code.push_str(&format!("pub enum {group_name} {{\n"));
for (key, item) in &ret_items {
code.push_str(&format!(
" {key}{},\n",
item.to_enum_args(QuoteType::Brace)?
));
}
code.push_str("}\n\n");
code.push_str(&format!("impl {group_name} {{\n"));
code.push_str(&format!(
" pub fn translate(&self, locale: {group_name}Locale) -> String {{\n"
));
code.push_str(" match self {\n");
for (key, item) in &ret_items {
let args = item.to_enum_args_slim();
code.push_str(&format!(
" {group_name}::{key}{args} => match locale {{\n"
));
for (locale, content) in &item.content {
match item.args.is_empty() {
true => code.push_str(&format!(
" {group_name}Locale::{locale} => \"{content}\".to_string(),\n"
)),
false => {
let mut new_content = content.clone();
for (key, value) in &item.args {
new_content = new_content
.replace(&format!("{{{key}:{value}}}"), &format!("{{{key}}}"));
}
code.push_str(&format!(
" {group_name}Locale::{locale} => format!(\"{new_content}\"),\n"
))
}
}
}
code.push_str(" }\n");
}
code.push_str(" }\n");
code.push_str(" }\n\n");
for (key, item) in &ret_items {
let snake_key = key.camel_to_snake();
code.push_str(&format!(
" pub fn {snake_key}({}) -> {group_name} {{\n",
item.to_enum_args(QuoteType::None)?
));
let sargs = item.to_enum_args_slim();
code.push_str(&format!(" {group_name}::{key}{sargs}\n"));
code.push_str(" }\n\n");
}
code.push_str("}\n\n");
}
let mut file = std::fs::File::create(out_file)?;
file.write_all(code.as_bytes())?;
file.flush()?;
Ok(())
}
enum QuoteType {
None,
Brace,
}
impl I18nItem {
pub fn to_enum_args(&self, qtype: QuoteType) -> anyhow::Result<String> {
if self.args.is_empty() {
return Ok("".to_string());
}
let mut ret = match qtype {
QuoteType::None => "".to_string(),
QuoteType::Brace => "{ ".to_string(),
};
for (arg, atype) in &self.args {
let atype = match &atype[..] {
"int" => "i64",
"float" => "f64",
"str" => "String",
_ => Err(anyhow!("unknown arg type: {atype}"))?,
};
ret.push_str(&format!("{arg}: {atype}, "));
}
ret.pop();
ret.pop();
ret.push_str(match qtype {
QuoteType::None => "",
QuoteType::Brace => " }",
});
Ok(ret)
}
pub fn to_enum_args_slim(&self) -> String {
if self.args.is_empty() {
return "".to_string();
}
let mut ret = " {".to_string();
for (arg, _) in &self.args {
ret.push_str(&format!(" {arg}, "));
}
ret.pop();
ret.pop();
ret.push_str(" }");
ret
}
}