M14: French interface catalog (#291)
Add Français (fr) as a complete UI catalog and select it for every French locale. Preferences lists it, and Auto follows fr, fr_FR, fr_CA and fr_BE. About credits labels now go through the catalog. Coverage tests check commands, All tools labels and every tl! literal. The bundled Latin faces cover the catalog. Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: iMhack <iMhack87@users.noreply.github.com>
This commit is contained in:
parent
1b62d47fa7
commit
cd8c753527
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
Every asset PdfCraft includes, bundles or uses to build its published material, with its author, source and licence. The policy is in [AGENTS.md](AGENTS.md) §1. The machine-readable list, with SHA-256 hashes, is [ATTRIBUTION.toml](ATTRIBUTION.toml). Licence texts are kept beside the assets and summarised in [NOTICE](NOTICE).
|
||||
|
||||
## In this repository (248)
|
||||
## In this repository (249)
|
||||
|
||||
| Asset | Title | Author | Licence | Source | Used for |
|
||||
|---|---|---|---|---|---|
|
||||
|
|
@ -13,6 +13,7 @@ Every asset PdfCraft includes, bundles or uses to build its published material,
|
|||
| `crates/ui-egui/src/i18n/cs.tsv` | Czech interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources | Czech UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs |
|
||||
| `crates/ui-egui/src/i18n/pt-br.tsv` | Brazilian Portuguese interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources | Brazilian Portuguese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs |
|
||||
| `crates/ui-egui/src/i18n/es.tsv` | Spanish interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources | Spanish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs |
|
||||
| `crates/ui-egui/src/i18n/fr.tsv` | French interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources | French UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs |
|
||||
| `crates/ui-egui/src/i18n/ja.tsv` | Japanese interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources | Japanese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs |
|
||||
| `assets/icons/accessibility.svg` | Lucide icon "accessibility" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/accessibility.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
|
||||
| `assets/icons/align-left.svg` | Lucide icon "align-left" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/align-left.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
|
||||
|
|
|
|||
|
|
@ -65,6 +65,17 @@ kind = "translation"
|
|||
usage = "Spanish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "db79f5eb4ac0072bb42186838d0a9e089652c7bd95f679fd2431c79c74725b74"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/fr.tsv"
|
||||
title = "French interface translation catalog"
|
||||
author = "PdfCraft contributors"
|
||||
source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources"
|
||||
licence = "MIT OR Apache-2.0"
|
||||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "French UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "2937b2b28e1947cb7f78bf3dff9d06c5d91ca29789980070a1b8518cf0cd1d59"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/ja.tsv"
|
||||
title = "Japanese interface translation catalog"
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ The gaps that matter most, in priority order. Agents: prefer these over adding P
|
|||
3. **Measure fidelity against Acrobat.** Build a side-by-side harness (render, text, form behaviour) over synthetic fixtures (`plan/acrobat/`, clean-room rules apply). Until then, parity numbers count features, not quality.
|
||||
4. **Editing existing content (M7).** Fonts, subsets, reflow and CJK. The most visible gap for Pro users.
|
||||
5. **Pro workflows.** Signatures with timestamps and LTV (M9), OCR beyond Latin (M10), Office import/export (M10), preflight and PDF/A/X/UA (M11), XFA (M12).
|
||||
6. **1.0 polish (M14).** Code-signed installers, localization (the catalog system is in place; route every dialog and panel string through `tl!`, then add languages), keyboard-only operation, a full screen-reader audit.
|
||||
6. **1.0 polish (M14).** Code-signed installers, localization (the catalog system is in place and French, Spanish, Japanese and both Chinese catalogs cover the UI; Czech and Brazilian Portuguese still cover menus only), keyboard-only operation, a full screen-reader audit.
|
||||
|
||||
Decided against for now (owner, 2026-10-05): self-installing updates and an update check at start. Help ▸ Check for updates is manual only.
|
||||
|
||||
|
|
@ -103,7 +103,7 @@ Hours are for a single agent (low–high). "Done" is the estimated fraction of t
|
|||
| M11 | Optimize, preflight, PDF/A/X/UA, print production | 200–350 | 18% | 165–290 | Done: new `optimize` crate: Reduce File Size and the PDF Optimizer (images measured where drawn, bicubic downsampling, JPEG/ZIP recompression only when smaller, discard objects and user data, Flate clean-up, resource merging, object streams). Missing: fonts and transparency panels, space audit, preflight, PDF/A/X/UA |
|
||||
| M12 | Accessibility, compare, measure, search, XFA | 200–380 | 23% | 155–295 | Done: new `a11y` crate with the Accessibility Checker (all 32 rules, report, Fix/Skip/Explain, options dialog and results panel, agent tools). Done since: 2D distance, perimeter and area measurements with persistent viewport calibration, snapping, live information and CSV export. Missing: autotag, Tags/Order/Content panels, Reading Order tool, alt-text workflow, compare, geospatial/3D measurement, search index, XFA |
|
||||
| M13 | Automation (MCP, Action Wizard, CLI) + AI providers | 60–120 | 45% | 33–66 | Done: MCP resources (document info, text, page images); headless tool table (123 tools incl. signing, optimizing, initial view, links, stamps, data exchange, comment review, forms authoring and scripts, redaction, sanitize, print, add content), opt-in MCP server over stdio, CLI `run`/`tools` (closed stdout pipes exit cleanly), UI control channel with drag. Missing: Action Wizard, AI providers |
|
||||
| M14 | 1.0 polish: performance, localization, installers | 120–250 | 5% | 115–240 | Done: PhotoCraft's translation system (`i18n/`: TSV catalogs, `tl!`, command-id and plural entries, system-language detection, strict catalog tests); every dialog, panel and notice goes through `tl!`; Japanese, Traditional Chinese and Simplified Chinese catalogs have ≈1,800 entries each; Simplified Chinese coverage is checked against the UI source; Czech and Brazilian Portuguese for menus. Missing: more catalogs filled in, Windows language detection, installers, performance budgets |
|
||||
| M14 | 1.0 polish: performance, localization, installers | 120–250 | 5% | 115–240 | Done: PhotoCraft's translation system (`i18n/`: TSV catalogs, `tl!`, command-id and plural entries, system-language detection, strict catalog tests); every dialog, panel and notice goes through `tl!`; Japanese, Traditional Chinese, Simplified Chinese, Spanish and French catalogs have ≈1,800 entries each; Simplified Chinese and French coverage is checked against the UI source; Czech and Brazilian Portuguese for menus. French locales (`fr`, `fr_FR`, `fr_CA`) follow the French catalog. Missing: more catalogs filled in, installers, performance budgets |
|
||||
| | **Total (original plan sizing)** | **2,085–3,840** | **≈ 35%** | **≈ 1,350–2,500 at the planned rate; ≈ 600–1,100 at the measured rate** | |
|
||||
|
||||
**Overall progress: about 30–35% of the effort (51.0% of features shipped).** The viewer and the core are far ahead of the editing features, because the viewer was built first so progress could be seen; rendering itself is still borrowed from `hayro`. See [Honest assessment](#honest-assessment-2026-10-05).
|
||||
|
|
@ -127,6 +127,8 @@ M0 → M1 → M2 → M3 → M4 must happen in order. After M4, M5–M12 can run
|
|||
|
||||
Newest first. One line per session: the date, what moved, and the new overall percentage.
|
||||
|
||||
- **2026-10-08 (M14, French):** Français (`fr`) is a complete interface catalog (about 1,880 entries), selected automatically for `fr`, `fr_FR`, `fr_CA` and other French locales, and from Preferences. Coverage tests check commands, All tools labels and every `tl!` literal; history labels keep captured names. Latin UI faces cover the catalog. Localization remains partial for Czech and Brazilian Portuguese; overall ≈ 30–35% (unchanged).
|
||||
|
||||
- **2026-10-08 (M14, right-to-left file names):** A tab with an Arabic or Hebrew file name showed replacement boxes. The interface now takes an `Arab` face from craft-fonts when the build input has one, and on desktop falls back to one font installed on the machine for characters no embedded face has (`PDFCRAFT_SYSTEM_FONTS=0` turns that off; README screenshots do). File names and paths in the tab strip, the recent-files list and Properties are put in display order, since egui joins the letters but does not reorder runs. Still missing: multi-line text (bookmarks, comments, search results), text input and a mirrored layout, so `misc.rtl-ui` stays planned; the craft-fonts revision pinned for releases predates its Arabic face, so release builds rely on the installed font until the pin moves. Overall estimate unchanged (about 30–35%).
|
||||
|
||||
- **2026-10-08 (community, M3, #214):** In single-page view, when the whole page fits, the wheel turns pages: one per wheel notch, one per trackpad swipe including its momentum. The rail gets a Page display button between rotate and zoom with Acrobat's view choices (Fit to width scrolling, Fit one full page, Actual size, Zoom to page level, Fit visible, Read mode, Full screen) and the page layouts. Preferences gets Default page display and Default zoom, which every new tab uses. In continuous view, Fit page and Fit height fit the largest page, so mixed page sizes no longer jump while scrolling. Overall estimate unchanged (about 30–35%).
|
||||
|
|
|
|||
|
|
@ -128,18 +128,29 @@ impl Contributor {
|
|||
|
||||
/// One line with everything we know, for tooltips.
|
||||
pub fn summary(&self) -> String {
|
||||
format!(
|
||||
"@{}: {} PRs, {} commits, +{} / −{} lines (Δ {}), +{} / −{} binary assets, {} – {}",
|
||||
self.login,
|
||||
self.prs,
|
||||
self.commits,
|
||||
group(self.lines_added),
|
||||
group(self.lines_deleted),
|
||||
signed(self.lines_delta()),
|
||||
self.binary_added,
|
||||
self.binary_deleted,
|
||||
day(self.first_commit),
|
||||
day(self.last_commit),
|
||||
let prs = group(self.prs);
|
||||
let commits = group(self.commits);
|
||||
let added = group(self.lines_added);
|
||||
let deleted = group(self.lines_deleted);
|
||||
let delta = signed(self.lines_delta());
|
||||
let bin_added = self.binary_added.to_string();
|
||||
let bin_deleted = self.binary_deleted.to_string();
|
||||
crate::i18n::fmt(
|
||||
crate::i18n::t(
|
||||
"@{login}: {prs} PRs, {commits} commits, +{added} / −{deleted} lines (Δ {delta}), +{bin_added} / −{bin_deleted} binary assets, {first} – {last}",
|
||||
),
|
||||
&[
|
||||
("login", self.login),
|
||||
("prs", &prs),
|
||||
("commits", &commits),
|
||||
("added", &added),
|
||||
("deleted", &deleted),
|
||||
("delta", &delta),
|
||||
("bin_added", &bin_added),
|
||||
("bin_deleted", &bin_deleted),
|
||||
("first", day(self.first_commit)),
|
||||
("last", day(self.last_commit)),
|
||||
],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -221,39 +232,40 @@ pub fn contributors_ui(ui: &mut egui::Ui) {
|
|||
let id = egui::Id::new("credits_view");
|
||||
let mut v = ui.data_mut(|d| d.get_temp::<View>(id)).unwrap_or_default();
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.label("Show");
|
||||
ui.label(crate::i18n::t("Show"));
|
||||
for m in NameMode::ALL {
|
||||
if ui.selectable_label(v.names == m, m.label()).clicked() {
|
||||
if ui.selectable_label(v.names == m, crate::i18n::t(m.label())).clicked() {
|
||||
v.names = m;
|
||||
}
|
||||
}
|
||||
ui.separator();
|
||||
ui.label("Sort");
|
||||
egui::ComboBox::from_id_salt("credits_sort").selected_text(v.key.label().0).show_ui(ui, |ui| {
|
||||
ui.label(crate::i18n::t("Sort"));
|
||||
egui::ComboBox::from_id_salt("credits_sort").selected_text(crate::i18n::t(v.key.label().0)).show_ui(ui, |ui| {
|
||||
for k in SortKey::ALL {
|
||||
if ui.selectable_label(v.key == k, k.label().0).clicked() {
|
||||
if ui.selectable_label(v.key == k, crate::i18n::t(k.label().0)).clicked() {
|
||||
v.key = k;
|
||||
v.ascending = k.default_ascending();
|
||||
}
|
||||
}
|
||||
});
|
||||
if ui.button(if v.ascending { "▲" } else { "▼" }).on_hover_text("Reverse the order").clicked() {
|
||||
if ui.button(if v.ascending { "▲" } else { "▼" }).on_hover_text(crate::i18n::t("Reverse the order")).clicked() {
|
||||
v.ascending = !v.ascending;
|
||||
}
|
||||
ui.separator();
|
||||
if ui.selectable_label(!v.table, "Grab bag").clicked() {
|
||||
if ui.selectable_label(!v.table, crate::i18n::t("Grab bag")).clicked() {
|
||||
v.table = false;
|
||||
}
|
||||
if ui.selectable_label(v.table, "Table").clicked() {
|
||||
if ui.selectable_label(v.table, crate::i18n::t("Table")).clicked() {
|
||||
v.table = true;
|
||||
}
|
||||
});
|
||||
let list = sorted(CONTRIBUTORS, v.names, v.key, v.ascending);
|
||||
ui.label(RichText::new(format!("{} contributors · {} commits", list.len(), group(TOTAL_COMMITS))).small().color(t.text_muted));
|
||||
let counts = crate::i18n::fmt(crate::i18n::t("{n} contributors · {c} commits"), &[("n", &list.len().to_string()), ("c", &group(TOTAL_COMMITS))]);
|
||||
ui.label(RichText::new(counts).small().color(t.text_muted));
|
||||
ui.separator();
|
||||
egui::ScrollArea::both().id_salt("credits_list").max_height(LIST_HEIGHT).auto_shrink([false, true]).show(ui, |ui| {
|
||||
if list.is_empty() {
|
||||
ui.label("No contributor data was built into this copy.");
|
||||
ui.label(crate::i18n::t("No contributor data was built into this copy."));
|
||||
} else if v.table {
|
||||
table(ui, &list, &mut v);
|
||||
} else {
|
||||
|
|
@ -274,7 +286,11 @@ fn table(ui: &mut egui::Ui, list: &[&Contributor], v: &mut View) {
|
|||
egui::Grid::new("credits_table").striped(true).num_columns(SortKey::ALL.len()).show(ui, |ui| {
|
||||
for k in SortKey::ALL {
|
||||
let arrow = if v.key == k { if v.ascending { " ▲" } else { " ▼" } } else { "" };
|
||||
if ui.button(RichText::new(format!("{}{arrow}", k.label().1)).strong()).on_hover_text(k.label().0).clicked() {
|
||||
if ui
|
||||
.button(RichText::new(format!("{}{arrow}", crate::i18n::t(k.label().1))).strong())
|
||||
.on_hover_text(crate::i18n::t(k.label().0))
|
||||
.clicked()
|
||||
{
|
||||
if v.key == k {
|
||||
v.ascending = !v.ascending;
|
||||
} else {
|
||||
|
|
@ -303,14 +319,14 @@ fn table(ui: &mut egui::Ui, list: &[&Contributor], v: &mut View) {
|
|||
/// About ▸ Models: AI models credited in Co-Authored-By trailers.
|
||||
pub fn models_ui(ui: &mut egui::Ui) {
|
||||
if MODELS.is_empty() {
|
||||
ui.label("No model credits were built into this copy.");
|
||||
ui.label(crate::i18n::t("No model credits were built into this copy."));
|
||||
return;
|
||||
}
|
||||
let assisted: u64 = MODELS.iter().map(|m| m.commits).max().unwrap_or(0).max(1);
|
||||
egui::ScrollArea::both().id_salt("credits_models").max_height(LIST_HEIGHT).auto_shrink([false, true]).show(ui, |ui| {
|
||||
egui::Grid::new("credits_models").striped(true).num_columns(6).show(ui, |ui| {
|
||||
for h in ["Company", "Model", "Version", "Commits", "% of all commits", "Lines +/−"] {
|
||||
ui.label(RichText::new(h).strong());
|
||||
ui.label(RichText::new(crate::i18n::t(h)).strong());
|
||||
}
|
||||
ui.end_row();
|
||||
for m in MODELS {
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -69,8 +69,13 @@ fn plural_pt(n: u64) -> usize {
|
|||
usize::from(n > 1)
|
||||
}
|
||||
|
||||
/// French (CLDR `fr`): 0 and 1 take the singular, everything else the plural.
|
||||
fn plural_fr(n: u64) -> usize {
|
||||
usize::from(n > 1)
|
||||
}
|
||||
|
||||
/// The registry. English first: it is the fallback and the source language.
|
||||
pub static LANGUAGES: [LangInfo; 7] = [
|
||||
pub static LANGUAGES: [LangInfo; 8] = [
|
||||
LangInfo { code: "en", name: "English", source: "", plural: plural_one_other, catalog: OnceLock::new() },
|
||||
LangInfo { code: "ja", name: "日本語", source: include_str!("ja.tsv"), plural: plural_none, catalog: OnceLock::new() },
|
||||
// Simplified Chinese; `zh`, `zh-CN`, `zh-SG` and `zh-Hans-*` locales resolve here (see `candidates`).
|
||||
|
|
@ -83,6 +88,8 @@ pub static LANGUAGES: [LangInfo; 7] = [
|
|||
LangInfo { code: "pt-br", name: "Português (Brasil)", source: include_str!("pt-br.tsv"), plural: plural_pt, catalog: OnceLock::new() },
|
||||
// Spanish (European vocabulary); every `es-*` locale (`es-ES`, `es-MX`, `es-419` ...) resolves here.
|
||||
LangInfo { code: "es", name: "Español", source: include_str!("es.tsv"), plural: plural_one_other, catalog: OnceLock::new() },
|
||||
// French; every `fr-*` locale (`fr-FR`, `fr-CA`, `fr-BE` ...) resolves here.
|
||||
LangInfo { code: "fr", name: "Français", source: include_str!("fr.tsv"), plural: plural_fr, catalog: OnceLock::new() },
|
||||
];
|
||||
|
||||
impl LangInfo {
|
||||
|
|
@ -376,7 +383,9 @@ mod tests {
|
|||
assert_eq!(lang_from_tag("en_US.UTF-8"), Some(Lang::EN));
|
||||
assert_eq!(lang_from_tag("C"), Some(Lang::EN));
|
||||
assert_eq!(lang_from_tag("POSIX"), Some(Lang::EN));
|
||||
assert_eq!(lang_from_tag("fr_FR"), None);
|
||||
assert_eq!(lang_from_tag("fr_FR"), Lang::from_code("fr"));
|
||||
assert_eq!(lang_from_tag("fr_CA.UTF-8"), Lang::from_code("fr"));
|
||||
assert_eq!(lang_from_tag("fr-BE"), Lang::from_code("fr"));
|
||||
assert_eq!(lang_from_tag("zh-TW"), Lang::from_code("zh-hant"));
|
||||
assert_eq!(lang_from_tag("zh_CN.UTF-8"), Lang::from_code("zh-hans"));
|
||||
assert_eq!(lang_from_tag("zh"), Lang::from_code("zh-hans"));
|
||||
|
|
@ -399,7 +408,7 @@ mod tests {
|
|||
#[test]
|
||||
fn macos_language_list_is_parsed() {
|
||||
assert_eq!(first_supported("(\n \"ja-JP\",\n \"en-US\"\n)\n"), Some(JA()));
|
||||
assert_eq!(first_supported("(\n \"fr-FR\",\n \"en-US\"\n)\n"), Some(Lang::EN));
|
||||
assert_eq!(first_supported("(\n \"fr-FR\",\n \"en-US\"\n)\n"), Lang::from_code("fr"));
|
||||
assert_eq!(first_supported("("), None);
|
||||
}
|
||||
|
||||
|
|
@ -412,7 +421,7 @@ mod tests {
|
|||
assert_eq!(first_supported(&format!("{tag}\r\n")), Lang::from_code("zh-hant"));
|
||||
}
|
||||
assert_eq!(first_supported("en-US\r\n"), Some(Lang::EN));
|
||||
assert_eq!(first_supported("fr-FR\r\n"), None);
|
||||
assert_eq!(first_supported("fr-FR\r\n"), Lang::from_code("fr"));
|
||||
assert_eq!(first_supported("\r\n"), None);
|
||||
}
|
||||
|
||||
|
|
@ -842,6 +851,111 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn french_is_registered() {
|
||||
let fr = Lang::from_code("fr").expect("fr registered");
|
||||
assert_eq!(fr.name(), "Français");
|
||||
assert_eq!(normalize_pref("FR"), Some("fr"));
|
||||
assert_eq!(lang_from_tag("fr_FR.UTF-8"), Some(fr));
|
||||
assert_eq!(lang_from_tag("fr-CA"), Some(fr));
|
||||
assert_eq!(first_supported("fr-FR\r\nen-US"), Some(fr));
|
||||
assert_eq!(tr(fr, "File"), "Fichier");
|
||||
assert_eq!(tr(fr, "Save as…"), "Enregistrer sous…");
|
||||
assert_eq!(tr(fr, "Bookmarks"), "Signets");
|
||||
assert_eq!(tr(fr, "Layers"), "Calques");
|
||||
assert_eq!(tr(fr, "Rapport de l'utilisateur.pdf"), "Rapport de l'utilisateur.pdf");
|
||||
assert_eq!((0..=3).map(|n| (fr.0.plural)(n)).collect::<Vec<_>>(), [0, 0, 1, 1]);
|
||||
assert_eq!(trn(fr, 0, "{n} page", "{n} pages"), "0 page");
|
||||
assert_eq!(trn(fr, 1, "{n} page", "{n} pages"), "1 page");
|
||||
assert_eq!(trn(fr, 2, "{n} page", "{n} pages"), "2 pages");
|
||||
assert_eq!(trn(fr, 1, "{n} field", "{n} fields"), "1 champ");
|
||||
assert_eq!(trn(fr, 2, "{n} field", "{n} fields"), "2 champs");
|
||||
let mut app = crate::PdfCraftApp::default();
|
||||
app.set_option("language", "fr").unwrap();
|
||||
assert_eq!(app.language, "fr");
|
||||
let mut restored = crate::PdfCraftApp::default();
|
||||
restored.restore(&app.persist());
|
||||
assert_eq!(restored.language, "fr");
|
||||
}
|
||||
|
||||
/// French translates every registered command and every All tools group, section and item.
|
||||
#[test]
|
||||
fn french_covers_commands_and_catalogue() {
|
||||
let fr = Lang::from_code("fr").expect("fr registered");
|
||||
for command in pdfcraft_engine::commands::COMMANDS {
|
||||
assert!(has(fr, command.label), "missing command: {}", command.label);
|
||||
if let Some(menu) = command.menu {
|
||||
assert!(has(fr, menu), "missing menu: {menu}");
|
||||
}
|
||||
}
|
||||
for group in pdfcraft_engine::catalog::TOOL_GROUPS {
|
||||
assert!(has(fr, group.label), "missing group: {}", group.label);
|
||||
for section in group.sections {
|
||||
assert!(has(fr, section.title), "missing section: {}", section.title);
|
||||
for item in section.items {
|
||||
assert!(has(fr, item.label), "missing item: {}", item.label);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// New tl!("literal") labels must not silently fall back to English.
|
||||
#[test]
|
||||
fn french_covers_ui_literals() {
|
||||
let fr = Lang::from_code("fr").expect("fr registered");
|
||||
let mut stack = vec![std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src")];
|
||||
let mut literals = std::collections::BTreeSet::new();
|
||||
while let Some(dir) = stack.pop() {
|
||||
for entry in std::fs::read_dir(dir).expect("UI source directory") {
|
||||
let path = entry.expect("UI source entry").path();
|
||||
if path.is_dir() {
|
||||
if path.file_name().is_some_and(|name| name != "i18n") {
|
||||
stack.push(path);
|
||||
}
|
||||
} else if path.extension().is_some_and(|ext| ext == "rs") {
|
||||
let source = std::fs::read_to_string(path).expect("UI source file").replace("\r\n", "\n");
|
||||
let mut rest = source.split("#[cfg(test)]\nmod ").next().unwrap_or_default();
|
||||
while let Some((_, after)) = rest.split_once("tl!(\"") {
|
||||
let mut escaped = false;
|
||||
let end = after
|
||||
.char_indices()
|
||||
.find_map(|(i, c)| {
|
||||
if c == '"' && !escaped {
|
||||
return Some(i);
|
||||
}
|
||||
escaped = c == '\\' && !escaped;
|
||||
None
|
||||
})
|
||||
.expect("closed tl! literal");
|
||||
let (raw, tail) = after.split_at(end);
|
||||
if tail.starts_with("\")") {
|
||||
let label: String = serde_json::from_str(&format!("\"{raw}\"")).expect("UI literal escapes");
|
||||
literals.insert(label);
|
||||
}
|
||||
rest = tail.strip_prefix('"').expect("closing quote");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(literals.len() > 900, "source scan found only {} literals", literals.len());
|
||||
let missing: Vec<_> = literals.iter().filter(|label| !has(fr, label)).collect();
|
||||
assert!(missing.is_empty(), "untranslated French UI literals: {missing:#?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn french_history_and_diagnostics_preserve_user_values() {
|
||||
let fr = Lang::from_code("fr").expect("fr registered");
|
||||
set_current(fr);
|
||||
assert_eq!(command_label("Undo Insert pages from Rapport {n}.pdf"), "Annuler Insérer des pages depuis Rapport {n}.pdf");
|
||||
assert_eq!(command_label("Redo Fill in Contact {key}"), "Rétablir Remplir Contact {key}");
|
||||
assert_eq!(action_label("Change Title"), "Modifier Titre");
|
||||
assert_eq!(action_label("Custom action {n}"), "Custom action {n}");
|
||||
assert_eq!(fmt(t("This page couldn't be displayed.\n{e}"), &[("e", "OS error {n}")]), "Impossible d'afficher cette page.\nOS error {n}");
|
||||
assert_eq!(fmt(t("{n} pages selected"), &[("n", "3")]), "3 pages sélectionnées");
|
||||
assert_eq!(tr(fr, "CheckBox"), "Case à cocher");
|
||||
set_current(Lang::EN);
|
||||
}
|
||||
|
||||
/// Every bundled catalog is well-formed and consistent with its sources.
|
||||
#[test]
|
||||
fn bundled_catalogs_are_consistent() {
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@ fn language_switch_preserves_document_and_command_ids() {
|
|||
let documents = ok(&mut h, &c, "ui.state", json!({}))["documents"].clone();
|
||||
assert_eq!(documents[0]["dirty"], true);
|
||||
let commands = ok(&mut h, &c, "ui.commands", json!({}));
|
||||
for code in ["ja", "zh-hans", "en"] {
|
||||
for code in ["ja", "zh-hans", "fr", "en"] {
|
||||
ok(&mut h, &c, "ui.set", json!({ "key": "language", "value": code }));
|
||||
h.run_steps(2);
|
||||
let state = ok(&mut h, &c, "ui.state", json!({}));
|
||||
|
|
|
|||
|
|
@ -133,16 +133,19 @@ fn ui_fonts_work_without_craft_fonts() {
|
|||
theme::install_fonts(&ctx);
|
||||
}
|
||||
|
||||
/// Latin-script catalogs (Czech, Brazilian Portuguese, Spanish) are drawn entirely by the app's own faces
|
||||
/// Latin-script catalogs (Czech, Brazilian Portuguese, Spanish, French) are drawn entirely by the app's own faces
|
||||
/// (Inter, JetBrains Mono): no letter falls through to egui's defaults or a CJK fallback. (egui's
|
||||
/// `has_glyphs` can't answer this: with only the primary face it is also the replacement face.)
|
||||
#[test]
|
||||
fn primary_ui_fonts_cover_latin_catalogs() {
|
||||
use skrifa::MetadataProvider as _;
|
||||
let defs = theme::font_definitions();
|
||||
for (code, catalog) in
|
||||
[("cs", include_str!("../src/i18n/cs.tsv")), ("pt-br", include_str!("../src/i18n/pt-br.tsv")), ("es", include_str!("../src/i18n/es.tsv"))]
|
||||
{
|
||||
for (code, catalog) in [
|
||||
("cs", include_str!("../src/i18n/cs.tsv")),
|
||||
("pt-br", include_str!("../src/i18n/pt-br.tsv")),
|
||||
("es", include_str!("../src/i18n/es.tsv")),
|
||||
("fr", include_str!("../src/i18n/fr.tsv")),
|
||||
] {
|
||||
let mut text = String::from("áčďéěíňóřšťúůýžÁČĎÉĚÍŇÓŘŠŤÚŮÝŽãõçâêôàÃÕÇÂÊÔÀñÑüÜ¿¡«»…");
|
||||
for line in catalog.lines().filter(|l| !l.starts_with('#')) {
|
||||
if let Some(translation) = line.split('\t').nth(2) {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
# Interface language
|
||||
|
||||
Choose **Menu > Edit > Preferences…** (Command-comma on macOS, Ctrl-comma elsewhere) **> Interface language** and select **Auto**, **English**, **日本語**, **简体中文**, **繁體中文**, **Čeština**, **Português (Brasil)** or **Español**. The change applies immediately and persists between launches. Command ids, document contents and file names are unchanged.
|
||||
Choose **Menu > Edit > Preferences…** (Command-comma on macOS, Ctrl-comma elsewhere) **> Interface language** and select **Auto**, **English**, **日本語**, **简体中文**, **繁體中文**, **Čeština**, **Português (Brasil)**, **Español** or **Français**. The change applies immediately and persists between launches. Command ids, document contents and file names are unchanged.
|
||||
|
||||
**Auto** (the default) follows the system language: `LC_ALL`, `LC_MESSAGES` or `LANG`, then the preferred-languages list on macOS. Any Portuguese locale (`pt_BR`, `pt_PT`) uses the Brazilian catalog, and Chinese locales for Taiwan, Hong Kong and Macau (`zh_TW`, `zh_HK`, `zh_MO`, `zh-Hant`) the Traditional Chinese one; other Chinese locales (`zh`, `zh_CN`, `zh_SG`, `zh-Hans`) the Simplified Chinese one. A system language without a catalog, such as French, shows English. On Windows, **Auto** uses the Windows display languages, in preference order, when no supported locale is selected by those environment variables. The query asks Windows directly (`GetUserPreferredUILanguages`, through the `sys-locale` crate) without starting a process, and is cached for the process. If detection fails or the display language has no catalog, PdfCraft shows English.
|
||||
**Auto** (the default) follows the system language: `LC_ALL`, `LC_MESSAGES` or `LANG`, then the preferred-languages list on macOS. Any Portuguese locale (`pt_BR`, `pt_PT`) uses the Brazilian catalog, any French locale (`fr`, `fr_FR`, `fr_CA`, `fr_BE`) the French one, and Chinese locales for Taiwan, Hong Kong and Macau (`zh_TW`, `zh_HK`, `zh_MO`, `zh-Hant`) the Traditional Chinese one; other Chinese locales (`zh`, `zh_CN`, `zh_SG`, `zh-Hans`) the Simplified Chinese one. A system language without a catalog shows English. On Windows, **Auto** uses the Windows display languages, in preference order, when no supported locale is selected by those environment variables. The query asks Windows directly (`GetUserPreferredUILanguages`, through the `sys-locale` crate) without starting a process, and is cached for the process. If detection fails or the display language has no catalog, PdfCraft shows English.
|
||||
|
||||
The control channel exposes the setting through `ui.set`:
|
||||
|
||||
|
|
@ -10,15 +10,15 @@ The control channel exposes the setting through `ui.set`:
|
|||
{"method":"ui.set","params":{"key":"language","value":"ja"}}
|
||||
```
|
||||
|
||||
The value is `auto` or a language code (`en`, `ja`, `zh-hans`, `zh-hant`, `cs`, `pt-br`, `es`), in any case. `ui.state` reports `language` as written in the preferences (`auto`, `en`, `ja`, `zh-hans`, `zh-hant`, `cs`, `pt-br` or `es`). Unknown values return an error without changing the current setting. Preferences saved before this setting existed follow the system language.
|
||||
The value is `auto` or a language code (`en`, `ja`, `zh-hans`, `zh-hant`, `cs`, `pt-br`, `es`, `fr`), in any case. `ui.state` reports `language` as written in the preferences (`auto`, `en`, `ja`, `zh-hans`, `zh-hant`, `cs`, `pt-br`, `es` or `fr`). Unknown values return an error without changing the current setting. Preferences saved before this setting existed follow the system language.
|
||||
|
||||
Dialogs, panels, menus, notices and history labels go through the catalog (`tl!`). Japanese, Traditional Chinese and Simplified Chinese translate them (about 1,800 entries each); Czech and Brazilian Portuguese cover the menus so far. Untranslated labels use English. Error details that come from the engine or the operating system are shown as they are, inside a translated frame ("操作「…」に失敗しました: …"). The command palette matches the translated label, the English label and the command id. Vertical Japanese PDF rendering is an existing viewer feature; this change does not add vertical text editing.
|
||||
Dialogs, panels, menus, notices and history labels go through the catalog (`tl!`). Japanese, Traditional Chinese, Simplified Chinese, Spanish and French translate them (about 1,800 entries each); Czech and Brazilian Portuguese cover the menus so far. Untranslated labels use English. Error details that come from the engine or the operating system are shown as they are, inside a translated frame ("操作「…」に失敗しました: …"). The command palette matches the translated label, the English label and the command id. Vertical Japanese PDF rendering is an existing viewer feature; this change does not add vertical text editing.
|
||||
|
||||
Japanese interface text uses BIZ UDPGothic from [craft-fonts](https://github.com/storytold/craft-fonts), an optional build input that every release includes (`CRAFT_FONTS_DIR`; see the README). A build made without it has no Japanese face, so Japanese labels show replacement boxes. Chinese is drawn with the same craft-fonts CJK faces (in Simplified Chinese the `Hans` faces come first, so a line never mixes faces with different baselines), so a character none of them has shows a replacement box (and every Chinese character does in a build without craft-fonts). Czech and Brazilian Portuguese need only the bundled Latin faces (tested in `tests/fonts.rs`).
|
||||
Japanese interface text uses BIZ UDPGothic from [craft-fonts](https://github.com/storytold/craft-fonts), an optional build input that every release includes (`CRAFT_FONTS_DIR`; see the README). A build made without it has no Japanese face, so Japanese labels show replacement boxes. Chinese is drawn with the same craft-fonts CJK faces (in Simplified Chinese the `Hans` faces come first, so a line never mixes faces with different baselines), so a character none of them has shows a replacement box (and every Chinese character does in a build without craft-fonts). Czech, Brazilian Portuguese, Spanish and French need only the bundled Latin faces (tested in `tests/fonts.rs`).
|
||||
|
||||
There is no right-to-left interface language yet. File names and document titles in Arabic or Hebrew are drawn with a craft-fonts `Arab` face when the build input has one; on desktop, a character no embedded face has is drawn with one font already installed on the machine (`PDFCRAFT_SYSTEM_FONTS=0` turns this off). The tab strip, the recent-files list and Properties show such names in display order; other places (bookmarks, comments, search results) have the glyphs but may order mixed-direction text wrongly.
|
||||
|
||||
Simplified Chinese coverage tests, following PhotoCraft's complete-catalog approach, check every registered command, All tools label and `tl!("literal")` in the UI source. Generated history labels and diagnostics are also tested with user-supplied names and error details. Catalog coverage does not guarantee glyph coverage: the release-pinned craft-fonts input currently has no `Hans` face, and the web build embeds only BIZ UDPGothic Regular. Chinese font delivery remains separate work, as discussed in [#112](https://github.com/storytold/pdfcraft/pull/112) and [#140](https://github.com/storytold/pdfcraft/pull/140).
|
||||
Simplified Chinese and French coverage tests, following PhotoCraft's complete-catalog approach, check every registered command, All tools label and `tl!("literal")` in the UI source. Generated history labels and diagnostics are also tested with user-supplied names and error details. Catalog coverage does not guarantee glyph coverage: the release-pinned craft-fonts input currently has no `Hans` face, and the web build embeds only BIZ UDPGothic Regular. Chinese font delivery remains separate work, as discussed in [#112](https://github.com/storytold/pdfcraft/pull/112) and [#140](https://github.com/storytold/pdfcraft/pull/140).
|
||||
|
||||
## How translations work
|
||||
|
||||
|
|
|
|||
|
|
@ -9806,8 +9806,8 @@ milestone = "M14"
|
|||
status = "partial"
|
||||
commands = ["app.preferences"]
|
||||
tools = []
|
||||
evidence = ["crates/ui-egui/src/i18n/mod.rs::windows_display_language_resolves_chinese_scripts", "crates/ui-egui/src/i18n/mod.rs::windows_display_language_query_returns_a_locale_tag", "crates/ui-egui/src/i18n/mod.rs::language_persists_and_invalid_input_keeps_current_language", "crates/ui-egui/src/i18n/mod.rs::bundled_catalogs_are_consistent", "crates/ui-egui/src/i18n/mod.rs::japanese_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::simplified_chinese_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::simplified_chinese_covers_ui_literals", "crates/ui-egui/src/i18n/mod.rs::simplified_chinese_history_and_diagnostics_preserve_user_values", "crates/ui-egui/tests/control.rs::simplified_chinese_signature_prompts_and_errors_keep_document_state", "crates/ui-egui/tests/control.rs::language_switch_preserves_document_and_command_ids", "crates/ui-egui/tests/control.rs::japanese_controls_and_search_keep_command_ids", "crates/ui-egui/tests/control.rs::japanese_dialogs_errors_and_custom_action_names"]
|
||||
notes = "Preferences switches Auto (system language)/English/Japanese/Simplified Chinese/Traditional Chinese/Czech/Brazilian Portuguese and persists; ui.set language auto|en|ja|zh-hans|zh-hant|cs|pt-br and ui.state expose it. TSV catalogs per language (same system as PhotoCraft); every dialog, panel and notice goes through tl!. Japanese, Traditional Chinese and Simplified Chinese cover commands, dialogs, panels, shortcuts and history labels; Czech and Brazilian Portuguese the menus. Engine/OS error details stay untranslated. Auto detects the Windows display language after locale environment overrides, with Chinese script/region mapping and English fallback. Missing: the other catalogs filled in, Chinese font delivery, a full localization audit."
|
||||
evidence = ["crates/ui-egui/src/i18n/mod.rs::windows_display_language_resolves_chinese_scripts", "crates/ui-egui/src/i18n/mod.rs::windows_display_language_query_returns_a_locale_tag", "crates/ui-egui/src/i18n/mod.rs::language_persists_and_invalid_input_keeps_current_language", "crates/ui-egui/src/i18n/mod.rs::bundled_catalogs_are_consistent", "crates/ui-egui/src/i18n/mod.rs::japanese_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::simplified_chinese_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::simplified_chinese_covers_ui_literals", "crates/ui-egui/src/i18n/mod.rs::simplified_chinese_history_and_diagnostics_preserve_user_values", "crates/ui-egui/tests/control.rs::simplified_chinese_signature_prompts_and_errors_keep_document_state", "crates/ui-egui/tests/control.rs::language_switch_preserves_document_and_command_ids", "crates/ui-egui/tests/control.rs::japanese_controls_and_search_keep_command_ids", "crates/ui-egui/tests/control.rs::japanese_dialogs_errors_and_custom_action_names", "crates/ui-egui/src/i18n/mod.rs::french_is_registered", "crates/ui-egui/src/i18n/mod.rs::french_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::french_covers_ui_literals", "crates/ui-egui/src/i18n/mod.rs::french_history_and_diagnostics_preserve_user_values"]
|
||||
notes = "Preferences switches Auto (system language)/English/Japanese/Simplified Chinese/Traditional Chinese/Czech/Brazilian Portuguese/Spanish/French and persists; ui.set language auto|en|ja|zh-hans|zh-hant|cs|pt-br|es|fr and ui.state expose it. TSV catalogs per language (same system as PhotoCraft); every dialog, panel and notice goes through tl!. Japanese, Traditional Chinese, Simplified Chinese, Spanish and French cover commands, dialogs, panels, shortcuts and history labels; Czech and Brazilian Portuguese the menus. French locales (fr, fr_FR, fr_CA, fr_BE) select the French catalog. Engine/OS error details stay untranslated. Auto detects the Windows display language after locale environment overrides, with Chinese script/region mapping and English fallback. Missing: Czech and Brazilian Portuguese dialogs, Chinese font delivery, a full localization audit."
|
||||
|
||||
[[feature]]
|
||||
id = "misc.prefs-reading"
|
||||
|
|
|
|||
Loading…
Reference in New Issue