Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion i18n-helpers/src/gettext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ fn strip_formatting(text: &str) -> String {
.collect()
}

fn translate(text: &str, catalog: &Catalog) -> anyhow::Result<String> {
pub(crate) fn translate(text: &str, catalog: &Catalog) -> anyhow::Result<String> {
let events = extract_events(text, None);
// Translation should always succeed.
let translated_events = translate_events(&events, catalog).expect("Failed to translate events");
Expand Down
178 changes: 177 additions & 1 deletion i18n-helpers/src/preprocessors/gettext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,183 @@
// See the License for the specific language governing permissions and
// limitations under the License.

use crate::gettext::{add_stripped_summary_translations, translate_book};
use crate::gettext::{add_stripped_summary_translations, translate, translate_book};
use anyhow::{anyhow, Context};
use mdbook::book::{Book, BookItem};
use mdbook::preprocess::{Preprocessor, PreprocessorContext};
use polib::catalog::Catalog;
use polib::po_file;
use std::path::PathBuf;

#[derive(Debug, Default, PartialEq, Eq)]
struct MetadataTranslations {
title: Option<String>,
description: Option<String>,
}

fn metadata_translations(
title: Option<&str>,
description: Option<&str>,
catalog: &Catalog,
) -> anyhow::Result<MetadataTranslations> {
let mut translations = MetadataTranslations::default();

if let Some(title) = title {
let translated = translate(title, catalog)?;
if translated.trim() != title.trim() {
translations.title = Some(translated);
}
}

if let Some(description) = description {
let translated = translate(description, catalog)?;
if translated.trim() != description.trim() {
translations.description = Some(translated);
}
}

Ok(translations)
}

fn metadata_script(translations: &MetadataTranslations) -> Option<String> {
if translations.title.is_none() && translations.description.is_none() {
return None;
}

// Serialize eagerly so we fail early if escaping fails for some reason.
let title_json = translations
.title
.as_ref()
.map(|t| serde_json::to_string(t))
.transpose()
.ok()?;
let description_json = translations
.description
.as_ref()
.map(|d| serde_json::to_string(d))
.transpose()
.ok()?;

let mut script =
String::from("\n\n<script>window.addEventListener('DOMContentLoaded', function () {\n");
if let Some(title) = title_json {
script.push_str(" const title = ");
script.push_str(&title);
script.push_str(
";\n if (title) {\n document.title = title;\n for (const el of document.querySelectorAll('.menu-title, .mobile-nav__title')) {\n el.textContent = title;\n }\n }\n",
);
}

if let Some(description) = description_json {
script.push_str(" const description = ");
script.push_str(&description);
script.push_str(
";\n if (description) {\n const meta = document.querySelector('meta[name=\"description\"]');\n if (meta) {\n meta.setAttribute('content', description);\n }\n }\n",
);
}

script.push_str("});</script>\n");
Some(script)
}

fn inject_metadata_script(book: &mut Book, translations: &MetadataTranslations) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm sorry, but I don't think this is a good approach: injecting JavaScript into every Markdown file (if I read it correctly) is very invasive and surprising.

People will have questions and I don't have answers for them 😄

Basically, if this is not supported in upstream mdbook, then we cannot support it here either.

if let Some(script) = metadata_script(translations) {
book.for_each_mut(|item| {
if let BookItem::Chapter(ch) = item {
ch.content.push_str(&script);
}
});
}
}

#[cfg(test)]
mod tests {
use super::*;
use mdbook::book::{Book, BookItem, Chapter};
use polib::message::Message;
use polib::metadata::CatalogMetadata;

fn catalog_with(entries: &[(&str, &str)]) -> Catalog {
let mut catalog = Catalog::new(CatalogMetadata::new());
for (msgid, msgstr) in entries {
let message = Message::build_singular()
.with_msgid((*msgid).to_string())
.with_msgstr((*msgstr).to_string())
.done();
catalog.append_or_update(message);
}
catalog
}

#[test]
fn injects_script_when_metadata_translated() {
let catalog = catalog_with(&[
("Original Title", "Titre traduit"),
("Original description", "Description traduite"),
]);

let translations = metadata_translations(
Some("Original Title"),
Some("Original description"),
&catalog,
)
.unwrap();
assert_eq!(
translations,
MetadataTranslations {
title: Some("Titre traduit".into()),
description: Some("Description traduite".into()),
}
);

let mut book = Book::new();
book.push_item(BookItem::Chapter(Chapter::new(
"Chapter",
"# Heading".into(),
"chapter.md",
vec![],
)));

inject_metadata_script(&mut book, &translations);

let mut contents = Vec::new();
book.for_each_mut(|item| {
if let BookItem::Chapter(ch) = item {
contents.push(ch.content.clone());
}
});

assert!(contents[0].contains("Titre traduit"));
assert!(contents[0].contains("Description traduite"));
assert!(contents[0].contains("window.addEventListener"));
}

#[test]
fn skips_script_when_no_translation() {
let catalog = catalog_with(&[("Same Title", "Same Title"), ("Same description", "")]);

let translations =
metadata_translations(Some("Same Title"), Some("Same description"), &catalog).unwrap();
assert_eq!(translations, MetadataTranslations::default());

let mut book = Book::new();
book.push_item(BookItem::Chapter(Chapter::new(
"Chapter",
"Content".into(),
"chapter.md",
vec![],
)));

inject_metadata_script(&mut book, &translations);

book.for_each_mut(|item| {
if let BookItem::Chapter(ch) = item {
assert!(!ch.content.contains("</script>"));
}
});
}
}

/// Check whether the book should be transalted.
///
/// The book should be translated if:
Expand Down Expand Up @@ -80,7 +250,13 @@ impl Preprocessor for Gettext {
if should_translate(ctx) {
let mut catalog = load_catalog(ctx)?;
add_stripped_summary_translations(&mut catalog);
let metadata = metadata_translations(
ctx.config.book.title.as_deref(),
ctx.config.book.description.as_deref(),
&catalog,
)?;
translate_book(&catalog, &mut book)?;
inject_metadata_script(&mut book, &metadata);
}
Ok(book)
}
Expand Down
37 changes: 36 additions & 1 deletion i18n-helpers/src/xgettext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,31 @@ where
})?,
};

