Skip to content

Commit 84b1a52

Browse files
committed
Translate the pb core
1 parent dc60e2f commit 84b1a52

17 files changed

Lines changed: 408 additions & 80 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/paperback-core/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ license.workspace = true
77
repository.workspace = true
88
description = "Core document parsing and reading logic for Paperback"
99

10+
[package.metadata.patois]
11+
translatable = true
12+
1013
[lib]
1114
name = "paperback_core"
1215
path = "src/lib.rs"
@@ -22,6 +25,7 @@ encoding_rs = "0.8.35"
2225
icu_properties = { version = "2.2.0", features = ["unicode_bidi"] }
2326
libchm = "0.2.0"
2427
office-crypto = "0.2.0"
28+
patois = { git = "https://github.com/trypsynth/patois.git" }
2529
pdfium = "0.10.4"
2630
percent-encoding = "2.3.2"
2731
pulldown-cmark = { version = "0.13.4", default-features = false, features = ["html"] }

crates/paperback-core/src/lib.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,10 @@ pub fn set_pdfium_library_path(path: String) {
2626
pdfium::set_library_location(&path);
2727
}
2828

29-
/// Minimal translation stub for library-internal strings (e.g. document content labels).
30-
/// The GUI binary sets up the real wxWidgets translation system independently; strings
31-
/// returned by this function are English only and are intended for non-GUI consumers
32-
/// (CLI, mobile bindings) or for embedding into document content.
29+
/// Translates library-internal strings (e.g. document content labels, parser error messages).
30+
/// Delegates to `patois`'s process-global registry: when the GUI binary calls `patois::init`
31+
/// at startup, these strings come back translated automatically. Non-GUI consumers (CLI,
32+
/// mobile bindings) that never call `patois::init` get the English source string back unchanged.
3333
pub(crate) fn t(s: &str) -> String {
34-
s.to_owned()
34+
patois::t(s)
3535
}

crates/paperback-core/src/parser.rs

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use anyhow::Result;
99

1010
use crate::{
1111
document::{Document, DocumentBuffer, Marker, MarkerType, ParserContext, ParserFlags},
12+
t,
1213
types::{HeadingInfo, ImageInfo, LinkInfo, ListInfo, ListItemInfo, SeparatorInfo, TableInfo},
1314
};
1415

@@ -126,15 +127,17 @@ pub fn parse_document(context: &ParserContext) -> Result<Document> {
126127
let path = Path::new(&context.file_path);
127128
let extension = context.forced_extension.as_ref().map_or_else(
128129
|| {
129-
path.extension()
130-
.and_then(|e| e.to_str())
131-
.ok_or_else(|| anyhow::anyhow!("No file extension found for: {}", context.file_path))
130+
path.extension().and_then(|e| e.to_str()).ok_or_else(|| {
131+
// TRANSLATORS: Error shown when a file has no extension to determine its format; {} is the file path
132+
anyhow::anyhow!(t("No file extension found for: {}").replace("{}", &context.file_path))
133+
})
132134
},
133135
|ext| Ok(ext.as_str()),
134136
)?;
135137
let parsers = ParserRegistry::global().get_parsers_for_extension(extension);
136138
if parsers.is_empty() {
137-
return Err(anyhow::anyhow!("No parser found for extension: .{extension}"));
139+
// TRANSLATORS: Error shown when no parser supports a file's extension; {} is the extension (without the leading dot)
140+
return Err(anyhow::anyhow!(t("No parser found for extension: .{}").replace("{}", extension)));
138141
}
139142
let mut last_error = None;
140143
for parser in parsers {
@@ -151,7 +154,10 @@ pub fn parse_document(context: &ParserContext) -> Result<Document> {
151154
}
152155
}
153156
}
154-
Err(last_error.unwrap_or_else(|| anyhow::anyhow!("All parsers failed for extension: .{extension}")))
157+
Err(last_error.unwrap_or_else(|| {
158+
// TRANSLATORS: Error shown when every parser for a file's extension failed; {} is the extension (without the leading dot)
159+
anyhow::anyhow!(t("All parsers failed for extension: .{}").replace("{}", extension))
160+
}))
155161
}
156162

157163
#[must_use]

