#![feature(iterator_try_collect)] use anyhow::{anyhow, Context, Result}; use clap::Parser; use std::{ collections::BTreeMap, fmt::Write as W2, fs::{read_to_string, File}, io::Write, path::{Path, PathBuf}, }; #[derive(Parser)] enum Args { ImportOldPot { input: PathBuf, output: PathBuf, }, ImportOldPo { reference: PathBuf, input: PathBuf, output: PathBuf, }, ExportJson { #[arg(long)] fallback: Option, input: PathBuf, output: PathBuf, }, ExportGodotCsv { input_dir: PathBuf, output: PathBuf, }, ExportPo { #[arg(long)] remap_ids: Option, #[arg(long)] fallback: Option, input: PathBuf, output: PathBuf, }, } static NATIVE_LANGUAGE_NAMES: &[(&str, &str)] = &[ ("en", "English"), ("de", "Deutsch"), ("fr", "Français"), ("nl", "Nederlands"), ("es", "Español"), ("eu", "Euskara"), ("ja", "日本語"), ("he", "עִברִית"), ("tr", "Türkçe"), ("fi", "Suomi"), ("ar", "العربية"), ("zh_Hans", "中文 (简化字)"), ("zh_Hant", "中文 (繁體字)"), ("pl", "Polski"), ("pt", "Português"), ("it", "Italiano"), ("ko", "한국인"), ("el", "ελληνικά"), ("ru", "русский"), ]; fn export_load(input: &Path, fallback: Option) -> Result> { let mut ini = load_ini(&input)?; if let Some(fallback) = fallback { let mut missing = 0; let f = load_ini(&fallback)?; for (k, v) in f { #[allow(clippy::map_entry)] if !ini.contains_key(&k) { if option_env!("SHOW_MISSING").is_some() { eprintln!("fallback: key {k:?} is missing"); } missing += 1; ini.insert(k, v); } } eprintln!("-- {missing} missing keys were substituted from fallback language") } for &(code, name) in NATIVE_LANGUAGE_NAMES { ini.insert(format!("c.settings.ui.language.{code}"), name.to_owned()); } Ok(ini) } fn main() -> Result<()> { let args = Args::parse(); match args { Args::ExportJson { fallback, input, output, } => { let ini = export_load(&input, fallback)?; File::create(output)?.write_all(serde_json::to_string(&ini)?.as_bytes())?; Ok(()) } Args::ExportPo { remap_ids: id_map, input, output, fallback, } => { let ini = export_load(&input, fallback)?; let id_map = id_map.map(|path| load_ini(&path)).transpose()?; File::create(output)?.write_all( format!( r#" msgid "" msgstr "" "Language: {}\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" {}"#, input.file_stem().unwrap().to_string_lossy(), ini.into_iter() .fold(String::new(), |mut a, (mut key, value)| { if let Some(id_map) = &id_map { if let Some(new_id) = id_map.get(&key) { key = new_id.to_owned() } } writeln!( a, "msgid {}\nmsgstr {}\n\n", serde_json::to_string(&key).unwrap(), serde_json::to_string(&value).unwrap(), ) .unwrap(); a }) ) .as_bytes(), )?; Ok(()) } Args::ExportGodotCsv { input_dir, output } => { let translations = input_dir .read_dir()? .flat_map(|e| { e.map_err(|e| anyhow!("{e}")) .and_then(|e| { if e.file_name().to_string_lossy().ends_with(".ini") { Ok(Some(( e.path().file_stem().unwrap().to_str().unwrap().to_string(), load_ini(&e.path())?, ))) } else { Ok(None) } }) .transpose() }) .try_collect::>()?; let langs = translations.keys().cloned().collect::>(); let mut tr_tr = BTreeMap::>::new(); for (k, v) in translations { for (kk, vv) in v { tr_tr.entry(kk).or_default().insert(k.clone(), vv); } } File::create(output)?.write_all( tr_tr .into_iter() .fold(format!("id,{}\n", langs.join(",")), |mut a, (k, v)| { writeln!( a, "{k},{}", v.values() .map(|s| serde_json::to_string(s).unwrap()) .collect::>() .join(",") ) .unwrap(); a }) .as_bytes(), )?; Ok(()) } Args::ImportOldPo { reference, input, output, } => { let reference = read_to_string(reference)?; let input = read_to_string(input)?; let id_reverse = reference .lines() .skip(1) .map(|l| { l.split_once("=") .map(|(k, v)| (v, k)) .ok_or(anyhow!("invalid ini")) }) .try_collect::>()?; let mut outmap = BTreeMap::new(); let mut mode = 0; let mut msgid = String::new(); let mut msgstr = String::new(); for (i, mut line) in input.lines().enumerate() { if line.starts_with("#") { continue; } if line.is_empty() { continue; } if let Some(rest) = line.strip_prefix("msgid ") { if !msgid.is_empty() { if let Some(id) = id_reverse.get(&msgid.as_str()) { outmap.insert(id.to_owned(), msgstr.clone()); } else { eprintln!("warning: message id {msgid:?} is unknown") } } line = rest; msgid = String::new(); mode = 1; } else if let Some(rest) = line.strip_prefix("msgstr ") { line = rest; msgstr = String::new(); mode = 2; } else if line.starts_with("msgctxt ") { mode = 0; eprintln!("warning: msgctxt not implemented (line {})", i + 1); continue; } let frag = serde_json::from_str::(line).context(anyhow!("line {}", i + 1))?; match mode { 0 => (), 1 => msgid.push_str(&frag), 2 => msgstr.push_str(&frag), _ => unreachable!(), }; } File::create(output)?.write_all( outmap .into_iter() .fold("[hurrycurry]\n".to_string(), |mut a, (k, v)| { writeln!(a, "{k}={v}").unwrap(); a }) .as_bytes(), )?; Ok(()) } Args::ImportOldPot { input, output } => { let output_raw = read_to_string(&output).unwrap_or("".to_owned()); let input = read_to_string(input)?; let mut output_flip = output_raw .lines() .skip(1) .map(|l| { l.split_once("=") .map(|(k, v)| (v.to_owned(), k.to_owned())) .ok_or(anyhow!("invalid ini")) }) .try_collect::>()?; let mut id = false; let mut msgid = String::new(); for (i, mut line) in input.lines().enumerate() { if line.starts_with("#") { continue; } if line.is_empty() { continue; } if let Some(rest) = line.strip_prefix("msgid ") { if !msgid.is_empty() && !output_flip.contains_key(&msgid) { output_flip.insert(msgid.replace("\n", "\\n"), format!("unknown{i}")); } line = rest; id = true; msgid = String::new(); } else if line.starts_with("msgctxt ") || line.starts_with("msgstr ") { id = false; continue; } if id { let frag = serde_json::from_str::(line).context(anyhow!("line {}", i + 1))?; msgid.push_str(frag.as_str()); } } let output_unflip = output_flip .into_iter() .map(|(v, k)| (k, v)) .collect::>(); File::create(output)?.write_all( output_unflip .into_iter() .fold("[hurrycurry]\n".to_string(), |mut a, (k, v)| { writeln!(a, "{k}={v}").unwrap(); a }) .as_bytes(), )?; Ok(()) } } } fn load_ini(path: &Path) -> Result> { read_to_string(path)? .lines() .skip(1) .map(|l| { let (k, v) = l.split_once("=").ok_or(anyhow!("'=' missing"))?; Ok::<_, anyhow::Error>((k.trim_end().to_owned(), v.trim_start().replace("%n", "\n"))) }) .try_collect() }