Fix crash of trans() function called on absent translation key (#793)

Add method get_translation(lang, key) into Config struct that retrieves
translated term from parsed configuration or error when either
desired language or key is missing.

Use the new method in Trans struct implementing global Tera function
trans().

Add unit test to cover both happy and error path for translation
retrieval in both config and templates crate.
This commit is contained in:
zdenek-crha 2019-09-03 10:51:41 +02:00 committed by Vincent Prouillet
parent e77adc56fd
commit 5aadd3d4f2
2 changed files with 80 additions and 15 deletions

View file

@ -8,6 +8,7 @@ use toml;
use toml::Value as Toml; use toml::Value as Toml;
use errors::Result; use errors::Result;
use errors::Error;
use highlighting::THEME_SET; use highlighting::THEME_SET;
use theme::Theme; use theme::Theme;
use utils::fs::read_file_with_error; use utils::fs::read_file_with_error;
@ -83,6 +84,8 @@ impl Default for Taxonomy {
} }
} }
type TranslateTerm = HashMap<String, String>;
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(default)] #[serde(default)]
pub struct Config { pub struct Config {
@ -100,8 +103,12 @@ pub struct Config {
pub default_language: String, pub default_language: String,
/// The list of supported languages outside of the default one /// The list of supported languages outside of the default one
pub languages: Vec<Language>, pub languages: Vec<Language>,
/// Languages list and translated strings /// Languages list and translated strings
pub translations: HashMap<String, Toml>, ///
/// The `String` key of `HashMap` is a language name, the value should be toml crate `Table`
/// with String key representing term and value another `String` representing its translation.
pub translations: HashMap<String, TranslateTerm>,
/// Whether to highlight all code blocks found in markdown files. Defaults to false /// Whether to highlight all code blocks found in markdown files. Defaults to false
pub highlight_code: bool, pub highlight_code: bool,
@ -299,6 +306,16 @@ impl Config {
// and this operation can be expensive. // and this operation can be expensive.
self.highlight_code = false; self.highlight_code = false;
} }
pub fn get_translation<S: AsRef<str>>(&self, lang: S, key: S) -> Result<String> {
let terms = self.translations.get(lang.as_ref()).ok_or_else(|| {
Error::msg(format!("Translation for language '{}' is missing", lang.as_ref()))
})?;
terms.get(key.as_ref()).ok_or_else(|| {
Error::msg(format!("Translation key '{}' for language '{}' is missing", key.as_ref(), lang.as_ref()))
}).map(|term| term.to_string())
}
} }
impl Default for Config { impl Default for Config {
@ -447,9 +464,7 @@ a_value = 10
assert_eq!(extra["a_value"].as_integer().unwrap(), 10); assert_eq!(extra["a_value"].as_integer().unwrap(), 10);
} }
#[test] const CONFIG_TRANSLATION: &str = r#"
fn can_use_language_configuration() {
let config = r#"
base_url = "https://remplace-par-ton-url.fr" base_url = "https://remplace-par-ton-url.fr"
default_language = "fr" default_language = "fr"
@ -459,14 +474,38 @@ title = "Un titre"
[translations.en] [translations.en]
title = "A title" title = "A title"
"#; "#;
let config = Config::parse(config); #[test]
fn can_use_language_configuration() {
let config = Config::parse(CONFIG_TRANSLATION);
assert!(config.is_ok()); assert!(config.is_ok());
let translations = config.unwrap().translations; let translations = config.unwrap().translations;
assert_eq!(translations["fr"]["title"].as_str().unwrap(), "Un titre"); assert_eq!(translations["fr"]["title"].as_str(), "Un titre");
assert_eq!(translations["en"]["title"].as_str().unwrap(), "A title"); assert_eq!(translations["en"]["title"].as_str(), "A title");
}
#[test]
fn can_use_present_translation() {
let config = Config::parse(CONFIG_TRANSLATION).unwrap();
assert_eq!(config.get_translation("fr", "title").unwrap(), "Un titre");
assert_eq!(config.get_translation("en", "title").unwrap(), "A title");
}
#[test]
fn error_on_absent_translation_lang() {
let config = Config::parse(CONFIG_TRANSLATION).unwrap();
let error = config.get_translation("absent", "key").unwrap_err();
assert_eq!("Translation for language 'absent' is missing", format!("{}", error));
}
#[test]
fn error_on_absent_translation_key() {
let config = Config::parse(CONFIG_TRANSLATION).unwrap();
let error = config.get_translation("en", "absent").unwrap_err();
assert_eq!("Translation key 'absent' for language 'en' is missing", format!("{}", error));
} }
#[test] #[test]

View file

@ -33,8 +33,12 @@ impl TeraFn for Trans {
let key = required_arg!(String, args.get("key"), "`trans` requires a `key` argument."); let key = required_arg!(String, args.get("key"), "`trans` requires a `key` argument.");
let lang = optional_arg!(String, args.get("lang"), "`trans`: `lang` must be a string.") let lang = optional_arg!(String, args.get("lang"), "`trans`: `lang` must be a string.")
.unwrap_or_else(|| self.config.default_language.clone()); .unwrap_or_else(|| self.config.default_language.clone());
let translations = &self.config.translations[lang.as_str()];
Ok(to_value(&translations[key.as_str()]).unwrap()) let term = self.config.get_translation(lang, key).map_err(|e| {
Error::chain("Failed to retreive term translation", e)
})?;
Ok(to_value(term).unwrap())
} }
} }
@ -505,9 +509,8 @@ mod tests {
assert!(static_fn.call(&args).is_err()); assert!(static_fn.call(&args).is_err());
} }
#[test]
fn can_translate_a_string() { const TRANS_CONFIG: &str = r#"
let trans_config = r#"
base_url = "https://remplace-par-ton-url.fr" base_url = "https://remplace-par-ton-url.fr"
default_language = "fr" default_language = "fr"
@ -517,10 +520,11 @@ title = "Un titre"
[translations.en] [translations.en]
title = "A title" title = "A title"
"#; "#;
let config = Config::parse(trans_config).unwrap(); #[test]
fn can_translate_a_string() {
let config = Config::parse(TRANS_CONFIG).unwrap();
let static_fn = Trans::new(config); let static_fn = Trans::new(config);
let mut args = HashMap::new(); let mut args = HashMap::new();
@ -533,4 +537,26 @@ title = "A title"
args.insert("lang".to_string(), to_value("fr").unwrap()); args.insert("lang".to_string(), to_value("fr").unwrap());
assert_eq!(static_fn.call(&args).unwrap(), "Un titre"); assert_eq!(static_fn.call(&args).unwrap(), "Un titre");
} }
#[test]
fn error_on_absent_translation_lang() {
let mut args = HashMap::new();
args.insert("lang".to_string(), to_value("absent").unwrap());
args.insert("key".to_string(), to_value("title").unwrap());
let config = Config::parse(TRANS_CONFIG).unwrap();
let error = Trans::new(config).call(&args).unwrap_err();
assert_eq!("Failed to retreive term translation", format!("{}", error));
}
#[test]
fn error_on_absent_translation_key() {
let mut args = HashMap::new();
args.insert("lang".to_string(), to_value("en").unwrap());
args.insert("key".to_string(), to_value("absent").unwrap());
let config = Config::parse(TRANS_CONFIG).unwrap();
let error = Trans::new(config).call(&args).unwrap_err();
assert_eq!("Failed to retreive term translation", format!("{}", error));
}
} }