| // 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, Result}; |
| use intl_model::Model; |
| |
| #[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(()) |
| } |
| } |