mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-24 10:08:00 -06:00
M14: add Lithuanian interface and date localization (#1401)
* M14: add Lithuanian interface localization Register Lithuanian UI and date languages with three integer plural forms and a 2,572-entry contributor-original catalog. Cover dynamic dialog labels, contextual calendar and certificate wording, commands, tools, and every key in the other bundled catalogs. Add real-shell calendar, inspector and password-dialog regressions; preserve PDF values and document state. Update localization documentation and asset attribution. Native-speaker/VLKK terminology review remains outstanding. Validation: cargo xtask ci passed; cargo-deny was skipped because it is not installed. Five rendered dialogs and contributor manual testing checked UI. Co-Authored-By: OpenAI gpt-6.1-sol <noreply@openai.com> * M14: preserve multilingual localization documentation Restore the existing language descriptions instead of replacing general localization guidance with Lithuanian-specific coverage. List every registered language and control-channel code; keep Lithuanian details alongside the existing catalog descriptions. Include the previously omitted Kazakh, Polish, Slovenian, Greek and Turkish catalogs in the shipped-language table without changing the historical coverage snapshot or existing assessments. Keep Lithuanian roadmap notes concise and additive. Validation: all 22 registered languages appear in documentation; every existing descriptive paragraph is retained. cargo xtask ci passed, with cargo-deny skipped because it is not installed. Co-Authored-By: OpenAI gpt-6.1-sol <noreply@openai.com> * M14: record completed Lithuanian native-speaker review The contributor confirms they are a native Lithuanian speaker and have reviewed the translation. Remove obsolete pending-review labels from the asset manifest and documentation; regenerate ATTRIBUTION.md. Remove the redundant generated-message disclaimer from the Lithuanian description, keeping the existing general localization documentation unchanged. Validation: cargo xtask ci passed; cargo-deny was skipped because it is not installed. Catalog content and its SHA-256 are unchanged. Co-Authored-By: OpenAI gpt-6.1-sol <noreply@openai.com> * Lithuanian: cover main's Copy Image labels, list lt last, check its glyphs Adds the six Copy Image rows from #1363, moves lt to the end of LANGUAGES and DATE_LANGUAGES (after tr), adds lt.tsv to the primary-font coverage test, writes the JavaScript preferences path with '>' as the other catalogs do (Inter has no ▸), and refreshes counts and hashes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: OpenAI gpt-6.1-sol <noreply@openai.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
OpenAI gpt-6.1-sol
Brandon Thomas
parent
a6d48707f7
commit
4bfd3c2729
+2
-1
@@ -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 (268)
|
||||
## In this repository (269)
|
||||
|
||||
| Asset | Title | Author | Licence | Source | Used for |
|
||||
|---|---|---|---|---|---|
|
||||
@@ -27,6 +27,7 @@ Every asset PdfCraft includes, bundles or uses to build its published material,
|
||||
| `crates/ui-egui/src/i18n/uk.tsv` | Ukrainian 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 | Ukrainian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs |
|
||||
| `crates/ui-egui/src/i18n/ar.tsv` | Arabic 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 | Arabic UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs |
|
||||
| `crates/ui-egui/src/i18n/tr.tsv` | Turkish 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 | Turkish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs |
|
||||
| `crates/ui-egui/src/i18n/lt.tsv` | Lithuanian interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels and user-facing messages; reviewed by a native Lithuanian-speaking contributor | Lithuanian 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 |
|
||||
| `assets/icons/align-center.svg` | Lucide icon "align-center" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/align-center.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
|
||||
|
||||
@@ -219,6 +219,17 @@ kind = "translation"
|
||||
usage = "Turkish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "4e70c6db374db37bf70a17e251675b94128fb11b4d6d20ef669b36fd4ae14ff1"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/lt.tsv"
|
||||
title = "Lithuanian interface translation catalog"
|
||||
author = "PdfCraft contributors"
|
||||
source = "Contributor-original: clean-room translations of PdfCraft's English UI labels and user-facing messages; reviewed by a native Lithuanian-speaking contributor"
|
||||
licence = "MIT OR Apache-2.0"
|
||||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Lithuanian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "d2e6be7cf74525310a203bed7434e7f69d2288d337ee696de2882df8da31a746"
|
||||
|
||||
[[asset]]
|
||||
path = "assets/icons/accessibility.svg"
|
||||
title = "Lucide icon \"accessibility\""
|
||||
|
||||
@@ -147,6 +147,7 @@ Ranked; detail and the full milestone table in [docs/roadmap.md](docs/roadmap.md
|
||||
- **2026-10-12 (M5, Fill & Sign text size):** The active Fill & Sign toolbar now chooses 4–300 pt for new text and dates; the in-place editor and saved annotation use that size, which persists in local settings. `fill_sign_add` accepts the same `font_size`. Feature and readiness estimates unchanged.
|
||||
- **2026-10-12 (community, M3, Greek search):** Case-insensitive Find treats normal, final and capital sigma (`σ`, `ς`, `Σ`) as the same letter, so `κόσμος` and `ΚΌΣΜΟΣ` find each other. Case-sensitive and whole-word options, glyph ranges and match limits retain their behavior. Synthetic text-search regressions cover both directions. The tab-menu regression selects the platform's Reveal label (Finder on macOS), restoring the macOS test without changing the menu. Estimates unchanged.
|
||||
- **2026-10-12 (M2, soft scans):** A shrunk, interpolated, axis-aligned image (a 300 dpi scan on a screen) is snapped to whole device pixels and shrunk once, not resampled a second time at a fractional scale and offset. On a synthetic scan page the error against an area-average reference fell from 3.4 to 1.6 grey levels at 96 dpi, and edge sharpness is within 2% of the reference (was 10% soft). Rotated, skewed, flipped and non-interpolated images are unchanged; downscaling of those is still open.
|
||||
- **2026-10-12 (M14, Lithuanian localization):** Added Lithuanian as another interface and date language alongside the existing languages, with a 2,578-entry catalog and three-form plurals. Calendar, inspector and password-dialog regressions check localized labels and unchanged document values. The contributor has reviewed the translation as a native Lithuanian speaker. Estimates unchanged.
|
||||
- **2026-10-12 (M12, XFA scripts turned off):** When a runaway script turns an XFA form's scripts off, the app now says so: a toast as it happens (at open or after a click), and the notice bar from then on, ahead of its other notices, with a JavaScript Console button. Before, the error was only in the console, so calculations and buttons just stopped. A notice bar the user had closed comes back for it; closed again, it stays closed. The engine reports the state (`Document::xfa_scripts_off`, and `xfa_scripts_off` in `doc_info`) instead of the UI reading error text. Two kittest UI tests (a runaway at open; a runaway click after the bar was closed), engine assertions and a `doc_info` tool test. Overall unchanged.
|
||||
- **2026-10-12 (M14, Windows low-DPI font-hinting candidate):** The Windows policy disables symmetric rendering for embedded JetBrains Mono only. On the locked-font macOS probe, horizontal alpha-edge energy per ink improves 2.78%, 3.87% and 1.59% at 100%, 125% and 150% scaling, and is down 0.79% at 200%; Inter is pixel-identical. Logical layout hashes and 8 MiB atlas size match, repeated fractional offsets add no atlas dimensions, and baseline/candidate time and RSS ranges overlap. Windows runtime and hardware remain unverified; estimates unchanged.
|
||||
- **2026-10-12 (M7, #1200 candidate):** Bounded local GoToR file specifications open in desktop tabs; explicit numeric page views apply, while named destinations open at the default view with a warning. The automation tool resolves relative to the source and uses existing `--root` confinement. Appearance-only Link Properties edits preserve the existing action. URL/network paths remain refused; `/NewWindow` is a tab hint. Estimates unchanged.
|
||||
|
||||
@@ -56,7 +56,7 @@ const AR_DAYS: [&str; 7] = ["الأحد", "الاثنين", "الثلاثاء",
|
||||
|
||||
/// The languages month and weekday names can be written in (Preferences ▸ Date format ▸ Language):
|
||||
/// the interface languages, in the same order and with the same names.
|
||||
pub static DATE_LANGUAGES: [DateLanguage; 21] = [
|
||||
pub static DATE_LANGUAGES: [DateLanguage; 22] = [
|
||||
DateLanguage {
|
||||
code: "en",
|
||||
name: "English",
|
||||
@@ -338,6 +338,28 @@ pub static DATE_LANGUAGES: [DateLanguage; 21] = [
|
||||
days: ["Pazar", "Pazartesi", "Salı", "Çarşamba", "Perşembe", "Cuma", "Cumartesi"],
|
||||
days_short: ["Paz", "Pzt", "Sal", "Çar", "Per", "Cum", "Cmt"],
|
||||
},
|
||||
DateLanguage {
|
||||
code: "lt",
|
||||
name: "Lietuvių",
|
||||
months: ["sausis", "vasaris", "kovas", "balandis", "gegužė", "birželis", "liepa", "rugpjūtis", "rugsėjis", "spalis", "lapkritis", "gruodis"],
|
||||
months_with_day: Some([
|
||||
"sausio",
|
||||
"vasario",
|
||||
"kovo",
|
||||
"balandžio",
|
||||
"gegužės",
|
||||
"birželio",
|
||||
"liepos",
|
||||
"rugpjūčio",
|
||||
"rugsėjo",
|
||||
"spalio",
|
||||
"lapkričio",
|
||||
"gruodžio",
|
||||
]),
|
||||
months_short: ["saus.", "vas.", "kov.", "bal.", "geg.", "birž.", "liep.", "rugpj.", "rugs.", "spal.", "lapkr.", "gruod."],
|
||||
days: ["sekmadienis", "pirmadienis", "antradienis", "trečiadienis", "ketvirtadienis", "penktadienis", "šeštadienis"],
|
||||
days_short: ["sk", "pr", "an", "tr", "kt", "pn", "št"],
|
||||
},
|
||||
];
|
||||
|
||||
/// The date language for `code` (any case), or `None` when there's none.
|
||||
@@ -651,6 +673,9 @@ mod tests {
|
||||
assert_eq!(s.today_text(Some("dddd d mmmm"), Some("de")).as_deref(), Ok("Dienstag 14 November"));
|
||||
assert!(s.set_date_language(Some("xx")).unwrap_err().contains("zh-hant"), "only the interface languages");
|
||||
assert_eq!(s.date_language(), Some("pt-br"), "an unknown language keeps the previous one");
|
||||
s.set_date_language(Some("LT")).unwrap();
|
||||
assert_eq!(s.date_language(), Some("lt"));
|
||||
assert_eq!(s.today_text(Some("dddd d mmmm"), None).as_deref(), Ok("antradienis 14 lapkričio"));
|
||||
s.set_date_language(Some("IT")).unwrap();
|
||||
assert_eq!(s.date_language(), Some("it"));
|
||||
assert_eq!(s.today_text_for_pdf(None, None).as_deref(), Ok("martedì 14 novembre"), "Italian names and accents can be written into a PDF");
|
||||
|
||||
@@ -451,6 +451,14 @@ fn panel_limits(panel: &Panel) -> PanelLimits {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn lithuanian_translates_every_dynamic_panel_title() {
|
||||
let lang = crate::i18n::Lang::from_code("lt").expect("Lithuanian is registered");
|
||||
for panel in Panel::ALL {
|
||||
assert_ne!(crate::i18n::tr(lang, panel.label()), panel.label(), "{} remains untranslated", panel.id());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tools_side_moves_to_both_edges_and_survives_closed_restore() {
|
||||
let mut workspace = Workspace::default();
|
||||
|
||||
@@ -218,7 +218,7 @@ fn calendar(ctx: &egui::Context, view: &mut DocView, field: egui::Rect, fmt: &st
|
||||
let len = (days_from_civil(if m == 12 { y + 1 } else { y }, if m == 12 { 1 } else { m + 1 }, 1) - days_from_civil(y, m, 1)) as usize;
|
||||
egui::Grid::new(("date-grid", view.id.0)).spacing([4.0, 2.0]).show(ui, |ui| {
|
||||
for d in ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"] {
|
||||
ui.label(egui::RichText::new(d).small());
|
||||
ui.label(egui::RichText::new(tl_ctx!("calendar weekday", d)).small());
|
||||
}
|
||||
ui.end_row();
|
||||
for cell in 0..(first + len).div_ceil(7) * 7 {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -58,6 +58,17 @@ fn plural_none(_: u64) -> usize {
|
||||
0
|
||||
}
|
||||
|
||||
/// Lithuanian: one (ending in 1 except 11), few (ending in 2–9 except 12–19), other otherwise.
|
||||
fn plural_lithuanian(n: u64) -> usize {
|
||||
match (n % 10, n % 100) {
|
||||
(1, 11) => 2,
|
||||
(1, _) => 0,
|
||||
(2..=9, 12..=19) => 2,
|
||||
(2..=9, _) => 1,
|
||||
_ => 2,
|
||||
}
|
||||
}
|
||||
|
||||
/// Czech: 1 → one, 2–4 → few, everything else (0, 5+) → other.
|
||||
fn plural_cs(n: u64) -> usize {
|
||||
match n {
|
||||
@@ -136,7 +147,7 @@ fn plural_sl(n: u64) -> usize {
|
||||
}
|
||||
|
||||
/// The registry. English first: it is the fallback and the source language.
|
||||
pub static LANGUAGES: [LangInfo; 21] = [
|
||||
pub static LANGUAGES: [LangInfo; 22] = [
|
||||
LangInfo { code: "en", name: "English", source: "", plural: plural_one_other, rtl: false, catalog: OnceLock::new() },
|
||||
LangInfo { code: "ja", name: "日本語", source: include_str!("ja.tsv"), plural: plural_none, rtl: false, catalog: OnceLock::new() },
|
||||
// Simplified Chinese; `zh`, `zh-CN`, `zh-SG` and `zh-Hans-*` locales resolve here (see `candidates`).
|
||||
@@ -194,6 +205,8 @@ pub static LANGUAGES: [LangInfo; 21] = [
|
||||
LangInfo { code: "el", name: "Ελληνικά", source: include_str!("el.tsv"), plural: plural_one_other, rtl: false, catalog: OnceLock::new() },
|
||||
// Turkish; every `tr-*` locale (`tr-TR`, `tr_TR.UTF-8`) resolves here.
|
||||
LangInfo { code: "tr", name: "Türkçe", source: include_str!("tr.tsv"), plural: plural_none, rtl: false, catalog: OnceLock::new() },
|
||||
// Lithuanian; every `lt-*` locale (`lt-LT`, `lt_LT.UTF-8`) resolves here. Three plural forms (one, few, other).
|
||||
LangInfo { code: "lt", name: "Lietuvių", source: include_str!("lt.tsv"), plural: plural_lithuanian, rtl: false, catalog: OnceLock::new() },
|
||||
];
|
||||
|
||||
impl LangInfo {
|
||||
@@ -851,6 +864,11 @@ mod tests {
|
||||
}
|
||||
|
||||
fn ui_literals() -> std::collections::BTreeSet<String> {
|
||||
scan_ui_literals(false)
|
||||
}
|
||||
|
||||
// Lithuanian also checks direct lookup calls and multiline literal arguments.
|
||||
fn scan_ui_literals(expanded: bool) -> std::collections::BTreeSet<String> {
|
||||
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() {
|
||||
@@ -862,8 +880,19 @@ mod tests {
|
||||
}
|
||||
} 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 source = if expanded { source.replace("crate::i18n::t(", "tl!(") } else { source };
|
||||
let mut rest = source.split("#[cfg(test)]\nmod ").next().unwrap_or_default();
|
||||
while let Some((_, after)) = rest.split_once("tl!(\"") {
|
||||
while let Some((_, after)) = rest.split_once(if expanded { "tl!(" } else { "tl!(\"" }) {
|
||||
let after = if expanded {
|
||||
let after = after.trim_start();
|
||||
let Some(after) = after.strip_prefix('"') else {
|
||||
rest = after;
|
||||
continue;
|
||||
};
|
||||
after
|
||||
} else {
|
||||
after
|
||||
};
|
||||
let mut escaped = false;
|
||||
let end = after
|
||||
.char_indices()
|
||||
@@ -876,7 +905,10 @@ mod tests {
|
||||
})
|
||||
.expect("closed tl! literal");
|
||||
let (raw, tail) = after.split_at(end);
|
||||
if tail.starts_with("\")") {
|
||||
let argument_end = tail.strip_prefix('"').expect("closing quote").trim_start();
|
||||
let literal_call =
|
||||
argument_end.starts_with(')') || argument_end.strip_prefix(',').is_some_and(|rest| rest.trim_start().starts_with(')'));
|
||||
if tail.starts_with("\")") || (expanded && literal_call) {
|
||||
let label: String = serde_json::from_str(&format!("\"{raw}\"")).expect("UI literal escapes");
|
||||
literals.insert(label);
|
||||
}
|
||||
@@ -2005,6 +2037,210 @@ mod tests {
|
||||
set_current(Lang::EN);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lithuanian_is_registered_and_persists() {
|
||||
let lt = Lang::from_code("lt").expect("lt registered");
|
||||
assert_eq!(lt.name(), "Lietuvių");
|
||||
assert_eq!(normalize_pref("LT"), Some("lt"));
|
||||
for tag in ["lt", "lt-LT", "lt_LT.UTF-8", "LT-lt"] {
|
||||
assert_eq!(lang_from_tag(tag), Some(lt), "{tag}");
|
||||
}
|
||||
assert_eq!(tr(lt, "File"), "Failas");
|
||||
assert_eq!(tr(lt, "Edit"), "Redaguoti");
|
||||
assert_eq!(tr(lt, "Save"), "Įrašyti");
|
||||
assert_eq!(tr(lt, "Interface language"), "Sąsajos kalba");
|
||||
assert_eq!(trn(lt, 1, "{n} page", "{n} pages"), "1 puslapis");
|
||||
assert_eq!(trn(lt, 2, "{n} page", "{n} pages"), "2 puslapiai");
|
||||
assert_eq!(trn(lt, 10, "{n} page", "{n} pages"), "10 puslapių");
|
||||
assert_eq!(trn(lt, 11, "{n} page", "{n} pages"), "11 puslapių");
|
||||
assert_eq!(trn(lt, 21, "{n} page", "{n} pages"), "21 puslapis");
|
||||
assert_eq!(trn(lt, 22, "{n} page", "{n} pages"), "22 puslapiai");
|
||||
assert_eq!(trn(lt, 12, "{n} object selected", "{n} objects selected"), "Pažymėta 12 objektų");
|
||||
assert_eq!(trn(lt, 22, "{n} object selected", "{n} objects selected"), "Pažymėti 22 objektai");
|
||||
let mut app = crate::PdfCraftApp::default();
|
||||
app.set_option("language", "LT").unwrap();
|
||||
assert_eq!(app.language, "lt");
|
||||
let mut restored = crate::PdfCraftApp::default();
|
||||
restored.restore(&app.persist());
|
||||
assert_eq!(restored.language, "lt");
|
||||
assert!(restored.set_option("language", "xx").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lithuanian_covers_commands_and_catalogue() {
|
||||
let lt = Lang::from_code("lt").expect("lt registered");
|
||||
let mut missing = std::collections::BTreeSet::new();
|
||||
for command in pdfcraft_engine::commands::COMMANDS {
|
||||
if !has(lt, command.label) {
|
||||
missing.insert(command.label);
|
||||
}
|
||||
if let Some(menu) = command.menu
|
||||
&& !has(lt, menu)
|
||||
{
|
||||
missing.insert(menu);
|
||||
}
|
||||
}
|
||||
for group in pdfcraft_engine::catalog::TOOL_GROUPS {
|
||||
if !has(lt, group.label) {
|
||||
missing.insert(group.label);
|
||||
}
|
||||
for section in group.sections {
|
||||
if !has(lt, section.title) {
|
||||
missing.insert(section.title);
|
||||
}
|
||||
for item in section.items {
|
||||
if !has(lt, item.label) {
|
||||
missing.insert(item.label);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(missing.is_empty(), "Lithuanian misses commands/tools: {missing:#?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lithuanian_covers_ui_literals() {
|
||||
let lt = Lang::from_code("lt").expect("lt registered");
|
||||
let literals = scan_ui_literals(true);
|
||||
assert!(literals.contains("Float panel"), "direct lookup literals are included");
|
||||
assert!(literals.contains("@{login}: {prs} PRs, {commits} commits, +{added} / −{deleted} lines (Δ {delta}), +{bin_added} / −{bin_deleted} binary assets, {first} – {last}"), "multiline literal arguments are included");
|
||||
let missing: Vec<_> = literals.iter().filter(|label| !has(lt, label)).collect();
|
||||
assert!(missing.is_empty(), "Untranslated Lithuanian UI labels: {missing:#?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lithuanian_covers_contextual_calendar_certificate_and_preference_labels() {
|
||||
let lt = Lang::from_code("lt").expect("lt registered");
|
||||
assert_eq!(tr_ctx(lt, "signature pad", "Type"), "Tekstu");
|
||||
for label in ["All tools", "Read", "E-Sign", "Tools", "View more", "View less"] {
|
||||
assert!(has(lt, label), "shell label remains untranslated: {label}");
|
||||
}
|
||||
assert_eq!(["Slow", "Normal", "Fast"].map(|label| tr_ctx(lt, "scroll speed", label)), ["Lėtas", "Įprastas", "Greitas"]);
|
||||
let months = [
|
||||
("January", "sausis", "sausio"),
|
||||
("February", "vasaris", "vasario"),
|
||||
("March", "kovas", "kovo"),
|
||||
("April", "balandis", "balandžio"),
|
||||
("May", "gegužė", "gegužės"),
|
||||
("June", "birželis", "birželio"),
|
||||
("July", "liepa", "liepos"),
|
||||
("August", "rugpjūtis", "rugpjūčio"),
|
||||
("September", "rugsėjis", "rugsėjo"),
|
||||
("October", "spalis", "spalio"),
|
||||
("November", "lapkritis", "lapkričio"),
|
||||
("December", "gruodis", "gruodžio"),
|
||||
];
|
||||
for (source, standalone, in_date) in months {
|
||||
assert_eq!(tr(lt, source), standalone);
|
||||
assert_eq!(tr_ctx(lt, "calendar date", source), in_date);
|
||||
}
|
||||
assert_eq!(
|
||||
["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"].map(|day| tr_ctx(lt, "calendar weekday", day)),
|
||||
["Sk", "Pr", "An", "Tr", "Kt", "Pn", "Št"]
|
||||
);
|
||||
assert_eq!(
|
||||
fmt(tr(lt, "{month} {day}, {y}"), &[("month", tr_ctx(lt, "calendar date", "March")), ("day", "9"), ("y", "2026")]),
|
||||
"2026 m. kovo 9 d."
|
||||
);
|
||||
assert_eq!(tr_ctx(lt, "certificate", "Subject"), "Sertifikato subjektas");
|
||||
for field in [
|
||||
"Issued to",
|
||||
"Issued by",
|
||||
"Valid from",
|
||||
"Valid to",
|
||||
"Intended usage",
|
||||
"Serial number",
|
||||
"Issuer",
|
||||
"Validity starts",
|
||||
"Validity ends",
|
||||
"Public key",
|
||||
"Basic constraints",
|
||||
"Key usage",
|
||||
"Self-signed",
|
||||
"SHA-1 digest",
|
||||
"SHA-256 digest",
|
||||
] {
|
||||
assert_ne!(tr_ctx(lt, "certificate", field), field, "certificate field remains untranslated: {field}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lithuanian_dialog_wording_preserves_resolution_and_selection_counts() {
|
||||
let lt = Lang::from_code("lt").expect("lt registered");
|
||||
assert_eq!(tr(lt, "for images above"), "kai raiška viršija");
|
||||
assert_eq!(
|
||||
fmt(tr(lt, "Checking Options ({on} of 32 in all categories)"), &[("on", "31")]),
|
||||
"Tikrinimo parinktys (visose kategorijose: 31 iš 32)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lithuanian_covers_bundled_catalog_keys_including_dynamic_dialog_labels() {
|
||||
// Compare English keys, not other languages' translations. Literal scans alone
|
||||
// miss labels passed through helpers, enum methods and conditional expressions.
|
||||
let lt = Lang::from_code("lt").expect("lt registered");
|
||||
let mut missing = std::collections::BTreeSet::new();
|
||||
for reference in LANGUAGES.iter().filter(|language| language.code != "lt") {
|
||||
let (entries, errors) = catalog::parse_entries(reference.source, reference.plural_forms());
|
||||
assert!(errors.is_empty(), "{}: invalid reference catalog: {errors:?}", reference.code);
|
||||
for entry in entries {
|
||||
let translated = match entry.context.as_str() {
|
||||
"" => lt.catalog().plain(&entry.source),
|
||||
"@id" => lt.catalog().id(&entry.source),
|
||||
"@plural" => entry.source.split_once('|').and_then(|(one, other)| lt.catalog().plural(one, other, 0)),
|
||||
context => lt.catalog().contextual(context, &entry.source),
|
||||
};
|
||||
if translated.is_none() {
|
||||
missing.insert((entry.context, entry.source));
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(missing.is_empty(), "Lithuanian misses bundled catalog keys: {missing:#?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lithuanian_covers_credits_labels() {
|
||||
let lt = Lang::from_code("lt").expect("lt registered");
|
||||
let labels = crate::credits::NameMode::ALL
|
||||
.iter()
|
||||
.map(|mode| mode.label())
|
||||
.chain(crate::credits::SortKey::ALL.iter().flat_map(|key| {
|
||||
let (menu, heading) = key.label();
|
||||
[menu, heading]
|
||||
}))
|
||||
.chain(crate::credits::MODEL_COLUMNS.iter().copied());
|
||||
let missing: Vec<_> = labels.filter(|label| !has(lt, label)).collect();
|
||||
assert!(missing.is_empty(), "Lithuanian misses credits labels: {missing:#?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lithuanian_wording_fixes_keep_context_and_meaning() {
|
||||
let lt = Lang::from_code("lt").expect("lt registered");
|
||||
assert_eq!(tr(lt, "of {name}"), "iš {name}");
|
||||
assert_eq!(tr(lt, "Active"), "Aktyvi", "the adjective describes a theme (tema)");
|
||||
for n in [1, 2, 12] {
|
||||
let renamed = trn(
|
||||
lt,
|
||||
n,
|
||||
"{n} field name collides with earlier files and will be renamed: {list}",
|
||||
"{n} field names collide with earlier files and will be renamed: {list}",
|
||||
);
|
||||
assert!(renamed.contains("ankstesniuose failuose esančių laukų pavadinimais"));
|
||||
assert!(renamed.contains("bus pervadint"));
|
||||
assert!(renamed.contains("{list}"));
|
||||
}
|
||||
assert_eq!(tr(lt, "Two-Up (Facing)"), "Du atgręžti puslapiai");
|
||||
assert_eq!(tr(lt, "Windows store · "), "„Windows“ sertifikatų saugykla · ");
|
||||
assert_eq!(
|
||||
tr(lt, "Adds invisible text over each word; the page image is not changed"),
|
||||
"Virš kiekvieno žodžio prideda nematomą tekstą; puslapio vaizdas nesikeičia"
|
||||
);
|
||||
assert!(
|
||||
tr(lt, "The fonts PdfCraft writes with can't draw “{c}” ({code}), so it shows as ? in every PDF viewer. The text is saved.")
|
||||
.contains("visose PDF žiūryklėse")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slovene_is_registered_and_persists() {
|
||||
let sl = Lang::from_code("sl").expect("sl registered");
|
||||
|
||||
@@ -274,6 +274,41 @@ fn theme_commands_and_options_report_preference_and_effective_colours() {
|
||||
assert_eq!(ok(&mut h, &c, "ui.state", json!({}))["theme_preference"], "System");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lithuanian_password_dialog_translates_dynamic_labels_and_validation() {
|
||||
let (mut h, c) = harness();
|
||||
ok(&mut h, &c, "ui.set", json!({ "key": "language", "value": "lt" }));
|
||||
ok(&mut h, &c, "ui.set", json!({ "key": "dialog", "value": "protect" }));
|
||||
h.run_steps(2);
|
||||
h.get_by_role_and_label(egui::accesskit::Role::RadioButton, "Peržiūrėti");
|
||||
h.get_by_role_and_label(egui::accesskit::Role::RadioButton, "Redaguoti").click();
|
||||
h.run_steps(2);
|
||||
assert!(!h.state().protect_draft.viewing);
|
||||
h.get_by_role_and_label(egui::accesskit::Role::PasswordInput, "Įveskite slaptažodį");
|
||||
h.get_by_role_and_label(egui::accesskit::Role::PasswordInput, "Pakartokite slaptažodį");
|
||||
h.state_mut().protect_draft.password = "abc".into();
|
||||
h.state_mut().protect_draft.confirm = "xyz".into();
|
||||
h.run_steps(2);
|
||||
h.get_by_label("Slaptažodžiai nesutampa.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lithuanian_inspector_title_updates_through_the_control_channel() {
|
||||
let (mut h, c) = harness();
|
||||
ok(&mut h, &c, "ui.set", json!({ "key": "panel", "value": "comments" }));
|
||||
h.run_steps(2);
|
||||
h.get_by_role_and_label(egui::accesskit::Role::Tab, "Inspector");
|
||||
let documents = ok(&mut h, &c, "ui.state", json!({}))["documents"].clone();
|
||||
ok(&mut h, &c, "ui.set", json!({ "key": "language", "value": "lt" }));
|
||||
h.run_steps(2);
|
||||
h.get_by_role_and_label(egui::accesskit::Role::Tab, "Inspektorius");
|
||||
assert!(h.query_by_role_and_label(egui::accesskit::Role::Tab, "Inspector").is_none());
|
||||
assert_eq!(ok(&mut h, &c, "ui.state", json!({}))["documents"], documents);
|
||||
ok(&mut h, &c, "ui.set", json!({ "key": "language", "value": "en" }));
|
||||
h.run_steps(2);
|
||||
h.get_by_role_and_label(egui::accesskit::Role::Tab, "Inspector");
|
||||
}
|
||||
|
||||
/// Switching the interface language changes labels only: documents, their dirty state and the
|
||||
/// command ids agents drive stay exactly the same.
|
||||
#[test]
|
||||
@@ -285,7 +320,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", "fr", "de", "uk", "en"] {
|
||||
for code in ["ja", "zh-hans", "fr", "de", "uk", "lt", "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!({}));
|
||||
@@ -304,7 +339,8 @@ fn language_switch_preserves_document_and_command_ids() {
|
||||
h.run_steps(2);
|
||||
}
|
||||
let error = call(&mut h, &c, "ui.set", json!({ "key": "language", "value": "xx" })).unwrap_err();
|
||||
assert!(error.contains("auto, en, ja"), "{error}");
|
||||
let codes = std::iter::once(i18n::AUTO).chain(i18n::Lang::all().map(i18n::Lang::code)).collect::<Vec<_>>().join(", ");
|
||||
assert_eq!(error, format!("language must be one of {codes}"));
|
||||
assert_eq!(ok(&mut h, &c, "ui.state", json!({}))["language"], "en");
|
||||
}
|
||||
|
||||
|
||||
@@ -263,6 +263,7 @@ fn primary_ui_fonts_cover_latin_and_cyrillic_catalogs() {
|
||||
("it", include_str!("../src/i18n/it.tsv")),
|
||||
("tr", include_str!("../src/i18n/tr.tsv")),
|
||||
("sl", include_str!("../src/i18n/sl.tsv")),
|
||||
("lt", include_str!("../src/i18n/lt.tsv")),
|
||||
] {
|
||||
let mut text = String::from("áčďéěíňóřšťúůýžÁČĎÉĚÍŇÓŘŠŤÚŮÝŽãõçâêôàÃÕÇÂÊÔÀñÑüÜäÄöÖőűß„“¿¡«»…ґҐєЄіІїЇәғқңөұүһӘҒҚҢӨҰҮҺ");
|
||||
for line in catalog.lines().filter(|l| !l.starts_with('#')) {
|
||||
|
||||
@@ -146,6 +146,34 @@ fn date_fields_offer_a_calendar() {
|
||||
assert_eq!(value(&h, "due"), vec![format!("{nm:02}/15/{ny}")], "picked in the field's format");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lithuanian_calendar_localizes_months_and_weekdays_without_changing_field_values() {
|
||||
use egui_kittest::kittest::Queryable;
|
||||
let mut h = harness();
|
||||
h.state_mut().set_option("language", "lt").unwrap();
|
||||
h.state_mut().apply_edit(pdfcraft_engine::Edit::AddField {
|
||||
page: 0,
|
||||
rect: [50.0, 40.0, 200.0, 60.0],
|
||||
kind: pdfcraft_engine::NewField::Date,
|
||||
name: Some("due".into()),
|
||||
});
|
||||
h.run_steps(4);
|
||||
click_field(&mut h, "due", 0);
|
||||
let (y, m, _) = h.state().session.today();
|
||||
let months = ["sausis", "vasaris", "kovas", "balandis", "gegužė", "birželis", "liepa", "rugpjūtis", "rugsėjis", "spalis", "lapkritis", "gruodis"];
|
||||
h.get_by_label(&format!("{y} m. {}", months[(m - 1) as usize]));
|
||||
for day in ["Sk", "Pr", "An", "Tr", "Kt", "Pn", "Št"] {
|
||||
h.get_by_label(day);
|
||||
}
|
||||
h.get_by_label("›").click();
|
||||
h.run_steps(2);
|
||||
let (ny, nm) = if m == 12 { (y + 1, 1) } else { (y, m + 1) };
|
||||
h.get_by_label(&format!("{ny} m. {}", months[(nm - 1) as usize]));
|
||||
h.get_by_label("15").click();
|
||||
h.run_steps(4);
|
||||
assert_eq!(value(&h, "due"), vec![format!("{nm:02}/15/{ny}")], "the PDF field keeps its own date format");
|
||||
}
|
||||
|
||||
/// The value of `name` in a saved PDF, read back by opening it in a fresh app.
|
||||
fn saved_value(path: &std::path::Path, name: &str) -> Vec<String> {
|
||||
let mut app = PdfCraftApp::new();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Localization parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** major (first per-language status, measured from the catalogs) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (list all registered interface languages; preserve the original coverage snapshot) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS)
|
||||
|
||||
Per-language status of PdfCraft's interface against Acrobat Pro's. How translations work and how to
|
||||
add a language: [localization.md](localization.md). Summary in [ROADMAP.md](../ROADMAP.md).
|
||||
@@ -60,13 +60,20 @@ none has translated help.
|
||||
| Hungarian | `hu` | 2,126 (99.2%) | partial | yes |
|
||||
| Ukrainian | `uk` | 2,142 (100%) | partial | yes |
|
||||
| Italian | `it` | 2,130 (99.4%) | partial | yes |
|
||||
| Kazakh | `kk` | 2,327 entries | partial | no |
|
||||
| Polish | `pl` | 2,287 entries | partial | yes |
|
||||
| Slovenian | `sl` | 2,324 entries | partial | no |
|
||||
| Greek | `el` | 2,341 entries | partial | no |
|
||||
| Turkish | `tr` | 2,313 entries | partial | yes |
|
||||
| Lithuanian | `lt` | 2,578 entries | partial; community native-speaker review ([#1401](https://github.com/storytold/pdfcraft/pull/1401)) | no |
|
||||
|
||||
**8 other languages** ship (15 catalogs plus English, 16 interface languages in Preferences).
|
||||
Acrobat languages PdfCraft lacks: Danish, Dutch, Finnish, Norwegian, Polish, Swedish, Turkish,
|
||||
Hebrew.
|
||||
**14 other languages** ship (21 catalogs plus English, 22 interface languages in Preferences).
|
||||
The entry counts without percentages were measured on 2026-10-12; they are not percentages of the older 2,143-key snapshot above. Their presence does not imply a detailed linguistic or runtime review.
|
||||
Acrobat languages PdfCraft lacks: Danish, Dutch, Finnish, Norwegian, Swedish, Hebrew.
|
||||
|
||||
## Revision history
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-12 | minor | Added current counts for Kazakh, Polish, Slovenian, Greek, Turkish and Lithuanian; retained existing language assessments and the original percentage snapshot |
|
||||
| 2026-10-10 | major | Created. Catalog counts measured from `crates/ui-egui/src/i18n/*.tsv` against the 2,143-key union; Acrobat's localizations from its bundle's `.lproj` and Sequences folder names |
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# Interface language
|
||||
|
||||
Choose **Menu > Edit > Preferences…** (Command-comma on macOS, Ctrl-comma elsewhere) **> Interface language** and select **Auto**, **English**, **日本語**, **简体中文**, **繁體中文**, **Русский**, **Български**, **Čeština**, **Português (Brasil)**, **Deutsch**, **Español**, **Français**, **తెలుగు**, **Magyar**, **Українська**, **Italiano**, **العربية** or **Türkçe**. 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)**, **Deutsch**, **Español**, **Français**, **తెలుగు**, **Magyar**, **Українська**, **Italiano**, **العربية** or **Slovenščina**. 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)**, **Deutsch**, **Español**, **Français**, **Русский**, **Қазақша**, **Български**, **తెలుగు**, **Magyar**, **Українська**, **Italiano**, **العربية**, **Polski**, **Slovenščina**, **Ελληνικά**, **Türkçe** or **Lietuvių**. The change applies immediately and persists between launches. Command ids, document contents and file names are unchanged.
|
||||
|
||||
Displayed keyboard shortcuts use the same active interface language. For example, German shows `Strg+K` and `Strg+Umschalt+S` in the toolbar, menus, command palette, tooltips and Keyboard shortcuts dialog; macOS keeps its modifier symbols (`⌘`, `⇧`, `⌃`). Key names have their own `key` catalog context, so the Delete key can read `Entf` while the command remains `Löschen`. A missing key-name entry falls back to the English key name. Actual bindings and machine-facing command ids remain stable.
|
||||
|
||||
@@ -14,7 +13,7 @@ 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`, `ru`, `bg`, `cs`, `pt-br`, `de`, `es`, `fr`, `te`, `hu`, `uk`, `it`, `ar`, `tr`), in any case. `ui.state` reports `language` as written in the preferences (`auto`, `en`, `ja`, `zh-hans`, `zh-hant`, `ru`, `bg`, `cs`, `pt-br`, `de`, `es`, `fr`, `te`, `hu`, `uk`, `it`, `ar` or `tr`). 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`, `de`, `es`, `fr`, `ru`, `kk`, `bg`, `te`, `hu`, `uk`, `it`, `ar`, `pl`, `sl`, `el`, `tr`, `lt`), in any case. `ui.state` reports `language` as written in the preferences (`auto`, `en`, `ja`, `zh-hans`, `zh-hant`, `cs`, `pt-br`, `de`, `es`, `fr`, `ru`, `kk`, `bg`, `te`, `hu`, `uk`, `it`, `ar`, `pl`, `sl`, `el`, `tr` or `lt`). 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, Simplified Chinese, Russian, Bulgarian, German, Spanish, French, Telugu, Hungarian, Ukrainian, Italian, Brazilian Portuguese and Arabic translate them (about 2,000–2,140 entries each); Czech and Turkish 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. Per-language coverage, script support and what each language still needs are in [localization-parity.md](localization-parity.md).
|
||||
Dialogs, panels, menus, notices and history labels go through the catalog (`tl!`). Japanese, Traditional Chinese, Simplified Chinese, Russian, Bulgarian, German, Spanish, French, Telugu, Hungarian, Ukrainian, Italian, Brazilian Portuguese, Arabic and Slovenian translate them (about 2,000–2,190 entries each); Czech covers 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. Per-language coverage, script support and what each language still needs are in [localization-parity.md](localization-parity.md).
|
||||
@@ -23,9 +22,11 @@ Whole sentences that the layers below word — what the accessibility checker fo
|
||||
|
||||
The Ukrainian catalog has 2,047 entries, written from the meaning of the English labels using Ukrainian computing vocabulary. Commands use infinitives; fixed-count messages use count-neutral wording. Plural-aware messages use the three [CLDR Ukrainian integer forms](https://www.unicode.org/cldr/charts/48/supplemental/language_plural_rules.html#uk): `1 сторінка`, `2 сторінки`, `5 сторінок`; 11–14 take the third form, while 21 and 22 return to the first and second. Calendar dates use genitive month names (`9 жовтня 2026`), while headings use standalone names (`Жовтень 2026`). Home recommendation cards grow to fit translated text; the toolbar search hint truncates before the shortcut, with the full label available on hover.
|
||||
|
||||
The Lithuanian catalog has 2,578 entries and three integer plural forms. Calendar headings use standalone month names; dates use genitive forms. Coverage tests check commands/tools, scanned literal and direct lookups, and source/context keys in the other bundled catalogs. Calendar, inspector and password-dialog UI regressions check dynamic labels and preserve document values. A native Lithuanian-speaking contributor has reviewed the translation.
|
||||
|
||||
Edit text reads Hebrew lines in typing order and writes them back in drawing order (`crates/edit/src/bidi.rs`); letters the page's own font lacks are drawn with the craft-fonts `Hebr` face (Noto Sans Hebrew) as Type 3 glyphs, and a build without one says so. Hebrew points (niqqud) can't be written yet. The craft-fonts revision pinned for CI and releases has no `Hebr` face yet, so until one is added releases report that error instead of drawing the missing letters.
|
||||
|
||||
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). Russian, Bulgarian, Czech, Brazilian Portuguese, German, Spanish, French, Hungarian, Italian and Turkish need only the bundled Latin faces (tested in `tests/fonts.rs`). Hungarian uses the same one/other plural rule as English: 1 is singular, and every other count — including 0 — is the other form. After a numeral the noun stays singular (`1 oldal`, `2 oldal`). Telugu is drawn with a craft-fonts `Telu` face (Noto Sans Telugu, kept in the web build too), whose conjuncts egui shapes; a build without one shows Telugu labels as replacement boxes, and the release-pinned craft-fonts input has no Telugu face yet.
|
||||
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). Russian, Bulgarian, Czech, Brazilian Portuguese, German, Spanish, French, Hungarian, Italian, Turkish and Lithuanian need only the bundled Latin faces (tested in `tests/fonts.rs`). Hungarian uses the same one/other plural rule as English: 1 is singular, and every other count — including 0 — is the other form. After a numeral the noun stays singular (`1 oldal`, `2 oldal`). Telugu is drawn with a craft-fonts `Telu` face (Noto Sans Telugu, kept in the web build too), whose conjuncts egui shapes; a build without one shows Telugu labels as replacement boxes, and the release-pinned craft-fonts input has no Telugu face yet.
|
||||
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). Russian, Bulgarian, Czech, Brazilian Portuguese, German, Spanish, French, Hungarian, Italian and Slovenian need only the bundled Latin faces (tested in `tests/fonts.rs`). Slovenian has four plural forms: one (1, 101, 201…), two (2, 102…), few (3, 4, 103…) and other (everything else, including 0, 5–20 and 21+), so `@plural` entries list four forms (`1 stran`, `2 strani`, `3 strani`, `5 strani`). Hungarian uses the same one/other plural rule as English: 1 is singular, and every other count — including 0 — is the other form. After a numeral the noun stays singular (`1 oldal`, `2 oldal`). Telugu is drawn with a craft-fonts `Telu` face (Noto Sans Telugu, kept in the web build too), whose conjuncts egui shapes; a build without one shows Telugu labels as replacement boxes, and the release-pinned craft-fonts input has no Telugu face yet.
|
||||
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). Until craft-fonts has an allowed `Hans` face, the desktop app draws a Chinese interface with a Chinese face already installed on the machine (Microsoft YaHei or JhengHei on Windows, PingFang, Hiragino Sans GB or STHeiti on macOS, WenQuanYi or Droid Sans Fallback on Linux; never Noto CJK or Source Han), placed before the Japanese faces so a line keeps one face (#826). The web build has no installed fonts to fall back on. Russian, Bulgarian, Czech, Brazilian Portuguese, German, Spanish, French, Hungarian and Italian need only the bundled Latin faces (tested in `tests/fonts.rs`). Hungarian uses the same one/other plural rule as English: 1 is singular, and every other count — including 0 — is the other form. After a numeral the noun stays singular (`1 oldal`, `2 oldal`). Telugu is drawn with a craft-fonts `Telu` face (Noto Sans Telugu, kept in the web build too), whose conjuncts egui shapes; a build without one shows Telugu labels as replacement boxes, and the release-pinned craft-fonts input has no Telugu face yet.
|
||||
|
||||
|
||||
+1
-1
@@ -97,7 +97,7 @@ total matches the parity estimate.
|
||||
| M11 | Optimize, preflight, PDF/A/X/UA, print production | 200–350 | 18% | 80–140 | 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 | 30% | 90–160 | Done: new `a11y` crate with the Accessibility Checker (all 32 rules, report, Fix/Skip/Explain, options dialog and results panel, agent tools). Locatable figure, annotation and form findings highlight their page bounds.<br>Done since: 2D distance, perimeter and area measurements with persistent viewport calibration, snapping, live information and CSV export; a bounded, text-only auto-tag proposal tool for one page. Missing: writing proposed tags and parent tree, field autotag, visual Tags/Order/Content panels, Reading Order tool, compare, geospatial/3D measurement, search index, XFA |
|
||||
| M13 | Automation (MCP, Action Wizard, CLI) + AI providers | 60–120 | 45% | 25–45 | 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<br>Done since: CLI run and MCP reject missing, empty, repeated and option-valued --root arguments before any tool/script execution or MCP initialization. A single-hyphen directory name remains valid; synthetic CLI regressions preserve a would-be overwritten file. `text_find` also caps the per-page match ranges it materializes at the remaining result limit plus one continuation match. |
|
||||
| M14 | 1.0 polish: performance, localization, installers | 120–250 | 15% | 80–150 | 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!`. Catalogs of ≈ 2,000–2,140 entries for Japanese, Simplified and Traditional Chinese, Russian, Bulgarian, German, Spanish, French, Telugu, Hungarian, Ukrainian, Italian, Brazilian Portuguese and Arabic (logical-order catalog put into display order; layout not yet mirrored); Czech covers the menus. Signed and notarized macOS builds, signed Windows installers (x64, x86, ARM64), AppImage, deb, rpm, Flatpak and FreeBSD packages. #307 (bounded large-PDF memory): shared resource indexes and parser metadata, lazy inspection, bounded caches; a 9,156-page file inspects in ≈ 1.8 s at ≈ 500 MiB instead of exceeding 10 GiB. Edits let go of the previous working file before writing the next (a 1 GiB file peaks at two copies while editing, not three; gap 13). Shortcut display uses localized modifier/key names across menus, toolbar, palette, Help and tooltips (#496). Validated candidate (macOS raster probe; Windows runtime unverified): the Windows policy changes only embedded JetBrains Mono; horizontal alpha-edge energy per ink improves 2.78%, 3.87% and 1.59% at 100%, 125% and 150%, and is down 0.79% at 200%. Inter is pixel-identical; layout hashes, glyph coverage and the 8 MiB atlas are unchanged. Timing and RSS ranges overlap, with no improvement claimed. Estimates unchanged. Missing: a right-to-left layout, a Chinese UI face in releases, Hindi/Indonesian/Korean/Vietnamese, file-backed input and global memory budgets, performance budgets, keyboard-only operation |
|
||||
| M14 | 1.0 polish: performance, localization, installers | 120–250 | 15% | 80–150 | 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!`. Catalogs of ≈ 2,000–2,140 entries for Japanese, Simplified and Traditional Chinese, Russian, Bulgarian, German, Spanish, French, Telugu, Hungarian, Ukrainian, Italian, Brazilian Portuguese and Arabic (logical-order catalog put into display order; layout not yet mirrored); Czech covers the menus. Signed and notarized macOS builds, signed Windows installers (x64, x86, ARM64), AppImage, deb, rpm, Flatpak and FreeBSD packages. #307 (bounded large-PDF memory): shared resource indexes and parser metadata, lazy inspection, bounded caches; a 9,156-page file inspects in ≈ 1.8 s at ≈ 500 MiB instead of exceeding 10 GiB. Edits let go of the previous working file before writing the next (a 1 GiB file peaks at two copies while editing, not three; gap 13). Shortcut display uses localized modifier/key names across menus, toolbar, palette, Help and tooltips (#496). Lithuanian adds a 2,578-entry catalog and date names, reviewed by a native-speaking contributor. Validated candidate (macOS raster probe; Windows runtime unverified): the Windows policy changes only embedded JetBrains Mono; horizontal alpha-edge energy per ink improves 2.78%, 3.87% and 1.59% at 100%, 125% and 150%, and is down 0.79% at 200%. Inter is pixel-identical; layout hashes, glyph coverage and the 8 MiB atlas are unchanged. Timing and RSS ranges overlap, with no improvement claimed. Estimates unchanged. Missing: a right-to-left layout, a Chinese UI face in releases, Hindi/Indonesian/Korean/Vietnamese, file-backed input and global memory budgets, performance budgets, keyboard-only operation |
|
||||
| M13 | Automation (MCP, Action Wizard, CLI) + AI providers | 60–120 | 45% | 25–45 | 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 | 15% | 80–150 | 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!`. Catalogs of ≈ 2,000–2,140 entries for Japanese, Simplified and Traditional Chinese, Russian, Bulgarian, German, Spanish, French, Telugu, Hungarian, Ukrainian, Italian, Brazilian Portuguese and Arabic (logical-order catalog put into display order; layout not yet mirrored); Czech covers the menus. Signed and notarized macOS builds, signed Windows installers (x64, x86, ARM64), AppImage, deb, rpm, Flatpak and FreeBSD packages. #307 (bounded large-PDF memory): shared resource indexes and parser metadata, lazy inspection, bounded caches; a 9,156-page file inspects in ≈ 1.8 s at ≈ 500 MiB instead of exceeding 10 GiB. Edits let go of the previous working file before writing the next (a 1 GiB file peaks at two copies while editing, not three; gap 13). Shortcut display uses localized modifier/key names across menus, toolbar, palette, Help and tooltips (#496). Traditional Chinese accessibility statuses, rule semantics and Taiwan form/table terms have catalog regressions. Missing: a right-to-left layout, a Chinese UI face in releases, Hindi/Indonesian/Korean/Vietnamese, file-backed input and global memory budgets, performance budgets, keyboard-only operation |
|
||||
| | **Total** | **2,085–3,840** | **≈ 49% ready** | **≈ 770–1,400** | Area view of the same work: [target-app-parity.md](target-app-parity.md#by-feature-area) |
|
||||
|
||||
Reference in New Issue
Block a user