blob: cce9cac46425da0ac8028a3502afe5536dfc9e03 [file] [log] [blame]
// Copyright 2020 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//! # Building the JSON data model from parsed dictionaries.
//!
//! Use [model_from_dictionaries] to build a parsed data model for internationalized
//! messages. This piece of code logically belongs to `intl_model`, but needed
//! to be factored out here, as it should only be compiled for the host toolchain.
use {
crate::message_ids,
crate::parser,
anyhow::Result,
intl_model::{Messages, Model},
std::collections::BTreeMap,
};
#[cfg(test)]
pub fn as_messages(pairs: &Vec<(u64, String)>) -> Messages {
let mut result = Messages::new();
for (k, v) in pairs {
result.insert(*k, v.clone());
}
result as Messages
}
/// Creates a new [Model] from the supplied dictionaries.
///
/// [source_locale_id] is the source locale (very typically would be "en-US" or some such);
/// [target_locale_id] is the target locale (i.e. the locale to translate into, e.g. "ru-RU");
/// source_dictionary is the dictionary of untranslated messages , presumably coming from the
/// declared source locale; and [target_dictionary] is the dictionary of available translated
/// messages, presumably in the target locale.
///
/// Both the source and target dictionaries are needed, because we need to determine whether
/// all messages are present. At the moment we require that all messages present in the source
/// dictionary are also present in the target dictionary. furthermore, messages that are
/// present in the target but not present in the source are omitted entirely.
pub fn model_from_dictionaries(
source_locale_id: &str,
source_dictionary: &parser::Dictionary,
target_locale_id: &str,
target_dictionary: &parser::Dictionary,
replace_with_warning: bool,
) -> Result<Model> {
let mut messages: Messages = BTreeMap::new();
for (name, message) in source_dictionary.iter() {
let message_id = message_ids::gen_id(name, message);
match target_dictionary.get(name) {
None => {
if replace_with_warning {
messages.insert(message_id, format!("UNTRANSLATED({})", &message));
} else {
return Err(anyhow::anyhow!(
"not found: translation for\n\tname: '{}'\n\tmessage: '{}'",
name,
message
));
}
}
Some(translated) => {
messages.insert(message_id, translated.to_string());
}
}
}
let messages = messages;
Ok(Model::from_parts(target_locale_id, source_locale_id, source_dictionary.len(), messages))
}
#[cfg(test)]
mod tests {
use super::*;
use anyhow::Context;
#[test]
fn build_from_dictionary() -> Result<()> {
struct TestCase {
name: &'static str,
// If set, a missing message is substituted.
substitute: bool,
source: parser::Dictionary,
target: parser::Dictionary,
expected_num_messages: usize,
expected: Vec<(u64, String)>,
}
let tests = vec![
TestCase {
name: "basic pseudo-French",
substitute: false,
source: parser::Dictionary::from_init(&vec![("string_name", "text_string")]),
target: parser::Dictionary::from_init(&vec![
// Behold my French-language-foo!
("string_name", "un_string_textique"),
]),
expected_num_messages: 1,
// The magic numbers here and below are stable IDs generated for messages
// by `message_ids::gen_id`.
expected: vec![(17128972970596363087, "un_string_textique".to_string())],
},
TestCase {
name: "known message ID",
substitute: false,
source: parser::Dictionary::from_init(&vec![("string_name", "text")]),
target: parser::Dictionary::from_init(&vec![("string_name", "le text")]),
expected_num_messages: 1,
// The magic number here is a known golden result from the call to:
// `message_ids::gen_id("string_name", "text")`. There should not
// be a change of the numeric message ID, unless `gen_id` algorithm
// is also modified.
expected: vec![(15068421743305203572, "le text".to_string())],
},
TestCase {
name: "several messages",
substitute: false,
source: parser::Dictionary::from_init(&vec![
("string_1", "text"),
("string_2", "more text"),
("string_3", "even more text"),
]),
target: parser::Dictionary::from_init(&vec![
("string_1", "le text"),
("string_2", "le more text"),
("string_3", "le even more text"),
("string_4", "le text which has been abandoned"),
]),
expected_num_messages: 3,
expected: vec![
(2935634291568311942, "le text".to_string()),
(14010255246293253599, "le more text".to_string()),
(11619890714301104023, "le even more text".to_string()),
],
},
TestCase {
name: "several messages",
substitute: true,
source: parser::Dictionary::from_init(&vec![
("string_1", "text"),
("string_2", "more text"),
("string_3", "even more text"),
]),
target: parser::Dictionary::from_init(&vec![
("string_1", "le text"),
("string_2", "le more text"),
]),
expected_num_messages: 3,
expected: vec![
(2935634291568311942, "le text".to_string()),
(14010255246293253599, "le more text".to_string()),
(11619890714301104023, "UNTRANSLATED(even more text)".to_string()),
],
},
];
for test in tests {
let model = model_from_dictionaries(
"und-x-src",
&test.source,
"und-x-dest",
&test.target,
test.substitute,
)
.with_context(|| format!("in test '{}', while building model", test.name))?;
let expected = Model::from_parts(
"und-x-dest",
"und-x-src",
test.expected_num_messages,
super::as_messages(&test.expected),
);
assert_eq!(expected, model);
}
Ok(())
}
#[test]
fn build_from_dictionary_fails() -> Result<()> {
struct TestCase {
name: &'static str,
source: parser::Dictionary,
target: parser::Dictionary,
}
let tests = vec![TestCase {
name: "untranslated messages are disallowed",
source: parser::Dictionary::from_init(&vec![
("string_1", "text"),
("string_2", "more text"),
("string_3", "even more text"),
("string_4", "untranslated text"),
]),
target: parser::Dictionary::from_init(&vec![
("string_1", "le text"),
("string_2", "le more text"),
("string_3", "le even more text"),
]),
}];
for test in tests {
let model = model_from_dictionaries(
"und-x-src",
&test.source,
"und-x-dest",
&test.target,
false,
);
match model {
Ok(_) => return Err(anyhow::anyhow!("unexpectedly passing test: '{}'", test.name)),
Err(_) => { /* expected */ }
}
}
Ok(())
}
}