Skip to content

Commit

Permalink
Add gettext command to generate translated output
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
mgeisler committed Sep 18, 2022
1 parent 8bc38c0 commit 8a64a4f
Show file tree
Hide file tree
Showing 3 changed files with 104 additions and 0 deletions.
101 changes: 101 additions & 0 deletions src/cmd/gettext.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
use crate::cmd::xgettext::extract_paragraphs;
use crate::get_book_dir;
use crate::utils;
use anyhow::anyhow;
use anyhow::Context;
use clap::{arg, App, Arg, ArgMatches};
use mdbook::book::Chapter;
use mdbook::BookItem;
use mdbook::MDBook;
use polib::catalog::Catalog;
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> "PO file to generate translation for"))
.arg(arg!([dir]
"Root directory for the book{n}\
(Defaults to the Current Directory when omitted)"
))
}

fn translate(text: &str, catalog: &Catalog) -> String {
let mut output = String::with_capacity(text.len());
let mut current_lineno = 1;

for (lineno, paragraph) in extract_paragraphs(text) {
// 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');
}

output
}

// 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()
.ok_or_else(|| anyhow!("Could not determine language from PO file {:?}", po_file))?;
let catalog = parse(po_file)
.map_err(|err| anyhow!(err.to_string()))
.with_context(|| format!("Could not parse PO file {:?}", po_file))?;
let dest_dir = book.root.join(match args.value_of("dest-dir") {
Some(path) => path.into(),
None => Path::new(&book.config.book.src).join(lang),
});

let summary_path = book_dir.join(&book.config.book.src).join("SUMMARY.md");
let summary = std::fs::read_to_string(&summary_path)?;
utils::fs::write_file(
&dest_dir,
"SUMMARY.md",
translate(&summary, &catalog).as_bytes(),
)?;

for item in book.iter() {
if let BookItem::Chapter(Chapter {
content,
path: Some(path),
..
}) = item
{
let output = translate(content, &catalog);
utils::fs::write_file(&dest_dir, path, output.as_bytes())?;
}
}

Ok(())
}
1 change: 1 addition & 0 deletions src/cmd/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
pub mod build;
pub mod clean;
pub mod gettext;
pub mod init;
#[cfg(feature = "serve")]
pub mod serve;
Expand Down
2 changes: 2 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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")
Expand Down

0 comments on commit 8a64a4f

Please sign in to comment.