Unverified Commit a7cbdcb4 authored by Katharina Fey's avatar Katharina Fey 🏴
Browse files

Adding yaml format layout, key parsing and default association

parent b6bac9ac
[package]
name = "traduki"
descrption = "Integrate translated assets into your application or library"
version = "0.1.0"
authors = ["Katharina Fey <kookie@spacekookie.de>"]
edition = "2018"
readme = "README.md"
[dependencies]
yaml-rust = "0.4"
\ No newline at end of file
//! Build support file that generates code to access translation data
fn main() {
std::env::set_var("TRADUKI_DEFAULT_LANG", "en_GB");
traduki::bootstrap("assets").unwrap();
}
......@@ -3,5 +3,6 @@
include!(concat!(env!("OUT_DIR"), "/traduki.rs"));
fn main() {
println!("{}", test_key());
println!("{}", concat!(env!("OUT_DIR"), "/traduki.rs"));
println!("{}", clap::test_key());
}
use std::env::var;
pub(crate) fn default() -> String {
var("TRADUKI_DEFAULT_LANG")
.expect("No default translation language set! You should set the key \
`TRADUKI_DEFAULT_LANG` to whatever you want the fallback language to be.")
.as_str().into()
}
......@@ -2,25 +2,39 @@
use std::collections::BTreeMap;
pub(crate) struct Key<'n, 'l, 'v> {
name: &'n str,
value: BTreeMap<&'l str, &'v str>,
#[derive(Debug, Eq, Ord, PartialEq, PartialOrd)]
pub(crate) struct Key {
name: String,
value: BTreeMap<String, String>,
}
impl<'n, 'l, 'v> Key<'n, 'l, 'v> {
pub(crate) fn new(name: &'n str) -> Self {
impl Key {
pub(crate) fn new(name: &str) -> Self {
Self {
name,
name: name.into(),
value: BTreeMap::new(),
}
}
/// Add a value for a specific language
pub(crate) fn add_value(mut self, lang: &'l str, value: &'v str) -> Self {
self.value.insert(lang, value);
pub(crate) fn add_value<S: Into<String>>(mut self, lang: S, value: S) -> Self {
self.value.insert(lang.into(), value.into());
self
}
/// Merge another key into this one
///
/// This function will panic if a key is being added that doesn't
/// exist in the default translation language!
pub(crate) fn merge<S: Into<String>>(&mut self, lang: S, mut other: Key) {
let lang = lang.into();
let value = other
.value
.remove(&lang)
.expect("Assets error: failed to merge two translation keys!");
self.value.insert(lang, value);
}
/// Generate code for this key
pub(crate) fn generate(self) -> String {
format!(
......@@ -41,15 +55,12 @@ pub(crate) fn {fun_name}() -> &'static str {{
format!("{}\n{},", acc, section)
})
},
default_section = format!(r#"_ => "{}""#, self
.value
.get(
std::env::var("TRADUKI_DEFAULT_LANG")
.expect("No default translation language set! You should set the key \
`TRADUKI_DEFAULT_LANG` to whatever you want the fallback language to be.")
.as_str()
)
.expect("Default translation language missing for key"))
default_section = format!(
r#"_ => "{}""#,
self.value
.get(&crate::env::default())
.expect("Default translation language missing for key")
)
)
}
}
//! traduki translation assets management toolkit
mod keys;
mod env;
mod key;
mod parser;
mod section;
pub(crate) use {key::Key, section::Section};
use std::{
env,
fs::{self, File},
io::Write,
path::Path,
};
pub fn bootstrap<S: Into<String>>(_: S) -> Result<(), &'static str> {
let env_var = env::var("OUT_DIR");
pub fn bootstrap<S: Into<String>>(assets: S) -> Result<(), &'static str> {
let env_var = std::env::var("OUT_DIR");
let out = Path::new(env_var.as_ref().unwrap());
let a = assets.into();
let assets = Path::new(&a);
parser::load_keys("whatevs", vec![&assets.join("app").join("en_GB.yml")]);
env::set_var("TRADUKI_DEFAULT_LANG", "en_GB");
let _ = fs::remove_dir_all(out);
fs::create_dir_all(out).unwrap();
let mut f = File::create(out.join("traduki.rs")).unwrap();
f.write_all(
keys::Key::new("test_key")
.add_value("en_GB", "hello world")
.add_value("eo", "saluton mondo")
Section::new("clap")
.add_key(
Key::new("test_key")
.add_value("en_GB", "hello world")
.add_value("eo", "saluton mondo"),
)
.add_child(
Section::new("insert").add_key(
Key::new("other_key")
.add_value("en_GB", "hello world")
.add_value("eo", "saluton mondo"),
),
)
.generate()
.as_bytes(),
)
......
#![allow(unused)]
use crate::{Key, Section};
use std::collections::BTreeMap;
use std::io::{self, Read};
use std::{
fs::{self, DirEntry, File},
path::Path,
};
use yaml_rust::{Yaml, YamlLoader};
pub(crate) fn parse_section(path: &Path) -> io::Result<Section> {
let name = path.file_name().unwrap().to_str().unwrap();
let (dirs, files) = split_dir(path)?;
Ok(fs::read_dir(path)?
.into_iter()
.fold(Section::new(name), |sec, entry| {
let entry = entry.unwrap();
if entry.file_type().unwrap().is_dir() {
sec.add_child(parse_section(&entry.path()).unwrap())
} else {
sec
}
}))
}
fn split_dir(path: &Path) -> io::Result<(Vec<DirEntry>, Vec<DirEntry>)> {
let (a, b): (Vec<_>, Vec<_>) = fs::read_dir(path)?
.into_iter()
.map(|entry| {
let e = entry.unwrap();
if e.file_type().unwrap().is_dir() {
(Some(e), None)
} else {
(None, Some(e))
}
})
.unzip();
Ok((
a.into_iter().filter_map(|e| e).collect(),
b.into_iter().filter_map(|e| e).collect(),
))
}
/// Load a single yaml file to a set of keys
///
/// The section name is only taken for debug and error message
/// purposes. The idea is that each key MUST be present in the
/// default translation, but varying languages can be missing keys
/// (they might not be fully translated).
///
/// Because the keys can fall-back to the default translation, it's
/// not a critical error. Bit missing the default language is.
pub(crate) fn load_keys(section: &str, path: Vec<&Path>) -> Vec<Key> {
let def = crate::env::default();
let mut map: BTreeMap<String, Vec<Yaml>> =
path.into_iter().fold(BTreeMap::new(), |mut map, path| {
let mut content = String::new();
let mut f = File::open(path).unwrap();
f.read_to_string(&mut content).unwrap();
let yamls = YamlLoader::load_from_str(&content).unwrap();
map.insert(path.file_name().unwrap().to_str().unwrap().into(), yamls);
map
});
println!("cargo:warning={:?}", map);
let mut default = parse_hash_tree(
&def,
map.remove(&format!("{}.yml", def))
.expect(&format!(
"Missing default translations for section `{}`!",
section
))
.remove(0),
);
// Build Key types, and merge them into the default key
for (lang, mut yaml) in map.into_iter() {
parse_hash_tree(&lang, yaml.remove(0))
.into_iter()
.for_each(|(k, key)| {
let def_key = default.get_mut(&k).expect(&format!(
"Assets error: the key `{}` was not present in the default language.\
The default language must contain all keys. Either remove the key from the secondary language,\
or switch the default language!",
k
));
def_key.merge(&lang, key);
});
}
default.into_iter().map(|(_, k)| k).collect()
}
fn parse_hash_tree(lang: &str, hash: Yaml) -> BTreeMap<String, Key> {
hash.as_hash()
.expect("YAML error: root element must be a map (hash)!")
.into_iter()
.fold(BTreeMap::new(), |mut map, (field_key, field)| {
let name = field_key
.as_str()
.expect("YAML error: only String hash keys are supported!");
map.insert(name.to_string(), parse_field(Key::new(name), lang, field));
map
})
}
/// Parse the type of yaml field and add it to the key
fn parse_field(mut k: Key, lang: &str, field: &Yaml) -> Key {
match field {
Yaml::Hash(h) => {
let reflow = h
.get(&Yaml::String("reflow".into()))
.map(|yaml| match yaml {
Yaml::Boolean(b) => *b,
_ => panic!("YAML error: 'reflow' key must be a boolean!"),
})
.unwrap_or(false);
let text = h
.get(&Yaml::String("text".into()))
.map(|yaml| match yaml {
Yaml::String(s) => s.clone(),
_ => panic!("YAML error: 'text' key must be a string!"),
})
.expect("YAML error: 'text' key is missing!");
k.add_value(
lang,
&if reflow {
text.replace("\n", " ")
} else {
text
}
.trim()
.to_string(),
)
}
Yaml::String(s) => k.add_value(lang, &s),
_ => panic!("YAML error: failed to parse key structure, neither string nor map (hash)!"),
}
}
use crate::key::Key;
use std::collections::BTreeSet;
#[derive(Eq, Ord, PartialEq, PartialOrd)]
pub(crate) struct Section {
name: String,
keys: BTreeSet<Key>,
children: BTreeSet<Section>,
}
impl Section {
pub(crate) fn new(name: &str) -> Self {
Self {
name: name.into(),
keys: BTreeSet::new(),
children: BTreeSet::new(),
}
}
pub(crate) fn add_key(mut self, key: Key) -> Self {
self.keys.insert(key);
self
}
pub(crate) fn add_child(mut self, child: Section) -> Self {
self.children.insert(child);
self
}
pub(crate) fn generate(self) -> String {
format!(
r"
#[allow(unused)]
mod {section_name} {{
{keys}
{children}
}}",
section_name = self.name,
keys = self
.keys
.into_iter()
.map(|k| k.generate())
.fold(String::new(), |acc, key| format!("{}\n\n{}", acc, key)),
children = self
.children
.into_iter()
.map(|sec| sec.generate())
.fold(String::new(), |acc, sec| format!("{}\n\n{}", acc, sec))
)
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment