blob: af92112ab377d3ebf6055bc6772cd0369f9e6e99 [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.
//! # 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(())
}
}