| // 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. |
| |
| //! # The JSON message internationalization format |
| //! |
| //! This module contains the code used to turn a dictionary of localized messages into a |
| //! JSON-formatted file usable by Fuchsia's localization lookup system. |
| //! |
| //! The data model used for the JSON schema is defined below. No formal JSON schema has been |
| //! specified yet. (But probably should be!) |
| |
| use { |
| anyhow::Result, |
| serde::{Deserialize, Serialize}, |
| std::collections::BTreeMap, |
| std::io, |
| }; |
| |
| /// The message catalog. Maps unique IDs (as generated by the message_id::gen_ids) to individual |
| /// message. The catalog does not know about the locale it is intended for, and additional |
| /// metadata is needed to assert that. See [Model] for details on how it fits together. |
| pub type Messages = BTreeMap<u64, String>; |
| |
| #[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 |
| } |
| |
| /// The data model for a set of internationalized messages. Every |
| /// file has a locale ID that it applies to, as well as the number |
| /// of total messages analyzed when this locale was produced |
| /// |
| /// Use [Model::from_json_reader] to make a new instance of the |
| /// Model from its JSON serialization. Use [Model::from_dictionaries] to make a new instance of |
| /// the Model based on the supplied dictionaries. |
| /// |
| /// In tests, you can use [Model::from_parts] to create [Model] quickly without any checks. |
| #[derive(Serialize, Deserialize, Debug, Clone, Default, Eq, PartialEq)] |
| pub struct Model { |
| /// The locale for the messages described in this file. An |
| /// example value could be "en-US". |
| locale_id: String, |
| |
| /// The source-of-truth locale from which this model was |
| /// generated. Source of truth locales exist so as to provide |
| /// fallback messages or such. |
| source_locale_id: String, |
| |
| /// The total number of messages that are expected to exist in |
| /// this bundle. This number is not the same as the number of |
| /// key-value pairs in [messages] below, though. |
| num_messages: usize, |
| |
| /// The message catalog, listing the mapping of message IDs to |
| /// respective messages. This mapping does not define a fallback |
| /// language. The ordering of the messages in this map is not |
| /// defined: all serializations that end up with an identical |
| /// map model are equally valid. |
| messages: Messages, |
| } |
| |
| impl Model { |
| pub fn locale(&self) -> &str { |
| &self.locale_id |
| } |
| |
| pub fn messages(&self) -> &Messages { |
| &self.messages |
| } |
| |
| /// Deserializes the Model from the supplied reader. Use to |
| /// create a functional Model from JSON. |
| pub fn from_json_reader<R: io::Read>(r: R) -> Result<Model> { |
| serde_json::from_reader(r).map_err(|e| e.into()) |
| } |
| |
| /// Writes the Model into the supplied writer encoded as JSON. Use to persist the Model into |
| /// JSON. The message ordering is not guaranteed. |
| pub fn to_json_writer<W: io::Write>(&self, writer: W) -> Result<()> { |
| serde_json::to_writer(writer, self) |
| .map_err(|e| -> io::Error { e.into() }) |
| .map_err(|e| -> anyhow::Error { e.into() }) |
| } |
| |
| /// Creates a [Model] quickly from supplied parts and without any checks, |
| /// for use in tests only. |
| pub fn from_parts( |
| locale_id: &str, |
| source_locale_id: &str, |
| num_messages: usize, |
| messages: Messages, |
| ) -> Model { |
| Model { |
| locale_id: locale_id.to_string(), |
| source_locale_id: source_locale_id.to_string(), |
| num_messages, |
| messages, |
| } |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| use anyhow::Context; |
| |
| #[test] |
| fn read_json_test() -> Result<()> { |
| let message = r#" |
| { |
| "locale_id": "ru-RU", |
| "source_locale_id": "en-US", |
| "num_messages": 1000, |
| "messages": { |
| "42": "Привет", |
| "100": "Что это сорок-два?" |
| } |
| } |
| "#; |
| let model = Model::from_json_reader(message.as_bytes()) |
| .with_context(|| format!("while loading JSON: \n{}", message))?; |
| assert_eq!(model.locale_id, "ru-RU"); |
| assert_eq!(model.num_messages, 1000); |
| assert_eq!(model.messages.get(&42).unwrap(), "Привет"); |
| Ok(()) |
| } |
| |
| /// Note that we don't care about the message ordering just yet. |
| #[test] |
| fn round_trip_test() -> Result<()> { |
| struct TestCase { |
| name: &'static str, |
| message: &'static str, |
| } |
| let tests = vec![ |
| TestCase { |
| name: "Random text in Russian", |
| message: r#" |
| { |
| "locale_id": "ru-RU", |
| "source_locale_id": "en-US", |
| "num_messages": 42, |
| "messages": { |
| "42": "Привет", |
| "100": "Что это сорок-два?" |
| } |
| } |
| "#, |
| }, |
| TestCase { |
| name: "Serbian pangram", |
| message: r#" |
| { |
| "locale_id": "sr-RS", |
| "source_locale_id": "en-US", |
| "num_messages": 2, |
| "messages": { |
| "1": "Љубазни фењерџија чађавог лица хоће да ми покаже штос" |
| } |
| } |
| "#, |
| }, |
| ]; |
| for test in tests { |
| let model = Model::from_json_reader(test.message.as_bytes()).with_context(|| { |
| format!("in test '{}', while loading JSON: \n{}", test.name, test.message) |
| })?; |
| let mut output: Vec<u8> = vec![]; |
| model |
| .to_json_writer(&mut output) |
| .with_context(|| format!("while writing JSON, in test '{}'", test.name))?; |
| let model2 = Model::from_json_reader(&output[..]) |
| .with_context(|| format!("while reading JSON again, in test '{}'", test.name))?; |
| assert_eq!(model, model2); |
| } |
| Ok(()) |
| } |
| } |