crates/paperback-core/src/parser/daisy.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ use crate::{
1616
util::{path::extract_title_from_path, toc::build_toc_from_headings},
1717
xml_to_text::XmlToText,
1818
},
19+
t,
1920
util::zip::read_zip_entry_by_name_with_password,
2021
};
2122

@@ -101,7 +102,8 @@ impl Parser for DaisyParser {
101102
buffer.add_marker(Marker::new(MarkerType::PageBreak, pb.offset).with_text(pb.text.clone()));
102103
}
103104
} else {
104-
anyhow::bail!("Failed to convert DTBook XML to text");
105+
// TRANSLATORS: Error shown when a DAISY book's DTBook XML fails to convert to plain text
106+
anyhow::bail!(t("Failed to convert DTBook XML to text"));
105107
}
106108
let mut toc_items = None;
107109
let ncx_path = archive
@@ -171,7 +173,8 @@ impl Parser for DaisyParser {
171173
});
172174
}
173175
}
174-
anyhow::bail!("ZIP archive does not appear to be a valid DAISY 3 or DAISY 2.02 book");
176+
// TRANSLATORS: Error shown when a ZIP file is not a recognizable DAISY 3 or DAISY 2.02 book
177+
anyhow::bail!(t("ZIP archive does not appear to be a valid DAISY 3 or DAISY 2.02 book"));
175178
}
176179
let file_content = std::fs::read_to_string(path)?;
177180
let (manifest_xml, metadata) = parse_opf_metadata_and_manifest(&file_content)?;
@@ -219,7 +222,8 @@ impl Parser for DaisyParser {
219222
});
220223
}
221224
}
222-
anyhow::bail!("Invalid DAISY .opf file or could not find DTBook XML in manifest");
225+
// TRANSLATORS: Error shown when a DAISY .opf file is invalid or its DTBook XML can't be located
226+
anyhow::bail!(t("Invalid DAISY .opf file or could not find DTBook XML in manifest"));
223227
}
224228
}
225229

crates/paperback-core/src/parser/epub.rs

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ use crate::{
1818
util::path::extract_title_from_path,
1919
xml_to_text::XmlToText,
2020
},
21+
t,
2122
types::{HeadingInfo, LinkInfo, ListInfo, ListItemInfo, SeparatorInfo, TableInfo},
2223
util::{
2324
text::{collapse_whitespace, trim_string, url_decode},
@@ -133,16 +134,20 @@ impl Parser for EpubParser {
133134
let package_node = opf_doc
134135
.descendants()
135136
.find(|n| n.node_type() == NodeType::Element && n.tag_name().name() == "package")
136-
.ok_or_else(|| anyhow::anyhow!("OPF package element missing"))?;
137+
// TRANSLATORS: Error shown when an EPUB's OPF document has no <package> element
138+
.ok_or_else(|| anyhow::anyhow!(t("OPF package element missing")))?;
137139
let (manifest, spine, nav_path, ncx_path, metadata) = parse_package(package_node, &opf_dir);
138140
let mut conversion = convert_spine_items(&mut archive, &manifest, &spine, context.render_tables_inline);
139141
if conversion.sections.is_empty() {
140142
let reason = if conversion.conversion_errors.is_empty() {
141-
String::from("no readable spine items")
143+
// TRANSLATORS: Reason given when an EPUB has no spine items that could be read
144+
t("no readable spine items")
142145
} else {
143-
format!("failed to convert spine items: {}", conversion.conversion_errors.join(", "))
146+
// TRANSLATORS: Reason given when EPUB spine items failed to convert; {} is a comma-separated list of underlying errors
147+
t("failed to convert spine items: {}").replace("{}", &conversion.conversion_errors.join(", "))
144148
};
145-
anyhow::bail!("EPUB has no readable content ({reason})");
149+
// TRANSLATORS: Error shown when an EPUB has no readable content; {} is the specific reason (see the two messages above)
150+
anyhow::bail!(t("EPUB has no readable content ({})").replace("{}", &reason));
146151
}
147152
let title = metadata
148153
.title
@@ -275,7 +280,8 @@ fn find_container_path<R: Read + Seek>(archive: &mut ZipArchive<R>) -> Result<St
275280
return Ok(path.to_string());
276281
}
277282
}
278-
anyhow::bail!("rootfile not found in container.xml")
283+
// TRANSLATORS: Error shown when an EPUB's container.xml is missing its rootfile reference
284+
anyhow::bail!(t("rootfile not found in container.xml"))
279285
}
280286

