#![allow(clippy::needless_doctest_main)]
use heck::{CamelCase, SnakeCase};
use indenter::CodeFormatter;
use once_cell::sync::Lazy;
use regex::Regex;
use std::collections::{HashMap, HashSet};
use std::fmt;
use std::fmt::Write as FmtWrite;
use std::fs;
use std::io;
use std::io::{Read, Write};
use std::path::Path;
static RE_PRINTF: Lazy<Regex> =
Lazy::new(|| Regex::new(r"%([-+#])?(\d+)?(\.\d+)?([dis@xXf])|[^%]+|%%|%$").unwrap());
static RE_LANG: Lazy<Regex> = Lazy::new(|| Regex::new(r"(\w+)(-(\w+))?").unwrap());
static RE_SECTION: Lazy<Regex> = Lazy::new(|| Regex::new(r"^\s*\[([^\]]+)\]").unwrap());
static RE_KEY_VALUE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"^\s*([^\s=;#]+)\s*=\s*(.+?)\s*$").unwrap());
type TwineData = HashMap<String, Vec<(String, String)>>;
pub fn build_translations<P: AsRef<Path>, O: AsRef<Path>>(
ini_files: &[P],
output_file: O,
) -> io::Result<()> {
let mut readers = ini_files
.iter()
.map(|file_path| {
let file_path = file_path.as_ref();
println!("cargo:rerun-if-changed={}", file_path.display());
fs::File::open(&file_path)
})
.collect::<io::Result<Vec<_>>>()?;
build_translations_from_readers(readers.as_mut_slice(), output_file)
}
pub fn build_translations_from_str<P: AsRef<Path>>(
strs: &[&str],
output_file: P,
) -> io::Result<()> {
let mut readers = strs.iter().map(io::Cursor::new).collect::<Vec<_>>();
build_translations_from_readers(readers.as_mut_slice(), output_file)
}
pub fn build_translations_from_readers<R: Read, P: AsRef<Path>>(
readers: &mut [R],
output_file: P,
) -> io::Result<()> {
let mut map = HashMap::new();
for reader in readers {
match read_twine_ini(reader) {
Err(err) => panic!("could not read Twine INI file: {}", err),
Ok(other_map) => map.extend(other_map),
}
}
let out_dir = std::env::var_os("OUT_DIR").unwrap();
let dest_path = Path::new(&out_dir).join(output_file);
let _ = fs::create_dir_all(dest_path.parent().unwrap());
let mut f = io::BufWriter::new(
fs::OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(dest_path)?,
);
write!(f, "{}", TwineFormatter { map })?;
Ok(())
}
fn read_twine_ini<R: Read>(reader: &mut R) -> io::Result<TwineData> {
use std::io::BufRead;
let mut map: TwineData = HashMap::new();
let mut section = None;
let reader = io::BufReader::new(reader);
for (i, line) in reader.lines().enumerate() {
let line = line?;
if let Some(caps) = RE_SECTION.captures(line.as_str()) {
section = Some(
map.entry(caps.get(1).unwrap().as_str().to_owned())
.or_default(),
);
}
if let Some(caps) = RE_KEY_VALUE.captures(line.as_str()) {
if let Some(section) = section.as_mut() {
section.push((
caps.get(1).unwrap().as_str().to_owned(),
caps.get(2).unwrap().as_str().to_owned(),
));
} else {
panic!("key-value outside section at line {}", i + 1);
}
}
}
Ok(map)
}
struct TwineFormatter {
map: TwineData,
}
impl fmt::Display for TwineFormatter {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let mut f = CodeFormatter::new(f, " ");
let mut all_languages = HashSet::new();
write!(
f,
r#"
#[macro_export]
macro_rules! t {{
"#,
)?;
f.indent(1);
let mut sorted: Vec<_> = self.map.iter().collect();
sorted.sort_unstable_by(|(a_key, _), (b_key, _)| a_key.cmp(b_key));
for (key, translations) in sorted {
let key = Self::normalize_key(key.as_str());
write!(
f,
r#"
({} $(, $fmt_args:expr)* => $lang:expr) => {{{{
match $lang {{
"#,
key,
)?;
f.indent(2);
self.generate_match_arms(&mut f, translations, &mut all_languages)?;
f.dedent(2);
write!(
f,
r#"
}}
}}}};
"#,
)?;
}
f.dedent(1);
write!(
f,
r#"
}}
"#,
)?;
write!(
f,
r#"
#[derive(Debug, Clone, Copy, PartialEq, Hash)]
#[allow(dead_code)]
pub enum Lang {{
"#,
)?;
f.indent(1);
let lang_variants: HashSet<_> = all_languages
.iter()
.map(|(lang, _)| lang.as_str())
.collect();
let mut lang_variants: Vec<_> = lang_variants.into_iter().collect();
lang_variants.sort_unstable();
for lang in lang_variants.iter() {
write!(
f,
r#"
{}(&'static str),
"#,
lang,
)?;
}
f.dedent(1);
write!(
f,
r#"
}}
impl Lang {{
pub fn all_languages() -> &'static [&'static Lang] {{
&[
"#,
)?;
f.indent(3);
let mut sorted_languages: Vec<_> = all_languages.iter().collect();
sorted_languages.sort_unstable();
for (lang, region) in sorted_languages {
write!(
f,
r#"
&Lang::{}({:?}),
"#,
lang,
region.as_deref().unwrap_or(""),
)?;
}
f.dedent(3);
write!(
f,
r#"
]
}}
}}
"#,
)?;
#[cfg(feature = "serde")]
{
let mut all_regions: Vec<_> = all_languages
.iter()
.filter_map(|(_, region)| region.as_deref())
.collect();
all_regions.sort_unstable_by(|a, b| a.cmp(b).reverse());
Self::generate_serde(&mut f, &lang_variants, &all_regions)?;
}
Ok(())
}
}
impl TwineFormatter {
#[allow(clippy::single_char_add_str)]
fn generate_match_arms(
&self,
f: &mut CodeFormatter<fmt::Formatter>,
translations: &[(String, String)],
all_languages: &mut HashSet<(String, Option<String>)>,
) -> fmt::Result {
let mut match_arms = Vec::new();
let mut default_out = None;
for (lang, text) in translations {
let mut out = String::new();
for caps in RE_PRINTF.captures_iter(text.as_str()) {
if let Some(type_) = caps.get(4) {
out.push_str("{:");
if let Some(flag) = caps.get(1) {
out.push_str(flag.as_str());
}
if let Some(width) = caps.get(2) {
out.push_str(width.as_str());
}
if let Some(precision) = caps.get(3) {
out.push_str(precision.as_str());
}
match type_.as_str() {
x @ "x" | x @ "X" => out.push_str(x),
_ => {}
}
out.push_str("}");
} else if &caps[0] == "%%" {
out.push_str("%");
} else {
out.push_str(&caps[0]);
}
}
if default_out.is_none() {
default_out = Some(out.clone());
}
let caps = RE_LANG.captures(lang.as_str()).expect("lang can be parsed");
let lang = caps
.get(1)
.expect("the language is always there")
.as_str()
.to_camel_case();
let region = caps.get(3);
all_languages.insert((lang.clone(), region.map(|x| x.as_str().to_string())));
match_arms.push((lang, region.map(|x| format!("{:?}", x.as_str())), out));
}
match_arms.sort_unstable_by(|(a_lang, a_region, _), (b_lang, b_region, _)| {
a_lang
.cmp(b_lang)
.then(a_region.is_none().cmp(&b_region.is_none()))
});
for (lang, region, format) in match_arms {
write!(
f,
r#"
$crate::Lang::{}({}) => format!({:?} $(, $fmt_args)*),
"#,
lang,
region.as_deref().unwrap_or("_"),
format,
)?;
}
if let Some(default_out) = default_out {
write!(
f,
r#"
_ => format!({:?} $(, $fmt_args)*),
"#,
default_out,
)?;
}
Ok(())
}
fn normalize_key(key: &str) -> String {
key.to_snake_case().replace(".", "__")
}
#[cfg(feature = "serde")]
fn generate_serde(
f: &mut CodeFormatter<fmt::Formatter>,
all_languages: &[&str],
all_regions: &[&str],
) -> fmt::Result {
write!(
f,
r#"
impl<'de> serde::Deserialize<'de> for Lang {{
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{{
use serde::de;
use std::fmt;
struct LangVisitor;
impl<'de> de::Visitor<'de> for LangVisitor {{
type Value = Lang;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {{
formatter.write_str("expected string")
}}
fn visit_str<E>(self, value: &str) -> Result<Lang, E>
where
E: de::Error,
{{
let mut it = value.splitn(2, '_');
let lang = it.next().unwrap();
let region = it.next().unwrap_or("");
let region = match region.to_lowercase().as_str() {{
"#,
)?;
f.indent(5);
for region in all_regions {
write!(
f,
r#"
{region:?} => {region:?},
"#,
region = region,
)?;
}
f.dedent(1);
write!(
f,
r#"
"" => "",
_ => {{
return Err(de::Error::invalid_value(
de::Unexpected::Str(region),
&"existing region",
));
}}
}};
match lang {{
"#,
)?;
f.indent(1);
for lang in all_languages {
write!(
f,
r#"
{:?} => Ok(Lang::{}(region)),
"#,
lang.to_snake_case(),
lang,
)?;
}
f.dedent(5);
write!(
f,
r#"
_ => {{
return Err(de::Error::invalid_value(
de::Unexpected::Str(region),
&"existing language",
));
}}
}}
}}
}}
deserializer.deserialize_str(LangVisitor)
}}
}}
impl serde::Serialize for Lang {{
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::ser::Serializer,
{{
match self {{
"#,
)?;
f.indent(3);
for lang in all_languages {
write!(
f,
r#"
Lang::{variant}(region) if region.is_empty() => serializer.serialize_str({lang:?}),
Lang::{variant}(region) => serializer.serialize_str(
&format!("{{}}_{{}}", {lang:?}, region),
),
"#,
variant = lang,
lang = lang.to_snake_case(),
)?;
}
f.dedent(3);
write!(
f,
r#"
}}
}}
}}
"#,
)?;
Ok(())
}
}