Skip to content

Commit 8a64a4f

Browse files
committed
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.
1 parent 8bc38c0 commit 8a64a4f

File tree

3 files changed

+104
-0
lines changed

3 files changed

+104
-0
lines changed

src/cmd/gettext.rs

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
use crate::cmd::xgettext::extract_paragraphs;
2+
use crate::get_book_dir;
3+
use crate::utils;
4+
use anyhow::anyhow;
5+
use anyhow::Context;
6+
use clap::{arg, App, Arg, ArgMatches};
7+
use mdbook::book::Chapter;
8+
use mdbook::BookItem;
9+
use mdbook::MDBook;
10+
use polib::catalog::Catalog;
11+
use polib::po_file::parse;
12+
use std::path::Path;
13+
14+
// Create clap subcommand arguments
15+
pub fn make_subcommand<'help>() -> App<'help> {
16+
App::new("gettext")
17+
.about("Output translated book")
18+
.arg(
19+
Arg::new("dest-dir")
20+
.short('d')
21+
.long("dest-dir")
22+
.value_name("dest-dir")
23+
.help(
24+
"Output directory for the translated book{n}\
25+
Relative paths are interpreted relative to the book's root directory{n}\
26+
If omitted, mdBook defaults to `./src/xx` where `xx` is the language of the PO file."
27+
),
28+
)
29+
.arg(arg!(<po> "PO file to generate translation for"))
30+
.arg(arg!([dir]
31+
"Root directory for the book{n}\
32+
(Defaults to the Current Directory when omitted)"
33+
))
34+
}
35+
36+
fn translate(text: &str, catalog: &Catalog) -> String {
37+
let mut output = String::with_capacity(text.len());
38+
let mut current_lineno = 1;
39+
40+
for (lineno, paragraph) in extract_paragraphs(text) {
41+
// Fill in blank lines between paragraphs. This is
42+
// important for code blocks where blank lines can
43+
// be significant.
44+
while current_lineno < lineno {
45+
output.push('\n');
46+
current_lineno += 1;
47+
}
48+
current_lineno += paragraph.lines().count();
49+
50+
let translated = catalog
51+
.find_message(paragraph)
52+
.and_then(|msg| msg.get_msgstr().ok())
53+
.filter(|msgstr| !msgstr.is_empty())
54+
.map(|msgstr| msgstr.as_str())
55+
.unwrap_or(paragraph);
56+
output.push_str(translated);
57+
output.push('\n');
58+
}
59+
60+
output
61+
}
62+
63+
// Gettext command implementation
64+
pub fn execute(args: &ArgMatches) -> mdbook::errors::Result<()> {
65+
let book_dir = get_book_dir(args);
66+
let book = MDBook::load(&book_dir)?;
67+
68+
let po_file = Path::new(args.value_of("po").unwrap());
69+
let lang = po_file
70+
.file_stem()
71+
.ok_or_else(|| anyhow!("Could not determine language from PO file {:?}", po_file))?;
72+
let catalog = parse(po_file)
73+
.map_err(|err| anyhow!(err.to_string()))
74+
.with_context(|| format!("Could not parse PO file {:?}", po_file))?;
75+
let dest_dir = book.root.join(match args.value_of("dest-dir") {
76+
Some(path) => path.into(),
77+
None => Path::new(&book.config.book.src).join(lang),
78+
});
79+
80+
let summary_path = book_dir.join(&book.config.book.src).join("SUMMARY.md");
81+
let summary = std::fs::read_to_string(&summary_path)?;
82+
utils::fs::write_file(
83+
&dest_dir,
84+
"SUMMARY.md",
85+
translate(&summary, &catalog).as_bytes(),
86+
)?;
87+
88+
for item in book.iter() {
89+
if let BookItem::Chapter(Chapter {
90+
content,
91+
path: Some(path),
92+
..
93+
}) = item
94+
{
95+
let output = translate(content, &catalog);
96+
utils::fs::write_file(&dest_dir, path, output.as_bytes())?;
97+
}
98+
}
99+
100+
Ok(())
101+
}

src/cmd/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
33
pub mod build;
44
pub mod clean;
5+
pub mod gettext;
56
pub mod init;
67
#[cfg(feature = "serve")]
78
pub mod serve;

src/main.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ fn main() {
3535
Some(("serve", sub_matches)) => cmd::serve::execute(sub_matches),
3636
Some(("test", sub_matches)) => cmd::test::execute(sub_matches),
3737
Some(("xgettext", sub_matches)) => cmd::xgettext::execute(sub_matches),
38+
Some(("gettext", sub_matches)) => cmd::gettext::execute(sub_matches),
3839
Some(("completions", sub_matches)) => (|| {
3940
let shell: Shell = sub_matches
4041
.value_of("shell")
@@ -78,6 +79,7 @@ fn create_clap_app() -> App<'static> {
7879
.subcommand(cmd::test::make_subcommand())
7980
.subcommand(cmd::clean::make_subcommand())
8081
.subcommand(cmd::xgettext::make_subcommand())
82+
.subcommand(cmd::gettext::make_subcommand())
8183
.subcommand(
8284
App::new("completions")
8385
.about("Generate shell completions for your shell to stdout")

0 commit comments

Comments
 (0)