281287
struct PackageMetadata {
@@ -386,7 +392,8 @@ fn convert_section(content: &str, render_tables_inline: bool) -> Result<SectionC
386392
id_positions: html_converter.get_id_positions().clone(),
387393
});
388394
}
389-
anyhow::bail!("unsupported content")
395+
// TRANSLATORS: Error shown when an EPUB spine item's content type cannot be converted
396+
anyhow::bail!(t("unsupported content"))
390397
}
391398

392399
fn resolve_href(current_path: &str, target: &str) -> String {

crates/paperback-core/src/parser/fb2.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use crate::{
1010
util::xml::{collect_element_text, find_child_element},
1111
xml_to_text::XmlToText,
1212
},
13+
t,
1314
};
1415

1516
type Metadata = (String, String);
@@ -42,7 +43,8 @@ impl Parser for Fb2Parser {
4243
});
4344
let mut converter = XmlToText::with_render_tables_inline(context.render_tables_inline);
4445
if !converter.convert(&xml_content) {
45-
anyhow::bail!("Failed to convert FB2 XML to text");
46+
// TRANSLATORS: Error shown when an FB2 (FictionBook) file's XML fails to convert to plain text
47+
anyhow::bail!(t("Failed to convert FB2 XML to text"));
4648
}
4749
let mut buffer = DocumentBuffer::new();
4850
buffer.append(&converter.get_text());

crates/paperback-core/src/parser/html.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use crate::{
99
html_to_text::{HtmlSourceMode, HtmlToText},
1010
util::{path::extract_title_from_path, toc::build_toc_from_headings},
1111
},
12+
t,
1213
util::encoding::convert_to_utf8,
1314
};
1415

@@ -31,12 +32,14 @@ impl Parser for HtmlParser {
3132
let bytes = fs::read(&context.file_path)
3233
.with_context(|| format!("Failed to open HTML file '{}'", context.file_path))?;
3334
if bytes.is_empty() {
34-
anyhow::bail!("HTML file is empty: {}", context.file_path);
35+
// TRANSLATORS: Error shown when an HTML file has no content; {} is the file path
36+
anyhow::bail!(t("HTML file is empty: {}").replace("{}", &context.file_path));
3537
}
3638
let html_content = convert_to_utf8(&bytes);
3739
let mut converter = HtmlToText::with_render_tables_inline(context.render_tables_inline);
3840
if !converter.convert(&html_content, HtmlSourceMode::NativeHtml) {
39-
anyhow::bail!("Failed to convert HTML to text: {}", context.file_path);
41+
// TRANSLATORS: Error shown when an HTML file fails to convert to plain text; {} is the file path
42+
anyhow::bail!(t("Failed to convert HTML to text: {}").replace("{}", &context.file_path));
4043
}
4144
let extracted_title = converter.get_title();
4245
let title = if extracted_title.is_empty() {

crates/paperback-core/src/parser/markdown.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use crate::{
1010
html_to_text::{HtmlSourceMode, HtmlToText},
1111
util::{path::extract_title_from_path, toc::build_toc_from_headings},
1212
},
13+
t,
1314
util::encoding::convert_to_utf8,
1415
};
1516

@@ -125,7 +126,8 @@ impl Parser for MarkdownParser {
125126
let html_content = markdown_to_html(&markdown_content);
126127
let mut converter = HtmlToText::with_render_tables_inline(context.render_tables_inline);
127128
if !converter.convert(&html_content, HtmlSourceMode::Markdown) {
128-
anyhow::bail!("Failed to convert Markdown to text: {}", context.file_path);
129+
// TRANSLATORS: Error shown when a Markdown file fails to convert to plain text; {} is the file path
130+
anyhow::bail!(t("Failed to convert Markdown to text: {}").replace("{}", &context.file_path));
129131
}
130132
let title = extract_title_from_path(&context.file_path);
131133
let text = converter.get_text();

0 commit comments

Comments
 (0)