From 65fb57182bd73345e3346d0e6173579b0554fe19 Mon Sep 17 00:00:00 2001 From: Martin Geisler Date: Sun, 24 Jul 2022 23:39:41 +0200 Subject: [PATCH] Add `gettext` command to generate translated output This command is the second part of a Gettext-based translation (i18n) workflow. It takes an `xx.po` file with translations and uses this to translate the chapters of the book. Paragraphs without a translation are kept in the original language. Part of the solution for #5. --- src/cmd/gettext.rs | 82 ++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 2 ++ 2 files changed, 84 insertions(+) create mode 100644 src/cmd/gettext.rs diff --git a/src/cmd/gettext.rs b/src/cmd/gettext.rs new file mode 100644 index 0000000000..5f5a0c6713 --- /dev/null +++ b/src/cmd/gettext.rs @@ -0,0 +1,82 @@ +use crate::cmd::xgettext::extract_paragraphs; +use crate::get_book_dir; +use crate::utils; +use clap::{arg, App, Arg, ArgMatches}; +use mdbook::MDBook; +use polib::po_file::parse; +use std::path::Path; + +// Create clap subcommand arguments +pub fn make_subcommand<'help>() -> App<'help> { + App::new("gettext") + .about("Output translated book") + .arg( + Arg::new("dest-dir") + .short('d') + .long("dest-dir") + .value_name("dest-dir") + .help( + "Output directory for the translated book{n}\ + Relative paths are interpreted relative to the book's root directory{n}\ + If omitted, mdBook defaults to `./src/xx` where `xx` is the language of the PO file." + ), + ) + .arg(arg!( "PO file to generate translation for")) + .arg(arg!([dir] + "Root directory for the book{n}\ + (Defaults to the Current Directory when omitted)" + )) +} + +// Gettext command implementation +pub fn execute(args: &ArgMatches) -> mdbook::errors::Result<()> { + let book_dir = get_book_dir(args); + let book = MDBook::load(&book_dir)?; + + let po_file = Path::new(args.value_of("po").unwrap()); + let lang = po_file.file_stem().unwrap(); + let catalog = parse(po_file).unwrap(); + let dest_dir = book.root.join( + args.value_of("dest-dir") + .map(|path| path.into()) + .unwrap_or(Path::new("src").join(lang)), + ); + + for item in book.iter() { + match item { + mdbook::BookItem::Chapter(chapter) if !chapter.is_draft_chapter() => { + let mut output = String::with_capacity(chapter.content.len()); + let mut current_lineno = 1; + + for (lineno, paragraph) in extract_paragraphs(&chapter.content) { + // Fill in blank lines between paragraphs. This is + // important for code blocks where blank lines can + // be significant. + while current_lineno < lineno { + output.push('\n'); + current_lineno += 1; + } + current_lineno += paragraph.lines().count(); + + let translated = catalog + .find_message(paragraph) + .and_then(|msg| msg.get_msgstr().ok()) + .filter(|msgstr| !msgstr.is_empty()) + .map(|msgstr| msgstr.as_str()) + .unwrap_or(paragraph); + output.push_str(translated); + output.push('\n'); + } + + utils::fs::write_file( + &dest_dir, + chapter.source_path.as_ref().unwrap(), + output.as_bytes(), + )?; + } + _ => {} + } + } + + Ok(()) +} diff --git a/src/main.rs b/src/main.rs index a8ede35bf7..fb55d52f90 100644 --- a/src/main.rs +++ b/src/main.rs @@ -35,6 +35,7 @@ fn main() { Some(("serve", sub_matches)) => cmd::serve::execute(sub_matches), Some(("test", sub_matches)) => cmd::test::execute(sub_matches), Some(("xgettext", sub_matches)) => cmd::xgettext::execute(sub_matches), + Some(("gettext", sub_matches)) => cmd::gettext::execute(sub_matches), Some(("completions", sub_matches)) => (|| { let shell: Shell = sub_matches .value_of("shell") @@ -78,6 +79,7 @@ fn create_clap_app() -> App<'static> { .subcommand(cmd::test::make_subcommand()) .subcommand(cmd::clean::make_subcommand()) .subcommand(cmd::xgettext::make_subcommand()) + .subcommand(cmd::gettext::make_subcommand()) .subcommand( App::new("completions") .about("Generate shell completions for your shell to stdout")