diff --git a/i18n-helpers/src/gettext.rs b/i18n-helpers/src/gettext.rs index 7733b312..3794fd5c 100644 --- a/i18n-helpers/src/gettext.rs +++ b/i18n-helpers/src/gettext.rs @@ -40,7 +40,7 @@ fn strip_formatting(text: &str) -> String { .collect() } -fn translate(text: &str, catalog: &Catalog) -> anyhow::Result { +pub(crate) fn translate(text: &str, catalog: &Catalog) -> anyhow::Result { let events = extract_events(text, None); // Translation should always succeed. let translated_events = translate_events(&events, catalog).expect("Failed to translate events"); diff --git a/i18n-helpers/src/preprocessors/gettext.rs b/i18n-helpers/src/preprocessors/gettext.rs index 42d5fd02..ff82f158 100644 --- a/i18n-helpers/src/preprocessors/gettext.rs +++ b/i18n-helpers/src/preprocessors/gettext.rs @@ -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, + description: Option, +} + +fn metadata_translations( + title: Option<&str>, + description: Option<&str>, + catalog: &Catalog, +) -> anyhow::Result { + 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 { + 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\n"); + Some(script) +} + +fn inject_metadata_script(book: &mut Book, translations: &MetadataTranslations) { + 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("")); + } + }); + } +} + /// Check whether the book should be transalted. /// /// The book should be translated if: @@ -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) } diff --git a/i18n-helpers/src/xgettext.rs b/i18n-helpers/src/xgettext.rs index 4b66798f..d9574bb2 100644 --- a/i18n-helpers/src/xgettext.rs +++ b/i18n-helpers/src/xgettext.rs @@ -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()))?; @@ -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", ""), @@ -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![ + ("book.toml:1", "My Translatable Book", ""), + ("book.toml:1", "A lovely book", ""), + ] + ); Ok(()) }