traduki 0.1.2

Integrate translated assets into your application or library
Documentation
#![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};

type FileEntries = Vec<DirEntry>;
type DirEntries = Vec<DirEntry>;

pub(crate) fn handle_root(path: &Path) -> io::Result<Vec<Section>> {
    fs::read_dir(path)?
        .into_iter()
        .filter_map(|entry| {
            let e = entry.unwrap();
            if e.file_type().unwrap().is_dir() {
                Some(parse_section(&e.path()))
            } else {
                eprintln!(
                    "cargo:warning=Skipping key in asset root: `{}`!",
                    e.path().file_name().unwrap().to_str().unwrap()
                );
                None
            }
        })
        .collect()
}

fn parse_section(path: &Path) -> io::Result<Section> {
    let name = path.file_name().unwrap().to_str().unwrap();

    let (dirs, files) = split_dir(path)?;

    let file_paths: Vec<_> = files.iter().map(|e| e.path()).collect();
    let section = load_keys(name, file_paths.iter().map(|p| p.as_path()).collect())
        .into_iter()
        .fold(Section::new(name), |sec, key| sec.add_key(key));

    dirs.iter().map(|e| e.path()).fold(Ok(section), |sec, pb| {
        let new = parse_section(pb.as_path())?;
        sec.and_then(|sec| Ok(sec.add_child(new)))
    })
}

fn split_dir(path: &Path) -> io::Result<(DirEntries, FileEntries)> {
    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.
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
        });

    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() {
        let lang = lang.replace(".yml", "");
        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)!"),
    }
}