// First, add all chapter names and part titles from SUMMARY.md.
// Include book level metadata from book.toml so translators can localize
// what readers see in the book chrome.
let book_toml_path = Path::new("book.toml");
if let Some(title) = ctx
.config
.book
.title
.as_ref()
.filter(|t| !t.trim().is_empty())
{
let source = build_source(book_toml_path, 1, granularity);
add_message(&mut catalog, title, &source, "");
}
if let Some(description) = ctx
.config
.book
.description
.as_ref()
.filter(|d| !d.trim().is_empty())
{
let source = build_source(book_toml_path, 1, granularity);
add_message(&mut catalog, description, &source, "");
}

// Next, add all chapter names and part titles from SUMMARY.md.
let summary_path = ctx.config.book.src.join("SUMMARY.md");
let summary = summary_reader(ctx.root.join(&summary_path))
.with_context(|| anyhow!("Failed to read {}", summary_path.display()))?;
Expand Down Expand Up @@ -382,6 +406,7 @@ mod tests {
"book.toml",
"[book]\n\
title = \"My Translatable Book\"\n\
description = \"A lovely book\"\n\
language = \"fr\"",
),
("src/SUMMARY.md", ""),
Expand All @@ -393,6 +418,16 @@ mod tests {
let catalog = &catalogs[&default_template_file()];
assert_eq!(catalog.metadata.project_id_version, "My Translatable Book");
assert_eq!(catalog.metadata.language, "fr");
assert_eq!(
catalog
.messages()
.map(|msg| (msg.source(), msg.msgid(), msg.comments()))
.collect::<Vec<_>>(),
vec![
("book.toml:1", "My Translatable Book", ""),
("book.toml:1", "A lovely book", ""),
]
);
Ok(())
}

Expand Down