blob: eca06935669133e50a79cdac6df1f4206a85af97 [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.
//! # JSON generation for intl
//!
//! `strings_to_json` is a program that takes a `strings.xml` file for a source language, a
//! compatible `strings.xml` file for a target language, and produces a Fuchsia localized resource.
//! Please see the `README.md` file in this program's directory for more information.
use {
anyhow::Context,
anyhow::Error,
anyhow::Result,
intl_strings::{json, parser, veprintln},
std::env,
std::fs::File,
std::io,
std::path::PathBuf,
structopt::StructOpt,
};
#[derive(Debug, StructOpt)]
#[structopt(
name = "Packages translated messages from strings.xml files into a JSON localized resource"
)]
struct Args {
#[structopt(
long = "source-locale",
help = "The locale ID for the locale that is used as the message source of truth. Example: en-US"
)]
source_locale: String,
#[structopt(
long = "target-locale",
help = "The locale ID for the locale that is used as the target for bundling. Example: nl-NL"
)]
target_locale: String,
#[structopt(long = "source-strings-file", help = "The path to the source strings.xml file")]
source_strings_file: PathBuf,
#[structopt(
long = "target-strings-file",
help = "The path to the strings.xml file containing translated messages"
)]
target_strings_file: PathBuf,
#[structopt(
long = "output",
help = "The path to the JSON file that should be generated as output"
)]
output: PathBuf,
#[structopt(long = "verbose", help = "Verbose output, for debugging")]
verbose: bool,
#[structopt(
long = "replace-missing-with-warning",
help = "Replaces a missing message 'foo' with 'UNTRANSLATED(foo)' instead of failing"
)]
replace_missing_with_warning: bool,
}
// All the input and output files needed for the JSON conversion.
struct SourcesSinks {
source_strings_file: io::BufReader<File>,
target_strings_file: io::BufReader<File>,
output_file: File,
}
fn open_single_read(what: &str, path: &PathBuf) -> Result<io::BufReader<File>> {
let input_str = path.to_str().with_context(|| {
format!("{} filename is not utf-8, what? Use --verbose flag to print the value.", what)
})?;
Ok(io::BufReader::new(
File::open(path).with_context(|| format!("could not open {}: {}", what, input_str))?,
))
}
/// Open the needed files, and handle usual errors.
fn open_files(args: &Args) -> Result<SourcesSinks, Error> {
let source_input = open_single_read("source_strings_file", &args.source_strings_file)?;
let target_input = open_single_read("target_strings_file", &args.target_strings_file)?;
let output_str = args.output.to_str().with_context(|| {
"output filename is not utf-8, what? Use --verbose flag to print the value."
})?;
let output = File::create(&args.output)
.with_context(|| format!("could not open output file: {}", output_str))?;
Ok(SourcesSinks {
source_strings_file: source_input,
target_strings_file: target_input,
output_file: output,
})
}
fn run(args: Args) -> Result<(), Error> {
veprintln!(args.verbose, "args: {:?}", args);
let file_gaggle = open_files(&args).with_context(|| "while opening files")?;
let source_reader = parser::Instance::reader(file_gaggle.source_strings_file);
let mut source_parser = parser::Instance::new(args.verbose);
let source_dictionary = source_parser.parse(source_reader).with_context(|| {
format!("while parsing source dictionary: {:?}", &args.source_strings_file)
})?;
// Repetitive, but allows us to avoid copying dictionaries, which could be large.
let target_reader = parser::Instance::reader(file_gaggle.target_strings_file);
let mut target_parser = parser::Instance::new(args.verbose);
let target_dictionary = target_parser.parse(target_reader).with_context(|| {
format!("while parsing target dictionary: {:?}", &args.target_strings_file)
})?;
let model = json::model_from_dictionaries(
&args.source_locale,
&source_dictionary,
&args.target_locale,
&target_dictionary,
args.replace_missing_with_warning,
)?;
// And at the very end, victoriously write the file out.
model.to_json_writer(file_gaggle.output_file)
}
fn main() -> Result<(), Error> {
env::set_var("RUST_BACKTRACE", "full");
run(Args::from_args())
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::io::Write;
use tempfile;
// This test is only used to confirm that a program call generates some
// output that looks meaningful. Refer to the unit tests in the library
// for tests that actually enforce the specification.
#[test]
fn basic() -> Result<(), Error> {
let en = tempfile::NamedTempFile::new()?;
write!(
en.as_file(),
r#"
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- comment -->
<string
name="string_name"
>string</string>
</resources>
"#
)
.with_context(|| "while writing 'en' tempfile")?;
let fr = tempfile::NamedTempFile::new()?;
write!(
fr.as_file(),
r#"
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- comment -->
<string
name="string_name"
>le stringue</string>
</resources>
"#
)
.with_context(|| "while writing 'fr' tempfile")?;
let fr_json = tempfile::NamedTempFile::new()?;
let args = Args {
source_locale: "en".to_string(),
target_locale: "fr".to_string(),
source_strings_file: en.path().to_path_buf(),
target_strings_file: fr.path().to_path_buf(),
output: fr_json.path().to_path_buf(),
verbose: false,
replace_missing_with_warning: false,
};
run(args)?;
let outcome = fs::read_to_string(fr_json.path())?;
assert_eq!(
r#"{"locale_id":"fr","source_locale_id":"en","num_messages":1,"messages":{"7134240810508078445":"le stringue"}}"#,
outcome
);
Ok(())
}
#[test]
fn early_comment_not_allowed() -> Result<(), Error> {
struct TestCase {
name: &'static str,
en: &'static str,
fr: &'static str,
}
let tests = vec![
TestCase {
name: "there is a wrong comment in en",
en: r#"
<!-- comment not allowed here -->
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- comment -->
<string
name="string_name"
>string</string>
</resources>
"#,
fr: r#"
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- comment -->
<string
name="string_name"
>le stringue</string>
</resources>
"#,
},
TestCase {
name: "there is a wrong comment in fr",
en: r#"
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- comment -->
<string
name="string_name"
>string</string>
</resources>
"#,
fr: r#"
<!-- comment not allowed here -->
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- comment -->
<string
name="string_name"
>le stringue</string>
</resources>
"#,
},
];
for test in tests.iter() {
let en = tempfile::NamedTempFile::new()?;
write!(en.as_file(), "{}", test.en).with_context(|| "while writing 'en' tempfile")?;
let fr = tempfile::NamedTempFile::new()?;
write!(fr.as_file(), "{}", test.fr).with_context(|| "while writing 'fr' tempfile")?;
let fr_json = tempfile::NamedTempFile::new()?;
let args = Args {
source_locale: "en".to_string(),
target_locale: "fr".to_string(),
source_strings_file: en.path().to_path_buf(),
target_strings_file: fr.path().to_path_buf(),
output: fr_json.path().to_path_buf(),
verbose: false,
replace_missing_with_warning: false,
};
if let Ok(_) = run(args) {
return Err(anyhow::anyhow!("unexpected OK in test: {}", &test.name));
}
}
Ok(())
}
}