M14: complete Japanese dialogs, panels, shortcuts and diagnostics (#141)
* i18n: Czech interface language
Adds Czech (Čeština, code `cs`) next to English and Japanese:
Preferences ▸ Interface language, `ui.set language cs`, `ui.state`
and persisted preferences all accept it.
- i18n: `Language::Cs`; `tr()` picks the language's table with a match
(English has none), panic-free. The CZECH table (83 entries) covers
every label passed to `.tr("…")` (main menu, View zoom/theme/side-panel
entries, Preferences dialog), every registered File/Edit/Pages/View/Help
command shown by `registry_menu`, and every key of the Japanese table.
Written clean-room from the English meaning, not from any Acrobat
localisation.
- Tests: Czech covers every registered menu title and label, every
`.tr("…")` literal found by scanning the crate's sources, and the
Japanese keys; nothing is left identical except an explicit KEEP_AS_IS
list ("OK"); ellipses match the source; keys are unique per language;
`cs` round-trips through set_option, ui.set/ui.state and persistence.
- theme: the font setup is split into `font_definitions()`, and a test
checks with skrifa (dev-dependency, already in the tree via egui) that
Inter Regular/Medium/SemiBold and JetBrains Mono draw all Czech letters
and every Czech translation, so nothing falls back to Shippori Mincho.
- The `language` error lists `cs`; docs/localization.md, the parity note
for misc.prefs-language and the ROADMAP log mention Czech.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* M14: translation catalogs, same system as PhotoCraft
Move the interface translations to PhotoCraft's i18n system: one TSV
catalog per language (crates/ui-egui/src/i18n/ja.tsv), the tl! macro,
entries keyed by command id, context or plural forms, and catalog tests
(format, duplicates, placeholders, command ids).
The 49 existing Japanese strings move over unchanged, plus "Auto". The
language preference now defaults to "auto" and follows the system
language (English when it has no catalog); "en" and "ja" still persist
and unknown values are rejected.
Update docs/localization.md, ROADMAP.md and the misc.prefs-language
parity entry.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* M14: complete Japanese dialogs, panels, shortcuts and diagnostics
Expand Japanese from 49 to 1830 interface entries, expose language preferences in the Edit menu, preserve opaque template and history values, and localize known application diagnostics only at the UI boundary. Add control, persistence, template and desktop/web glyph regressions.
Co-authored-by: Codex <noreply@openai.com>
* i18n: add Brazilian Portuguese (pt-br) UI translation
Adds Language::PtBr (code pt-br, endonym Português (Brasil)) with serde rename so it persists as "pt-br", registers it in Language::ALL (Preferences ▸ Interface language and ui.set/ui.state) and translates the same 49 menu/command keys as the Japanese table. Extends the i18n tests (table parity, codes, persistence) and updates docs/localization.md.
* M14: strict catalog validation, one-pass fmt, translation assets
Fold in the infrastructure parts of #175:
- `fmt` fills placeholders in one pass, so inserted values (a file name
containing `{n}`) are never substituted again.
- Catalog validation rejects unknown escapes, unknown `@` contexts,
placeholder, ellipsis and plural-form mismatches and duplicates. The
bundled catalogs must have no errors (tests); at run time a bad line is
still only skipped and logged, so the rest of the catalog loads.
- `.tsv` files count as assets in `cargo xtask assets`, with a
`translation` kind that may never come from Adobe; ja.tsv gets its
ATTRIBUTION.toml entry.
- Control-channel test: switching the language keeps documents, their
dirty state and command ids.
Also make the drawing language thread-local, as in PhotoCraft, so
parallel kittest harnesses can't change each other's language.
Co-Authored-By: 才倾梦华V <185041488+CQMHV@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M14: Brazilian Portuguese as a TSV catalog
Move #165's Portuguese table into crates/ui-egui/src/i18n/pt-br.tsv
(same 49 strings, plus Auto) and register it in LANGUAGES with
PhotoCraft's Portuguese plural rule (0 and 1 singular). Any Portuguese
locale (pt_BR, pt_PT, pt) follows the Brazilian catalog, as in
PhotoCraft. The catalog is an attributed asset (ATTRIBUTION.toml), and
the docs and parity notes list the new code.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M14: Czech as a TSV catalog
Move #68's Czech table into crates/ui-egui/src/i18n/cs.tsv (PrintCraft
labels renamed PdfCraft, plus Auto) and register it with PhotoCraft's
Czech plural rule (one / few / other). #68's tests become catalog
tests: Czech covers every registered menu title and menu command label,
keeps ellipses, uses no three-dot ellipsis or stray whitespace, and only
leaves "OK" untranslated; the code persists and `cz` is rejected. The
source-scanning `.tr("…")` test is dropped: strings now go through tl!
and the catalog tests check consistency.
The font-coverage test moves to tests/fonts.rs and covers both Latin
catalogs (cs, pt-br) against Inter and JetBrains Mono. It keeps skrifa
as a dev-dependency (the version pdfcraft-fonts already uses): egui's
has_glyphs can't answer it, since a family with only the primary face
also uses that face as its replacement face.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M14: route every UI string through the catalog; Japanese as ja.tsv
Port #141's wiring to the catalog API from land/106:
- `crate::i18n::tr(ui, s)` becomes `tl!(s)`, `tr_fmt`/`tr_template`
become `i18n::fmt(tl!(..), ..)`, and `Language::get`/`store` (the
egui temp-data copy of the language) are gone: the drawing language
is the per-thread `i18n::current()`, set from the preference at the
start of each frame. Worker threads (export, OCR, batch actions) take
the UI's language with them for their summaries.
- ja.rs (1,830 pairs) becomes ja.tsv, merged with the existing entries
and renamed PrintCraft -> PdfCraft. The 29 diagnostic templates of
errors.rs are dropped.
- `Language::diagnostic` and errors.rs are dropped: matching engine and
OS messages against English templates breaks silently whenever the
wording changes. Error details are shown as they are, inside the
translated frame ("{label} failed: {e}").
- History labels (`command_label`/`action_label`) move into i18n and keep
captured file and field names; registry menus prefer `@id` entries.
- Preferences: Auto plus every registered language, relabelled at once.
- Tests: Japanese covers every command and All tools entry; Japanese
labels have glyphs with the desktop faces and with the web subset
(with craft-fonts); history labels; #141's control tests, with the
error-detail expectation updated.
Co-Authored-By: shuiyue-cmyk <291597411+shuiyue-cmyk@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M14: Preferences in the Edit menu, Cmd/Ctrl-comma
Split out of #141: register app.preferences in the Edit menu with the
platform's usual shortcut (Command-comma on macOS, Ctrl-comma
elsewhere), so the interface language can be found and changed with no
document open. Shortcut keys may now be punctuation (egui's key name
","). #141's control test drives the menu item, the language selector
and the shortcut in Japanese and English.
Co-Authored-By: sakamoto824 <sakamoto824@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Petr Leo Compel <petrleocompel@gmail.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Simon Leblanc <contact@leblanc-simon.eu>
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: RicSchonfelder <ricschonfelder@icloud.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: 才倾梦华V <185041488+CQMHV@users.noreply.github.com>
Co-authored-by: shuiyue-cmyk <291597411+shuiyue-cmyk@users.noreply.github.com>
This commit is contained in:
parent
f0f923b82e
commit
a4340d34c8
|
|
@ -41,7 +41,7 @@ licence = "MIT OR Apache-2.0"
|
|||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Japanese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "404a932baa7943958c56df19c912065ab08b6acc477dde63dc60fcacb2f54be8"
|
||||
sha256 = "b72162877bf38bbbb0849e9a5462b952517d9b4208b0b38c3da019cf1756ce1c"
|
||||
|
||||
[[asset]]
|
||||
path = "assets/icons/accessibility.svg"
|
||||
|
|
|
|||
|
|
@ -353,6 +353,13 @@ cargo xtask demo-pdf # build the showcase PDF used
|
|||
cargo xtask screenshots # regenerate every screenshot in this README
|
||||
```
|
||||
|
||||
The interface language is chosen in **Menu → Edit → Preferences…** (Command-comma on macOS,
|
||||
Ctrl-comma elsewhere, also with no document open; Auto follows the system language;
|
||||
see [docs/localization.md](docs/localization.md)) and saved. Japanese covers commands,
|
||||
dialogs, panels and keyboard shortcuts. Command search accepts the translated label, the
|
||||
English label and the stable command id; filenames, PDF contents, author names, custom
|
||||
action names and error details from the engine or the operating system keep their own text.
|
||||
|
||||
Japanese fonts come from [craft-fonts](https://github.com/storytold/craft-fonts), an optional build
|
||||
input that every release includes. To build with them (Japanese interface text, and Japanese text in
|
||||
edited PDFs):
|
||||
|
|
|
|||
|
|
@ -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 | 20% | 160–305 | Done: new `a11y` crate with the Accessibility Checker (all 32 rules, report, Fix/Skip/Explain, options dialog and results panel, agent tools). Missing: autotag, Tags/Order/Content panels, Reading Order tool, alt-text workflow, compare, measure, 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`, UI control channel with drag. Missing: Action Wizard, AI providers |
|
||||
| M14 | 1.0 polish: performance, localization, installers | 120–250 | 2% | 118–245 | Done: PhotoCraft's translation system (`i18n/`: TSV catalogs, `tl!`, command-id and plural entries, system-language detection, catalog tests) with English and the first Japanese strings (menus, Preferences). Missing: dialogs and panels through `tl!`, more languages, 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 complete for them (≈1,800 entries), Czech and Brazilian Portuguese for menus. Missing: more catalogs filled in, Windows language detection, 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 (49.5% 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).
|
||||
|
|
@ -135,6 +135,7 @@ Newest first. One line per session: the date, what moved, and the new overall pe
|
|||
|
||||
- **2026-10-07 (fork, default-workspace-mode):** Preferences ▸ Documents and view remembers the workspace used for newly opened PDFs. All Tools remains the default; explicit CLI/control mode overrides last for the session and PDF Initial View metadata stays independent. Overall estimate unchanged (≈ 30–35%).
|
||||
|
||||
- **2026-10-07 (community, M14, #141):** Every dialog, panel, notice, shortcut list and native-picker title goes through the catalog (`tl!`, `i18n::fmt`), and Japanese grows from 49 to about 1,800 entries (`ja.tsv`); worker threads (export, OCR, actions) write their summaries in the UI language; history labels keep captured file and field names; the command palette searches translated and English labels and ids. Engine and OS error text is no longer matched against English templates: it is shown as is, inside a translated frame. Localization remains partial for other languages.
|
||||
- **2026-10-07 (session 15):** Five community issues, each with a regression test: in two-page view, Next page moves a whole spread (#77, #70); Export to Word opens in Word again, with control characters dropped and receipt-length pages kept within Word's 22 in, checked in Microsoft Word (#78, #72); macOS opens PDFs from Finder, Open With and the Dock (Apple events through the audited `fmv-macos-events`, PDF declared in the Info.plist), and the packaging no longer describes the image editor it was copied from (#82, #73); Add text keeps the box being typed when you click elsewhere, with Done and Discard beside it, reproduced and checked with real OS input (#86, #74); sharper, higher-contrast text, most visible on Windows, with every text colour at WCAG AA in both themes (#84, #76; the console window was already gone in 0.2.1). Still open: #79 (layered "not verified" signature appearances), #80 (native macOS menu bar), #81 (Homebrew and Scoop). P0 88%, P1 53%; 805 features 50.2% shipped (53.7% weighted). ≈ 30–35%.
|
||||
- **2026-10-06 (community navigation):** Linux-only middle-button vertical scrolling in the document and Organize grid, toggled by clicking the wheel, with cancellation that protects page tools and selection. Scrolling stays on after releasing the wheel, even if the pointer moved before release. Manual feedback led to a Chromium-style distance^2.2 speed curve, a 15-screen-point dead zone, and continuous redraws that use elapsed frame time; the former linear speed cap is removed. Headless UI/control regressions cover fractional movement, consistent travel at 30/60/120/144 fps, pause/resume and cancellation. Other platforms retain their existing middle-button input behavior, with a regression that checks the custom scroller neither blocks input nor changes the cursor or consumes Escape. No new dependency. Automatic scrolling remains partial because keyboard-started timed scrolling is absent. Overall effort estimate remains ≈ 30–35%.
|
||||
- **2026-10-06 (session 14):** Issues: move and resize existing text boxes in Edit text (#59, closes #40); Windows release builds open no console window and the Start Menu shortcut has its icon (#58, #57; rolled out to every Craft app); native Windows ARM64 installers, installed and run on an ARM64 runner in CI (#66, #71; the intermittent ARM64 test crash was a WARP shader-JIT race between parallel GPU-rendering tests, not our code); XFA forms are detected and explained instead of failing silently (#65, #60 stays open for real XFA). Also: squiggly underline tool, comment opacity, Preferences ▸ Identity, drag-and-drop/hover tests; community table export to Word/HTML/RTF (#62, with linear-time detection and a column cap). #63 (timestamps/LTV with outbound network) held for review fixes and an owner decision on network access. ≈ 30–35% (unchanged).
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ pub struct Shortcut {
|
|||
pub shift: bool,
|
||||
/// ⌃ on macOS (in addition to ⌘); unused elsewhere.
|
||||
pub mac_ctrl: bool,
|
||||
/// Key name: a letter, a digit, or `Delete`.
|
||||
/// Key name: a letter, a digit, punctuation such as `,`, or `Delete`.
|
||||
pub key: &'static str,
|
||||
}
|
||||
|
||||
|
|
@ -263,7 +263,7 @@ pub const COMMANDS: &[CommandSpec] = &[
|
|||
c("export.docx", "Export to Word…", FILE, None, Document, "file-text"),
|
||||
c("export.html", "Export to HTML…", FILE, None, Document, "file-symlink"),
|
||||
c("export.rtf", "Export to RTF…", FILE, None, Document, "file-text"),
|
||||
c("app.preferences", "Preferences…", None, None, Nothing, "settings"),
|
||||
c("app.preferences", "Preferences…", EDIT, Some(Shortcut::cmd(",")), Nothing, "settings"),
|
||||
c("tools.js_console", "JavaScript console…", None, Some(Shortcut::cmd("J")), Document, "square-terminal"),
|
||||
c("tools.document_js", "Document JavaScripts…", None, None, Modification, "file-code"),
|
||||
c("ocr.recognize", "Recognize text…", None, None, Modification, "scan-text"),
|
||||
|
|
|
|||
|
|
@ -72,10 +72,10 @@ pub(crate) fn options_body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens)
|
|||
let o = &mut app.a11y_options;
|
||||
o.to = o.to.clamp(1, pages);
|
||||
o.from = o.from.clamp(1, o.to);
|
||||
ui.label(egui::RichText::new("Accessibility Checker Options").font(theme::semibold(18.0)));
|
||||
ui.label(egui::RichText::new(tl!("Accessibility Checker Options")).font(theme::semibold(18.0)));
|
||||
ui.add_space(8.0);
|
||||
let group = |ui: &mut egui::Ui, title: &str, body: &mut dyn FnMut(&mut egui::Ui)| {
|
||||
ui.label(egui::RichText::new(title).font(theme::semibold(13.0)));
|
||||
ui.label(egui::RichText::new(tl!(title)).font(theme::semibold(13.0)));
|
||||
egui::Frame::new().fill(t.hover).corner_radius(egui::CornerRadius::same(6)).inner_margin(egui::Margin::same(10)).show(ui, |ui| {
|
||||
ui.set_width(ui.available_width());
|
||||
body(ui);
|
||||
|
|
@ -83,31 +83,31 @@ pub(crate) fn options_body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens)
|
|||
ui.add_space(8.0);
|
||||
};
|
||||
group(ui, "Report Options", &mut |ui| {
|
||||
ui.checkbox(&mut o.create_report, "Create accessibility report");
|
||||
ui.checkbox(&mut o.create_report, tl!("Create accessibility report"));
|
||||
});
|
||||
group(ui, "Page Range", &mut |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.radio_value(&mut o.all_pages, true, "All pages in document");
|
||||
ui.radio_value(&mut o.all_pages, false, "Pages from");
|
||||
ui.radio_value(&mut o.all_pages, true, tl!("All pages in document"));
|
||||
ui.radio_value(&mut o.all_pages, false, tl!("Pages from"));
|
||||
ui.add_enabled(!o.all_pages, egui::DragValue::new(&mut o.from).range(1..=pages));
|
||||
ui.label("to");
|
||||
ui.label(tl!("to"));
|
||||
ui.add_enabled(!o.all_pages, egui::DragValue::new(&mut o.to).range(1..=pages));
|
||||
});
|
||||
});
|
||||
let on = o.rules.len();
|
||||
group(ui, &format!("Checking Options ({on} of 32 in all categories)"), &mut |ui| {
|
||||
group(ui, &crate::i18n::fmt(tl!("Checking Options ({on} of 32 in all categories)"), &[("on", &on.to_string())]), &mut |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Category:");
|
||||
egui::ComboBox::from_id_salt("a11y-category").selected_text(o.category.label()).width(260.0).show_ui(ui, |ui| {
|
||||
ui.label(tl!("Category:"));
|
||||
egui::ComboBox::from_id_salt("a11y-category").selected_text(tl!(o.category.label())).width(260.0).show_ui(ui, |ui| {
|
||||
for c in Category::ALL {
|
||||
ui.selectable_value(&mut o.category, c, c.label());
|
||||
ui.selectable_value(&mut o.category, c, tl!(c.label()));
|
||||
}
|
||||
});
|
||||
});
|
||||
ui.add_space(4.0);
|
||||
for r in Rule::ALL.into_iter().filter(|r| r.category() == o.category) {
|
||||
let mut checked = o.rules.contains(&r);
|
||||
if ui.checkbox(&mut checked, r.description()).changed() {
|
||||
if ui.checkbox(&mut checked, tl!(r.description())).changed() {
|
||||
if checked {
|
||||
o.rules.insert(r);
|
||||
} else {
|
||||
|
|
@ -117,24 +117,24 @@ pub(crate) fn options_body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens)
|
|||
}
|
||||
ui.add_space(4.0);
|
||||
ui.horizontal(|ui| {
|
||||
if ui.button("Select All").clicked() {
|
||||
if ui.button(tl!("Select All")).clicked() {
|
||||
o.rules.extend(Rule::ALL.into_iter().filter(|r| r.category() == o.category));
|
||||
}
|
||||
if ui.button("Clear All").clicked() {
|
||||
if ui.button(tl!("Clear All")).clicked() {
|
||||
let c = o.category;
|
||||
o.rules.retain(|r| r.category() != c);
|
||||
}
|
||||
});
|
||||
});
|
||||
ui.checkbox(&mut o.show_dialog, "Show this dialog when the Checker starts");
|
||||
ui.checkbox(&mut o.show_dialog, tl!("Show this dialog when the Checker starts"));
|
||||
ui.add_space(10.0);
|
||||
let (mut start, mut cancel) = (false, false);
|
||||
ui.horizontal(|ui| {
|
||||
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
|
||||
if widgets::pill_button(ui, "Start Checking", true).clicked() {
|
||||
if widgets::pill_button(ui, tl!("Start Checking"), true).clicked() {
|
||||
start = true;
|
||||
}
|
||||
if widgets::pill_button(ui, "Cancel", false).clicked() {
|
||||
if widgets::pill_button(ui, tl!("Cancel"), false).clicked() {
|
||||
cancel = true;
|
||||
}
|
||||
})
|
||||
|
|
@ -162,9 +162,9 @@ pub(crate) fn panel(ui: &mut egui::Ui, t: &Tokens, state: &mut A11yState, doc: D
|
|||
ui.vertical_centered(|ui| {
|
||||
ui.add(icons::image("accessibility", 32.0, t.text_faint));
|
||||
ui.add_space(6.0);
|
||||
ui.label(egui::RichText::new("This document hasn't been checked yet.").color(t.text_muted));
|
||||
ui.label(egui::RichText::new(tl!("This document hasn't been checked yet.")).color(t.text_muted));
|
||||
ui.add_space(6.0);
|
||||
if widgets::pill_button(ui, "Check for accessibility", true).clicked() {
|
||||
if widgets::pill_button(ui, tl!("Check for accessibility"), true).clicked() {
|
||||
action = Some(PanelAction::Options);
|
||||
}
|
||||
});
|
||||
|
|
@ -174,17 +174,19 @@ pub(crate) fn panel(ui: &mut egui::Ui, t: &Tokens, state: &mut A11yState, doc: D
|
|||
ui.horizontal(|ui| {
|
||||
ui.label(
|
||||
egui::RichText::new(if failed == 0 {
|
||||
"No issues found".to_string()
|
||||
tl!("No issues found").to_string()
|
||||
} else if failed == 1 {
|
||||
tl!("1 issue").to_string()
|
||||
} else {
|
||||
format!("{failed} issue{}", if failed == 1 { "" } else { "s" })
|
||||
crate::i18n::fmt(tl!("{n} issues"), &[("n", &failed.to_string())])
|
||||
})
|
||||
.color(t.text_muted),
|
||||
);
|
||||
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
|
||||
if widgets::pill_button(ui, "Check again", false).clicked() {
|
||||
if widgets::pill_button(ui, tl!("Check again"), false).clicked() {
|
||||
action = Some(PanelAction::CheckAgain);
|
||||
}
|
||||
if widgets::pill_button(ui, "Report", false).on_hover_text("Show the accessibility report").clicked() {
|
||||
if widgets::pill_button(ui, tl!("Report"), false).on_hover_text(tl!("Show the accessibility report")).clicked() {
|
||||
action = Some(PanelAction::ShowReport);
|
||||
}
|
||||
});
|
||||
|
|
@ -194,12 +196,23 @@ pub(crate) fn panel(ui: &mut egui::Ui, t: &Tokens, state: &mut A11yState, doc: D
|
|||
let rules: Vec<_> = report.in_category(c).collect();
|
||||
let issues = rules.iter().filter(|r| r.status == Status::Failed).count();
|
||||
let open = !state.collapsed.contains(&c);
|
||||
let head = if issues > 0 { format!("{} ({issues} issue{})", c.label(), if issues == 1 { "" } else { "s" }) } else { c.label().to_string() };
|
||||
let head = if issues == 0 {
|
||||
tl!(c.label()).to_string()
|
||||
} else if issues == 1 {
|
||||
crate::i18n::fmt(tl!("{label} (1 issue)"), &[("label", tl!(c.label()))])
|
||||
} else {
|
||||
crate::i18n::fmt(tl!("{label} ({n} issues)"), &[("label", tl!(c.label())), ("n", &issues.to_string())])
|
||||
};
|
||||
let clicked = ui
|
||||
.horizontal(|ui| {
|
||||
let toggle =
|
||||
icons::button(ui, if open { "chevron-down" } else { "chevron-right" }, 20.0, false, if open { "Collapse" } else { "Expand" })
|
||||
.clicked();
|
||||
let toggle = icons::button(
|
||||
ui,
|
||||
if open { "chevron-down" } else { "chevron-right" },
|
||||
20.0,
|
||||
false,
|
||||
if open { tl!("Collapse") } else { tl!("Expand") },
|
||||
)
|
||||
.clicked();
|
||||
let color = if issues > 0 { FAILED } else { t.text };
|
||||
let l = ui.add(egui::Label::new(egui::RichText::new(&head).font(theme::semibold(13.0)).color(color)).sense(egui::Sense::click()));
|
||||
toggle || l.clicked()
|
||||
|
|
@ -223,7 +236,12 @@ pub(crate) fn panel(ui: &mut egui::Ui, t: &Tokens, state: &mut A11yState, doc: D
|
|||
ui.add(icons::image(icon, 15.0, color));
|
||||
ui.add(
|
||||
egui::Label::new(
|
||||
egui::RichText::new(format!("{} - {}", r.rule.name(), r.status.label())).font(theme::regular(12.5)).color(t.text),
|
||||
egui::RichText::new(crate::i18n::fmt(
|
||||
tl!("{rule} - {status}"),
|
||||
&[("rule", tl!(r.rule.name())), ("status", tl!(r.status.label()))],
|
||||
))
|
||||
.font(theme::regular(12.5))
|
||||
.color(t.text),
|
||||
)
|
||||
.sense(egui::Sense::click()),
|
||||
)
|
||||
|
|
@ -234,35 +252,35 @@ pub(crate) fn panel(ui: &mut egui::Ui, t: &Tokens, state: &mut A11yState, doc: D
|
|||
}
|
||||
row.context_menu(|ui| {
|
||||
ui.set_min_width(170.0);
|
||||
if ui.add_enabled(fixable(r.rule) && r.status == Status::Failed, egui::Button::new("Fix")).clicked() {
|
||||
if ui.add_enabled(fixable(r.rule) && r.status == Status::Failed, egui::Button::new(tl!("Fix"))).clicked() {
|
||||
action = Some(PanelAction::Fix(r.rule));
|
||||
ui.close();
|
||||
}
|
||||
if ui.add_enabled(r.status != Status::Skipped, egui::Button::new("Skip Rule")).clicked() {
|
||||
if ui.add_enabled(r.status != Status::Skipped, egui::Button::new(tl!("Skip Rule"))).clicked() {
|
||||
action = Some(PanelAction::Skip(r.rule));
|
||||
ui.close();
|
||||
}
|
||||
if ui.button("Explain").clicked() {
|
||||
if ui.button(tl!("Explain")).clicked() {
|
||||
state.explain = if state.explain == Some(r.rule) { None } else { Some(r.rule) };
|
||||
ui.close();
|
||||
}
|
||||
ui.separator();
|
||||
if ui.button("Check Again").clicked() {
|
||||
if ui.button(tl!("Check Again")).clicked() {
|
||||
action = Some(PanelAction::CheckAgain);
|
||||
ui.close();
|
||||
}
|
||||
if ui.button("Show Report").clicked() {
|
||||
if ui.button(tl!("Show Report")).clicked() {
|
||||
action = Some(PanelAction::ShowReport);
|
||||
ui.close();
|
||||
}
|
||||
if ui.button("Options…").clicked() {
|
||||
if ui.button(tl!("Options…")).clicked() {
|
||||
action = Some(PanelAction::Options);
|
||||
ui.close();
|
||||
}
|
||||
});
|
||||
if state.explain == Some(r.rule) {
|
||||
egui::Frame::NONE.inner_margin(egui::Margin { left: 46, right: 4, top: 0, bottom: 6 }).show(ui, |ui| {
|
||||
ui.label(egui::RichText::new(r.rule.explanation()).small().color(t.text_muted));
|
||||
ui.label(egui::RichText::new(tl!(r.rule.explanation())).small().color(t.text_muted));
|
||||
});
|
||||
}
|
||||
if state.expanded.contains(&r.rule) {
|
||||
|
|
@ -271,7 +289,7 @@ pub(crate) fn panel(ui: &mut egui::Ui, t: &Tokens, state: &mut A11yState, doc: D
|
|||
for f in &r.findings {
|
||||
let l = ui.add(egui::Label::new(egui::RichText::new(&f.message).small().color(t.text_muted)).sense(egui::Sense::click()));
|
||||
if let Some(p) = f.page
|
||||
&& l.on_hover_text("Go to the page").clicked()
|
||||
&& l.on_hover_text(tl!("Go to the page")).clicked()
|
||||
{
|
||||
action = Some(PanelAction::GoTo(p));
|
||||
}
|
||||
|
|
@ -300,7 +318,7 @@ impl PdfCraftApp {
|
|||
let o = &self.a11y_options;
|
||||
let options = Options { rules: o.rules.clone(), pages: (!o.all_pages).then(|| (o.from.max(1) - 1..o.to.max(o.from)).collect()) };
|
||||
let Some(report) = doc.accessibility_check(&options) else {
|
||||
self.notify("This document can't be checked: it couldn't be read for editing");
|
||||
self.notify_tr("This document can't be checked: it couldn't be read for editing");
|
||||
return;
|
||||
};
|
||||
// Rules skipped by hand stay skipped when checking again.
|
||||
|
|
@ -313,11 +331,13 @@ impl PdfCraftApp {
|
|||
self.a11y.report = Some((id, report));
|
||||
self.right = Some(RightPanel::Accessibility);
|
||||
self.notify(if failed == 0 {
|
||||
"The checker found no problems in this document.".to_string()
|
||||
tl!("The checker found no problems in this document.").to_string()
|
||||
} else if failed == 1 {
|
||||
tl!("The checker found 1 problem which may prevent the document from being fully accessible.").to_string()
|
||||
} else {
|
||||
format!(
|
||||
"The checker found {failed} problem{} which may prevent the document from being fully accessible.",
|
||||
if failed == 1 { "" } else { "s" }
|
||||
crate::i18n::fmt(
|
||||
tl!("The checker found {n} problems which may prevent the document from being fully accessible."),
|
||||
&[("n", &failed.to_string())],
|
||||
)
|
||||
});
|
||||
if self.a11y_options.create_report {
|
||||
|
|
@ -338,7 +358,8 @@ impl PdfCraftApp {
|
|||
let (y, m, d) = self.session.today();
|
||||
let html = pdfcraft_engine::a11y::report_html(report, &doc.name, &format!("{y}-{m:02}-{d:02}"));
|
||||
let stem = doc.name.trim_end_matches(".pdf").trim_end_matches(".PDF").to_string();
|
||||
self.write_files(&[(format!("{stem} Accessibility Report.html"), std::sync::Arc::new(html.into_bytes()))], "Save the accessibility report");
|
||||
let title = tl!("Save the accessibility report").to_string();
|
||||
self.write_files(&[(format!("{stem} Accessibility Report.html"), std::sync::Arc::new(html.into_bytes()))], &title);
|
||||
}
|
||||
|
||||
pub(crate) fn a11y_action(&mut self, index: usize, action: PanelAction) {
|
||||
|
|
@ -373,7 +394,7 @@ impl PdfCraftApp {
|
|||
}
|
||||
}
|
||||
}
|
||||
Some(Err(e)) => self.notify(e),
|
||||
Some(Err(e)) => self.notify_error(e),
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
|
|
@ -429,13 +450,15 @@ pub(crate) fn alt_body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) ->
|
|||
let ctx = ui.ctx().clone();
|
||||
let doc = app.alt_draft.doc.and_then(|id| app.session.get(id));
|
||||
let d = &mut app.alt_draft;
|
||||
ui.label(egui::RichText::new("Set Alternate Text").font(theme::semibold(18.0)));
|
||||
ui.label(egui::RichText::new(tl!("Set Alternate Text")).font(theme::semibold(18.0)));
|
||||
ui.add_space(8.0);
|
||||
let n = d.figures.len();
|
||||
if n == 0 {
|
||||
ui.label(egui::RichText::new("This document has no tagged figures.").color(t.text_muted));
|
||||
ui.label(egui::RichText::new(tl!("This document has no tagged figures.")).color(t.text_muted));
|
||||
let mut cancel = false;
|
||||
ui.horizontal(|ui| ui.with_layout(Layout::right_to_left(Align::Center), |ui| cancel = widgets::pill_button(ui, "Close", true).clicked()));
|
||||
ui.horizontal(|ui| {
|
||||
ui.with_layout(Layout::right_to_left(Align::Center), |ui| cancel = widgets::pill_button(ui, tl!("Close"), true).clicked())
|
||||
});
|
||||
return (false, cancel);
|
||||
}
|
||||
d.index = d.index.min(n - 1);
|
||||
|
|
@ -445,8 +468,11 @@ pub(crate) fn alt_body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) ->
|
|||
{
|
||||
d.preview = figure_picture(&ctx, doc, &d.figures[i]).map(|tex| (i, tex));
|
||||
}
|
||||
let page = d.figures[i].page.map(|p| format!(" on page {}", p + 1)).unwrap_or_default();
|
||||
ui.label(egui::RichText::new(format!("Figure {} of {n}{page}", i + 1)).color(t.text_muted));
|
||||
let page = d.figures[i].page.map(|p| crate::i18n::fmt(tl!(" on page {p}"), &[("p", &(p + 1).to_string())])).unwrap_or_default();
|
||||
ui.label(
|
||||
egui::RichText::new(crate::i18n::fmt(tl!("Figure {i} of {n}{page}"), &[("i", &(i + 1).to_string()), ("n", &n.to_string()), ("page", &page)]))
|
||||
.color(t.text_muted),
|
||||
);
|
||||
ui.add_space(6.0);
|
||||
egui::Frame::new().fill(t.hover).corner_radius(egui::CornerRadius::same(6)).inner_margin(egui::Margin::same(8)).show(ui, |ui| {
|
||||
let (area, _) = ui.allocate_exact_size(egui::vec2(380.0, 220.0), egui::Sense::hover());
|
||||
|
|
@ -458,30 +484,31 @@ pub(crate) fn alt_body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) ->
|
|||
ui.painter().image(tex.id(), fit, egui::Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1.0, 1.0)), egui::Color32::WHITE);
|
||||
}
|
||||
None => {
|
||||
ui.painter().text(area.center(), egui::Align2::CENTER_CENTER, "No preview", theme::regular(13.0), t.text_faint);
|
||||
ui.painter().text(area.center(), egui::Align2::CENTER_CENTER, tl!("No preview"), theme::regular(13.0), t.text_faint);
|
||||
}
|
||||
}
|
||||
});
|
||||
ui.add_space(8.0);
|
||||
ui.add_enabled_ui(!d.decorative[i], |ui| {
|
||||
let l = ui.label("Alternate text");
|
||||
ui.add(egui::TextEdit::multiline(&mut d.texts[i]).desired_rows(3).desired_width(380.0).hint_text("Describe the figure")).labelled_by(l.id);
|
||||
let l = ui.label(tl!("Alternate text"));
|
||||
ui.add(egui::TextEdit::multiline(&mut d.texts[i]).desired_rows(3).desired_width(380.0).hint_text(tl!("Describe the figure")))
|
||||
.labelled_by(l.id);
|
||||
});
|
||||
ui.checkbox(&mut d.decorative[i], "Decorative figure");
|
||||
ui.checkbox(&mut d.decorative[i], tl!("Decorative figure"));
|
||||
ui.add_space(10.0);
|
||||
let (mut save, mut cancel) = (false, false);
|
||||
ui.horizontal(|ui| {
|
||||
if ui.add_enabled_ui(i > 0, |ui| icons::button(ui, "chevron-left", 28.0, false, "Previous figure")).inner.clicked() {
|
||||
if ui.add_enabled_ui(i > 0, |ui| icons::button(ui, "chevron-left", 28.0, false, tl!("Previous figure"))).inner.clicked() {
|
||||
d.index -= 1;
|
||||
}
|
||||
if ui.add_enabled_ui(i + 1 < n, |ui| icons::button(ui, "chevron-right", 28.0, false, "Next figure")).inner.clicked() {
|
||||
if ui.add_enabled_ui(i + 1 < n, |ui| icons::button(ui, "chevron-right", 28.0, false, tl!("Next figure"))).inner.clicked() {
|
||||
d.index += 1;
|
||||
}
|
||||
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
|
||||
if widgets::pill_button(ui, "Save & Close", true).clicked() {
|
||||
if widgets::pill_button(ui, tl!("Save & Close"), true).clicked() {
|
||||
save = true;
|
||||
}
|
||||
if widgets::pill_button(ui, "Cancel", false).clicked() {
|
||||
if widgets::pill_button(ui, tl!("Cancel"), false).clicked() {
|
||||
cancel = true;
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -40,19 +40,22 @@ impl PdfCraftApp {
|
|||
/// folder override in tests), in the background.
|
||||
pub fn run_action_on(&mut self, action: Action, files: Vec<(String, Vec<u8>)>) {
|
||||
if self.action_run.is_some() {
|
||||
self.notify("An action is already running");
|
||||
self.notify_tr("An action is already running");
|
||||
return;
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let dir = match &self.export_dir_override {
|
||||
Some(d) => Some(std::path::PathBuf::from(d)),
|
||||
None => rfd::FileDialog::new().set_title("Choose a folder for the results").pick_folder(),
|
||||
None => rfd::FileDialog::new().set_title(tl!("Choose a folder for the results").to_string()).pick_folder(),
|
||||
};
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let Some(dir) = dir else { return };
|
||||
let progress = Arc::new(Mutex::new(RunProgress { total: files.len(), ..Default::default() }));
|
||||
let p = progress.clone();
|
||||
// The summary is written on the worker thread: draw it in the UI's language.
|
||||
let lang = crate::i18n::current();
|
||||
let work = move || {
|
||||
crate::i18n::set_current(lang);
|
||||
let (mut ok, mut failed) = (0, Vec::new());
|
||||
for (i, (name, bytes)) in files.into_iter().enumerate() {
|
||||
if let Ok(mut s) = p.lock() {
|
||||
|
|
@ -67,12 +70,14 @@ impl PdfCraftApp {
|
|||
});
|
||||
match r {
|
||||
Ok(()) => ok += 1,
|
||||
Err(e) => failed.push(format!("{name}: {e}")),
|
||||
Err(e) => failed.push(format!("{name}: {}", e)),
|
||||
}
|
||||
}
|
||||
let mut msg = format!("{}: {ok} file{} done", action.name, if ok == 1 { "" } else { "s" });
|
||||
let done =
|
||||
if ok == 1 { crate::i18n::fmt(tl!("1 file done"), &[]) } else { crate::i18n::fmt(tl!("{n} files done"), &[("n", &ok.to_string())]) };
|
||||
let mut msg = format!("{}: {done}", action.name);
|
||||
if !failed.is_empty() {
|
||||
msg.push_str(&format!("; failed: {}", failed.join("; ")));
|
||||
msg.push_str(&crate::i18n::fmt(tl!("; failed: {list}"), &[("list", &failed.join("; "))]));
|
||||
}
|
||||
if let Ok(mut s) = p.lock() {
|
||||
s.done = s.total;
|
||||
|
|
@ -101,7 +106,10 @@ impl PdfCraftApp {
|
|||
self.notify(m);
|
||||
}
|
||||
Some(Err((done, total))) => {
|
||||
let m = format!("Running action… file {} of {}", (done + 1).min(total.max(1)), total.max(1));
|
||||
let m = crate::i18n::fmt(
|
||||
tl!("Running action… file {d} of {t}"),
|
||||
&[("d", &(done + 1).min(total.max(1)).to_string()), ("t", &total.max(1).to_string())],
|
||||
);
|
||||
if self.toast.as_ref().is_none_or(|t| t.0 != m) {
|
||||
self.notify(m);
|
||||
}
|
||||
|
|
@ -121,7 +129,7 @@ impl PdfCraftApp {
|
|||
let paths = match &self.action_files_override {
|
||||
Some(p) => p.iter().map(std::path::PathBuf::from).collect(),
|
||||
None => rfd::FileDialog::new()
|
||||
.set_title(format!("Files for {}", action.name))
|
||||
.set_title(crate::i18n::fmt(tl!("Files for {name}"), &[("name", &action.name)]))
|
||||
.add_filter("PDF", &["pdf"])
|
||||
.pick_files()
|
||||
.unwrap_or_default(),
|
||||
|
|
@ -131,7 +139,7 @@ impl PdfCraftApp {
|
|||
let name = p.file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or_default();
|
||||
match std::fs::read(&p) {
|
||||
Ok(b) => files.push((name, b)),
|
||||
Err(e) => return self.notify(format!("Couldn't read {name}: {e}")),
|
||||
Err(e) => return self.notify_fmt("Couldn't read {name}: {e}", &[("name", &name), ("e", &e.to_string())]),
|
||||
}
|
||||
}
|
||||
if !files.is_empty() {
|
||||
|
|
@ -181,7 +189,7 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> bool
|
|||
if app.wizard.editing.is_some() {
|
||||
return edit_body(ui, app, t);
|
||||
}
|
||||
ui.label(egui::RichText::new("Action Wizard").font(theme::semibold(18.0)));
|
||||
ui.label(egui::RichText::new(tl!("Action Wizard")).font(theme::semibold(18.0)));
|
||||
ui.add_space(8.0);
|
||||
let actions = app.all_actions();
|
||||
if app.wizard.selected.as_ref().is_none_or(|s| !actions.iter().any(|a| &a.name == s)) {
|
||||
|
|
@ -195,7 +203,8 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> bool
|
|||
ui.set_min_height(260.0);
|
||||
for a in &actions {
|
||||
let on = app.wizard.selected.as_deref() == Some(a.name.as_str());
|
||||
if ui.selectable_label(on, &a.name).clicked() {
|
||||
let name = if a.builtin { tl!(&a.name) } else { &a.name };
|
||||
if ui.selectable_label(on, name).clicked() {
|
||||
app.wizard.selected = Some(a.name.clone());
|
||||
}
|
||||
}
|
||||
|
|
@ -203,15 +212,17 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> bool
|
|||
});
|
||||
ui.vertical(|ui| {
|
||||
let Some(a) = actions.iter().find(|a| app.wizard.selected.as_deref() == Some(a.name.as_str())) else { return };
|
||||
ui.label(egui::RichText::new(&a.name).font(theme::semibold(15.0)));
|
||||
let name = if a.builtin { tl!(&a.name) } else { &a.name };
|
||||
ui.label(egui::RichText::new(name).font(theme::semibold(15.0)));
|
||||
if !a.description.is_empty() {
|
||||
ui.label(egui::RichText::new(&a.description).color(t.text_muted));
|
||||
let description = if a.builtin { tl!(&a.description) } else { &a.description };
|
||||
ui.label(egui::RichText::new(description).color(t.text_muted));
|
||||
}
|
||||
ui.add_space(6.0);
|
||||
ui.label(egui::RichText::new("Steps").font(theme::semibold(13.0)));
|
||||
ui.label(egui::RichText::new(tl!("Steps")).font(theme::semibold(13.0)));
|
||||
for (i, s) in a.steps.iter().enumerate() {
|
||||
let arg = s.arg().filter(|x| !x.is_empty()).map(|x| format!(": {x}")).unwrap_or_default();
|
||||
ui.label(format!("{}. {}{arg}", i + 1, s.label()));
|
||||
ui.label(format!("{}. {}{arg}", i + 1, tl!(s.label())));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -219,25 +230,25 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> bool
|
|||
let selected_custom = app.wizard.selected.as_ref().and_then(|n| app.custom_actions.iter().position(|a| &a.name == n));
|
||||
let mut close = false;
|
||||
ui.horizontal(|ui| {
|
||||
if ui.button("New Action…").clicked() {
|
||||
if ui.button(tl!("New Action…")).clicked() {
|
||||
app.wizard.editing = Some((None, Action { name: String::new(), description: String::new(), steps: Vec::new(), builtin: false }));
|
||||
}
|
||||
if let Some(i) = selected_custom {
|
||||
if ui.button("Edit…").clicked() {
|
||||
if ui.button(tl!("Edit…")).clicked() {
|
||||
let a = app.custom_actions[i].clone();
|
||||
app.wizard.editing = Some((Some(a.name.clone()), a));
|
||||
}
|
||||
if ui.button("Delete").clicked() {
|
||||
if ui.button(tl!("Delete")).clicked() {
|
||||
app.custom_actions.remove(i);
|
||||
app.wizard.selected = None;
|
||||
}
|
||||
}
|
||||
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
|
||||
if widgets::pill_button(ui, "Start", true).clicked() {
|
||||
if widgets::pill_button(ui, tl!("Start"), true).clicked() {
|
||||
close = true;
|
||||
app.start_selected_action();
|
||||
}
|
||||
if widgets::pill_button(ui, "Close", false).clicked() {
|
||||
if widgets::pill_button(ui, tl!("Close"), false).clicked() {
|
||||
close = true;
|
||||
}
|
||||
});
|
||||
|
|
@ -247,33 +258,33 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> bool
|
|||
|
||||
fn edit_body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> bool {
|
||||
let Some((original, a)) = app.wizard.editing.as_mut() else { return false };
|
||||
ui.label(egui::RichText::new(if original.is_some() { "Edit Action" } else { "New Action" }).font(theme::semibold(18.0)));
|
||||
ui.label(egui::RichText::new(if original.is_some() { tl!("Edit Action") } else { tl!("New Action") }).font(theme::semibold(18.0)));
|
||||
ui.add_space(8.0);
|
||||
egui::Grid::new("action-edit").num_columns(2).spacing([12.0, 8.0]).show(ui, |ui| {
|
||||
ui.label("Action name:");
|
||||
ui.label(tl!("Action name:"));
|
||||
ui.add(egui::TextEdit::singleline(&mut a.name).desired_width(320.0).id_salt("action-name"));
|
||||
ui.end_row();
|
||||
ui.label("Description:");
|
||||
ui.label(tl!("Description:"));
|
||||
ui.add(egui::TextEdit::singleline(&mut a.description).desired_width(320.0).id_salt("action-description"));
|
||||
ui.end_row();
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
ui.label(egui::RichText::new("Steps").font(theme::semibold(13.0)));
|
||||
ui.label(egui::RichText::new(tl!("Steps")).font(theme::semibold(13.0)));
|
||||
let mut remove = None;
|
||||
egui::Frame::new().fill(t.hover).corner_radius(egui::CornerRadius::same(6)).inner_margin(egui::Margin::same(8)).show(ui, |ui| {
|
||||
ui.set_width(ui.available_width());
|
||||
if a.steps.is_empty() {
|
||||
ui.label(egui::RichText::new("Add the steps this action runs, in order.").color(t.text_muted));
|
||||
ui.label(egui::RichText::new(tl!("Add the steps this action runs, in order.")).color(t.text_muted));
|
||||
}
|
||||
for (i, s) in a.steps.iter_mut().enumerate() {
|
||||
ui.push_id(i, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(format!("{}. {}", i + 1, s.label()));
|
||||
ui.label(format!("{}. {}", i + 1, tl!(s.label())));
|
||||
if let Some(arg) = s.arg_mut() {
|
||||
ui.add(egui::TextEdit::singleline(arg).desired_width(220.0));
|
||||
}
|
||||
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
|
||||
if ui.button("Remove").clicked() {
|
||||
if ui.button(tl!("Remove")).clicked() {
|
||||
remove = Some(i);
|
||||
}
|
||||
});
|
||||
|
|
@ -287,12 +298,12 @@ fn edit_body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> bool {
|
|||
let all = Step::all();
|
||||
let add = &mut app.wizard.add;
|
||||
ui.horizontal(|ui| {
|
||||
egui::ComboBox::from_id_salt("action-add-step").selected_text(all[(*add).min(all.len() - 1)].label()).width(240.0).show_ui(ui, |ui| {
|
||||
egui::ComboBox::from_id_salt("action-add-step").selected_text(tl!(all[(*add).min(all.len() - 1)].label())).width(240.0).show_ui(ui, |ui| {
|
||||
for (i, s) in all.iter().enumerate() {
|
||||
ui.selectable_value(add, i, s.label());
|
||||
ui.selectable_value(add, i, tl!(s.label()));
|
||||
}
|
||||
});
|
||||
if ui.button("Add Step").clicked() {
|
||||
if ui.button(tl!("Add Step")).clicked() {
|
||||
a.steps.push(all[(*add).min(all.len() - 1)].clone());
|
||||
}
|
||||
});
|
||||
|
|
@ -301,16 +312,16 @@ fn edit_body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> bool {
|
|||
let taken =
|
||||
builtin().iter().any(|b| b.name == name) || app.custom_actions.iter().any(|c| c.name == name && original.as_deref() != Some(name.as_str()));
|
||||
if taken {
|
||||
ui.label(egui::RichText::new("An action with this name already exists.").small().color(t.text_muted));
|
||||
ui.label(egui::RichText::new(tl!("An action with this name already exists.")).small().color(t.text_muted));
|
||||
}
|
||||
let (mut done, mut cancel) = (false, false);
|
||||
let ok = !name.is_empty() && !taken && !a.steps.is_empty();
|
||||
ui.horizontal(|ui| {
|
||||
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
|
||||
if ui.add_enabled_ui(ok, |ui| widgets::pill_button(ui, "Save", true)).inner.clicked() {
|
||||
if ui.add_enabled_ui(ok, |ui| widgets::pill_button(ui, tl!("Save"), true)).inner.clicked() {
|
||||
done = true;
|
||||
}
|
||||
if widgets::pill_button(ui, "Cancel", false).clicked() {
|
||||
if widgets::pill_button(ui, tl!("Cancel"), false).clicked() {
|
||||
cancel = true;
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -922,7 +922,7 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) {
|
|||
let info = &doc.info;
|
||||
if info.pages.is_empty() {
|
||||
app.views[index].auto_scroll.cancel();
|
||||
ui.centered_and_justified(|ui| ui.label("This document has no pages."));
|
||||
ui.centered_and_justified(|ui| ui.label(tl!("This document has no pages.")));
|
||||
return;
|
||||
}
|
||||
let want_thumbs = app.right == Some(RightPanel::Pages) || app.views[index].organize || app.dialog == Some(crate::Dialog::Print);
|
||||
|
|
@ -1132,14 +1132,9 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) {
|
|||
26.0,
|
||||
Color32::from_rgb(0xC8, 0x3A, 0x3A),
|
||||
);
|
||||
let msg = ui.fonts_mut(|f| {
|
||||
f.layout(
|
||||
format!("This page couldn't be displayed.\n{err}"),
|
||||
theme::regular(12.5),
|
||||
Color32::from_rgb(0x6A, 0x2A, 0x2A),
|
||||
(r.width() - 40.0).max(80.0),
|
||||
)
|
||||
});
|
||||
let message = crate::i18n::fmt(tl!("This page couldn't be displayed.\n{e}"), &[("e", err)]);
|
||||
let msg =
|
||||
ui.fonts_mut(|f| f.layout(message, theme::regular(12.5), Color32::from_rgb(0x6A, 0x2A, 0x2A), (r.width() - 40.0).max(80.0)));
|
||||
painter.galley(pos2(r.center().x - msg.size().x / 2.0, r.center().y), msg, Color32::BLACK);
|
||||
} else {
|
||||
let (pw_pt, ph_pt) = (info.pages[i].width.max(1.0), info.pages[i].height.max(1.0));
|
||||
|
|
@ -1425,9 +1420,11 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) {
|
|||
if sr.contains(p) {
|
||||
ui.ctx().set_cursor_icon(egui::CursorIcon::PointingHand);
|
||||
let label = match &l.target {
|
||||
LinkTarget::Page(n) => format!("Go to page {}", info.pages.get(*n).map(|p| p.label.as_str()).unwrap_or("?")),
|
||||
LinkTarget::Page(n) => {
|
||||
crate::i18n::fmt(tl!("Go to page {p}"), &[("p", info.pages.get(*n).map(|p| p.label.as_str()).unwrap_or("?"))])
|
||||
}
|
||||
LinkTarget::Uri(u) => u.clone(),
|
||||
LinkTarget::Other(s) => format!("{s} action"),
|
||||
LinkTarget::Other(s) => crate::i18n::fmt(tl!("{s} action"), &[("s", s)]),
|
||||
};
|
||||
hover_text = Some((p, label));
|
||||
if resp.clicked() && tool == QuickTool::Select && !consumed {
|
||||
|
|
@ -1489,41 +1486,41 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) {
|
|||
let others = view.prepare.also.clone();
|
||||
let anchor = (name.clone(), wi);
|
||||
let mut pick = |ui: &mut egui::Ui, op: A, label: &str| {
|
||||
if ui.add_enabled(can_modify, egui::Button::new(label)).clicked() {
|
||||
if ui.add_enabled(can_modify, egui::Button::new(tl!(label))).clicked() {
|
||||
view.pending_edit = crate::prepare::arrange(&form, &anchor, &others, op);
|
||||
ui.close();
|
||||
}
|
||||
};
|
||||
ui.menu_button("Align", |ui| {
|
||||
ui.menu_button(tl!("Align"), |ui| {
|
||||
pick(ui, A::AlignLeft, "Left");
|
||||
pick(ui, A::AlignRight, "Right");
|
||||
pick(ui, A::AlignTop, "Top");
|
||||
pick(ui, A::AlignBottom, "Bottom");
|
||||
pick(ui, A::AlignCenterV, "Vertically");
|
||||
pick(ui, A::AlignCenterH, "Horizontally");
|
||||
pick(ui, A::AlignCenterV, "Center vertically");
|
||||
pick(ui, A::AlignCenterH, "Center horizontally");
|
||||
});
|
||||
ui.menu_button("Distribute", |ui| {
|
||||
ui.menu_button(tl!("Distribute"), |ui| {
|
||||
pick(ui, A::DistributeH, "Horizontally");
|
||||
pick(ui, A::DistributeV, "Vertically");
|
||||
});
|
||||
ui.menu_button("Set Fields to Same Size", |ui| {
|
||||
ui.menu_button(tl!("Set Fields to Same Size"), |ui| {
|
||||
pick(ui, A::SameHeight, "Height");
|
||||
pick(ui, A::SameWidth, "Width");
|
||||
pick(ui, A::SameSize, "Both");
|
||||
});
|
||||
ui.separator();
|
||||
}
|
||||
if ui.button("Properties…").clicked() {
|
||||
if ui.button(tl!("Properties…")).clicked() {
|
||||
field_menu = Some(FieldMenu::Properties);
|
||||
ui.close();
|
||||
}
|
||||
if ui.add_enabled(can_modify, egui::Button::new("Duplicate…")).clicked() {
|
||||
if ui.add_enabled(can_modify, egui::Button::new(tl!("Duplicate…"))).clicked() {
|
||||
field_menu = Some(FieldMenu::Duplicate(name.clone()));
|
||||
ui.close();
|
||||
}
|
||||
ui.separator();
|
||||
let delete = if view.prepare.also.is_empty() { "Delete" } else { "Delete selected fields" };
|
||||
if ui.add_enabled(can_modify, egui::Button::new(delete)).clicked() {
|
||||
if ui.add_enabled(can_modify, egui::Button::new(tl!(delete))).clicked() {
|
||||
view.pending_edit = crate::prepare::delete_selected(view);
|
||||
ui.close();
|
||||
}
|
||||
|
|
@ -1640,7 +1637,7 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) {
|
|||
match clicked_link {
|
||||
Some(LinkTarget::Page(p)) => view.go_to_page(p),
|
||||
Some(LinkTarget::Uri(u)) => app.request_document_url(&u, crate::LinkOrigin::Link),
|
||||
Some(LinkTarget::Other(s)) => app.notify(format!("{s} actions run in the JavaScript engine (M6)")),
|
||||
Some(LinkTarget::Other(s)) => app.notify_fmt("{s} actions run in the JavaScript engine (M6)", &[("s", &s)]),
|
||||
None => {}
|
||||
}
|
||||
if tool == QuickTool::Crop && cropped {
|
||||
|
|
@ -1723,7 +1720,11 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) {
|
|||
app.dialog = Some(crate::Dialog::PageBoxes);
|
||||
}
|
||||
if let Some(n) = form_notice {
|
||||
app.notify(n);
|
||||
match n {
|
||||
crate::forms_ui::FormNotice::Security => app.notify_tr("The document's security settings don't allow filling in form fields"),
|
||||
crate::forms_ui::FormNotice::ReadOnly(name) => app.notify_fmt("{name} is read-only", &[("name", &name)]),
|
||||
crate::forms_ui::FormNotice::NoAction(name) => app.notify_fmt("{name} has no action", &[("name", &name)]),
|
||||
}
|
||||
}
|
||||
if let Some((name, action)) = app.views[index].forms.button.take() {
|
||||
run_button(app, index, &name, action);
|
||||
|
|
@ -1753,7 +1754,7 @@ fn run_button(app: &mut PdfCraftApp, index: usize, name: &str, action: pdfcraft_
|
|||
"PrevPage" => app.views[index].step_page(false),
|
||||
"FirstPage" => app.views[index].go_to_page(0),
|
||||
"LastPage" => app.views[index].go_to_page(pages.saturating_sub(1)),
|
||||
other => app.notify(format!("{name}: the {other} action isn't supported yet")),
|
||||
other => app.notify_fmt("{name}: the {other} action isn't supported yet", &[("name", name), ("other", other)]),
|
||||
},
|
||||
B::Uri(u) => app.request_document_url(&u, crate::LinkOrigin::Button),
|
||||
B::GoTo(p) => app.views[index].go_to_page(p.min(pages.saturating_sub(1))),
|
||||
|
|
@ -1781,9 +1782,10 @@ fn run_button(app: &mut PdfCraftApp, index: usize, name: &str, action: pdfcraft_
|
|||
}
|
||||
}
|
||||
B::Alert(m) => app.notify(m),
|
||||
B::Submit(url) => {
|
||||
app.notify(format!("{name} submits the form to {url}; PdfCraft doesn't send form data. Save the document to keep your entries."))
|
||||
}
|
||||
B::Submit(url) => app.notify_fmt(
|
||||
"{name} submits the form to {url}; PdfCraft doesn't send form data. Save the document to keep your entries.",
|
||||
&[("name", name), ("url", &url)],
|
||||
),
|
||||
B::ImportIcon => app.choose_field_image(name),
|
||||
B::Script(js) => {
|
||||
let id = app.views[index].id;
|
||||
|
|
@ -1818,7 +1820,7 @@ fn find_bar(view: &mut DocView, pages: usize, area: Rect, ui: &mut egui::Ui, t:
|
|||
ui.add(icons::image("search", 16.0, t.text_muted));
|
||||
let edit = egui::TextEdit::singleline(&mut find.query)
|
||||
.id(egui::Id::new("find-input"))
|
||||
.hint_text("Find text")
|
||||
.hint_text(tl!("Find text"))
|
||||
.desired_width(220.0)
|
||||
.frame(egui::Frame::NONE);
|
||||
let r = ui.add(edit);
|
||||
|
|
@ -1836,28 +1838,39 @@ fn find_bar(view: &mut DocView, pages: usize, area: Rect, ui: &mut egui::Ui, t:
|
|||
let status = if find.query.trim().is_empty() {
|
||||
String::new()
|
||||
} else if find.matches.is_empty() {
|
||||
if searched < pages { format!("Searching… {searched}/{pages}") } else { "No matches".into() }
|
||||
if searched < pages {
|
||||
crate::i18n::fmt(tl!("Searching… {s}/{p}"), &[("s", &searched.to_string()), ("p", &pages.to_string())])
|
||||
} else {
|
||||
tl!("No matches").to_string()
|
||||
}
|
||||
} else {
|
||||
let more = if searched < pages { "+" } else { "" };
|
||||
format!("{} of {}{more}", find.current.map(|c| c + 1).unwrap_or(0), find.matches.len())
|
||||
crate::i18n::fmt(
|
||||
tl!("{c} of {n}{more}"),
|
||||
&[
|
||||
("c", &find.current.map(|c| c + 1).unwrap_or(0).to_string()),
|
||||
("n", &find.matches.len().to_string()),
|
||||
("more", more),
|
||||
],
|
||||
)
|
||||
};
|
||||
ui.label(egui::RichText::new(status).font(theme::regular(12.0)).color(t.text_muted));
|
||||
if icons::button(ui, "chevron-up", 26.0, false, "Previous (⇧⌘G)").clicked() {
|
||||
if icons::button(ui, "chevron-up", 26.0, false, tl!("Previous (⇧⌘G)")).clicked() {
|
||||
step = Some(false);
|
||||
}
|
||||
if icons::button(ui, "chevron-down", 26.0, false, "Next (⌘G)").clicked() {
|
||||
if icons::button(ui, "chevron-down", 26.0, false, tl!("Next (⌘G)")).clicked() {
|
||||
step = Some(true);
|
||||
}
|
||||
let opts = icons::button(ui, "settings-2", 26.0, find.case_sensitive || find.whole_words, "Find options");
|
||||
let opts = icons::button(ui, "settings-2", 26.0, find.case_sensitive || find.whole_words, tl!("Find options"));
|
||||
egui::Popup::menu(&opts).show(|ui| {
|
||||
let a = ui.checkbox(&mut find.whole_words, "Whole words only").changed();
|
||||
let b = ui.checkbox(&mut find.case_sensitive, "Case-sensitive").changed();
|
||||
let a = ui.checkbox(&mut find.whole_words, tl!("Whole words only")).changed();
|
||||
let b = ui.checkbox(&mut find.case_sensitive, tl!("Case-sensitive")).changed();
|
||||
if a || b {
|
||||
// Search again with the new options.
|
||||
find.case_query.clear();
|
||||
}
|
||||
});
|
||||
if icons::button(ui, "x", 26.0, false, "Close (Esc)").clicked() {
|
||||
if icons::button(ui, "x", 26.0, false, tl!("Close (Esc)")).clicked() {
|
||||
close = true;
|
||||
}
|
||||
});
|
||||
|
|
@ -1895,18 +1908,18 @@ fn notices(
|
|||
info: &DocInfo,
|
||||
secured: bool,
|
||||
repaired: bool,
|
||||
signed: Option<(&str, Color32, String)>,
|
||||
signed: Option<(&str, Color32, &str, String)>,
|
||||
ui: &mut egui::Ui,
|
||||
t: &Tokens,
|
||||
) -> Option<Notice> {
|
||||
if let Some((icon, color, text)) = signed {
|
||||
if let Some((icon, color, template, arg)) = signed {
|
||||
let mut open = false;
|
||||
egui::Frame::NONE.fill(t.accent_soft).inner_margin(egui::Margin::symmetric(14, 7)).show(ui, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.add(icons::image(icon, 16.0, color));
|
||||
ui.label(egui::RichText::new(text).color(t.text));
|
||||
ui.label(egui::RichText::new(crate::i18n::fmt(tl!(template), &[("by", &arg)])).color(t.text));
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
if crate::widgets::pill_button(ui, "Signature panel", false).clicked() {
|
||||
if crate::widgets::pill_button(ui, tl!("Signature panel"), false).clicked() {
|
||||
open = true;
|
||||
}
|
||||
});
|
||||
|
|
@ -1920,23 +1933,27 @@ fn notices(
|
|||
let mut open_security = false;
|
||||
let mut open_repairs = false;
|
||||
let msg = if secured {
|
||||
Some(("lock", "This document is secured. Some changes are restricted by its security settings.".to_string(), false))
|
||||
Some(("lock", tl!("This document is secured. Some changes are restricted by its security settings.").to_string(), false))
|
||||
} else if info.xfa == Some(pdfcraft_render::Xfa::Dynamic) {
|
||||
Some((
|
||||
"triangle-alert",
|
||||
"This is a dynamic XFA form, which PdfCraft can't display yet. What you see is the file's placeholder page.".to_string(),
|
||||
tl!("This is a dynamic XFA form, which PdfCraft can't display yet. What you see is the file's placeholder page.").to_string(),
|
||||
false,
|
||||
))
|
||||
} else if info.xfa == Some(pdfcraft_render::Xfa::Static) {
|
||||
Some((
|
||||
"triangle-alert",
|
||||
"This form also contains XFA data, which PdfCraft doesn't read yet. You can fill its fields, but Acrobat may show the XFA values instead.".to_string(),
|
||||
tl!("This form also contains XFA data, which PdfCraft doesn't read yet. You can fill its fields, but Acrobat may show the XFA values instead.").to_string(),
|
||||
true,
|
||||
))
|
||||
} else if !info.fields.is_empty() {
|
||||
Some(("text-cursor-input", format!("This document contains {} interactive form fields.", info.fields.len()), true))
|
||||
Some((
|
||||
"text-cursor-input",
|
||||
crate::i18n::fmt(tl!("This document contains {n} interactive form fields."), &[("n", &info.fields.len().to_string())]),
|
||||
true,
|
||||
))
|
||||
} else if repaired {
|
||||
Some(("bandage", "This file was damaged and has been repaired. Saving keeps the repaired version.".to_string(), false))
|
||||
Some(("bandage", tl!("This file was damaged and has been repaired. Saving keeps the repaired version.").to_string(), false))
|
||||
} else if !info.warnings.is_empty() {
|
||||
Some(("triangle-alert", info.warnings[0].clone(), false))
|
||||
} else {
|
||||
|
|
@ -1948,19 +1965,19 @@ fn notices(
|
|||
ui.add(icons::image(icon, 16.0, t.accent_text));
|
||||
ui.label(egui::RichText::new(text).color(t.text));
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
if icons::button(ui, "x", 22.0, false, "Dismiss").clicked() {
|
||||
if icons::button(ui, "x", 22.0, false, tl!("Dismiss")).clicked() {
|
||||
view.notice_dismissed = true;
|
||||
}
|
||||
if fields {
|
||||
let label = if view.highlight_fields { "Hide field highlights" } else { "Highlight fields" };
|
||||
let label = if view.highlight_fields { tl!("Hide field highlights") } else { tl!("Highlight fields") };
|
||||
if crate::widgets::pill_button(ui, label, view.highlight_fields).clicked() {
|
||||
view.highlight_fields = !view.highlight_fields;
|
||||
}
|
||||
}
|
||||
if secured && crate::widgets::pill_button(ui, "Security settings", false).clicked() {
|
||||
if secured && crate::widgets::pill_button(ui, tl!("Security settings"), false).clicked() {
|
||||
open_security = true;
|
||||
}
|
||||
if repaired && !secured && info.fields.is_empty() && crate::widgets::pill_button(ui, "Details", false).clicked() {
|
||||
if repaired && !secured && info.fields.is_empty() && crate::widgets::pill_button(ui, tl!("Details"), false).clicked() {
|
||||
open_repairs = true;
|
||||
}
|
||||
});
|
||||
|
|
@ -1986,10 +2003,10 @@ fn quick_bar(app: &mut PdfCraftApp, area: Rect, ui: &mut egui::Ui) {
|
|||
.show(ui, |ui| {
|
||||
ui.spacing_mut().item_spacing.y = 2.0;
|
||||
ui.vertical(|ui| {
|
||||
if icons::button(ui, "mouse-pointer-2", 32.0, app.quick_tool == QuickTool::Select, "Select (V)").clicked() {
|
||||
if icons::button(ui, "mouse-pointer-2", 32.0, app.quick_tool == QuickTool::Select, tl!("Select (V)")).clicked() {
|
||||
app.quick_tool = QuickTool::Select;
|
||||
}
|
||||
if icons::button(ui, "hand", 32.0, app.quick_tool == QuickTool::Hand, "Hand (H)").clicked() {
|
||||
if icons::button(ui, "hand", 32.0, app.quick_tool == QuickTool::Hand, tl!("Hand (H)")).clicked() {
|
||||
app.quick_tool = QuickTool::Hand;
|
||||
}
|
||||
// Comment ▸, Highlight ▸, Draw ▸ (Acrobat's comment toolbar groups). Clicking a
|
||||
|
|
@ -1997,7 +2014,7 @@ fn quick_bar(app: &mut PdfCraftApp, area: Rect, ui: &mut egui::Ui) {
|
|||
for g in 0..comments::GROUPS.len() {
|
||||
let current = app.comment_prefs.group_tool[g];
|
||||
let active = matches!(app.quick_tool, QuickTool::Comment(t) if t.group() == g);
|
||||
let resp = icons::button(ui, current.icon(), 32.0, active, current.label());
|
||||
let resp = icons::button(ui, current.icon(), 32.0, active, tl!(current.label()));
|
||||
// A small corner triangle marks the flyout.
|
||||
let r = resp.rect;
|
||||
let tri = [r.right_bottom() + vec2(-4.0, -4.0), r.right_bottom() + vec2(-9.0, -4.0), r.right_bottom() + vec2(-4.0, -9.0)];
|
||||
|
|
@ -2021,7 +2038,7 @@ fn quick_bar(app: &mut PdfCraftApp, area: Rect, ui: &mut egui::Ui) {
|
|||
ui.painter().text(
|
||||
row.left_center() + vec2(34.0, 0.0),
|
||||
Align2::LEFT_CENTER,
|
||||
tool.label(),
|
||||
tl!(tool.label()),
|
||||
theme::regular(13.0),
|
||||
t.text,
|
||||
);
|
||||
|
|
@ -2035,7 +2052,8 @@ fn quick_bar(app: &mut PdfCraftApp, area: Rect, ui: &mut egui::Ui) {
|
|||
);
|
||||
}
|
||||
let click = click.on_hover_cursor(egui::CursorIcon::PointingHand);
|
||||
click.widget_info(|| egui::WidgetInfo::selected(egui::WidgetType::Button, true, on, tool.label()));
|
||||
let info = tl!(tool.label()).to_string();
|
||||
click.widget_info(|| egui::WidgetInfo::selected(egui::WidgetType::Button, true, on, info.clone()));
|
||||
if click.clicked() {
|
||||
app.execute(tool.command());
|
||||
ui.close();
|
||||
|
|
@ -2054,7 +2072,7 @@ fn quick_bar(app: &mut PdfCraftApp, area: Rect, ui: &mut egui::Ui) {
|
|||
if current_fill.is_some() { shown.icon() } else { "pen-line" },
|
||||
32.0,
|
||||
current_fill.is_some(),
|
||||
"Fill & Sign",
|
||||
tl!("Fill & Sign"),
|
||||
);
|
||||
let r = resp.rect;
|
||||
let tri = [r.right_bottom() + vec2(-4.0, -4.0), r.right_bottom() + vec2(-9.0, -4.0), r.right_bottom() + vec2(-4.0, -9.0)];
|
||||
|
|
@ -2085,7 +2103,7 @@ fn quick_bar(app: &mut PdfCraftApp, area: Rect, ui: &mut egui::Ui) {
|
|||
ui.painter().text(
|
||||
row.left_center() + vec2(34.0, 0.0),
|
||||
Align2::LEFT_CENTER,
|
||||
tool.label(),
|
||||
tl!(tool.label()),
|
||||
theme::regular(13.0),
|
||||
t.text,
|
||||
);
|
||||
|
|
@ -2098,7 +2116,8 @@ fn quick_bar(app: &mut PdfCraftApp, area: Rect, ui: &mut egui::Ui) {
|
|||
t.accent,
|
||||
);
|
||||
}
|
||||
click.widget_info(|| egui::WidgetInfo::selected(egui::WidgetType::Button, true, on, tool.label()));
|
||||
let info = tl!(tool.label()).to_string();
|
||||
click.widget_info(|| egui::WidgetInfo::selected(egui::WidgetType::Button, true, on, info.clone()));
|
||||
if click.clicked() {
|
||||
app.execute(tool.command());
|
||||
ui.close();
|
||||
|
|
@ -2129,9 +2148,9 @@ fn organize_toolbar(view: &mut DocView, info: &DocInfo, editable: bool, ui: &mut
|
|||
ui.horizontal(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 4.0;
|
||||
let label = match view.selected.len() {
|
||||
0 => format!("Page {} of {n}", view.current + 1),
|
||||
1 => "1 page selected".to_string(),
|
||||
k => format!("{k} pages selected"),
|
||||
0 => crate::i18n::fmt(tl!("Page {p} of {n}"), &[("p", &(view.current + 1).to_string()), ("n", &n.to_string())]),
|
||||
1 => tl!("1 page selected").to_string(),
|
||||
k => crate::i18n::fmt(tl!("{n} pages selected"), &[("n", &k.to_string())]),
|
||||
};
|
||||
// Fixed width so the buttons never shift as the selection text changes.
|
||||
ui.add_sized([150.0, 30.0], egui::Label::new(egui::RichText::new(label).font(theme::medium(13.0)).color(t.text_muted)).truncate());
|
||||
|
|
@ -2143,26 +2162,26 @@ fn organize_toolbar(view: &mut DocView, info: &DocInfo, editable: bool, ui: &mut
|
|||
view.pending_edit = Some(Edit::RotatePages { pages: targets.clone(), degrees: 90 });
|
||||
}
|
||||
let can_delete = targets.len() < n;
|
||||
if ui.add_enabled_ui(can_delete, |ui| icons::button(ui, "trash-2", 30.0, false, "Delete pages (Delete)")).inner.clicked() {
|
||||
if ui.add_enabled_ui(can_delete, |ui| icons::button(ui, "trash-2", 30.0, false, tl!("Delete pages (Delete)"))).inner.clicked() {
|
||||
view.pending_edit = Some(Edit::DeletePages { pages: targets.clone() });
|
||||
}
|
||||
if icons::button(ui, "file-plus", 30.0, false, "Insert a blank page after the selection").clicked() {
|
||||
if icons::button(ui, "file-plus", 30.0, false, tl!("Insert a blank page after the selection")).clicked() {
|
||||
let c = info.pages[last].crop;
|
||||
let (w, h) = ((c[2] - c[0]).abs().max(1.0) as f64, (c[3] - c[1]).abs().max(1.0) as f64);
|
||||
view.pending_edit = Some(Edit::InsertBlankPage { at: last + 1, width: w, height: h });
|
||||
}
|
||||
if icons::button(ui, "file-input", 30.0, false, "Insert pages from a file…").clicked() {
|
||||
if icons::button(ui, "file-input", 30.0, false, tl!("Insert pages from a file…")).clicked() {
|
||||
view.pending_action = Some(ViewAction::InsertFromFile);
|
||||
}
|
||||
if icons::button(ui, "file-output", 30.0, false, "Extract pages to a new document").clicked() {
|
||||
if icons::button(ui, "file-output", 30.0, false, tl!("Extract pages to a new document")).clicked() {
|
||||
view.pending_action = Some(ViewAction::Extract);
|
||||
}
|
||||
if icons::button(ui, "scissors", 30.0, false, "Split into files…").clicked() {
|
||||
if icons::button(ui, "scissors", 30.0, false, tl!("Split into files…")).clicked() {
|
||||
view.pending_action = Some(ViewAction::Split);
|
||||
}
|
||||
// Select ▸ all, odd, even, landscape, portrait pages (Acrobat's page range
|
||||
// selection in Organize Pages).
|
||||
let sel = icons::button(ui, "list", 30.0, false, "Select pages");
|
||||
let sel = icons::button(ui, "list", 30.0, false, tl!("Select pages"));
|
||||
egui::Popup::menu(&sel).show(|ui| {
|
||||
use pdfcraft_engine::{PageOrientation as O, PageParity as P, filter_pages};
|
||||
let all: Vec<usize> = (0..n).collect();
|
||||
|
|
@ -2173,26 +2192,26 @@ fn organize_toolbar(view: &mut DocView, info: &DocInfo, editable: bool, ui: &mut
|
|||
("Landscape pages", P::Both, O::Landscape),
|
||||
("Portrait pages", P::Both, O::Portrait),
|
||||
] {
|
||||
if ui.button(label).clicked() {
|
||||
if ui.button(tl!(label)).clicked() {
|
||||
view.select_pages(&filter_pages(info, &all, parity, orient));
|
||||
ui.close();
|
||||
}
|
||||
}
|
||||
if ui.button("None").clicked() {
|
||||
if ui.button(tl!("None")).clicked() {
|
||||
view.select_pages(&[]);
|
||||
ui.close();
|
||||
}
|
||||
});
|
||||
ui.add_space(8.0);
|
||||
if ui.add_enabled_ui(first > 0, |ui| icons::button(ui, "chevron-left", 30.0, false, "Move earlier")).inner.clicked() {
|
||||
if ui.add_enabled_ui(first > 0, |ui| icons::button(ui, "chevron-left", 30.0, false, tl!("Move earlier"))).inner.clicked() {
|
||||
view.pending_edit = Some(Edit::MovePages { pages: targets.clone(), to: first - 1 });
|
||||
}
|
||||
if ui.add_enabled_ui(last + 1 < n, |ui| icons::button(ui, "chevron-right", 30.0, false, "Move later")).inner.clicked() {
|
||||
if ui.add_enabled_ui(last + 1 < n, |ui| icons::button(ui, "chevron-right", 30.0, false, tl!("Move later"))).inner.clicked() {
|
||||
view.pending_edit = Some(Edit::MovePages { pages: targets.clone(), to: first + 1 });
|
||||
}
|
||||
});
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
if widgets::ghost_button(ui, "x", "Close").on_hover_text("Back to the document").clicked() {
|
||||
if widgets::ghost_button(ui, "x", tl!("Close")).on_hover_text(tl!("Back to the document")).clicked() {
|
||||
view.organize = false;
|
||||
}
|
||||
});
|
||||
|
|
@ -2285,9 +2304,8 @@ fn organize_grid(view: &mut DocView, info: &DocInfo, pool: &RenderPool, editable
|
|||
if resp.drag_stopped() {
|
||||
drop = true;
|
||||
}
|
||||
resp.widget_info(|| {
|
||||
egui::WidgetInfo::selected(egui::WidgetType::Checkbox, true, view.selected.contains(&i), format!("Page {}", p.label))
|
||||
});
|
||||
let info = crate::i18n::fmt(tl!("Page {label}"), &[("label", &p.label)]);
|
||||
resp.widget_info(|| egui::WidgetInfo::selected(egui::WidgetType::Checkbox, true, view.selected.contains(&i), info.clone()));
|
||||
let s = (cell.x - 44.0) / p.width.max(1.0);
|
||||
let size = vec2(p.width * s, p.height * s).min(vec2(cell.x - 44.0, cell.y - 56.0));
|
||||
let pr = Rect::from_center_size(pos2(c.center().x, c.top() + 16.0 + size.y / 2.0), size);
|
||||
|
|
@ -2330,15 +2348,15 @@ fn organize_grid(view: &mut DocView, info: &DocInfo, pool: &RenderPool, editable
|
|||
view.selected = [i].into();
|
||||
view.current = i;
|
||||
}
|
||||
if ui.add_enabled(editable, egui::Button::new("Cut")).clicked() {
|
||||
if ui.add_enabled(editable, egui::Button::new(tl!("Cut"))).clicked() {
|
||||
view.pending_action = Some(ViewAction::CopyPages { cut: true });
|
||||
ui.close();
|
||||
}
|
||||
if ui.button("Copy").clicked() {
|
||||
if ui.button(tl!("Copy")).clicked() {
|
||||
view.pending_action = Some(ViewAction::CopyPages { cut: false });
|
||||
ui.close();
|
||||
}
|
||||
if ui.add_enabled(editable, egui::Button::new("Paste after")).clicked() {
|
||||
if ui.add_enabled(editable, egui::Button::new(tl!("Paste after"))).clicked() {
|
||||
view.pending_action = Some(ViewAction::PastePages);
|
||||
ui.close();
|
||||
}
|
||||
|
|
@ -2357,7 +2375,7 @@ fn organize_grid(view: &mut DocView, info: &DocInfo, pool: &RenderPool, editable
|
|||
ui.painter().text(
|
||||
p + vec2(14.0, 14.0),
|
||||
Align2::LEFT_TOP,
|
||||
format!("{} page{}", pages.len(), if pages.len() == 1 { "" } else { "s" }),
|
||||
if pages.len() == 1 { tl!("1 page").to_string() } else { crate::i18n::fmt(tl!("{n} pages"), &[("n", &pages.len().to_string())]) },
|
||||
theme::medium(12.0),
|
||||
t.accent_text,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ pub fn tab_strip(app: &mut PdfCraftApp, ui: &mut egui::Ui) {
|
|||
}
|
||||
ui.horizontal_centered(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 4.0;
|
||||
if icons::button(ui, "house", 28.0, app.active.is_none(), "Home").clicked() {
|
||||
if icons::button(ui, "house", 28.0, app.active.is_none(), tl!("Home")).clicked() {
|
||||
app.active = None;
|
||||
}
|
||||
let mut close = None;
|
||||
|
|
@ -39,19 +39,19 @@ pub fn tab_strip(app: &mut PdfCraftApp, ui: &mut egui::Ui) {
|
|||
app.request_close_tab(i);
|
||||
}
|
||||
ui.add_space(4.0);
|
||||
if widgets::ghost_button(ui, "plus", "Open").on_hover_text("Open a PDF (⌘O)").clicked() {
|
||||
if widgets::ghost_button(ui, "plus", tl!("Open")).on_hover_text(tl!("Open a PDF (⌘O)")).clicked() {
|
||||
app.open_dialog();
|
||||
}
|
||||
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
|
||||
let (icon, next, tip) = match app.theme {
|
||||
ThemeKind::Light => ("moon", ThemeKind::Dark, "Dark gray theme"),
|
||||
ThemeKind::Dark => ("sun", ThemeKind::Light, "Light theme"),
|
||||
ThemeKind::Light => ("moon", ThemeKind::Dark, tl!("Dark gray theme")),
|
||||
ThemeKind::Dark => ("sun", ThemeKind::Light, tl!("Light theme")),
|
||||
};
|
||||
if icons::button(ui, icon, 28.0, false, tip).clicked() {
|
||||
if icons::button(ui, icon, 28.0, false, tl!(tip)).clicked() {
|
||||
let ctx = ui.ctx().clone();
|
||||
app.set_theme(&ctx, next);
|
||||
}
|
||||
if icons::button(ui, "circle-help", 28.0, false, "Keyboard shortcuts").clicked() {
|
||||
if icons::button(ui, "circle-help", 28.0, false, tl!("Keyboard shortcuts")).clicked() {
|
||||
app.dialog = Some(Dialog::Shortcuts);
|
||||
}
|
||||
// One click to the community, from anywhere in the app.
|
||||
|
|
@ -100,7 +100,7 @@ fn tab(ui: &mut egui::Ui, t: &Tokens, name: &str, dirty: bool, active: bool, clo
|
|||
if x.clicked() {
|
||||
*close = Some(index);
|
||||
}
|
||||
resp.on_hover_text(if dirty { format!("{name} — unsaved changes") } else { name.to_string() })
|
||||
resp.on_hover_text(if dirty { crate::i18n::fmt(tl!("{name} — unsaved changes"), &[("name", name)]) } else { name.to_string() })
|
||||
}
|
||||
|
||||
pub fn mode_bar(app: &mut PdfCraftApp, ui: &mut egui::Ui) {
|
||||
|
|
@ -118,7 +118,7 @@ pub fn mode_bar(app: &mut PdfCraftApp, ui: &mut egui::Ui) {
|
|||
for (mode, label) in
|
||||
[(Mode::AllTools, "All tools"), (Mode::Read, "Read"), (Mode::Edit, "Edit"), (Mode::Convert, "Convert"), (Mode::Sign, "E-Sign")]
|
||||
{
|
||||
if widgets::mode_tab(ui, label, app.mode == mode).clicked() {
|
||||
if widgets::mode_tab(ui, tl!(label), app.mode == mode).clicked() {
|
||||
app.select_mode(mode);
|
||||
}
|
||||
}
|
||||
|
|
@ -126,18 +126,18 @@ pub fn mode_bar(app: &mut PdfCraftApp, ui: &mut egui::Ui) {
|
|||
ui.spacing_mut().item_spacing.x = 4.0;
|
||||
let has_doc = app.active.is_some();
|
||||
ui.add_enabled_ui(has_doc, |ui| {
|
||||
if icons::button(ui, "printer", 32.0, false, "Print (⌘P)").clicked() {
|
||||
if icons::button(ui, "printer", 32.0, false, tl!("Print (⌘P)")).clicked() {
|
||||
app.run_command("print.dialog");
|
||||
}
|
||||
if icons::button(ui, "save", 32.0, false, "Save (M4)").clicked() {
|
||||
if icons::button(ui, "save", 32.0, false, tl!("Save (M4)")).clicked() {
|
||||
app.run_command("file.save");
|
||||
}
|
||||
if icons::button(ui, "info", 32.0, false, "Document properties (⌘D)").clicked() {
|
||||
if icons::button(ui, "info", 32.0, false, tl!("Document properties (⌘D)")).clicked() {
|
||||
app.dialog = Some(Dialog::Properties(PropsTab::Description));
|
||||
}
|
||||
});
|
||||
ui.add_space(8.0);
|
||||
if widgets::search_box(ui, "Find tools and commands", 260.0).clicked() {
|
||||
if widgets::search_box(ui, tl!("Find tools and commands"), 260.0).clicked() {
|
||||
app.palette_open = true;
|
||||
}
|
||||
});
|
||||
|
|
@ -156,7 +156,7 @@ fn main_menu(app: &mut PdfCraftApp, ui: &mut egui::Ui) {
|
|||
ui.menu_button(tl!("View"), |ui| {
|
||||
if let Some(i) = app.active {
|
||||
let v = &mut app.views[i];
|
||||
ui.label(egui::RichText::new("Zoom").color(t.text_faint).small());
|
||||
ui.label(egui::RichText::new(tl!("Zoom")).color(t.text_faint).small());
|
||||
if widgets::menu_item(ui, tl!("Actual size"), "⌘1").clicked() {
|
||||
v.set_zoom(1.0);
|
||||
}
|
||||
|
|
@ -166,46 +166,47 @@ fn main_menu(app: &mut PdfCraftApp, ui: &mut egui::Ui) {
|
|||
if widgets::menu_item(ui, tl!("Fit to width"), "⌘2").clicked() {
|
||||
v.fit = Fit::Width;
|
||||
}
|
||||
if widgets::menu_item(ui, "Fit to height", "").clicked() {
|
||||
if widgets::menu_item(ui, tl!("Fit to height"), "").clicked() {
|
||||
v.fit = Fit::Height;
|
||||
v.goto = Some((v.current, 0.0));
|
||||
}
|
||||
if widgets::menu_item(ui, "Fit visible", "⌘3").clicked() {
|
||||
if widgets::menu_item(ui, tl!("Fit visible"), "⌘3").clicked() {
|
||||
ui.close();
|
||||
app.execute("view.fit_visible");
|
||||
return;
|
||||
}
|
||||
if widgets::menu_item(ui, "Zoom in", "⌘+").clicked() {
|
||||
if widgets::menu_item(ui, tl!("Zoom in"), "⌘+").clicked() {
|
||||
v.zoom_step(true);
|
||||
}
|
||||
if widgets::menu_item(ui, "Zoom out", "⌘−").clicked() {
|
||||
if widgets::menu_item(ui, tl!("Zoom out"), "⌘−").clicked() {
|
||||
v.zoom_step(false);
|
||||
}
|
||||
if widgets::menu_item(ui, "Rotate view clockwise", "⇧⌘+").clicked() {
|
||||
if widgets::menu_item(ui, tl!("Rotate view clockwise"), "⇧⌘+").clicked() {
|
||||
v.rotate_view(true);
|
||||
}
|
||||
if widgets::menu_item(ui, "Rotate view counterclockwise", "⇧⌘−").clicked() {
|
||||
if widgets::menu_item(ui, tl!("Rotate view counterclockwise"), "⇧⌘−").clicked() {
|
||||
v.rotate_view(false);
|
||||
}
|
||||
ui.separator();
|
||||
ui.label(egui::RichText::new("Page navigation").color(t.text_faint).small());
|
||||
if ui.add_enabled(!v.back.is_empty(), egui::Button::new("Previous view").shortcut_text("⌘[")).clicked() {
|
||||
ui.label(egui::RichText::new(tl!("Page navigation")).color(t.text_faint).small());
|
||||
if ui.add_enabled(!v.back.is_empty(), egui::Button::new(tl!("Previous view")).shortcut_text("⌘[")).clicked() {
|
||||
v.view_history(false);
|
||||
}
|
||||
if ui.add_enabled(!v.forward.is_empty(), egui::Button::new("Next view").shortcut_text("⌘]")).clicked() {
|
||||
if ui.add_enabled(!v.forward.is_empty(), egui::Button::new(tl!("Next view")).shortcut_text("⌘]")).clicked() {
|
||||
v.view_history(true);
|
||||
}
|
||||
ui.separator();
|
||||
ui.label(egui::RichText::new("Page display").color(t.text_faint).small());
|
||||
ui.label(egui::RichText::new(tl!("Page display")).color(t.text_faint).small());
|
||||
for (l, label) in
|
||||
[(PageLayout::Continuous, "Continuous scrolling"), (PageLayout::TwoUp, "Two-page view"), (PageLayout::Single, "Single page")]
|
||||
{
|
||||
if ui.radio(v.layout == l, label).clicked() {
|
||||
if ui.radio(v.layout == l, tl!(label)).clicked() {
|
||||
v.layout = l;
|
||||
v.goto = Some((v.current, 0.0));
|
||||
}
|
||||
}
|
||||
if ui.add_enabled(v.layout == PageLayout::TwoUp, egui::Checkbox::new(&mut v.cover, "Show cover page in two-page view")).changed() {
|
||||
if ui.add_enabled(v.layout == PageLayout::TwoUp, egui::Checkbox::new(&mut v.cover, tl!("Show cover page in two-page view"))).changed()
|
||||
{
|
||||
v.goto = Some((v.current, 0.0));
|
||||
}
|
||||
ui.separator();
|
||||
|
|
@ -213,32 +214,32 @@ fn main_menu(app: &mut PdfCraftApp, ui: &mut egui::Ui) {
|
|||
crate::commands::registry_menu(app, ui, "View");
|
||||
ui.menu_button(tl!("Display theme"), |ui| {
|
||||
let ctx = ui.ctx().clone();
|
||||
if ui.radio(app.follow_system_theme, "Use system setting").clicked() {
|
||||
if ui.radio(app.follow_system_theme, tl!("Use system setting")).clicked() {
|
||||
app.follow_system_theme = true;
|
||||
}
|
||||
if ui.radio(!app.follow_system_theme && app.theme == ThemeKind::Light, "Light gray").clicked() {
|
||||
if ui.radio(!app.follow_system_theme && app.theme == ThemeKind::Light, tl!("Light gray")).clicked() {
|
||||
app.follow_system_theme = false;
|
||||
app.set_theme(&ctx, ThemeKind::Light);
|
||||
}
|
||||
if ui.radio(!app.follow_system_theme && app.theme == ThemeKind::Dark, "Dark gray").clicked() {
|
||||
if ui.radio(!app.follow_system_theme && app.theme == ThemeKind::Dark, tl!("Dark gray")).clicked() {
|
||||
app.follow_system_theme = false;
|
||||
app.set_theme(&ctx, ThemeKind::Dark);
|
||||
}
|
||||
});
|
||||
ui.menu_button(tl!("Side panels"), |ui| {
|
||||
for (p, label) in [
|
||||
(RightPanel::Comments, "Comments"),
|
||||
(RightPanel::Bookmarks, "Bookmarks"),
|
||||
(RightPanel::Pages, "Pages"),
|
||||
(RightPanel::Fields, "Fields"),
|
||||
(RightPanel::Layers, "Layers"),
|
||||
(RightPanel::Attachments, "Attachments"),
|
||||
(RightPanel::Signatures, "Signatures"),
|
||||
(RightPanel::Accessibility, "Accessibility Checker"),
|
||||
(RightPanel::Search, "Search"),
|
||||
(RightPanel::Compare, "Compare"),
|
||||
(RightPanel::Comments, tl!("Comments")),
|
||||
(RightPanel::Bookmarks, tl!("Bookmarks")),
|
||||
(RightPanel::Pages, tl!("Pages")),
|
||||
(RightPanel::Fields, tl!("Fields")),
|
||||
(RightPanel::Layers, tl!("Layers")),
|
||||
(RightPanel::Attachments, tl!("Attachments")),
|
||||
(RightPanel::Signatures, tl!("Signatures")),
|
||||
(RightPanel::Accessibility, tl!("Accessibility Checker")),
|
||||
(RightPanel::Search, tl!("Search")),
|
||||
(RightPanel::Compare, tl!("Compare")),
|
||||
] {
|
||||
if ui.radio(app.right == Some(p), label).clicked() {
|
||||
if ui.radio(app.right == Some(p), tl!(label)).clicked() {
|
||||
app.right = Some(p);
|
||||
}
|
||||
}
|
||||
|
|
@ -271,7 +272,7 @@ pub fn right_rail(app: &mut PdfCraftApp, ui: &mut egui::Ui) {
|
|||
ui.spacing_mut().item_spacing.y = 4.0;
|
||||
let mut rail_button = |ui: &mut egui::Ui, panel: RightPanel, icon: &str, tip: &str, has: bool| {
|
||||
let selected = app.right == Some(panel);
|
||||
let r = icons::button(ui, icon, 34.0, selected, tip);
|
||||
let r = icons::button(ui, icon, 34.0, selected, tl!(tip));
|
||||
if has && !selected {
|
||||
let c = r.rect.right_top() + vec2(-8.0, 8.0);
|
||||
ui.painter().circle_filled(c, 3.0, t.accent);
|
||||
|
|
@ -295,26 +296,26 @@ pub fn right_rail(app: &mut PdfCraftApp, ui: &mut egui::Ui) {
|
|||
let view = &mut app.views[index];
|
||||
ui.with_layout(Layout::bottom_up(Align::Center), |ui| {
|
||||
ui.spacing_mut().item_spacing.y = 2.0;
|
||||
if icons::button(ui, "zoom-out", 32.0, false, "Zoom out (⌘−)").clicked() {
|
||||
if icons::button(ui, "zoom-out", 32.0, false, tl!("Zoom out (⌘−)")).clicked() {
|
||||
view.zoom_step(false);
|
||||
}
|
||||
if icons::button(ui, "zoom-in", 32.0, false, "Zoom in (⌘+)").clicked() {
|
||||
if icons::button(ui, "zoom-in", 32.0, false, tl!("Zoom in (⌘+)")).clicked() {
|
||||
view.zoom_step(true);
|
||||
}
|
||||
if icons::button(ui, "rotate-cw", 32.0, false, "Rotate view clockwise (⇧⌘+)").clicked() {
|
||||
if icons::button(ui, "rotate-cw", 32.0, false, tl!("Rotate view clockwise (⇧⌘+)")).clicked() {
|
||||
view.rotate_view(true);
|
||||
}
|
||||
let fit_icon = if view.fit == Fit::Width { "maximize" } else { "columns-2" };
|
||||
if icons::button(ui, fit_icon, 32.0, false, "Toggle fit page / fit width").clicked() {
|
||||
if icons::button(ui, fit_icon, 32.0, false, tl!("Toggle fit page / fit width")).clicked() {
|
||||
view.fit = if view.fit == Fit::Width { Fit::Page } else { Fit::Width };
|
||||
view.goto = Some((view.current, 0.0));
|
||||
}
|
||||
ui.label(egui::RichText::new(format!("{:.0}%", view.zoom * 100.0)).font(theme::regular(10.5)).color(t.text_faint));
|
||||
ui.add_space(6.0);
|
||||
if icons::button(ui, "chevron-down", 30.0, false, "Next page").clicked() {
|
||||
if icons::button(ui, "chevron-down", 30.0, false, tl!("Next page")).clicked() {
|
||||
view.step_page(true);
|
||||
}
|
||||
if icons::button(ui, "chevron-up", 30.0, false, "Previous page").clicked() {
|
||||
if icons::button(ui, "chevron-up", 30.0, false, tl!("Previous page")).clicked() {
|
||||
view.step_page(false);
|
||||
}
|
||||
ui.label(egui::RichText::new(page_count.to_string()).font(theme::regular(11.0)).color(t.text_muted));
|
||||
|
|
@ -330,7 +331,7 @@ pub fn right_rail(app: &mut PdfCraftApp, ui: &mut egui::Ui) {
|
|||
.corner_radius(CornerRadius::same(5))
|
||||
.show(ui, |ui| ui.add(edit))
|
||||
.inner
|
||||
.on_hover_text("Current page — type a page number or label (such as iv) and press Enter");
|
||||
.on_hover_text(tl!("Current page — type a page number or label (such as iv) and press Enter"));
|
||||
if r.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) {
|
||||
// A page label first (logical page numbers, as Acrobat), then a number.
|
||||
let typed = view.page_input.clone();
|
||||
|
|
|
|||
|
|
@ -24,16 +24,16 @@ enum RowAction {
|
|||
}
|
||||
|
||||
pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> (bool, bool) {
|
||||
ui.label(egui::RichText::new("Combine files").font(theme::semibold(18.0)));
|
||||
ui.label(egui::RichText::new(tl!("Combine files")).font(theme::semibold(18.0)));
|
||||
ui.add_space(4.0);
|
||||
ui.label(egui::RichText::new("Files are combined in this order. Leave Pages empty to take every page.").small().color(t.text_faint));
|
||||
ui.label(egui::RichText::new(tl!("Files are combined in this order. Leave Pages empty to take every page.")).small().color(t.text_faint));
|
||||
ui.add_space(8.0);
|
||||
let mut action = None;
|
||||
let n = app.combine_draft.len();
|
||||
egui::ScrollArea::vertical().max_height(360.0).auto_shrink([false, true]).show(ui, |ui| {
|
||||
egui::Grid::new("combine-files").num_columns(4).min_col_width(40.0).spacing([10.0, 6.0]).striped(true).show(ui, |ui| {
|
||||
ui.label(egui::RichText::new("File").color(t.text_muted));
|
||||
ui.label(egui::RichText::new("Pages").color(t.text_muted));
|
||||
ui.label(egui::RichText::new(tl!("File")).color(t.text_muted));
|
||||
ui.label(egui::RichText::new(tl!("Pages")).color(t.text_muted));
|
||||
ui.label("");
|
||||
ui.label("");
|
||||
ui.end_row();
|
||||
|
|
@ -41,19 +41,21 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> (boo
|
|||
ui.horizontal(|ui| {
|
||||
ui.add(icons::image("file-text", 16.0, t.icon));
|
||||
ui.add(egui::Label::new(&f.name).truncate());
|
||||
ui.label(egui::RichText::new(format!("{} page{}", f.pages, if f.pages == 1 { "" } else { "s" })).small().color(t.text_faint));
|
||||
let count =
|
||||
if f.pages == 1 { tl!("1 page").to_string() } else { crate::i18n::fmt(tl!("{n} pages"), &[("n", &f.pages.to_string())]) };
|
||||
ui.label(egui::RichText::new(count).small().color(t.text_faint));
|
||||
});
|
||||
ui.add_sized([120.0, 22.0], egui::TextEdit::singleline(&mut f.range).hint_text("All pages"))
|
||||
.on_hover_text(format!("Pages of {} to combine, e.g. 1-3, 6", f.name));
|
||||
ui.add_sized([120.0, 22.0], egui::TextEdit::singleline(&mut f.range).hint_text(tl!("All pages")))
|
||||
.on_hover_text(crate::i18n::fmt(tl!("Pages of {name} to combine, e.g. 1-3, 6"), &[("name", &f.name)]));
|
||||
ui.horizontal(|ui| {
|
||||
if ui.add_enabled_ui(i > 0, |ui| icons::button(ui, "chevron-up", 24.0, false, "Move up")).inner.clicked() {
|
||||
if ui.add_enabled_ui(i > 0, |ui| icons::button(ui, "chevron-up", 24.0, false, tl!("Move up"))).inner.clicked() {
|
||||
action = Some(RowAction::Up(i));
|
||||
}
|
||||
if ui.add_enabled_ui(i + 1 < n, |ui| icons::button(ui, "chevron-down", 24.0, false, "Move down")).inner.clicked() {
|
||||
if ui.add_enabled_ui(i + 1 < n, |ui| icons::button(ui, "chevron-down", 24.0, false, tl!("Move down"))).inner.clicked() {
|
||||
action = Some(RowAction::Down(i));
|
||||
}
|
||||
});
|
||||
if icons::button(ui, "trash-2", 24.0, false, &format!("Remove {}", f.name)).clicked() {
|
||||
if icons::button(ui, "trash-2", 24.0, false, &crate::i18n::fmt(tl!("Remove {name}"), &[("name", &f.name)])).clicked() {
|
||||
action = Some(RowAction::Remove(i));
|
||||
}
|
||||
ui.end_row();
|
||||
|
|
@ -71,16 +73,20 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> (boo
|
|||
ui.add_space(10.0);
|
||||
let (mut go, mut cancel) = (false, false);
|
||||
ui.horizontal(|ui| {
|
||||
if widgets::pill_button(ui, "Add files…", false).clicked() {
|
||||
if widgets::pill_button(ui, tl!("Add files…"), false).clicked() {
|
||||
app.combine_dialog();
|
||||
}
|
||||
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
|
||||
let ok = app.combine_draft.len() >= 2;
|
||||
if ui.add_enabled_ui(ok, |ui| widgets::pill_button(ui, "Combine", true)).inner.on_disabled_hover_text("Add at least two files").clicked()
|
||||
if ui
|
||||
.add_enabled_ui(ok, |ui| widgets::pill_button(ui, tl!("Combine"), true))
|
||||
.inner
|
||||
.on_disabled_hover_text(tl!("Add at least two files"))
|
||||
.clicked()
|
||||
{
|
||||
go = true;
|
||||
}
|
||||
if widgets::pill_button(ui, "Cancel", false).clicked() {
|
||||
if widgets::pill_button(ui, tl!("Cancel"), false).clicked() {
|
||||
cancel = true;
|
||||
}
|
||||
});
|
||||
|
|
@ -95,7 +101,7 @@ impl PdfCraftApp {
|
|||
let bytes = Arc::new(bytes);
|
||||
match pdfcraft_render::inspect(bytes.clone(), None) {
|
||||
Ok(info) => self.combine_draft.push(CombineFile { name, bytes, pages: info.pages.len(), range: String::new() }),
|
||||
Err(e) => self.notify(format!("Couldn't add {name}: {e}")),
|
||||
Err(e) => self.notify_fmt("Couldn't add {name}: {e}", &[("name", &name), ("e", &e.to_string())]),
|
||||
}
|
||||
}
|
||||
self.dialog = Some(Dialog::Combine);
|
||||
|
|
@ -113,8 +119,11 @@ impl PdfCraftApp {
|
|||
.map(|f| (crate::files::strip_pdf(&f.name).to_string(), f.bytes, Some(f.range).filter(|r| !r.trim().is_empty())))
|
||||
.collect();
|
||||
match self.session.combine_ranges(&sources) {
|
||||
Ok(bytes) => self.open_created("Combined.pdf", bytes, &format!("Combined {count} files")),
|
||||
Err(e) => self.notify(format!("Couldn't combine files: {e}")),
|
||||
Ok(bytes) => {
|
||||
let message = crate::i18n::fmt(tl!("Combined {n} files"), &[("n", &count.to_string())]);
|
||||
self.open_created("Combined.pdf", bytes, &message)
|
||||
}
|
||||
Err(e) => self.notify_fmt("Couldn't combine files: {e}", &[("e", &e.to_string())]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ impl PdfCraftApp {
|
|||
match pdfcraft_engine::guard(|| self.execute_unguarded(id)) {
|
||||
Ok(done) => done,
|
||||
Err(m) => {
|
||||
self.notify(format!("That didn't work: an internal error stopped it ({m}). Your documents are unchanged."));
|
||||
self.notify_fmt("That didn't work: an internal error stopped it ({m}). Your documents are unchanged.", &[("m", m.as_str())]);
|
||||
true
|
||||
}
|
||||
}
|
||||
|
|
@ -33,33 +33,33 @@ impl PdfCraftApp {
|
|||
let Some(spec) = commands::command(id) else { return false };
|
||||
if !self.command_enabled(spec) {
|
||||
let why = match spec.needs {
|
||||
commands::Needs::Undo => "Nothing to undo".to_string(),
|
||||
commands::Needs::Redo => "Nothing to redo".to_string(),
|
||||
commands::Needs::FillForms if self.active.is_some() => "This document has no form fields you can fill in".to_string(),
|
||||
commands::Needs::HasComments if self.active.is_some() => "This document has no comments to flatten".to_string(),
|
||||
commands::Needs::HasFields if self.active.is_some() => "This document has no form fields to flatten".to_string(),
|
||||
commands::Needs::Undo => tl!("Nothing to undo").to_string(),
|
||||
commands::Needs::Redo => tl!("Nothing to redo").to_string(),
|
||||
commands::Needs::FillForms if self.active.is_some() => tl!("This document has no form fields you can fill in").to_string(),
|
||||
commands::Needs::HasComments if self.active.is_some() => tl!("This document has no comments to flatten").to_string(),
|
||||
commands::Needs::HasFields if self.active.is_some() => tl!("This document has no form fields to flatten").to_string(),
|
||||
commands::Needs::HasRedactions if self.active.is_some() => {
|
||||
"There are no redaction marks (mark text, areas or pages first)".to_string()
|
||||
tl!("There are no redaction marks (mark text, areas or pages first)").to_string()
|
||||
}
|
||||
commands::Needs::Marks(k) if self.active.is_some() => {
|
||||
let kind = match k {
|
||||
pdfcraft_engine::MarkKind::HeaderFooter => tl!("header or footer"),
|
||||
pdfcraft_engine::MarkKind::Watermark => tl!("watermark"),
|
||||
pdfcraft_engine::MarkKind::Background => tl!("background"),
|
||||
};
|
||||
crate::i18n::fmt(tl!("This document has no {kind} to change"), &[("kind", kind)])
|
||||
}
|
||||
commands::Needs::Marks(k) if self.active.is_some() => format!(
|
||||
"This document has no {} to change",
|
||||
match k {
|
||||
pdfcraft_engine::MarkKind::HeaderFooter => "header or footer",
|
||||
pdfcraft_engine::MarkKind::Watermark => "watermark",
|
||||
pdfcraft_engine::MarkKind::Background => "background",
|
||||
}
|
||||
),
|
||||
commands::Needs::Security | commands::Needs::ProtectedSecurity if self.active.is_some() => {
|
||||
if self.active_ids().and_then(|(_, id)| self.session.get(id)).is_some_and(|d| d.allows_security_change()) {
|
||||
"This document isn't password-protected".to_string()
|
||||
tl!("This document isn't password-protected").to_string()
|
||||
} else {
|
||||
"Only the document's owner can change its security (open it with the permissions password)".to_string()
|
||||
tl!("Only the document's owner can change its security (open it with the permissions password)").to_string()
|
||||
}
|
||||
}
|
||||
commands::Needs::Assembly | commands::Needs::Modification | commands::Needs::Annotate if self.active.is_some() => {
|
||||
"The document's security settings don't allow this change".to_string()
|
||||
tl!("The document's security settings don't allow this change").to_string()
|
||||
}
|
||||
_ => "Open a document first".to_string(),
|
||||
_ => tl!("Open a document first").to_string(),
|
||||
};
|
||||
self.notify(why);
|
||||
return false;
|
||||
|
|
@ -90,7 +90,7 @@ impl PdfCraftApp {
|
|||
}
|
||||
"protect.remove" => {
|
||||
if self.apply_edit(Edit::RemoveProtection) {
|
||||
self.notify("Security will be removed when you save");
|
||||
self.notify_tr("Security will be removed when you save");
|
||||
}
|
||||
}
|
||||
"page.number" => {
|
||||
|
|
@ -245,7 +245,7 @@ impl PdfCraftApp {
|
|||
self.quick_tool = crate::QuickTool::EditText;
|
||||
self.left = crate::LeftPanel::Tool("edit");
|
||||
self.left_open = true;
|
||||
self.notify("Click text or an image to edit it");
|
||||
self.notify_tr("Click text or an image to edit it");
|
||||
}
|
||||
"edit.advanced_search" => {
|
||||
if let Some(i) = self.active {
|
||||
|
|
@ -267,7 +267,7 @@ impl PdfCraftApp {
|
|||
"edit.image" => self.add_image_dialog(),
|
||||
"edit.link" => {
|
||||
self.quick_tool = crate::QuickTool::Link;
|
||||
self.notify("Drag a rectangle to create a link; double-click a link to edit it");
|
||||
self.notify_tr("Drag a rectangle to create a link; double-click a link to edit it");
|
||||
}
|
||||
"edit.links_from_urls" => self.links_from_urls(),
|
||||
"edit.remove_links" => {
|
||||
|
|
@ -304,7 +304,7 @@ impl PdfCraftApp {
|
|||
"redact.apply" => {
|
||||
let marks = active.and_then(|i| self.session.get(self.views[i].id)).map_or(0, |d| d.redaction_marks());
|
||||
if marks == 0 {
|
||||
self.notify("There are no redaction marks to apply");
|
||||
self.notify_tr("There are no redaction marks to apply");
|
||||
} else {
|
||||
self.dialog = Some(Dialog::RedactApply);
|
||||
}
|
||||
|
|
@ -334,7 +334,7 @@ impl PdfCraftApp {
|
|||
"sign.digital" | "sign.certify" => {
|
||||
let certify = id == "sign.certify";
|
||||
self.quick_tool = crate::QuickTool::SignArea { certify };
|
||||
self.notify("Drag to draw the area where the signature should appear.");
|
||||
self.notify_tr("Drag to draw the area where the signature should appear.");
|
||||
}
|
||||
"sign.certify_invisible" => {
|
||||
let page = active.map_or(0, |i| self.views[i].current);
|
||||
|
|
@ -351,13 +351,13 @@ impl PdfCraftApp {
|
|||
if let Some(i) = self.active
|
||||
&& let Err(e) = self.fit_visible(i)
|
||||
{
|
||||
self.notify(format!("Couldn't fit the visible content: {e}"));
|
||||
self.notify_fmt("Couldn't fit the visible content: {e}", &[("e", &e.to_string())]);
|
||||
}
|
||||
}
|
||||
"view.marquee_zoom" => self.quick_tool = crate::QuickTool::MarqueeZoom,
|
||||
"edit.snapshot" => {
|
||||
self.quick_tool = crate::QuickTool::Snapshot;
|
||||
self.notify("Drag a rectangle around the area to copy");
|
||||
self.notify_tr("Drag a rectangle around the area to copy");
|
||||
}
|
||||
"page.copy" => self.copy_pages(false),
|
||||
"page.cut" => self.copy_pages(true),
|
||||
|
|
@ -415,7 +415,7 @@ impl PdfCraftApp {
|
|||
if let Some(i) = active {
|
||||
self.views[i].forms.focus = None;
|
||||
}
|
||||
self.notify(format!("Click on the page to add a {}, or drag to set its size", tool.label().to_lowercase()));
|
||||
self.notify_fmt("Click on the page to add a {tool}, or drag to set its size", &[("tool", &tl!(tool.label()).to_lowercase())]);
|
||||
}
|
||||
"sign.fill.signature.remove" => self.signature = None,
|
||||
"sign.fill.initials.remove" => self.initials = None,
|
||||
|
|
@ -447,7 +447,7 @@ impl PdfCraftApp {
|
|||
}
|
||||
"page.crop" => {
|
||||
self.quick_tool = crate::QuickTool::Crop;
|
||||
self.notify("Drag a rectangle on a page to crop it; double-click a page for Set Page Boxes");
|
||||
self.notify_tr("Drag a rectangle on a page to crop it; double-click a page for Set Page Boxes");
|
||||
}
|
||||
"page.boxes" => {
|
||||
self.boxes_draft.seeded = None;
|
||||
|
|
@ -505,7 +505,7 @@ pub(crate) fn registry_menu(app: &mut PdfCraftApp, ui: &mut egui::Ui, menu: &str
|
|||
let mac = cfg!(target_os = "macos") || cfg!(target_arch = "wasm32");
|
||||
for spec in commands::menu(menu) {
|
||||
let label = commands::current_label(spec, &app.session, app.active_ids().map(|(_, id)| id));
|
||||
let label = crate::i18n::tr_id(crate::i18n::current(), spec.id, &label);
|
||||
let label = crate::i18n::menu_label(spec.id, &label);
|
||||
let shortcut = spec.shortcut.map(|s| s.label(mac)).unwrap_or_default();
|
||||
let enabled = app.command_enabled(spec);
|
||||
let resp = ui.add_enabled(enabled, egui::Button::new(label).shortcut_text(shortcut));
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ impl PdfCraftApp {
|
|||
let picked = match self.attach_override.take() {
|
||||
Some(f) => Some(f),
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
None => rfd::FileDialog::new().set_title("Attach a file").pick_file().and_then(|p| {
|
||||
None => rfd::FileDialog::new().set_title(tl!("Attach a file")).pick_file().and_then(|p| {
|
||||
let data = std::fs::read(&p).ok()?;
|
||||
Some((p.file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or_default(), data))
|
||||
}),
|
||||
|
|
@ -75,7 +75,7 @@ impl PdfCraftApp {
|
|||
style.width = w;
|
||||
}
|
||||
self.comment_prefs.set_style(tool, style);
|
||||
self.notify(format!("New {} comments will look like this one", tool.label().to_lowercase()));
|
||||
self.notify_fmt("New {tool} comments will look like this one", &[("tool", &tl!(tool.label()).to_lowercase())]);
|
||||
}
|
||||
|
||||
/// Open Comment Properties for the comment at `(page, index)` of the active document.
|
||||
|
|
@ -131,11 +131,13 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> (boo
|
|||
})
|
||||
.unwrap_or_default();
|
||||
let Some(d) = app.comment_props.as_mut() else { return (false, true) };
|
||||
let kind = crate::comments_panel::subtype_label(&d.edited.subtype).to_string();
|
||||
ui.label(egui::RichText::new(format!("{kind} Properties")).font(theme::semibold(18.0)));
|
||||
let kind = tl!(crate::comments_panel::subtype_label(&d.edited.subtype)).to_string();
|
||||
ui.label(egui::RichText::new(crate::i18n::fmt(tl!("{kind} Properties"), &[("kind", &kind)])).font(theme::semibold(18.0)));
|
||||
ui.add_space(6.0);
|
||||
ui.horizontal(|ui| {
|
||||
for (tab, label) in [(PropsTab::Appearance, "Appearance"), (PropsTab::General, "General"), (PropsTab::ReviewHistory, "Review History")] {
|
||||
for (tab, label) in
|
||||
[(PropsTab::Appearance, tl!("Appearance")), (PropsTab::General, tl!("General")), (PropsTab::ReviewHistory, tl!("Review History"))]
|
||||
{
|
||||
if widgets::mode_tab(ui, label, d.tab == tab).clicked() {
|
||||
d.tab = tab;
|
||||
}
|
||||
|
|
@ -147,12 +149,12 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> (boo
|
|||
match d.tab {
|
||||
PropsTab::Appearance => {
|
||||
if !e.restylable {
|
||||
ui.label(egui::RichText::new("This comment's appearance can't be changed yet.").color(t.text_muted));
|
||||
ui.label(egui::RichText::new(tl!("This comment's appearance can't be changed yet.")).color(t.text_muted));
|
||||
}
|
||||
ui.add_enabled_ui(e.restylable, |ui| {
|
||||
egui::Grid::new("comment-props").num_columns(2).spacing([12.0, 10.0]).show(ui, |ui| {
|
||||
if let Some(icon) = e.icon.as_mut() {
|
||||
ui.label("Icon");
|
||||
ui.label(tl!("Icon"));
|
||||
egui::ComboBox::from_id_salt("note-icon").selected_text(icon.name()).show_ui(ui, |ui| {
|
||||
for i in ICONS {
|
||||
ui.selectable_value(icon, i, i.name());
|
||||
|
|
@ -160,19 +162,19 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> (boo
|
|||
});
|
||||
ui.end_row();
|
||||
}
|
||||
ui.label("Colour");
|
||||
ui.label(tl!("Colour"));
|
||||
if let Some(c) = swatch_grid(ui, e.color) {
|
||||
e.color = Some(c);
|
||||
}
|
||||
ui.end_row();
|
||||
ui.label("Opacity");
|
||||
ui.label(tl!("Opacity"));
|
||||
let mut pct = e.opacity * 100.0;
|
||||
if ui.add(egui::Slider::new(&mut pct, 0.0..=100.0).suffix("%")).changed() {
|
||||
e.opacity = pct / 100.0;
|
||||
}
|
||||
ui.end_row();
|
||||
if let Some(w) = e.width.as_mut() {
|
||||
ui.label("Thickness");
|
||||
ui.label(tl!("Thickness"));
|
||||
ui.add(egui::Slider::new(w, 0.5..=12.0).step_by(0.5).suffix(" pt"));
|
||||
ui.end_row();
|
||||
}
|
||||
|
|
@ -181,34 +183,34 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> (boo
|
|||
}
|
||||
PropsTab::General => {
|
||||
egui::Grid::new("comment-general").num_columns(2).spacing([12.0, 10.0]).show(ui, |ui| {
|
||||
let l = ui.label("Author");
|
||||
let l = ui.label(tl!("Author"));
|
||||
ui.add(egui::TextEdit::singleline(&mut e.author).desired_width(280.0)).labelled_by(l.id);
|
||||
ui.end_row();
|
||||
let l = ui.label("Subject");
|
||||
let l = ui.label(tl!("Subject"));
|
||||
ui.add(egui::TextEdit::singleline(&mut e.subject).desired_width(280.0)).labelled_by(l.id);
|
||||
ui.end_row();
|
||||
ui.label("Modified");
|
||||
ui.label(tl!("Modified"));
|
||||
ui.label(e.modified.as_deref().map(pdfcraft_render::pretty_date).unwrap_or_else(|| "—".into()));
|
||||
ui.end_row();
|
||||
});
|
||||
}
|
||||
PropsTab::ReviewHistory => {
|
||||
if history.is_empty() {
|
||||
ui.label(egui::RichText::new("No status has been set.").color(t.text_muted));
|
||||
ui.label(egui::RichText::new(tl!("No status has been set.")).color(t.text_muted));
|
||||
}
|
||||
for (state, who, when) in &history {
|
||||
ui.label(format!("{state} — {who} {when}"));
|
||||
ui.label(format!("{} — {who} {when}", tl!(state)));
|
||||
}
|
||||
}
|
||||
}
|
||||
ui.add_space(12.0);
|
||||
ui.checkbox(&mut d.edited.locked, "Locked");
|
||||
ui.checkbox(&mut d.edited.locked, tl!("Locked"));
|
||||
let (mut apply, mut cancel) = (false, false);
|
||||
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
|
||||
if widgets::pill_button(ui, "OK", true).clicked() {
|
||||
if widgets::pill_button(ui, tl!("OK"), true).clicked() {
|
||||
apply = true;
|
||||
}
|
||||
if widgets::pill_button(ui, "Cancel", false).clicked() {
|
||||
if widgets::pill_button(ui, tl!("Cancel"), false).clicked() {
|
||||
cancel = true;
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1028,10 +1028,10 @@ pub(crate) fn composer(ctx: &egui::Context, view: &mut DocView, info: &DocInfo,
|
|||
});
|
||||
ui.add_space(6.0);
|
||||
let hint = match c.kind {
|
||||
ComposerKind::TextBox | ComposerKind::Callout { .. } => "Type text",
|
||||
ComposerKind::Caret => "Text to insert",
|
||||
ComposerKind::Replace => "Replacement text",
|
||||
_ => "Add a comment",
|
||||
ComposerKind::TextBox | ComposerKind::Callout { .. } => tl!("Type text"),
|
||||
ComposerKind::Caret => tl!("Text to insert"),
|
||||
ComposerKind::Replace => tl!("Replacement text"),
|
||||
_ => tl!("Add a comment"),
|
||||
};
|
||||
let edit =
|
||||
ui.add(egui::TextEdit::multiline(&mut c.text).hint_text(hint).desired_rows(3).desired_width(f32::INFINITY).id_salt("composer-text"));
|
||||
|
|
@ -1048,13 +1048,16 @@ pub(crate) fn composer(ctx: &egui::Context, view: &mut DocView, info: &DocInfo,
|
|||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
let can_post = !c.text.trim().is_empty() || matches!(c.kind, ComposerKind::Edit(_));
|
||||
if ui
|
||||
.add_enabled(can_post, egui::Button::new(egui::RichText::new("Post").color(Color32::WHITE)).fill(t.accent).corner_radius(14))
|
||||
.add_enabled(
|
||||
can_post,
|
||||
egui::Button::new(egui::RichText::new(tl!("Post")).color(Color32::WHITE)).fill(t.accent).corner_radius(14),
|
||||
)
|
||||
.clicked()
|
||||
|| (enter && can_post)
|
||||
{
|
||||
post = true;
|
||||
}
|
||||
if ui.add(egui::Button::new("Cancel").corner_radius(14)).clicked() {
|
||||
if ui.add(egui::Button::new(tl!("Cancel")).corner_radius(14)).clicked() {
|
||||
cancel = true;
|
||||
}
|
||||
});
|
||||
|
|
@ -1153,7 +1156,7 @@ pub(crate) fn context_menu(ui: &mut egui::Ui, view: &mut DocView, info: &DocInfo
|
|||
match selected {
|
||||
Some(a) => {
|
||||
let (page, index) = (a.page, a.index);
|
||||
if ui.add_enabled(allowed, egui::Button::new("Edit text…")).clicked() {
|
||||
if ui.add_enabled(allowed, egui::Button::new(tl!("Edit text…"))).clicked() {
|
||||
view.comments.composer = Some(Composer {
|
||||
page,
|
||||
at: [a.rect[2] as f64, a.rect[3] as f64],
|
||||
|
|
@ -1163,22 +1166,22 @@ pub(crate) fn context_menu(ui: &mut egui::Ui, view: &mut DocView, info: &DocInfo
|
|||
});
|
||||
ui.close();
|
||||
}
|
||||
if ui.add_enabled(allowed, egui::Button::new("Reply")).clicked() {
|
||||
if ui.add_enabled(allowed, egui::Button::new(tl!("Reply"))).clicked() {
|
||||
view.comments.reveal = true;
|
||||
action = Some(CanvasAction::OpenComments);
|
||||
ui.close();
|
||||
}
|
||||
ui.add_enabled_ui(allowed, |ui| {
|
||||
ui.menu_button("Set status", |ui| {
|
||||
ui.menu_button(tl!("Set status"), |ui| {
|
||||
for s in [ReviewState::None, ReviewState::Accepted, ReviewState::Cancelled, ReviewState::Completed, ReviewState::Rejected] {
|
||||
if ui.button(s.name()).clicked() {
|
||||
if ui.button(tl!(s.name())).clicked() {
|
||||
action =
|
||||
Some(CanvasAction::Edit(Box::new(Edit::SetAnnotationStatus { page, index, state: s, author: prefs.author.clone() })));
|
||||
ui.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
ui.menu_button("Colour", |ui| {
|
||||
ui.menu_button(tl!("Colour"), |ui| {
|
||||
if let Some(c) = swatch_grid(ui, a.color.map(|c| c.map(f64::from))) {
|
||||
action =
|
||||
Some(CanvasAction::Edit(Box::new(Edit::StyleAnnotation { page, index, color: Some(c), opacity: None, width: None })));
|
||||
|
|
@ -1188,37 +1191,37 @@ pub(crate) fn context_menu(ui: &mut egui::Ui, view: &mut DocView, info: &DocInfo
|
|||
});
|
||||
let thread: Vec<&pdfcraft_render::Annotation> = info.annotations.iter().filter(|r| a.name.is_some() && r.in_reply_to == a.name).collect();
|
||||
let marked = crate::comments_panel::is_marked(&thread);
|
||||
if ui.add_enabled(allowed, egui::Button::new(if marked { "Remove checkmark" } else { "Mark with checkmark" })).clicked() {
|
||||
if ui.add_enabled(allowed, egui::Button::new(tl!(if marked { "Remove checkmark" } else { "Mark with checkmark" }))).clicked() {
|
||||
action = Some(CanvasAction::Edit(Box::new(Edit::MarkAnnotation { page, index, marked: !marked, author: prefs.author.clone() })));
|
||||
ui.close();
|
||||
}
|
||||
if ui.button("Copy text").clicked() {
|
||||
if ui.button(tl!("Copy text")).clicked() {
|
||||
ui.ctx().copy_text(a.contents.clone().unwrap_or_default());
|
||||
ui.close();
|
||||
}
|
||||
ui.separator();
|
||||
if ui.add_enabled(allowed && !a.locked, egui::Button::new("Delete")).clicked() {
|
||||
if ui.add_enabled(allowed && !a.locked, egui::Button::new(tl!("Delete"))).clicked() {
|
||||
view.comments.selected = None;
|
||||
action = Some(CanvasAction::Edit(Box::new(Edit::DeleteAnnotation { page, index })));
|
||||
ui.close();
|
||||
}
|
||||
if ui.add_enabled(allowed, egui::Button::new("Properties…")).clicked() {
|
||||
if ui.add_enabled(allowed, egui::Button::new(tl!("Properties…"))).clicked() {
|
||||
action = Some(CanvasAction::Properties(page, index));
|
||||
ui.close();
|
||||
}
|
||||
if ui.add_enabled(tool_for(a).is_some(), egui::Button::new("Make Current Properties Default")).clicked() {
|
||||
if ui.add_enabled(tool_for(a).is_some(), egui::Button::new(tl!("Make Current Properties Default"))).clicked() {
|
||||
view.comments.default_request = Some((page, index));
|
||||
ui.close();
|
||||
}
|
||||
}
|
||||
None => {
|
||||
if let Some((page, at)) = view.comments.context_at
|
||||
&& ui.add_enabled(allowed, egui::Button::new("Add a sticky note here")).clicked()
|
||||
&& ui.add_enabled(allowed, egui::Button::new(tl!("Add a sticky note here"))).clicked()
|
||||
{
|
||||
view.comments.composer = Some(Composer { page, at, kind: ComposerKind::Note, text: String::new(), focus: true });
|
||||
ui.close();
|
||||
}
|
||||
if ui.button("Comments panel").clicked() {
|
||||
if ui.button(tl!("Comments panel")).clicked() {
|
||||
action = Some(CanvasAction::OpenComments);
|
||||
ui.close();
|
||||
}
|
||||
|
|
@ -1248,7 +1251,7 @@ pub fn swatch_grid(ui: &mut egui::Ui, current: Option<Rgb>) -> Option<Rgb> {
|
|||
if on || resp.hovered() {
|
||||
ui.painter().circle_stroke(r.center(), 11.0, Stroke::new(1.5, SELECT_BLUE));
|
||||
}
|
||||
if resp.on_hover_text(*name).clicked() {
|
||||
if resp.on_hover_text(tl!(name)).clicked() {
|
||||
picked = Some(*c);
|
||||
}
|
||||
if i % 5 == 4 {
|
||||
|
|
@ -1262,34 +1265,34 @@ pub fn swatch_grid(ui: &mut egui::Ui, current: Option<Rgb>) -> Option<Rgb> {
|
|||
/// The comment tools' extra quick-bar controls: pin, colour, opacity and thickness.
|
||||
pub(crate) fn quick_bar_controls(ui: &mut egui::Ui, tool: CommentTool, prefs: &mut CommentPrefs) {
|
||||
let style = prefs.style(tool);
|
||||
if icons::button(ui, "pin", 32.0, prefs.pinned, if prefs.pinned { "Keep tool selected: on" } else { "Keep tool selected" }).clicked() {
|
||||
if icons::button(ui, "pin", 32.0, prefs.pinned, if prefs.pinned { tl!("Keep tool selected: on") } else { tl!("Keep tool selected") }).clicked() {
|
||||
prefs.pinned = !prefs.pinned;
|
||||
}
|
||||
let (r, resp) = ui.allocate_exact_size(vec2(32.0, 32.0), Sense::click());
|
||||
ui.painter().circle_filled(r.center(), 9.0, color32(style.color));
|
||||
ui.painter().circle_stroke(r.center(), 9.0, Stroke::new(1.0, Color32::from_black_alpha(50)));
|
||||
let resp = resp.on_hover_text("Colour");
|
||||
let resp = resp.on_hover_text(tl!("Colour"));
|
||||
egui::Popup::menu(&resp).align(egui::RectAlign::RIGHT_START).show(|ui| {
|
||||
if let Some(c) = swatch_grid(ui, Some(style.color)) {
|
||||
prefs.set_color(tool, c);
|
||||
ui.close();
|
||||
}
|
||||
});
|
||||
let resp = icons::button(ui, "blend", 32.0, false, "Opacity");
|
||||
let resp = icons::button(ui, "blend", 32.0, false, tl!("Opacity"));
|
||||
egui::Popup::menu(&resp).align(egui::RectAlign::RIGHT_START).close_behavior(egui::PopupCloseBehavior::CloseOnClickOutside).show(|ui| {
|
||||
ui.set_min_width(180.0);
|
||||
let mut percent = (style.opacity * 100.0).round();
|
||||
ui.label(egui::RichText::new("Opacity").font(theme::semibold(12.0)));
|
||||
ui.label(egui::RichText::new(tl!("Opacity")).font(theme::semibold(12.0)));
|
||||
if ui.add(egui::Slider::new(&mut percent, 10.0..=100.0).step_by(5.0).suffix(" %")).changed() {
|
||||
prefs.set_opacity(tool, percent / 100.0);
|
||||
}
|
||||
});
|
||||
if tool.has_width() {
|
||||
let resp = icons::button(ui, "sliders-horizontal", 32.0, false, "Line thickness");
|
||||
let resp = icons::button(ui, "sliders-horizontal", 32.0, false, tl!("Line thickness"));
|
||||
egui::Popup::menu(&resp).align(egui::RectAlign::RIGHT_START).close_behavior(egui::PopupCloseBehavior::CloseOnClickOutside).show(|ui| {
|
||||
ui.set_min_width(180.0);
|
||||
let mut w = style.width;
|
||||
ui.label(egui::RichText::new("Line thickness").font(theme::semibold(12.0)));
|
||||
ui.label(egui::RichText::new(tl!("Line thickness")).font(theme::semibold(12.0)));
|
||||
if ui.add(egui::Slider::new(&mut w, 0.5..=12.0).step_by(0.5).suffix(" pt")).changed() {
|
||||
prefs.set_width(tool, w);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ pub(crate) fn show(
|
|||
let mut edit = None;
|
||||
// Search (the header's magnifier toggles it).
|
||||
if let Some(q) = view.comments.search.as_mut() {
|
||||
let r = ui.add(egui::TextEdit::singleline(q).hint_text("Search comments").desired_width(f32::INFINITY).id_salt("comment-search"));
|
||||
let r = ui.add(egui::TextEdit::singleline(q).hint_text(tl!("Search comments")).desired_width(f32::INFINITY).id_salt("comment-search"));
|
||||
if view.comments.search_focus {
|
||||
r.request_focus();
|
||||
view.comments.search_focus = false;
|
||||
|
|
@ -118,7 +118,7 @@ pub(crate) fn show(
|
|||
if allowed {
|
||||
let r = ui.add(
|
||||
egui::TextEdit::singleline(&mut view.comments.add_box)
|
||||
.hint_text("Add a comment")
|
||||
.hint_text(tl!("Add a comment"))
|
||||
.desired_width(f32::INFINITY)
|
||||
.margin(egui::Margin::symmetric(8, 6))
|
||||
.id_salt("comment-add-box"),
|
||||
|
|
@ -144,9 +144,9 @@ pub(crate) fn show(
|
|||
ui.vertical_centered(|ui| {
|
||||
ui.add(icons::image("message-square-text", 36.0, t.text_faint));
|
||||
ui.add_space(8.0);
|
||||
ui.label(egui::RichText::new("No comments yet.").color(t.text_muted));
|
||||
ui.label(egui::RichText::new(tl!("No comments yet.")).color(t.text_muted));
|
||||
if allowed {
|
||||
ui.label(egui::RichText::new("Pick a tool in the quick bar, or type above.").color(t.text_faint).small());
|
||||
ui.label(egui::RichText::new(tl!("Pick a tool in the quick bar, or type above.")).color(t.text_faint).small());
|
||||
}
|
||||
});
|
||||
return edit;
|
||||
|
|
@ -181,12 +181,16 @@ pub(crate) fn show(
|
|||
SortBy::Color => roots.sort_by_key(|a| color_key(a)),
|
||||
}
|
||||
if roots.is_empty() {
|
||||
ui.label(egui::RichText::new("No comments match.").color(t.text_muted));
|
||||
ui.label(egui::RichText::new(tl!("No comments match.")).color(t.text_muted));
|
||||
}
|
||||
// Group headers follow the sort key (no headers when sorted by date).
|
||||
// `lang` is copied up front so the closure below never borrows `ui`.
|
||||
let group_of = |a: &Annotation| -> Option<String> {
|
||||
match sort {
|
||||
SortBy::Page => Some(format!("Page {}", info.pages.get(a.page).map(|p| p.label.as_str()).unwrap_or("?"))),
|
||||
SortBy::Page => {
|
||||
let label = info.pages.get(a.page).map(|p| p.label.as_str()).unwrap_or("?");
|
||||
Some(crate::i18n::fmt(tl!("Page {label}"), &[("label", label)]))
|
||||
}
|
||||
SortBy::Author => Some(a.author.clone().unwrap_or_else(|| "Unknown author".into())),
|
||||
SortBy::Type => Some(kind_label(a).to_string()),
|
||||
SortBy::Color => Some(if a.color.is_some() { format!("#{}", color_key(a)) } else { "No colour".into() }),
|
||||
|
|
@ -200,7 +204,9 @@ pub(crate) fn show(
|
|||
let g = group_of(a);
|
||||
if g.is_some() && g != group {
|
||||
ui.add_space(6.0);
|
||||
ui.label(egui::RichText::new(g.clone().unwrap_or_default()).font(theme::semibold(12.5)).color(t.text_muted));
|
||||
let title = g.clone().unwrap_or_default();
|
||||
let shown = if sort == SortBy::Author && a.author.is_some() { title.as_str() } else { tl!(&title) };
|
||||
ui.label(egui::RichText::new(shown).font(theme::semibold(12.5)).color(t.text_muted));
|
||||
group = g;
|
||||
}
|
||||
} else if a.page != page {
|
||||
|
|
@ -209,7 +215,11 @@ pub(crate) fn show(
|
|||
ui.add_space(6.0);
|
||||
ui.horizontal(|ui| {
|
||||
ui.add(icons::image("chevron-down", 14.0, t.text_muted));
|
||||
ui.label(egui::RichText::new(format!("Page {}", info.pages[page].label)).font(theme::semibold(12.5)).color(t.text_muted));
|
||||
ui.label(
|
||||
egui::RichText::new(crate::i18n::fmt(tl!("Page {label}"), &[("label", &info.pages[page].label)]))
|
||||
.font(theme::semibold(12.5))
|
||||
.color(t.text_muted),
|
||||
);
|
||||
ui.with_layout(Layout::right_to_left(Align::Center), |ui| ui.label(egui::RichText::new(n.to_string()).color(t.text_faint).small()));
|
||||
});
|
||||
ui.add_space(2.0);
|
||||
|
|
@ -253,27 +263,27 @@ fn card(
|
|||
let mut buttons_left = f32::INFINITY;
|
||||
let head = ui.horizontal(|ui| {
|
||||
avatar(ui, t, Some(subtype_icon(&a.subtype)), None, color);
|
||||
ui.label(egui::RichText::new(a.author.as_deref().unwrap_or("Unknown author")).font(theme::semibold(12.5)).color(t.text));
|
||||
ui.label(egui::RichText::new(a.author.as_deref().unwrap_or_else(|| tl!("Unknown author"))).font(theme::semibold(12.5)).color(t.text));
|
||||
if let Some(m) = &a.modified {
|
||||
ui.add(egui::Label::new(egui::RichText::new(m).font(theme::regular(11.0)).color(t.text_faint)).truncate());
|
||||
}
|
||||
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
|
||||
if selected && allowed {
|
||||
let more = icons::button(ui, "ellipsis", 22.0, false, "More");
|
||||
let more = icons::button(ui, "ellipsis", 22.0, false, tl!("More"));
|
||||
open_menu = more.clicked();
|
||||
more_rect = Some(more.rect);
|
||||
}
|
||||
// Acrobat's per-comment checkmark: a toggle on the selected card, a mark otherwise.
|
||||
let tip = if marked { "Remove checkmark" } else { "Mark with checkmark" };
|
||||
let tip = if marked { tl!("Remove checkmark") } else { tl!("Mark with checkmark") };
|
||||
if selected && allowed {
|
||||
if icons::button(ui, "circle-check", 22.0, marked, tip).clicked() {
|
||||
edit = Some(Edit::MarkAnnotation { page: a.page, index: a.index, marked: !marked, author: prefs.author.clone() });
|
||||
}
|
||||
} else if marked {
|
||||
ui.add(icons::image("circle-check", 14.0, t.accent)).on_hover_text("Marked");
|
||||
ui.add(icons::image("circle-check", 14.0, t.accent)).on_hover_text(tl!("Marked"));
|
||||
}
|
||||
if a.locked {
|
||||
ui.add(icons::image("lock", 13.0, t.text_faint)).on_hover_text("Locked");
|
||||
ui.add(icons::image("lock", 13.0, t.text_faint)).on_hover_text(tl!("Locked"));
|
||||
}
|
||||
buttons_left = ui.min_rect().left();
|
||||
});
|
||||
|
|
@ -286,19 +296,21 @@ fn card(
|
|||
r.request_focus();
|
||||
}
|
||||
ui.horizontal(|ui| {
|
||||
if ui.add(egui::Button::new(egui::RichText::new("Save").color(Color32::WHITE)).fill(t.accent).corner_radius(12)).clicked()
|
||||
if ui
|
||||
.add(egui::Button::new(egui::RichText::new(tl!("Save")).color(Color32::WHITE)).fill(t.accent).corner_radius(12))
|
||||
.clicked()
|
||||
&& let Some((page, index, text)) = view.comments.editing.take()
|
||||
{
|
||||
edit = Some(Edit::SetAnnotationContents { page, index, text });
|
||||
}
|
||||
if ui.button("Cancel").clicked() || ui.input(|i| i.key_pressed(egui::Key::Escape)) {
|
||||
if ui.button(tl!("Cancel")).clicked() || ui.input(|i| i.key_pressed(egui::Key::Escape)) {
|
||||
view.comments.editing = None;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
let body = a.contents.as_deref().unwrap_or("");
|
||||
let text = if body.is_empty() {
|
||||
egui::RichText::new(kind_label(a)).italics().color(t.text_faint)
|
||||
egui::RichText::new(tl!(kind_label(a))).italics().color(t.text_faint)
|
||||
} else {
|
||||
egui::RichText::new(body).color(t.text)
|
||||
};
|
||||
|
|
@ -312,7 +324,7 @@ fn card(
|
|||
ui.add_space(4.0);
|
||||
ui.horizontal(|ui| {
|
||||
ui.add(icons::image(icon, 13.0, c));
|
||||
ui.label(egui::RichText::new(label).font(theme::medium(11.5)).color(c));
|
||||
ui.label(egui::RichText::new(tl!(label)).font(theme::medium(11.5)).color(c));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -324,7 +336,7 @@ fn card(
|
|||
ui.vertical(|ui| {
|
||||
ui.spacing_mut().item_spacing.y = 2.0;
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(egui::RichText::new(r.author.as_deref().unwrap_or("Reply")).font(theme::semibold(12.0)));
|
||||
ui.label(egui::RichText::new(r.author.as_deref().unwrap_or_else(|| tl!("Reply"))).font(theme::semibold(12.0)));
|
||||
if let Some(m) = &r.modified {
|
||||
ui.add(egui::Label::new(egui::RichText::new(m).font(theme::regular(11.0)).color(t.text_faint)).truncate());
|
||||
}
|
||||
|
|
@ -344,13 +356,13 @@ fn card(
|
|||
ui.horizontal(|ui| {
|
||||
let r = ui.add(
|
||||
egui::TextEdit::singleline(&mut view.comments.reply)
|
||||
.hint_text("Add a reply")
|
||||
.hint_text(tl!("Add a reply"))
|
||||
.desired_width(ui.available_width() - 56.0)
|
||||
.id_salt(("comment-reply", key)),
|
||||
);
|
||||
let enter = r.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter));
|
||||
let ok = !view.comments.reply.trim().is_empty();
|
||||
if (ui.add_enabled(ok, egui::Button::new("Post").corner_radius(12)).clicked() || enter) && ok {
|
||||
if (ui.add_enabled(ok, egui::Button::new(tl!("Post")).corner_radius(12)).clicked() || enter) && ok {
|
||||
let text = std::mem::take(&mut view.comments.reply).trim().to_string();
|
||||
edit = Some(Edit::ReplyToAnnotation { page: a.page, index: a.index, text, author: prefs.author.clone() });
|
||||
}
|
||||
|
|
@ -378,7 +390,7 @@ fn card(
|
|||
let click = ui
|
||||
.interact(hit, ui.id().with(("card", key)), Sense::click())
|
||||
.on_hover_cursor(egui::CursorIcon::PointingHand)
|
||||
.on_hover_text(format!("{} — click to show it on the page", kind_label(a)));
|
||||
.on_hover_text(crate::i18n::fmt(tl!("{kind} — click to show it on the page"), &[("kind", tl!(kind_label(a)))]));
|
||||
if click.clicked() {
|
||||
view.comments.selected = Some(key);
|
||||
view.comments.reply.clear();
|
||||
|
|
@ -399,36 +411,36 @@ fn card(
|
|||
.layout(Layout::top_down_justified(Align::Min))
|
||||
.show(|ui| {
|
||||
ui.set_min_width(160.0);
|
||||
if ui.button("Edit").clicked() {
|
||||
if ui.button(tl!("Edit")).clicked() {
|
||||
view.comments.editing = Some((a.page, a.index, a.contents.clone().unwrap_or_default()));
|
||||
ui.close();
|
||||
}
|
||||
ui.menu_button("Set status", |ui| {
|
||||
ui.menu_button(tl!("Set status"), |ui| {
|
||||
for s in [ReviewState::None, ReviewState::Accepted, ReviewState::Cancelled, ReviewState::Completed, ReviewState::Rejected] {
|
||||
if ui.button(s.name()).clicked() {
|
||||
if ui.button(tl!(s.name())).clicked() {
|
||||
edit = Some(Edit::SetAnnotationStatus { page: a.page, index: a.index, state: s, author: prefs.author.clone() });
|
||||
ui.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
if ui.button(if marked { "Remove checkmark" } else { "Mark with checkmark" }).clicked() {
|
||||
if ui.button(tl!(if marked { "Remove checkmark" } else { "Mark with checkmark" })).clicked() {
|
||||
edit = Some(Edit::MarkAnnotation { page: a.page, index: a.index, marked: !marked, author: prefs.author.clone() });
|
||||
ui.close();
|
||||
}
|
||||
if ui.button("Copy text").clicked() {
|
||||
if ui.button(tl!("Copy text")).clicked() {
|
||||
ui.ctx().copy_text(a.contents.clone().unwrap_or_default());
|
||||
ui.close();
|
||||
}
|
||||
if ui.button("Properties…").clicked() {
|
||||
if ui.button(tl!("Properties…")).clicked() {
|
||||
view.comments.props_request = Some((a.page, a.index));
|
||||
ui.close();
|
||||
}
|
||||
if ui.add_enabled(crate::comments::tool_for(a).is_some(), egui::Button::new("Make Current Properties Default")).clicked() {
|
||||
if ui.add_enabled(crate::comments::tool_for(a).is_some(), egui::Button::new(tl!("Make Current Properties Default"))).clicked() {
|
||||
view.comments.default_request = Some((a.page, a.index));
|
||||
ui.close();
|
||||
}
|
||||
ui.separator();
|
||||
if ui.add_enabled(!a.locked, egui::Button::new("Delete")).clicked() {
|
||||
if ui.add_enabled(!a.locked, egui::Button::new(tl!("Delete"))).clicked() {
|
||||
view.comments.selected = None;
|
||||
edit = Some(Edit::DeleteAnnotation { page: a.page, index: a.index });
|
||||
ui.close();
|
||||
|
|
@ -482,13 +494,17 @@ pub(crate) fn header_controls(ui: &mut egui::Ui, info: &DocInfo, view: &mut DocV
|
|||
&& cv.hidden_statuses.is_empty()
|
||||
&& cv.hidden_colors.is_empty()
|
||||
&& cv.hidden_checks.is_empty());
|
||||
let more = icons::button(ui, "ellipsis", 26.0, false, "More options");
|
||||
let more = icons::button(ui, "ellipsis", 26.0, false, tl!("More options"));
|
||||
egui::Popup::menu(&more).show(|ui| {
|
||||
ui.set_min_width(220.0);
|
||||
ui.label(egui::RichText::new("Sort by").small());
|
||||
for (s, label) in
|
||||
[(SortBy::Page, "Page"), (SortBy::Author, "Author"), (SortBy::Date, "Date"), (SortBy::Type, "Type"), (SortBy::Color, "Colour")]
|
||||
{
|
||||
ui.label(egui::RichText::new(tl!("Sort by")).small());
|
||||
for (s, label) in [
|
||||
(SortBy::Page, tl!("Page")),
|
||||
(SortBy::Author, tl!("Author")),
|
||||
(SortBy::Date, tl!("Date")),
|
||||
(SortBy::Type, tl!("Type")),
|
||||
(SortBy::Color, tl!("Colour")),
|
||||
] {
|
||||
if ui.radio(cv.sort == s, label).clicked() {
|
||||
cv.sort = s;
|
||||
ui.close();
|
||||
|
|
@ -496,18 +512,18 @@ pub(crate) fn header_controls(ui: &mut egui::Ui, info: &DocInfo, view: &mut DocV
|
|||
}
|
||||
ui.separator();
|
||||
for (id, label) in [
|
||||
("comment.hide_all", if hidden { "Show all comments" } else { "Hide all comments" }),
|
||||
("comment.summarize", "Create PDF comment summary…"),
|
||||
("comment.export", "Export all to data file…"),
|
||||
("comment.import", "Import comments…"),
|
||||
("comment.hide_all", if hidden { tl!("Show all comments") } else { tl!("Hide all comments") }),
|
||||
("comment.summarize", tl!("Create PDF comment summary…")),
|
||||
("comment.export", tl!("Export all to data file…")),
|
||||
("comment.import", tl!("Import comments…")),
|
||||
] {
|
||||
if ui.button(label).clicked() {
|
||||
if ui.button(tl!(label)).clicked() {
|
||||
command = Some(id);
|
||||
ui.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
let filter = icons::button(ui, "filter", 26.0, active, "Filter comments");
|
||||
let filter = icons::button(ui, "filter", 26.0, active, tl!("Filter comments"));
|
||||
egui::Popup::menu(&filter).close_behavior(egui::PopupCloseBehavior::CloseOnClickOutside).show(|ui| {
|
||||
ui.set_min_width(200.0);
|
||||
let toggle = |ui: &mut egui::Ui, list: &mut Vec<String>, value: &str, label: &str| {
|
||||
|
|
@ -520,29 +536,29 @@ pub(crate) fn header_controls(ui: &mut egui::Ui, info: &DocInfo, view: &mut DocV
|
|||
}
|
||||
}
|
||||
};
|
||||
ui.label(egui::RichText::new("Type").small());
|
||||
ui.label(egui::RichText::new(tl!("Type")).small());
|
||||
for ty in &types {
|
||||
toggle(ui, &mut cv.hidden_types, ty, ty);
|
||||
toggle(ui, &mut cv.hidden_types, ty, tl!(ty));
|
||||
}
|
||||
ui.separator();
|
||||
ui.label(egui::RichText::new("Author").small());
|
||||
ui.label(egui::RichText::new(tl!("Author")).small());
|
||||
for a in &authors {
|
||||
toggle(ui, &mut cv.hidden_authors, a, if a.is_empty() { "Unknown author" } else { a });
|
||||
toggle(ui, &mut cv.hidden_authors, a, if a.is_empty() { tl!("Unknown author") } else { a });
|
||||
}
|
||||
ui.separator();
|
||||
ui.label(egui::RichText::new("Status").small());
|
||||
ui.label(egui::RichText::new(tl!("Status")).small());
|
||||
for s in ["None", "Accepted", "Rejected", "Cancelled", "Completed"] {
|
||||
toggle(ui, &mut cv.hidden_statuses, s, s);
|
||||
toggle(ui, &mut cv.hidden_statuses, s, tl!(s));
|
||||
}
|
||||
ui.separator();
|
||||
ui.label(egui::RichText::new("Colour").small());
|
||||
ui.label(egui::RichText::new(tl!("Colour")).small());
|
||||
for (key, c) in &colors {
|
||||
ui.horizontal(|ui| {
|
||||
let name = crate::comments::SWATCHES
|
||||
.iter()
|
||||
.find(|(_, s)| c.is_some_and(|c| (0..3).all(|i| (s[i] as f32 - c[i]).abs() < 0.02)))
|
||||
.map(|(n, _)| n.to_string())
|
||||
.unwrap_or_else(|| if key.is_empty() { "No colour".into() } else { format!("#{key}") });
|
||||
.map(|(n, _)| tl!(n).to_string())
|
||||
.unwrap_or_else(|| if key.is_empty() { tl!("No colour").to_string() } else { format!("#{key}") });
|
||||
toggle(ui, &mut cv.hidden_colors, key, &name);
|
||||
if let Some(c) = c {
|
||||
let (r, _) = ui.allocate_exact_size(vec2(12.0, 12.0), Sense::hover());
|
||||
|
|
@ -555,11 +571,11 @@ pub(crate) fn header_controls(ui: &mut egui::Ui, info: &DocInfo, view: &mut DocV
|
|||
});
|
||||
}
|
||||
ui.separator();
|
||||
ui.label(egui::RichText::new("Checkmark").small());
|
||||
ui.label(egui::RichText::new(tl!("Checkmark")).small());
|
||||
for s in ["Checked", "Unchecked"] {
|
||||
toggle(ui, &mut cv.hidden_checks, s, s);
|
||||
toggle(ui, &mut cv.hidden_checks, s, tl!(s));
|
||||
}
|
||||
if active && ui.button("Show all").clicked() {
|
||||
if active && ui.button(tl!("Show all")).clicked() {
|
||||
cv.hidden_types.clear();
|
||||
cv.hidden_authors.clear();
|
||||
cv.hidden_statuses.clear();
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ pub fn colour(kind: Kind) -> Color32 {
|
|||
|
||||
/// The Compare Files dialog: pick the older document. Returns (compare, cancel).
|
||||
pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> (bool, bool) {
|
||||
ui.label(egui::RichText::new("Compare Files").font(theme::semibold(18.0)));
|
||||
ui.label(egui::RichText::new(tl!("Compare Files")).font(theme::semibold(18.0)));
|
||||
ui.add_space(8.0);
|
||||
let Some((_, new)) = app.active_ids() else { return (false, true) };
|
||||
let others: Vec<(DocId, String)> =
|
||||
|
|
@ -47,28 +47,29 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> (boo
|
|||
}
|
||||
let new_name = app.session.get(new).map(|d| d.name.clone()).unwrap_or_default();
|
||||
egui::Grid::new("compare-files").num_columns(2).spacing([12.0, 10.0]).show(ui, |ui| {
|
||||
ui.label("Old file:");
|
||||
let shown = others.iter().find(|(id, _)| Some(*id) == app.compare_old).map_or("Open the older version first", |(_, n)| n.as_str());
|
||||
ui.label(tl!("Old file:"));
|
||||
let shown =
|
||||
others.iter().find(|(id, _)| Some(*id) == app.compare_old).map_or(tl!("Open the older version first").to_string(), |(_, n)| n.clone());
|
||||
egui::ComboBox::from_id_salt("compare-old").selected_text(shown).width(320.0).show_ui(ui, |ui| {
|
||||
for (id, n) in &others {
|
||||
ui.selectable_value(&mut app.compare_old, Some(*id), n);
|
||||
}
|
||||
});
|
||||
ui.end_row();
|
||||
ui.label("New file:");
|
||||
ui.label(tl!("New file:"));
|
||||
ui.label(egui::RichText::new(new_name).strong());
|
||||
ui.end_row();
|
||||
});
|
||||
ui.add_space(4.0);
|
||||
ui.label(egui::RichText::new("Text is compared word by word; the differences are shown on the new file.").small().color(t.text_muted));
|
||||
ui.label(egui::RichText::new(tl!("Text is compared word by word; the differences are shown on the new file.")).small().color(t.text_muted));
|
||||
ui.add_space(10.0);
|
||||
let (mut go, mut cancel) = (false, false);
|
||||
ui.horizontal(|ui| {
|
||||
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
|
||||
if ui.add_enabled_ui(app.compare_old.is_some(), |ui| widgets::pill_button(ui, "Compare", true)).inner.clicked() {
|
||||
if ui.add_enabled_ui(app.compare_old.is_some(), |ui| widgets::pill_button(ui, tl!("Compare"), true)).inner.clicked() {
|
||||
go = true;
|
||||
}
|
||||
if widgets::pill_button(ui, "Cancel", false).clicked() {
|
||||
if widgets::pill_button(ui, tl!("Cancel"), false).clicked() {
|
||||
cancel = true;
|
||||
}
|
||||
})
|
||||
|
|
@ -79,33 +80,33 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> (boo
|
|||
/// The Compare panel.
|
||||
pub(crate) fn panel(ui: &mut egui::Ui, t: &Tokens, state: &Option<CompareState>, id: DocId) -> Option<PanelAction> {
|
||||
let Some(s) = state.as_ref().filter(|s| s.new == id) else {
|
||||
ui.label(egui::RichText::new("Compare this file with an older version: Compare files… in All tools.").color(t.text_muted));
|
||||
ui.label(egui::RichText::new(tl!("Compare this file with an older version: Compare files… in All tools.")).color(t.text_muted));
|
||||
return None;
|
||||
};
|
||||
let mut action = None;
|
||||
let c = &s.result;
|
||||
if c.identical() {
|
||||
ui.label("No differences in the text.");
|
||||
ui.label(tl!("No differences in the text."));
|
||||
}
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
for k in [Kind::Replaced, Kind::Inserted, Kind::Deleted] {
|
||||
let (dot, _) = ui.allocate_exact_size(egui::vec2(10.0, 10.0), egui::Sense::hover());
|
||||
ui.painter().rect_filled(dot, egui::CornerRadius::same(2), colour(k));
|
||||
ui.label(format!("{} {}", c.count(k), k.label()));
|
||||
ui.label(crate::i18n::fmt(tl!("{n} {kind}"), &[("n", &c.count(k).to_string()), ("kind", tl!(k.label()))]));
|
||||
}
|
||||
let (dot, _) = ui.allocate_exact_size(egui::vec2(10.0, 10.0), egui::Sense::hover());
|
||||
ui.painter().rect_filled(dot, egui::CornerRadius::same(2), VISUAL);
|
||||
ui.label(format!("{} Visual", s.visual.len()));
|
||||
ui.label(crate::i18n::fmt(tl!("{n} {kind}"), &[("n", &s.visual.len().to_string()), ("kind", tl!("Visual"))]));
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
ui.horizontal(|ui| {
|
||||
if ui.button("Mark as comments").clicked() {
|
||||
if ui.button(tl!("Mark as comments")).clicked() {
|
||||
action = Some(PanelAction::Mark);
|
||||
}
|
||||
if ui.button("Report…").clicked() {
|
||||
if ui.button(tl!("Report…")).clicked() {
|
||||
action = Some(PanelAction::Report);
|
||||
}
|
||||
if ui.button("Clear").clicked() {
|
||||
if ui.button(tl!("Clear")).clicked() {
|
||||
action = Some(PanelAction::Clear);
|
||||
}
|
||||
});
|
||||
|
|
@ -122,7 +123,14 @@ pub(crate) fn panel(ui: &mut egui::Ui, t: &Tokens, state: &Option<CompareState>,
|
|||
let (bar, _) = ui.allocate_exact_size(egui::vec2(4.0, 34.0), egui::Sense::hover());
|
||||
ui.painter().rect_filled(bar, egui::CornerRadius::same(2), colour(ch.kind));
|
||||
ui.vertical(|ui| {
|
||||
ui.label(egui::RichText::new(format!("{} · page {}", ch.kind.label(), ch.new.page + 1)).small().color(t.text_muted));
|
||||
ui.label(
|
||||
egui::RichText::new(crate::i18n::fmt(
|
||||
tl!("{kind} · page {p}"),
|
||||
&[("kind", tl!(ch.kind.label())), ("p", &(ch.new.page + 1).to_string())],
|
||||
))
|
||||
.small()
|
||||
.color(t.text_muted),
|
||||
);
|
||||
ui.selectable_label(selected, text)
|
||||
})
|
||||
.inner
|
||||
|
|
@ -162,12 +170,14 @@ impl PdfCraftApp {
|
|||
self.compare = Some(crate::compare_ui::CompareState { old, new, result, visual, selected: None });
|
||||
self.right = Some(RightPanel::Compare);
|
||||
self.notify(if n == 0 {
|
||||
"The text is the same".to_string()
|
||||
tl!("The text is the same").to_string()
|
||||
} else if n == 1 {
|
||||
tl!("1 difference found").to_string()
|
||||
} else {
|
||||
format!("{n} difference{} found", if n == 1 { "" } else { "s" })
|
||||
crate::i18n::fmt(tl!("{n} differences found"), &[("n", &n.to_string())])
|
||||
});
|
||||
}
|
||||
Err(e) => self.notify(e.to_string()),
|
||||
Err(e) => self.notify_error(e),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -189,17 +199,21 @@ impl PdfCraftApp {
|
|||
if let Some(info) = self.session.get(new).map(|d| d.info.clone()) {
|
||||
self.views[index].document_changed(&info);
|
||||
}
|
||||
self.notify(format!("Added {n} comment{}", if n == 1 { "" } else { "s" }));
|
||||
self.notify(if n == 1 {
|
||||
tl!("Added 1 comment").to_string()
|
||||
} else {
|
||||
crate::i18n::fmt(tl!("Added {n} comments"), &[("n", &n.to_string())])
|
||||
});
|
||||
}
|
||||
Err(e) => self.notify(e.to_string()),
|
||||
Err(e) => self.notify_error(e),
|
||||
},
|
||||
PanelAction::Report => match self.session.compare_report(old, new) {
|
||||
Ok(bytes) => {
|
||||
if let Err(e) = self.open_bytes("Compare Report.pdf", None, bytes.to_vec()) {
|
||||
self.notify(e);
|
||||
self.notify_error(e);
|
||||
}
|
||||
}
|
||||
Err(e) => self.notify(e.to_string()),
|
||||
Err(e) => self.notify_error(e),
|
||||
},
|
||||
PanelAction::Clear => {
|
||||
self.compare = None;
|
||||
|
|
|
|||
|
|
@ -289,7 +289,7 @@ pub(crate) fn editor(ctx: &egui::Context, view: &mut DocView, info: &DocInfo, ad
|
|||
.desired_rows(1)
|
||||
.background_color(Color32::from_rgba_unmultiplied(255, 255, 255, 235))
|
||||
.text_color(Color32::from_rgb(cr, cg, cb))
|
||||
.hint_text("Type text")
|
||||
.hint_text(tl!("Type text"))
|
||||
.id_salt("added-text-edit"),
|
||||
);
|
||||
if t.focus {
|
||||
|
|
@ -350,7 +350,7 @@ fn finish(cv: &mut ContentView, d: TextDraft, added: &[Added]) -> Option<Edit> {
|
|||
/// Returns the changed style; for a selected item the caller turns it into an update.
|
||||
pub(crate) fn format_panel(ui: &mut egui::Ui, t: &Tokens, style: &AddedText) -> Option<AddedText> {
|
||||
let mut s = style.clone();
|
||||
widgets::section_title(ui, "Format text");
|
||||
widgets::section_title(ui, tl!("Format text"));
|
||||
ui.horizontal(|ui| {
|
||||
egui::ComboBox::from_id_salt("font-family").selected_text(s.family.label()).width(110.0).show_ui(ui, |ui| {
|
||||
for f in [FontFamily::Helvetica, FontFamily::Times, FontFamily::Courier] {
|
||||
|
|
@ -365,18 +365,18 @@ pub(crate) fn format_panel(ui: &mut egui::Ui, t: &Tokens, style: &AddedText) ->
|
|||
});
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
if ui.selectable_label(s.bold, egui::RichText::new("B").strong()).on_hover_text("Bold").clicked() {
|
||||
if ui.selectable_label(s.bold, egui::RichText::new("B").strong()).on_hover_text(tl!("Bold")).clicked() {
|
||||
s.bold = !s.bold;
|
||||
}
|
||||
if ui.selectable_label(s.italic, egui::RichText::new("I").italics()).on_hover_text("Italic").clicked() {
|
||||
if ui.selectable_label(s.italic, egui::RichText::new("I").italics()).on_hover_text(tl!("Italic")).clicked() {
|
||||
s.italic = !s.italic;
|
||||
}
|
||||
ui.separator();
|
||||
for (a, icon, tip) in [
|
||||
(TextAlign::Left, "align-left", "Align left"),
|
||||
(TextAlign::Center, "align-center", "Centre"),
|
||||
(TextAlign::Right, "align-right", "Align right"),
|
||||
(TextAlign::Justify, "align-justify", "Justify"),
|
||||
(TextAlign::Left, "align-left", tl!("Align left")),
|
||||
(TextAlign::Center, "align-center", tl!("Centre")),
|
||||
(TextAlign::Right, "align-right", tl!("Align right")),
|
||||
(TextAlign::Justify, "align-justify", tl!("Justify")),
|
||||
] {
|
||||
if crate::icons::button(ui, icon, 26.0, s.align == a, tip).clicked() {
|
||||
s.align = a;
|
||||
|
|
@ -387,7 +387,7 @@ pub(crate) fn format_panel(ui: &mut egui::Ui, t: &Tokens, style: &AddedText) ->
|
|||
if let Some(picked) = crate::comments::swatch_grid(ui, Some(c)) {
|
||||
s.color = picked;
|
||||
}
|
||||
ui.label(egui::RichText::new("Standard fonts; text outside Windows-1252 isn't supported yet.").small().color(t.text_faint));
|
||||
ui.label(egui::RichText::new(tl!("Standard fonts; text outside Windows-1252 isn't supported yet.")).small().color(t.text_faint));
|
||||
(s != *style).then_some(s)
|
||||
}
|
||||
|
||||
|
|
@ -395,7 +395,7 @@ pub(crate) fn format_panel(ui: &mut egui::Ui, t: &Tokens, style: &AddedText) ->
|
|||
pub(crate) fn hint(ui: &mut egui::Ui, t: &Tokens) {
|
||||
ui.label(
|
||||
egui::RichText::new(
|
||||
"Click on the page to add text; each click starts a new box, and ✓ finishes. Drag items to move them, drag a corner to resize, double-click text to edit it.",
|
||||
tl!("Click on the page to add text; each click starts a new box, and ✓ finishes. Drag items to move them, drag a corner to resize, double-click text to edit it."),
|
||||
)
|
||||
.small()
|
||||
.color(t.text_faint),
|
||||
|
|
@ -413,8 +413,8 @@ impl crate::PdfCraftApp {
|
|||
// Tests and automation set `save_override` and never see a native dialog.
|
||||
Some(_) => None,
|
||||
None => rfd::FileDialog::new()
|
||||
.add_filter("Images", &["png", "jpg", "jpeg", "tif", "tiff", "gif", "bmp", "jp2", "j2k", "jpx"])
|
||||
.set_title("Choose an image")
|
||||
.add_filter(tl!("Images").to_string(), &["png", "jpg", "jpeg", "tif", "tiff", "gif", "bmp", "jp2", "j2k", "jpx"])
|
||||
.set_title(tl!("Choose an image"))
|
||||
.pick_file(),
|
||||
};
|
||||
let Some(path) = picked else { return };
|
||||
|
|
@ -423,11 +423,11 @@ impl crate::PdfCraftApp {
|
|||
let name = path.file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or_default();
|
||||
self.add_image(name, bytes);
|
||||
}
|
||||
Err(e) => self.notify(format!("Couldn't read {}: {e}", path.display())),
|
||||
Err(e) => self.notify_fmt("Couldn't read {name}: {e}", &[("name", &path.display().to_string()), ("e", &e.to_string())]),
|
||||
}
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
self.notify("Adding images arrives on the web with file pickers for images");
|
||||
self.notify_tr("Adding images arrives on the web with file pickers for images");
|
||||
}
|
||||
|
||||
/// Edit text & images ▸ Replace Image: pick a file to draw in a page image's place.
|
||||
|
|
@ -438,8 +438,8 @@ impl crate::PdfCraftApp {
|
|||
Some(p) if p.ends_with(".png") || p.ends_with(".jpg") => Some(std::path::PathBuf::from(p)),
|
||||
Some(_) => None,
|
||||
None => rfd::FileDialog::new()
|
||||
.add_filter("Images", &["png", "jpg", "jpeg", "tif", "tiff", "gif", "bmp", "jp2", "j2k", "jpx"])
|
||||
.set_title("Replace image")
|
||||
.add_filter(tl!("Images"), &["png", "jpg", "jpeg", "tif", "tiff", "gif", "bmp", "jp2", "j2k", "jpx"])
|
||||
.set_title(tl!("Replace image"))
|
||||
.pick_file(),
|
||||
};
|
||||
let Some(path) = picked else { return };
|
||||
|
|
@ -452,11 +452,14 @@ impl crate::PdfCraftApp {
|
|||
change: pdfcraft_engine::ImageEdit::Replace { name, bytes: std::sync::Arc::new(bytes) },
|
||||
});
|
||||
}
|
||||
Err(e) => self.notify(format!("Couldn't read {}: {e}", path.display())),
|
||||
Err(e) => self.notify_fmt("Couldn't read {name}: {e}", &[("name", &path.display().to_string()), ("e", &e.to_string())]),
|
||||
}
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
self.notify(format!("Replacing images on page {} arrives on the web with image pickers ({index})", page + 1));
|
||||
self.notify_fmt(
|
||||
"Replacing images on page {p} arrives on the web with image pickers ({i})",
|
||||
&[("p", &(page + 1).to_string()), ("i", &index.to_string())],
|
||||
);
|
||||
}
|
||||
|
||||
/// Edit text & images ▸ Save Image As.
|
||||
|
|
@ -467,7 +470,7 @@ impl crate::PdfCraftApp {
|
|||
Some(Ok((ext, bytes))) => {
|
||||
self.write_files(&[(format!("Image page {} #{}.{ext}", page + 1, index + 1), std::sync::Arc::new(bytes))], "Save image");
|
||||
}
|
||||
Some(Err(e)) => self.notify(format!("Couldn't save the image: {e}")),
|
||||
Some(Err(e)) => self.notify_fmt("Couldn't save the image: {e}", &[("e", &e.to_string())]),
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
|
|
@ -480,8 +483,8 @@ impl crate::PdfCraftApp {
|
|||
Some(p) if p.ends_with(".png") || p.ends_with(".jpg") => Some(std::path::PathBuf::from(p)),
|
||||
Some(_) => None,
|
||||
None => rfd::FileDialog::new()
|
||||
.add_filter("Images", &["png", "jpg", "jpeg", "tif", "tiff", "gif", "bmp", "jp2", "j2k", "jpx"])
|
||||
.set_title("Select Icon")
|
||||
.add_filter(tl!("Images"), &["png", "jpg", "jpeg", "tif", "tiff", "gif", "bmp", "jp2", "j2k", "jpx"])
|
||||
.set_title(tl!("Select Icon"))
|
||||
.pick_file(),
|
||||
};
|
||||
let Some(path) = picked else { return };
|
||||
|
|
@ -489,11 +492,11 @@ impl crate::PdfCraftApp {
|
|||
Ok(bytes) => {
|
||||
self.apply_edit(Edit::SetFieldImage { name: name.to_string(), image: std::sync::Arc::new(bytes) });
|
||||
}
|
||||
Err(e) => self.notify(format!("Couldn't read {}: {e}", path.display())),
|
||||
Err(e) => self.notify_fmt("Couldn't read {name}: {e}", &[("name", &path.display().to_string()), ("e", &e.to_string())]),
|
||||
}
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
self.notify(format!("{name}: choosing images arrives on the web with file pickers for images"));
|
||||
self.notify_fmt("{name}: choosing images arrives on the web with file pickers for images", &[("name", &name)]);
|
||||
}
|
||||
|
||||
/// Edit image ▸ Replace: pick a file for the selected image.
|
||||
|
|
@ -504,8 +507,8 @@ impl crate::PdfCraftApp {
|
|||
Some(p) if p.ends_with(".png") || p.ends_with(".jpg") => Some(std::path::PathBuf::from(p)),
|
||||
Some(_) => None,
|
||||
None => rfd::FileDialog::new()
|
||||
.add_filter("Images", &["png", "jpg", "jpeg", "tif", "tiff", "gif", "bmp", "jp2", "j2k", "jpx"])
|
||||
.set_title("Replace image")
|
||||
.add_filter(tl!("Images"), &["png", "jpg", "jpeg", "tif", "tiff", "gif", "bmp", "jp2", "j2k", "jpx"])
|
||||
.set_title(tl!("Replace image"))
|
||||
.pick_file(),
|
||||
};
|
||||
let Some(path) = picked else { return };
|
||||
|
|
@ -514,13 +517,13 @@ impl crate::PdfCraftApp {
|
|||
let name = path.file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or_default();
|
||||
self.apply_edit(Edit::ReplaceImage { page, index, name, bytes: std::sync::Arc::new(bytes) });
|
||||
}
|
||||
Err(e) => self.notify(format!("Couldn't read {}: {e}", path.display())),
|
||||
Err(e) => self.notify_fmt("Couldn't read {name}: {e}", &[("name", &path.display().to_string()), ("e", &e.to_string())]),
|
||||
}
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
let _ = (page, index);
|
||||
self.notify("Replacing images arrives on the web with image file pickers");
|
||||
self.notify_tr("Replacing images arrives on the web with image file pickers");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -546,7 +549,7 @@ pub(crate) enum ImageAction {
|
|||
/// Edit image: rotate, flip, crop, replace (shown while an added image is selected).
|
||||
pub(crate) fn image_panel(ui: &mut egui::Ui, t: &Tokens, img: &pdfcraft_engine::AddedImage) -> Option<ImageAction> {
|
||||
let mut out = None;
|
||||
widgets::section_title(ui, "Edit image");
|
||||
widgets::section_title(ui, tl!("Edit image"));
|
||||
ui.horizontal(|ui| {
|
||||
let mut i = img.clone();
|
||||
let r = i.rect;
|
||||
|
|
@ -556,27 +559,27 @@ pub(crate) fn image_panel(ui: &mut egui::Ui, t: &Tokens, img: &pdfcraft_engine::
|
|||
let (cx, cy, w, h) = ((r[0] + r[2]) / 2.0, (r[1] + r[3]) / 2.0, r[2] - r[0], r[3] - r[1]);
|
||||
i.rect = [cx - h / 2.0, cy - w / 2.0, cx + h / 2.0, cy + w / 2.0];
|
||||
};
|
||||
if crate::icons::button(ui, "rotate-ccw", 28.0, false, "Rotate counterclockwise").clicked() {
|
||||
if crate::icons::button(ui, "rotate-ccw", 28.0, false, tl!("Rotate counterclockwise")).clicked() {
|
||||
turn(&mut i, 1);
|
||||
out = Some(ImageAction::Update(AddedContent::Image(i.clone())));
|
||||
}
|
||||
if crate::icons::button(ui, "rotate-cw", 28.0, false, "Rotate clockwise").clicked() {
|
||||
if crate::icons::button(ui, "rotate-cw", 28.0, false, tl!("Rotate clockwise")).clicked() {
|
||||
turn(&mut i, 3);
|
||||
out = Some(ImageAction::Update(AddedContent::Image(i.clone())));
|
||||
}
|
||||
if crate::icons::button(ui, "flip-horizontal-2", 28.0, false, "Flip horizontal").clicked() {
|
||||
if crate::icons::button(ui, "flip-horizontal-2", 28.0, false, tl!("Flip horizontal")).clicked() {
|
||||
i.flip_h = !i.flip_h;
|
||||
out = Some(ImageAction::Update(AddedContent::Image(i.clone())));
|
||||
}
|
||||
if crate::icons::button(ui, "flip-vertical-2", 28.0, false, "Flip vertical").clicked() {
|
||||
if crate::icons::button(ui, "flip-vertical-2", 28.0, false, tl!("Flip vertical")).clicked() {
|
||||
i.flip_v = !i.flip_v;
|
||||
out = Some(ImageAction::Update(AddedContent::Image(i.clone())));
|
||||
}
|
||||
if crate::icons::button(ui, "replace", 28.0, false, "Replace image").clicked() {
|
||||
if crate::icons::button(ui, "replace", 28.0, false, tl!("Replace image")).clicked() {
|
||||
out = Some(ImageAction::Replace);
|
||||
}
|
||||
});
|
||||
ui.label(egui::RichText::new("Crop (% trimmed from each side)").small().color(t.text_muted));
|
||||
ui.label(egui::RichText::new(tl!("Crop (% trimmed from each side)")).small().color(t.text_muted));
|
||||
let mut crop = img.crop.map(|v| (v * 100.0).round());
|
||||
let mut changed = false;
|
||||
ui.horizontal(|ui| {
|
||||
|
|
|
|||
|
|
@ -100,10 +100,10 @@ impl PdfCraftApp {
|
|||
match clip {
|
||||
Some(c) => {
|
||||
if let Err(e) = self.create_from_clip(c) {
|
||||
self.notify(format!("Couldn't create a PDF: {e}"));
|
||||
self.notify_fmt("Couldn't create a PDF: {e}", &[("e", &e.to_string())]);
|
||||
}
|
||||
}
|
||||
None => self.notify("The clipboard has no image or text"),
|
||||
None => self.notify_tr("The clipboard has no image or text"),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -123,7 +123,7 @@ impl PdfCraftApp {
|
|||
pub(crate) fn create_blank(&mut self) {
|
||||
let created = self.session.create_blank(612.0, 792.0, 1).map_err(|e| e.to_string());
|
||||
if let Err(e) = self.open_created_bytes("Untitled.pdf", created) {
|
||||
self.notify(format!("Couldn't create a PDF: {e}"));
|
||||
self.notify_fmt("Couldn't create a PDF: {e}", &[("e", &e.to_string())]);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -132,8 +132,8 @@ impl PdfCraftApp {
|
|||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
let Some(files) = rfd::FileDialog::new()
|
||||
.add_filter("Images", &["png", "jpg", "jpeg", "tif", "tiff", "gif", "bmp", "jp2", "j2k", "jpx"])
|
||||
.set_title("Choose images")
|
||||
.add_filter(tl!("Images"), &["png", "jpg", "jpeg", "tif", "tiff", "gif", "bmp", "jp2", "j2k", "jpx"])
|
||||
.set_title(tl!("Choose images"))
|
||||
.pick_files()
|
||||
else {
|
||||
return;
|
||||
|
|
@ -143,7 +143,7 @@ impl PdfCraftApp {
|
|||
match std::fs::read(&f) {
|
||||
Ok(b) => images.push((f.file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or_default(), b)),
|
||||
Err(e) => {
|
||||
self.notify(format!("Couldn't read {}: {e}", f.display()));
|
||||
self.notify_fmt("Couldn't read {name}: {e}", &[("name", &f.display().to_string()), ("e", &e.to_string())]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
@ -151,7 +151,7 @@ impl PdfCraftApp {
|
|||
self.create_from_images(images);
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
self.notify("On the web, open or drop an image to convert it");
|
||||
self.notify_tr("On the web, open or drop an image to convert it");
|
||||
}
|
||||
|
||||
/// One new document from images (tests and automation call this directly).
|
||||
|
|
@ -162,7 +162,7 @@ impl PdfCraftApp {
|
|||
let name = if images.len() == 1 { format!("{}.pdf", stem(&images[0].0)) } else { "Images.pdf".to_string() };
|
||||
let created = self.session.create_from_images(&images).map_err(|e| e.to_string());
|
||||
if let Err(e) = self.open_created_bytes(&name, created) {
|
||||
self.notify(format!("Couldn't create a PDF: {e}"));
|
||||
self.notify_fmt("Couldn't create a PDF: {e}", &[("e", &e.to_string())]);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -187,17 +187,22 @@ impl PdfCraftApp {
|
|||
let (bytes, detail) = match result {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
self.notify(format!("Couldn't optimize the file: {e}"));
|
||||
self.notify_fmt("Couldn't optimize the file: {e}", &[("e", &e.to_string())]);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let saved = |app: &mut PdfCraftApp, place: String| {
|
||||
let pct = 100.0 * (1.0 - bytes.len() as f64 / before.max(1) as f64);
|
||||
app.notify(format!(
|
||||
"Saved {place}: {} → {} ({pct:.0}% smaller){detail}",
|
||||
crate::panels::human_size(before),
|
||||
crate::panels::human_size(bytes.len())
|
||||
));
|
||||
app.notify_fmt(
|
||||
"Saved {place}: {before} → {after} ({pct}% smaller){detail}",
|
||||
&[
|
||||
("place", &place),
|
||||
("before", &crate::panels::human_size(before)),
|
||||
("after", &crate::panels::human_size(bytes.len())),
|
||||
("pct", &format!("{pct:.0}")),
|
||||
("detail", &detail),
|
||||
],
|
||||
);
|
||||
};
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
|
|
@ -208,13 +213,13 @@ impl PdfCraftApp {
|
|||
let Some(path) = path else { return };
|
||||
match crate::editing::write_atomically(&path, &bytes) {
|
||||
Ok(()) => saved(self, path),
|
||||
Err(e) => self.notify(format!("Couldn't write {path}: {e}")),
|
||||
Err(e) => self.notify_fmt("Couldn't write {path}: {e}", &[("path", &path), ("e", &e.to_string())]),
|
||||
}
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
match crate::editing::download(&name, &bytes) {
|
||||
Ok(()) => saved(self, name),
|
||||
Err(e) => self.notify(format!("Couldn't download {name}: {e}")),
|
||||
Err(e) => self.notify_fmt("Couldn't download {name}: {e}", &[("name", &name), ("e", &e.to_string())]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -77,15 +77,15 @@ pub fn show(app: &mut PdfCraftApp, ctx: &egui::Context) {
|
|||
w.hovered.bg_stroke = egui::Stroke::new(1.0, t.text_muted);
|
||||
match dialog {
|
||||
Dialog::Properties(tab) => {
|
||||
ui.label(egui::RichText::new("Document Properties").font(theme::semibold(18.0)));
|
||||
ui.label(egui::RichText::new(tl!("Document Properties")).font(theme::semibold(18.0)));
|
||||
ui.add_space(8.0);
|
||||
ui.horizontal(|ui| {
|
||||
for (tb, label) in [
|
||||
(PropsTab::Description, "Description"),
|
||||
(PropsTab::InitialView, "Initial View"),
|
||||
(PropsTab::Security, "Security"),
|
||||
(PropsTab::Fonts, "Fonts"),
|
||||
(PropsTab::Advanced, "Advanced"),
|
||||
(PropsTab::Description, tl!("Description")),
|
||||
(PropsTab::InitialView, tl!("Initial View")),
|
||||
(PropsTab::Security, tl!("Security")),
|
||||
(PropsTab::Fonts, tl!("Fonts")),
|
||||
(PropsTab::Advanced, tl!("Advanced")),
|
||||
] {
|
||||
if widgets::mode_tab(ui, label, tab == tb).clicked() {
|
||||
next = Dialog::Properties(tb);
|
||||
|
|
@ -97,7 +97,8 @@ pub fn show(app: &mut PdfCraftApp, ctx: &egui::Context) {
|
|||
let Some(doc) = app.session.get(id) else { return };
|
||||
let i = &doc.info;
|
||||
let row = |ui: &mut egui::Ui, k: &str, v: String| {
|
||||
ui.label(egui::RichText::new(k).color(t.text_muted));
|
||||
// Row labels are UI chrome; values are document data and stay as they are.
|
||||
ui.label(egui::RichText::new(tl!(k)).color(t.text_muted));
|
||||
ui.label(if v.is_empty() { egui::RichText::new("—").color(t.text_faint) } else { egui::RichText::new(v) });
|
||||
ui.end_row();
|
||||
};
|
||||
|
|
@ -108,7 +109,7 @@ pub fn show(app: &mut PdfCraftApp, ctx: &egui::Context) {
|
|||
match app.props_draft.as_mut() {
|
||||
Some((_, draft)) if doc.allows_modification() => {
|
||||
for (k, v) in INFO_KEYS.iter().zip(draft.iter_mut()) {
|
||||
let l = ui.label(egui::RichText::new(*k).color(t.text_muted));
|
||||
let l = ui.label(egui::RichText::new(tl!(k)).color(t.text_muted));
|
||||
ui.add(
|
||||
egui::TextEdit::singleline(v)
|
||||
.desired_width(420.0)
|
||||
|
|
@ -135,39 +136,40 @@ pub fn show(app: &mut PdfCraftApp, ctx: &egui::Context) {
|
|||
let editable = doc.allows_modification();
|
||||
let pages = i.pages.len();
|
||||
let Some((_, v)) = app.view_draft.as_mut() else { return };
|
||||
ui.label(egui::RichText::new("Layout and Magnification").font(theme::semibold(12.5)));
|
||||
ui.label(egui::RichText::new(tl!("Layout and Magnification")).font(theme::semibold(12.5)));
|
||||
ui.end_row();
|
||||
ui.label("Navigation tab");
|
||||
ui.label(tl!("Navigation tab"));
|
||||
ui.add_enabled_ui(editable, |ui| {
|
||||
egui::ComboBox::from_id_salt("iv-nav")
|
||||
.selected_text(
|
||||
format!("{:?}", v.navigation).replace("PageOnly", "Page Only").replace("Pages", "Pages Panel and Page"),
|
||||
)
|
||||
.selected_text(tl!(format!("{:?}", v.navigation)
|
||||
.replace("PageOnly", "Page Only")
|
||||
.replace("Pages", "Pages Panel and Page")
|
||||
.as_str()))
|
||||
.show_ui(ui, |ui| {
|
||||
for (n, l) in [
|
||||
(N::PageOnly, "Page Only"),
|
||||
(N::Bookmarks, "Bookmarks Panel and Page"),
|
||||
(N::Pages, "Pages Panel and Page"),
|
||||
(N::Attachments, "Attachments Panel and Page"),
|
||||
(N::Layers, "Layers Panel and Page"),
|
||||
(N::PageOnly, tl!("Page Only")),
|
||||
(N::Bookmarks, tl!("Bookmarks Panel and Page")),
|
||||
(N::Pages, tl!("Pages Panel and Page")),
|
||||
(N::Attachments, tl!("Attachments Panel and Page")),
|
||||
(N::Layers, tl!("Layers Panel and Page")),
|
||||
] {
|
||||
ui.selectable_value(&mut v.navigation, n, l);
|
||||
}
|
||||
});
|
||||
});
|
||||
ui.end_row();
|
||||
ui.label("Page layout");
|
||||
ui.label(tl!("Page layout"));
|
||||
ui.add_enabled_ui(editable, |ui| {
|
||||
let names = [
|
||||
(L::Default, "Default"),
|
||||
(L::SinglePage, "Single Page"),
|
||||
(L::SinglePageContinuous, "Single Page Continuous"),
|
||||
(L::TwoUp, "Two-Up (Facing)"),
|
||||
(L::TwoUpContinuous, "Two-Up Continuous (Facing)"),
|
||||
(L::TwoUpCoverPage, "Two-Up (Cover Page)"),
|
||||
(L::TwoUpContinuousCoverPage, "Two-Up Continuous (Cover Page)"),
|
||||
(L::Default, tl!("Default")),
|
||||
(L::SinglePage, tl!("Single Page")),
|
||||
(L::SinglePageContinuous, tl!("Single Page Continuous")),
|
||||
(L::TwoUp, tl!("Two-Up (Facing)")),
|
||||
(L::TwoUpContinuous, tl!("Two-Up Continuous (Facing)")),
|
||||
(L::TwoUpCoverPage, tl!("Two-Up (Cover Page)")),
|
||||
(L::TwoUpContinuousCoverPage, tl!("Two-Up Continuous (Cover Page)")),
|
||||
];
|
||||
let shown = names.iter().find(|(l, _)| *l == v.layout).map_or("Default", |(_, n)| n);
|
||||
let shown = names.iter().find(|(l, _)| *l == v.layout).map_or(tl!("Default"), |(_, n)| n);
|
||||
egui::ComboBox::from_id_salt("iv-layout").selected_text(shown).show_ui(ui, |ui| {
|
||||
for (l, n) in names {
|
||||
ui.selectable_value(&mut v.layout, l, n);
|
||||
|
|
@ -175,20 +177,20 @@ pub fn show(app: &mut PdfCraftApp, ctx: &egui::Context) {
|
|||
});
|
||||
});
|
||||
ui.end_row();
|
||||
ui.label("Magnification");
|
||||
ui.label(tl!("Magnification"));
|
||||
ui.add_enabled_ui(editable, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
let names = [
|
||||
(M::Default, "Default"),
|
||||
(M::ActualSize, "Actual Size"),
|
||||
(M::FitPage, "Fit Page"),
|
||||
(M::FitWidth, "Fit Width"),
|
||||
(M::FitHeight, "Fit Height"),
|
||||
(M::FitVisible, "Fit Visible"),
|
||||
(M::Default, tl!("Default")),
|
||||
(M::ActualSize, tl!("Actual Size")),
|
||||
(M::FitPage, tl!("Fit Page")),
|
||||
(M::FitWidth, tl!("Fit Width")),
|
||||
(M::FitHeight, tl!("Fit Height")),
|
||||
(M::FitVisible, tl!("Fit Visible")),
|
||||
];
|
||||
let shown = match v.magnification {
|
||||
M::Percent(p) => format!("{p:.0}%"),
|
||||
m => names.iter().find(|(x, _)| *x == m).map_or("Default", |(_, n)| n).to_string(),
|
||||
m => names.iter().find(|(x, _)| *x == m).map_or(tl!("Default").to_string(), |(_, n)| n.to_string()),
|
||||
};
|
||||
egui::ComboBox::from_id_salt("iv-mag").selected_text(shown).show_ui(ui, |ui| {
|
||||
for (m, n) in names {
|
||||
|
|
@ -201,86 +203,86 @@ pub fn show(app: &mut PdfCraftApp, ctx: &egui::Context) {
|
|||
});
|
||||
});
|
||||
ui.end_row();
|
||||
ui.label("Open to page");
|
||||
ui.label(tl!("Open to page"));
|
||||
ui.add_enabled_ui(editable, |ui| {
|
||||
let mut p = v.page + 1;
|
||||
if ui.add(egui::DragValue::new(&mut p).range(1..=pages.max(1))).changed() {
|
||||
v.page = p - 1;
|
||||
}
|
||||
ui.label(format!("of {pages}"));
|
||||
ui.label(crate::i18n::fmt(tl!("of {pages}"), &[("pages", &pages.to_string())]));
|
||||
});
|
||||
ui.end_row();
|
||||
ui.label(egui::RichText::new("Window Options").font(theme::semibold(12.5)));
|
||||
ui.label(egui::RichText::new(tl!("Window Options")).font(theme::semibold(12.5)));
|
||||
ui.end_row();
|
||||
ui.label("");
|
||||
ui.add_enabled_ui(editable, |ui| {
|
||||
ui.vertical(|ui| {
|
||||
ui.checkbox(&mut v.fit_window, "Resize window to initial page");
|
||||
ui.checkbox(&mut v.center_window, "Center window on screen");
|
||||
ui.checkbox(&mut v.full_screen, "Open in Full Screen mode");
|
||||
ui.checkbox(&mut v.fit_window, tl!("Resize window to initial page"));
|
||||
ui.checkbox(&mut v.center_window, tl!("Center window on screen"));
|
||||
ui.checkbox(&mut v.full_screen, tl!("Open in Full Screen mode"));
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Show:");
|
||||
ui.radio_value(&mut v.display_title, false, "File Name");
|
||||
ui.radio_value(&mut v.display_title, true, "Document Title");
|
||||
ui.label(tl!("Show:"));
|
||||
ui.radio_value(&mut v.display_title, false, tl!("File Name"));
|
||||
ui.radio_value(&mut v.display_title, true, tl!("Document Title"));
|
||||
});
|
||||
});
|
||||
});
|
||||
ui.end_row();
|
||||
ui.label(egui::RichText::new("User Interface Options").font(theme::semibold(12.5)));
|
||||
ui.label(egui::RichText::new(tl!("User Interface Options")).font(theme::semibold(12.5)));
|
||||
ui.end_row();
|
||||
ui.label("");
|
||||
ui.add_enabled_ui(editable, |ui| {
|
||||
ui.vertical(|ui| {
|
||||
ui.checkbox(&mut v.hide_menubar, "Hide menu bar");
|
||||
ui.checkbox(&mut v.hide_toolbar, "Hide toolbars");
|
||||
ui.checkbox(&mut v.hide_window_ui, "Hide window controls");
|
||||
ui.checkbox(&mut v.hide_menubar, tl!("Hide menu bar"));
|
||||
ui.checkbox(&mut v.hide_toolbar, tl!("Hide toolbars"));
|
||||
ui.checkbox(&mut v.hide_window_ui, tl!("Hide window controls"));
|
||||
});
|
||||
});
|
||||
ui.end_row();
|
||||
}
|
||||
PropsTab::Security => match doc.security_summary() {
|
||||
None => {
|
||||
row(ui, "Security method", "No security".into());
|
||||
row(ui, "Restrictions", "None — everything is allowed".into());
|
||||
if doc.allows_security_change() && ui.button("Protect using password…").clicked() {
|
||||
row(ui, "Security method", tl!("No security").to_string());
|
||||
row(ui, "Restrictions", tl!("None — everything is allowed").to_string());
|
||||
if doc.allows_security_change() && ui.button(tl!("Protect using password…")).clicked() {
|
||||
link_command = Some("protect.password");
|
||||
}
|
||||
}
|
||||
Some(sec) => {
|
||||
row(ui, "Security method", "Password security".into());
|
||||
row(ui, "Security method", tl!("Password security").to_string());
|
||||
row(ui, "Encryption", sec.method.clone());
|
||||
row(
|
||||
ui,
|
||||
"Opened with",
|
||||
if sec.pending {
|
||||
"— (protection is applied when you save)".into()
|
||||
tl!("— (protection is applied when you save)").to_string()
|
||||
} else if sec.owner {
|
||||
"Owner password (no restrictions apply)".into()
|
||||
tl!("Owner password (no restrictions apply)").to_string()
|
||||
} else {
|
||||
"User password".into()
|
||||
tl!("User password").to_string()
|
||||
},
|
||||
);
|
||||
if doc.allows_security_change() {
|
||||
ui.horizontal(|ui| {
|
||||
if ui.button("Change settings…").clicked() {
|
||||
if ui.button(tl!("Change settings…")).clicked() {
|
||||
link_command = Some("protect.password");
|
||||
}
|
||||
if ui.button("Remove security").clicked() {
|
||||
if ui.button(tl!("Remove security")).clicked() {
|
||||
link_command = Some("protect.remove");
|
||||
}
|
||||
});
|
||||
}
|
||||
let p = sec.permissions;
|
||||
let yes = |b: bool| if b { "Allowed".to_string() } else { "Not allowed".to_string() };
|
||||
let yes = |b: bool| tl!(if b { "Allowed" } else { "Not allowed" }).to_string();
|
||||
row(
|
||||
ui,
|
||||
"Printing",
|
||||
if !p.print() {
|
||||
"Not allowed".into()
|
||||
tl!("Not allowed").to_string()
|
||||
} else if p.print_high_quality() {
|
||||
"High resolution".into()
|
||||
tl!("High resolution").to_string()
|
||||
} else {
|
||||
"Low resolution".into()
|
||||
tl!("Low resolution").to_string()
|
||||
},
|
||||
);
|
||||
row(ui, "Changing the document", yes(p.modify()));
|
||||
|
|
@ -293,7 +295,7 @@ pub fn show(app: &mut PdfCraftApp, ctx: &egui::Context) {
|
|||
},
|
||||
PropsTab::Fonts => {
|
||||
if i.fonts.is_empty() {
|
||||
row(ui, "Fonts", "No fonts are referenced by the pages.".into());
|
||||
row(ui, "Fonts", tl!("No fonts are referenced by the pages.").to_string());
|
||||
}
|
||||
for f in &i.fonts {
|
||||
let mut detail = f.kind.clone();
|
||||
|
|
@ -326,11 +328,14 @@ pub fn show(app: &mut PdfCraftApp, ctx: &egui::Context) {
|
|||
// Each incremental update is a revision; earlier ones open as their own document.
|
||||
let ends = doc.revision_ends();
|
||||
if ends.len() > 1 {
|
||||
ui.label(egui::RichText::new("Revisions").color(t.text_muted));
|
||||
ui.label(egui::RichText::new(tl!("Revisions")).color(t.text_muted));
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.label(ends.len().to_string());
|
||||
for n in (1..ends.len()).rev().take(12) {
|
||||
if ui.small_button(format!("View revision {n}")).on_hover_text("Open the file as it was saved then").clicked()
|
||||
if ui
|
||||
.small_button(crate::i18n::fmt(tl!("View revision {n}"), &[("n", &n.to_string())]))
|
||||
.on_hover_text(tl!("Open the file as it was saved then"))
|
||||
.clicked()
|
||||
{
|
||||
open_revision = Some(n);
|
||||
}
|
||||
|
|
@ -341,18 +346,18 @@ pub fn show(app: &mut PdfCraftApp, ctx: &egui::Context) {
|
|||
// Reading Options: binding and language.
|
||||
if let Some((_, v)) = app.view_draft.as_mut() {
|
||||
let editable = doc.allows_modification();
|
||||
ui.label(egui::RichText::new("Binding").color(t.text_muted));
|
||||
ui.label(egui::RichText::new(tl!("Binding")).color(t.text_muted));
|
||||
ui.add_enabled_ui(editable, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.radio_value(&mut v.right_to_left, false, "Left Edge");
|
||||
ui.radio_value(&mut v.right_to_left, true, "Right Edge");
|
||||
ui.radio_value(&mut v.right_to_left, false, tl!("Left Edge"));
|
||||
ui.radio_value(&mut v.right_to_left, true, tl!("Right Edge"));
|
||||
});
|
||||
});
|
||||
ui.end_row();
|
||||
let l = ui.label(egui::RichText::new("Language").color(t.text_muted));
|
||||
let l = ui.label(egui::RichText::new(tl!("Language")).color(t.text_muted));
|
||||
let mut lang = v.language.clone().unwrap_or_default();
|
||||
if ui
|
||||
.add_enabled(editable, egui::TextEdit::singleline(&mut lang).hint_text("e.g. en-US").desired_width(160.0))
|
||||
.add_enabled(editable, egui::TextEdit::singleline(&mut lang).hint_text(tl!("e.g. en-US")).desired_width(160.0))
|
||||
.labelled_by(l.id)
|
||||
.changed()
|
||||
{
|
||||
|
|
@ -362,10 +367,10 @@ pub fn show(app: &mut PdfCraftApp, ctx: &egui::Context) {
|
|||
}
|
||||
// What was repaired while reading a damaged file (fidelity: never silent).
|
||||
let repairs = doc.repair_log();
|
||||
row(ui, "Repairs", if repairs.is_empty() { "None".to_string() } else { repairs.len().to_string() });
|
||||
row(ui, "Repairs", if repairs.is_empty() { tl!("None").to_string() } else { repairs.len().to_string() });
|
||||
if !repairs.is_empty() {
|
||||
ui.label("");
|
||||
egui::CollapsingHeader::new("Repair log").show(ui, |ui| {
|
||||
egui::CollapsingHeader::new(tl!("Repair log")).show(ui, |ui| {
|
||||
for r in &repairs {
|
||||
ui.add(egui::Label::new(egui::RichText::new(r).small()).wrap());
|
||||
}
|
||||
|
|
@ -377,7 +382,7 @@ pub fn show(app: &mut PdfCraftApp, ctx: &egui::Context) {
|
|||
});
|
||||
}
|
||||
Dialog::Split => {
|
||||
ui.label(egui::RichText::new("Split document").font(theme::semibold(18.0)));
|
||||
ui.label(egui::RichText::new(tl!("Split document")).font(theme::semibold(18.0)));
|
||||
ui.add_space(8.0);
|
||||
let Some((vi, id)) = app.active_ids() else { return };
|
||||
let n = app.session.get(id).map(|d| d.info.pages.len()).unwrap_or(0);
|
||||
|
|
@ -388,27 +393,31 @@ pub fn show(app: &mut PdfCraftApp, ctx: &egui::Context) {
|
|||
if selected.is_empty() && draft.mode == M::Selection {
|
||||
draft.mode = M::Pages;
|
||||
}
|
||||
ui.radio_value(&mut draft.mode, M::Pages, "Number of pages");
|
||||
ui.radio_value(&mut draft.mode, M::Pages, tl!("Number of pages"));
|
||||
ui.add_enabled_ui(draft.mode == M::Pages, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_space(24.0);
|
||||
ui.label("Pages per file");
|
||||
ui.label(tl!("Pages per file"));
|
||||
ui.add(egui::DragValue::new(&mut draft.every).range(1..=n.max(1)));
|
||||
});
|
||||
});
|
||||
ui.radio_value(&mut draft.mode, M::Size, "File size");
|
||||
ui.radio_value(&mut draft.mode, M::Size, tl!("File size"));
|
||||
ui.add_enabled_ui(draft.mode == M::Size, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_space(24.0);
|
||||
ui.label("At most");
|
||||
ui.label(tl!("At most"));
|
||||
ui.add(egui::DragValue::new(&mut draft.size_mb).range(0.05..=2000.0).speed(0.1).suffix(" MB"));
|
||||
});
|
||||
});
|
||||
ui.add_enabled_ui(!marks.is_empty(), |ui| {
|
||||
ui.radio_value(&mut draft.mode, M::Bookmarks, format!("Top-level bookmarks ({})", marks.len()))
|
||||
ui.radio_value(
|
||||
&mut draft.mode,
|
||||
M::Bookmarks,
|
||||
crate::i18n::fmt(tl!("Top-level bookmarks ({n})"), &[("n", &marks.len().to_string())]),
|
||||
)
|
||||
});
|
||||
ui.add_enabled_ui(!selected.is_empty(), |ui| {
|
||||
ui.radio_value(&mut draft.mode, M::Selection, "Before each selected page (select pages in Organize)")
|
||||
ui.radio_value(&mut draft.mode, M::Selection, tl!("Before each selected page (select pages in Organize)"))
|
||||
});
|
||||
let plan = match draft.mode {
|
||||
M::Pages => crate::SplitPlan::By(pdfcraft_engine::SplitBy::PageCount(draft.every)),
|
||||
|
|
@ -425,8 +434,14 @@ pub fn show(app: &mut PdfCraftApp, ctx: &egui::Context) {
|
|||
};
|
||||
ui.add_space(8.0);
|
||||
let text = match files {
|
||||
Some(f) => format!("Creates {f} file{} from {n} pages.", if f == 1 { "" } else { "s" }),
|
||||
None => format!("Each file holds as many of the {n} pages as fit."),
|
||||
Some(f) => {
|
||||
if f == 1 {
|
||||
crate::i18n::fmt(tl!("Creates 1 file from {n} pages."), &[("n", &n.to_string())])
|
||||
} else {
|
||||
crate::i18n::fmt(tl!("Creates {f} files from {n} pages."), &[("f", &f.to_string()), ("n", &n.to_string())])
|
||||
}
|
||||
}
|
||||
None => crate::i18n::fmt(tl!("Each file holds as many of the {n} pages as fit."), &[("n", &n.to_string())]),
|
||||
};
|
||||
ui.label(egui::RichText::new(text).color(t.text_muted));
|
||||
if files.is_none_or(|f| f > 1) {
|
||||
|
|
@ -439,44 +454,44 @@ pub fn show(app: &mut PdfCraftApp, ctx: &egui::Context) {
|
|||
close = true;
|
||||
return;
|
||||
};
|
||||
ui.label(egui::RichText::new("Replace Pages").font(theme::semibold(18.0)));
|
||||
ui.label(egui::RichText::new(tl!("Replace Pages")).font(theme::semibold(18.0)));
|
||||
ui.add_space(8.0);
|
||||
d.to = d.to.clamp(1, count);
|
||||
d.from = d.from.clamp(1, d.to);
|
||||
let n = d.to - d.from + 1;
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Original: replace pages");
|
||||
ui.label(tl!("Original: replace pages"));
|
||||
ui.add(egui::DragValue::new(&mut d.from).range(1..=count));
|
||||
ui.label("to");
|
||||
ui.label(tl!("to"));
|
||||
ui.add(egui::DragValue::new(&mut d.to).range(1..=count));
|
||||
ui.label(egui::RichText::new(format!("of {count}")).color(t.text_muted));
|
||||
ui.label(egui::RichText::new(crate::i18n::fmt(tl!("of {name}"), &[("name", &count.to_string())])).color(t.text_muted));
|
||||
});
|
||||
let max_start = d.src_pages.saturating_sub(n) + 1;
|
||||
d.src_from = d.src_from.clamp(1, max_start.max(1));
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(format!("Replacement: pages of {}", d.name));
|
||||
ui.label(crate::i18n::fmt(tl!("Replacement: pages of {name}"), &[("name", &d.name)]));
|
||||
ui.add(egui::DragValue::new(&mut d.src_from).range(1..=max_start.max(1)));
|
||||
ui.label(format!("to {}", d.src_from + n - 1));
|
||||
ui.label(egui::RichText::new(format!("of {}", d.src_pages)).color(t.text_muted));
|
||||
ui.label(crate::i18n::fmt(tl!("to {n}"), &[("n", &(d.src_from + n - 1).to_string())]));
|
||||
ui.label(egui::RichText::new(crate::i18n::fmt(tl!("of {name}"), &[("name", &d.src_pages.to_string())])).color(t.text_muted));
|
||||
});
|
||||
let fits = n <= d.src_pages;
|
||||
ui.add_space(6.0);
|
||||
ui.label(
|
||||
egui::RichText::new(if fits {
|
||||
"Only the page content changes: links, comments, form fields and bookmarks on the original pages stay."
|
||||
tl!("Only the page content changes: links, comments, form fields and bookmarks on the original pages stay.")
|
||||
} else {
|
||||
"The replacement file doesn't have that many pages."
|
||||
tl!("The replacement file doesn't have that many pages.")
|
||||
})
|
||||
.small()
|
||||
.color(t.text_faint),
|
||||
);
|
||||
ui.add_space(10.0);
|
||||
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
|
||||
if ui.add_enabled_ui(fits, |ui| widgets::pill_button(ui, "OK", true)).inner.clicked() {
|
||||
if ui.add_enabled_ui(fits, |ui| widgets::pill_button(ui, tl!("OK"), true)).inner.clicked() {
|
||||
replace_now = true;
|
||||
close = true;
|
||||
}
|
||||
if widgets::pill_button(ui, "Cancel", false).clicked() {
|
||||
if widgets::pill_button(ui, tl!("Cancel"), false).clicked() {
|
||||
close = true;
|
||||
}
|
||||
});
|
||||
|
|
@ -530,18 +545,23 @@ pub fn show(app: &mut PdfCraftApp, ctx: &egui::Context) {
|
|||
}
|
||||
Dialog::Extract => {
|
||||
let count = app.active_ids().map_or(0, |(i, _)| app.views[i].target_pages().len());
|
||||
ui.label(egui::RichText::new("Extract pages").font(theme::semibold(18.0)));
|
||||
ui.label(egui::RichText::new(tl!("Extract pages")).font(theme::semibold(18.0)));
|
||||
ui.add_space(8.0);
|
||||
ui.label(format!("{count} page{} selected.", if count == 1 { "" } else { "s" }));
|
||||
ui.checkbox(&mut app.extract_draft.delete, "Delete pages after extracting");
|
||||
ui.checkbox(&mut app.extract_draft.separate, "Extract pages as separate files");
|
||||
let text = if count == 1 {
|
||||
tl!("1 page selected.").to_string()
|
||||
} else {
|
||||
crate::i18n::fmt(tl!("{n} pages selected."), &[("n", &count.to_string())])
|
||||
};
|
||||
ui.label(text);
|
||||
ui.checkbox(&mut app.extract_draft.delete, tl!("Delete pages after extracting"));
|
||||
ui.checkbox(&mut app.extract_draft.separate, tl!("Extract pages as separate files"));
|
||||
ui.add_space(12.0);
|
||||
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
|
||||
if widgets::pill_button(ui, "Extract", true).clicked() {
|
||||
if widgets::pill_button(ui, tl!("Extract"), true).clicked() {
|
||||
extract_now = true;
|
||||
close = true;
|
||||
}
|
||||
if widgets::pill_button(ui, "Cancel", false).clicked() {
|
||||
if widgets::pill_button(ui, tl!("Cancel"), false).clicked() {
|
||||
close = true;
|
||||
}
|
||||
});
|
||||
|
|
@ -551,70 +571,70 @@ pub fn show(app: &mut PdfCraftApp, ctx: &egui::Context) {
|
|||
use pdfcraft_engine::{PageOrientation as O, PageParity as P};
|
||||
let n = app.active_ids().and_then(|(_, id)| app.session.get(id)).map_or(1, |d| d.info.pages.len());
|
||||
let d = &mut app.rotate_draft;
|
||||
ui.label(egui::RichText::new("Rotate Pages").font(theme::semibold(18.0)));
|
||||
ui.label(egui::RichText::new(tl!("Rotate Pages")).font(theme::semibold(18.0)));
|
||||
ui.add_space(8.0);
|
||||
egui::Grid::new("rotate-pages").num_columns(2).spacing([12.0, 8.0]).show(ui, |ui| {
|
||||
ui.label("Direction:");
|
||||
ui.label(tl!("Direction:"));
|
||||
egui::ComboBox::from_id_salt("rotate-dir")
|
||||
.selected_text(match d.degrees {
|
||||
270 => "Counterclockwise 90 degrees",
|
||||
180 => "180 degrees",
|
||||
_ => "Clockwise 90 degrees",
|
||||
270 => tl!("Counterclockwise 90 degrees"),
|
||||
180 => tl!("180 degrees"),
|
||||
_ => tl!("Clockwise 90 degrees"),
|
||||
})
|
||||
.show_ui(ui, |ui| {
|
||||
ui.selectable_value(&mut d.degrees, 90, "Clockwise 90 degrees");
|
||||
ui.selectable_value(&mut d.degrees, 270, "Counterclockwise 90 degrees");
|
||||
ui.selectable_value(&mut d.degrees, 180, "180 degrees");
|
||||
ui.selectable_value(&mut d.degrees, 90, tl!("Clockwise 90 degrees"));
|
||||
ui.selectable_value(&mut d.degrees, 270, tl!("Counterclockwise 90 degrees"));
|
||||
ui.selectable_value(&mut d.degrees, 180, tl!("180 degrees"));
|
||||
});
|
||||
ui.end_row();
|
||||
ui.label("Pages:");
|
||||
ui.label(tl!("Pages:"));
|
||||
ui.vertical(|ui| {
|
||||
ui.radio_value(&mut d.which, 0, "All");
|
||||
ui.radio_value(&mut d.which, 1, "Selection");
|
||||
ui.radio_value(&mut d.which, 0, tl!("All"));
|
||||
ui.radio_value(&mut d.which, 1, tl!("Selection"));
|
||||
ui.horizontal(|ui| {
|
||||
ui.radio_value(&mut d.which, 2, "From");
|
||||
ui.radio_value(&mut d.which, 2, tl!("From"));
|
||||
ui.add_enabled(d.which == 2, egui::DragValue::new(&mut d.from).range(1..=n));
|
||||
ui.label("to");
|
||||
ui.label(tl!("to"));
|
||||
ui.add_enabled(d.which == 2, egui::DragValue::new(&mut d.to).range(1..=n));
|
||||
ui.label(format!("of {n}"));
|
||||
ui.label(crate::i18n::fmt(tl!("of {name}"), &[("name", &n.to_string())]));
|
||||
});
|
||||
});
|
||||
ui.end_row();
|
||||
ui.label("Rotate:");
|
||||
ui.label(tl!("Rotate:"));
|
||||
egui::ComboBox::from_id_salt("rotate-parity")
|
||||
.selected_text(match d.parity {
|
||||
P::Both => "Even and Odd Pages",
|
||||
P::Even => "Even Pages Only",
|
||||
P::Odd => "Odd Pages Only",
|
||||
P::Both => tl!("Even and Odd Pages"),
|
||||
P::Even => tl!("Even Pages Only"),
|
||||
P::Odd => tl!("Odd Pages Only"),
|
||||
})
|
||||
.show_ui(ui, |ui| {
|
||||
ui.selectable_value(&mut d.parity, P::Both, "Even and Odd Pages");
|
||||
ui.selectable_value(&mut d.parity, P::Even, "Even Pages Only");
|
||||
ui.selectable_value(&mut d.parity, P::Odd, "Odd Pages Only");
|
||||
ui.selectable_value(&mut d.parity, P::Both, tl!("Even and Odd Pages"));
|
||||
ui.selectable_value(&mut d.parity, P::Even, tl!("Even Pages Only"));
|
||||
ui.selectable_value(&mut d.parity, P::Odd, tl!("Odd Pages Only"));
|
||||
});
|
||||
ui.end_row();
|
||||
ui.label("");
|
||||
egui::ComboBox::from_id_salt("rotate-orient")
|
||||
.selected_text(match d.orientation {
|
||||
O::Both => "Landscape and Portrait Pages",
|
||||
O::Landscape => "Landscape Pages",
|
||||
O::Portrait => "Portrait Pages",
|
||||
O::Both => tl!("Landscape and Portrait Pages"),
|
||||
O::Landscape => tl!("Landscape Pages"),
|
||||
O::Portrait => tl!("Portrait Pages"),
|
||||
})
|
||||
.show_ui(ui, |ui| {
|
||||
ui.selectable_value(&mut d.orientation, O::Both, "Landscape and Portrait Pages");
|
||||
ui.selectable_value(&mut d.orientation, O::Landscape, "Landscape Pages");
|
||||
ui.selectable_value(&mut d.orientation, O::Portrait, "Portrait Pages");
|
||||
ui.selectable_value(&mut d.orientation, O::Both, tl!("Landscape and Portrait Pages"));
|
||||
ui.selectable_value(&mut d.orientation, O::Landscape, tl!("Landscape Pages"));
|
||||
ui.selectable_value(&mut d.orientation, O::Portrait, tl!("Portrait Pages"));
|
||||
});
|
||||
ui.end_row();
|
||||
});
|
||||
d.to = d.to.max(d.from);
|
||||
ui.add_space(12.0);
|
||||
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
|
||||
if widgets::pill_button(ui, "OK", true).clicked() {
|
||||
if widgets::pill_button(ui, tl!("OK"), true).clicked() {
|
||||
rotate_now = true;
|
||||
close = true;
|
||||
}
|
||||
if widgets::pill_button(ui, "Cancel", false).clicked() {
|
||||
if widgets::pill_button(ui, tl!("Cancel"), false).clicked() {
|
||||
close = true;
|
||||
}
|
||||
});
|
||||
|
|
@ -626,26 +646,26 @@ pub fn show(app: &mut PdfCraftApp, ctx: &egui::Context) {
|
|||
close = true;
|
||||
return;
|
||||
};
|
||||
ui.label(egui::RichText::new("Duplicate Field").font(theme::semibold(18.0)));
|
||||
ui.label(egui::RichText::new(tl!("Duplicate Field")).font(theme::semibold(18.0)));
|
||||
ui.add_space(8.0);
|
||||
ui.label(format!("Duplicate \"{}\" onto:", d.name));
|
||||
ui.radio_value(&mut d.all, true, "All pages");
|
||||
ui.label(crate::i18n::fmt(tl!("Duplicate \"{name}\" onto:"), &[("name", &d.name)]));
|
||||
ui.radio_value(&mut d.all, true, tl!("All pages"));
|
||||
ui.horizontal(|ui| {
|
||||
ui.radio_value(&mut d.all, false, "From");
|
||||
ui.radio_value(&mut d.all, false, tl!("From"));
|
||||
ui.add_enabled(!d.all, egui::DragValue::new(&mut d.from).range(1..=n));
|
||||
ui.label("to");
|
||||
ui.label(tl!("to"));
|
||||
ui.add_enabled(!d.all, egui::DragValue::new(&mut d.to).range(1..=n));
|
||||
ui.label(format!("of {n}"));
|
||||
ui.label(crate::i18n::fmt(tl!("of {name}"), &[("name", &n.to_string())]));
|
||||
});
|
||||
d.to = d.to.max(d.from);
|
||||
ui.add_space(12.0);
|
||||
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
|
||||
if widgets::pill_button(ui, "OK", true).clicked() {
|
||||
if widgets::pill_button(ui, tl!("OK"), true).clicked() {
|
||||
let pages: Vec<usize> = if d.all { (0..n).collect() } else { (d.from - 1..d.to.min(n)).collect() };
|
||||
duplicate_now = Some(Edit::DuplicateField { name: d.name.clone(), pages });
|
||||
close = true;
|
||||
}
|
||||
if widgets::pill_button(ui, "Cancel", false).clicked() {
|
||||
if widgets::pill_button(ui, tl!("Cancel"), false).clicked() {
|
||||
close = true;
|
||||
}
|
||||
});
|
||||
|
|
@ -694,26 +714,26 @@ pub fn show(app: &mut PdfCraftApp, ctx: &egui::Context) {
|
|||
return;
|
||||
}
|
||||
Dialog::SummarizeComments => {
|
||||
ui.label(egui::RichText::new("Summarize Options").font(theme::semibold(18.0)));
|
||||
ui.label(egui::RichText::new(tl!("Summarize Options")).font(theme::semibold(18.0)));
|
||||
ui.add_space(8.0);
|
||||
ui.label("Choose a layout:");
|
||||
let _ = ui.radio(true, "Comments only");
|
||||
ui.label(tl!("Choose a layout:"));
|
||||
let _ = ui.radio(true, tl!("Comments only"));
|
||||
ui.add_space(6.0);
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Sort comments by:");
|
||||
egui::ComboBox::from_id_salt("summary-sort").selected_text(app.summary_sort.name()).show_ui(ui, |ui| {
|
||||
ui.label(tl!("Sort comments by:"));
|
||||
egui::ComboBox::from_id_salt("summary-sort").selected_text(tl!(app.summary_sort.name())).show_ui(ui, |ui| {
|
||||
for s in pdfcraft_engine::SummarySort::ALL {
|
||||
ui.selectable_value(&mut app.summary_sort, s, s.name());
|
||||
ui.selectable_value(&mut app.summary_sort, s, tl!(s.name()));
|
||||
}
|
||||
});
|
||||
});
|
||||
ui.add_space(12.0);
|
||||
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
|
||||
if widgets::pill_button(ui, "Create PDF Comment Summary", true).clicked() {
|
||||
if widgets::pill_button(ui, tl!("Create PDF Comment Summary"), true).clicked() {
|
||||
summarize_now = true;
|
||||
close = true;
|
||||
}
|
||||
if widgets::pill_button(ui, "Cancel", false).clicked() {
|
||||
if widgets::pill_button(ui, tl!("Cancel"), false).clicked() {
|
||||
close = true;
|
||||
}
|
||||
});
|
||||
|
|
@ -721,16 +741,19 @@ pub fn show(app: &mut PdfCraftApp, ctx: &egui::Context) {
|
|||
}
|
||||
Dialog::Revert => {
|
||||
let name = app.active_ids().and_then(|(_, id)| app.session.get(id)).map(|d| d.name.clone()).unwrap_or_default();
|
||||
ui.label(egui::RichText::new("Revert").font(theme::semibold(18.0)));
|
||||
ui.label(egui::RichText::new(tl!("Revert")).font(theme::semibold(18.0)));
|
||||
ui.add_space(8.0);
|
||||
ui.label(format!("Revert to the previously saved version of \"{name}\"? Changes since then can't be undone afterwards."));
|
||||
ui.label(crate::i18n::fmt(
|
||||
tl!("Revert to the previously saved version of “{name}”? Changes since then can't be undone afterwards."),
|
||||
&[("name", &name)],
|
||||
));
|
||||
ui.add_space(12.0);
|
||||
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
|
||||
if widgets::pill_button(ui, "Revert", true).clicked() {
|
||||
if widgets::pill_button(ui, tl!("Revert"), true).clicked() {
|
||||
revert_now = true;
|
||||
close = true;
|
||||
}
|
||||
if widgets::pill_button(ui, "Cancel", false).clicked() {
|
||||
if widgets::pill_button(ui, tl!("Cancel"), false).clicked() {
|
||||
close = true;
|
||||
}
|
||||
});
|
||||
|
|
@ -890,7 +913,7 @@ pub fn show(app: &mut PdfCraftApp, ctx: &egui::Context) {
|
|||
}
|
||||
Dialog::NumberPages => {
|
||||
use pdfcraft_engine::LabelStyle as L;
|
||||
ui.label(egui::RichText::new("Number pages").font(theme::semibold(18.0)));
|
||||
ui.label(egui::RichText::new(tl!("Number pages")).font(theme::semibold(18.0)));
|
||||
ui.add_space(8.0);
|
||||
let Some((_, id)) = app.active_ids() else { return };
|
||||
let n = app.session.get(id).map(|d| d.info.pages.len()).unwrap_or(1).max(1);
|
||||
|
|
@ -907,22 +930,22 @@ pub fn show(app: &mut PdfCraftApp, ctx: &egui::Context) {
|
|||
.inner
|
||||
};
|
||||
egui::Grid::new("number_pages").num_columns(2).spacing([12.0, 8.0]).show(ui, |ui| {
|
||||
ui.label("Pages");
|
||||
ui.label(tl!("Pages"));
|
||||
ui.horizontal(|ui| {
|
||||
ui.add(egui::DragValue::new(&mut d.from).range(1..=n));
|
||||
ui.label("to");
|
||||
ui.label(tl!("to"));
|
||||
ui.add(egui::DragValue::new(&mut d.to).range(1..=n));
|
||||
ui.label(egui::RichText::new(format!("of {n}")).color(t.text_muted));
|
||||
ui.label(egui::RichText::new(crate::i18n::fmt(tl!("of {name}"), &[("name", &n.to_string())])).color(t.text_muted));
|
||||
});
|
||||
ui.end_row();
|
||||
ui.label("Style");
|
||||
ui.label(tl!("Style"));
|
||||
let styles = [
|
||||
(L::Decimal, "1, 2, 3"),
|
||||
(L::LowerRoman, "i, ii, iii"),
|
||||
(L::UpperRoman, "I, II, III"),
|
||||
(L::LowerAlpha, "a, b, c"),
|
||||
(L::UpperAlpha, "A, B, C"),
|
||||
(L::None, "None (prefix only)"),
|
||||
(L::None, tl!("None (prefix only)")),
|
||||
];
|
||||
let current = styles.iter().find(|(s, _)| *s == d.style).map_or("1, 2, 3", |(_, l)| *l);
|
||||
egui::ComboBox::from_id_salt("label_style").selected_text(current).show_ui(ui, |ui| {
|
||||
|
|
@ -931,11 +954,11 @@ pub fn show(app: &mut PdfCraftApp, ctx: &egui::Context) {
|
|||
}
|
||||
});
|
||||
ui.end_row();
|
||||
let l = ui.label("Prefix");
|
||||
let l = ui.label(tl!("Prefix"));
|
||||
boxed(ui, &mut |ui| ui.add(egui::TextEdit::singleline(&mut d.prefix).desired_width(160.0).frame(egui::Frame::NONE)))
|
||||
.labelled_by(l.id);
|
||||
ui.end_row();
|
||||
ui.label("Start");
|
||||
ui.label(tl!("Start"));
|
||||
ui.add(egui::DragValue::new(&mut d.start).range(1..=99_999));
|
||||
ui.end_row();
|
||||
});
|
||||
|
|
@ -947,16 +970,19 @@ pub fn show(app: &mut PdfCraftApp, ctx: &egui::Context) {
|
|||
} else {
|
||||
format!("{}, {} … {}", label(d.start), label(d.start + 1), label(d.start + (d.to - d.from) as u32))
|
||||
};
|
||||
ui.label(egui::RichText::new(format!("Labels: {preview}. Later pages keep their labels.")).color(t.text_muted));
|
||||
ui.label(
|
||||
egui::RichText::new(crate::i18n::fmt(tl!("Labels: {preview}. Later pages keep their labels."), &[("preview", &preview)]))
|
||||
.color(t.text_muted),
|
||||
);
|
||||
number_now = Some(Edit::NumberPages { from: d.from - 1, to: d.to - 1, style: d.style, prefix: d.prefix.clone(), first: d.start });
|
||||
}
|
||||
Dialog::Recovery => {
|
||||
ui.horizontal(|ui| {
|
||||
ui.add(crate::icons::image("clock-3", 22.0, t.accent));
|
||||
ui.label(egui::RichText::new("Recover unsaved documents?").font(theme::semibold(18.0)));
|
||||
ui.label(egui::RichText::new(tl!("Recover unsaved documents?")).font(theme::semibold(18.0)));
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
ui.label("PdfCraft didn't shut down normally. These documents had changes that were autosaved:");
|
||||
ui.label(tl!("PdfCraft didn't shut down normally. These documents had changes that were autosaved:"));
|
||||
ui.add_space(8.0);
|
||||
let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0);
|
||||
egui::Grid::new("recoverable").num_columns(2).spacing([18.0, 6.0]).show(ui, |ui| {
|
||||
|
|
@ -964,43 +990,43 @@ pub fn show(app: &mut PdfCraftApp, ctx: &egui::Context) {
|
|||
ui.label(egui::RichText::new(&m.name).font(theme::medium(13.0)));
|
||||
let mins = now.saturating_sub(m.saved_at) / 60;
|
||||
let when = match mins {
|
||||
0 => "just now".to_string(),
|
||||
1..=59 => format!("{mins} min ago"),
|
||||
_ => format!("{} h ago", mins / 60),
|
||||
0 => tl!("just now").to_string(),
|
||||
1..=59 => crate::i18n::fmt(tl!("{n} min ago"), &[("n", &mins.to_string())]),
|
||||
_ => crate::i18n::fmt(tl!("{n} h ago"), &[("n", &(mins / 60).to_string())]),
|
||||
};
|
||||
let lock = if m.encrypted { " · password-protected" } else { "" };
|
||||
let lock = if m.encrypted { tl!(" · password-protected") } else { "" };
|
||||
ui.label(egui::RichText::new(format!("{when}{lock}")).color(t.text_muted));
|
||||
ui.end_row();
|
||||
}
|
||||
});
|
||||
}
|
||||
Dialog::Shortcuts => {
|
||||
ui.label(egui::RichText::new("Keyboard shortcuts").font(theme::semibold(18.0)));
|
||||
ui.label(egui::RichText::new(tl!("Keyboard shortcuts")).font(theme::semibold(18.0)));
|
||||
ui.add_space(8.0);
|
||||
let mac = cfg!(target_os = "macos") || cfg!(target_arch = "wasm32");
|
||||
// Registered commands first (always in sync with the real bindings), then the
|
||||
// keys the document view handles itself.
|
||||
let mut rows: Vec<(String, String)> = pdfcraft_engine::commands::COMMANDS
|
||||
.iter()
|
||||
.filter_map(|c| c.shortcut.map(|k| (k.label(mac), c.label.trim_end_matches('…').to_string())))
|
||||
.filter_map(|c| c.shortcut.map(|k| (k.label(mac), tl!(c.label).trim_end_matches('…').to_string())))
|
||||
.collect();
|
||||
for (k, v) in [
|
||||
("⌘G / ⇧⌘G", "Next / previous match"),
|
||||
("⌘C", "Copy selected text"),
|
||||
("Double-click", "Select a word"),
|
||||
("Esc", "Clear selection / close find"),
|
||||
("⌘1", "Actual size"),
|
||||
("⌘0", "Zoom to page level"),
|
||||
("⌘2", "Fit to width"),
|
||||
("⌘3", "Fit visible"),
|
||||
("⌘+ / ⌘−", "Zoom in / out (also pinch or ⌘-scroll)"),
|
||||
("⇧⌘+ / ⇧⌘−", "Rotate view"),
|
||||
("Home / End", "First / last page"),
|
||||
("⌘← / ⌘→", "Previous / next page"),
|
||||
("Delete", "Delete selected pages (Organize)"),
|
||||
("⌘A", "Select all pages (Organize)"),
|
||||
("⌘G / ⇧⌘G", tl!("Next / previous match")),
|
||||
("⌘C", tl!("Copy selected text")),
|
||||
("Double-click", tl!("Select a word")),
|
||||
("Esc", tl!("Clear selection / close find")),
|
||||
("⌘1", tl!("Actual size")),
|
||||
("⌘0", tl!("Zoom to page level")),
|
||||
("⌘2", tl!("Fit to width")),
|
||||
("⌘3", tl!("Fit visible")),
|
||||
("⌘+ / ⌘−", tl!("Zoom in / out (also pinch or ⌘-scroll)")),
|
||||
("⇧⌘+ / ⇧⌘−", tl!("Rotate view")),
|
||||
("Home / End", tl!("First / last page")),
|
||||
("⌘← / ⌘→", tl!("Previous / next page")),
|
||||
("Delete", tl!("Delete selected pages (Organize)")),
|
||||
("⌘A", tl!("Select all pages (Organize)")),
|
||||
] {
|
||||
rows.push((k.to_string(), v.to_string()));
|
||||
rows.push((tl!(k).to_string(), tl!(v).to_string()));
|
||||
}
|
||||
egui::ScrollArea::vertical().max_height(460.0).show(ui, |ui| {
|
||||
egui::Grid::new("keys").num_columns(2).spacing([24.0, 6.0]).show(ui, |ui| {
|
||||
|
|
@ -1017,11 +1043,11 @@ pub fn show(app: &mut PdfCraftApp, ctx: &egui::Context) {
|
|||
widgets::artcraft_mark(ui, 40.0);
|
||||
ui.vertical(|ui| {
|
||||
ui.label(egui::RichText::new("PdfCraft").font(theme::semibold(20.0)));
|
||||
ui.label(format!("Version {}", env!("CARGO_PKG_VERSION")));
|
||||
ui.label(crate::i18n::fmt(tl!("Version {v}"), &[("v", env!("CARGO_PKG_VERSION"))]));
|
||||
});
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
ui.label("A clean-room, open-source PDF application written in Rust. MIT OR Apache-2.0.");
|
||||
ui.label(tl!("A clean-room, open-source PDF application written in Rust. MIT OR Apache-2.0."));
|
||||
ui.label(
|
||||
egui::RichText::new(
|
||||
"Rendering: hayro (bootstrap) · UI: egui · Icons: Lucide (ISC) · Fonts: Inter, JetBrains Mono, Dancing Script (OFL)",
|
||||
|
|
@ -1031,7 +1057,7 @@ pub fn show(app: &mut PdfCraftApp, ctx: &egui::Context) {
|
|||
);
|
||||
ui.add_space(12.0);
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(egui::RichText::new("Part of").color(t.text_muted));
|
||||
ui.label(egui::RichText::new(tl!("Part of")).color(t.text_muted));
|
||||
widgets::artcraft_logo(ui, 16.0);
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
|
|
@ -1044,39 +1070,39 @@ pub fn show(app: &mut PdfCraftApp, ctx: &egui::Context) {
|
|||
let changed = draft_changes(app).is_some_and(|c| !c.is_empty());
|
||||
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
|
||||
if dialog == Dialog::Recovery {
|
||||
if widgets::pill_button(ui, "Recover", true).clicked() {
|
||||
if widgets::pill_button(ui, tl!("Recover"), true).clicked() {
|
||||
recover = Some(true);
|
||||
close = true;
|
||||
}
|
||||
if widgets::pill_button(ui, "Discard", false).clicked() {
|
||||
if widgets::pill_button(ui, tl!("Discard"), false).clicked() {
|
||||
recover = Some(false);
|
||||
close = true;
|
||||
}
|
||||
} else if dialog == Dialog::NumberPages {
|
||||
if widgets::pill_button(ui, "OK", true).clicked() {
|
||||
if widgets::pill_button(ui, tl!("OK"), true).clicked() {
|
||||
apply_number = true;
|
||||
close = true;
|
||||
}
|
||||
if widgets::pill_button(ui, "Cancel", false).clicked() {
|
||||
if widgets::pill_button(ui, tl!("Cancel"), false).clicked() {
|
||||
close = true;
|
||||
}
|
||||
} else if dialog == Dialog::Split {
|
||||
if ui.add_enabled_ui(split_ready.is_some(), |ui| widgets::pill_button(ui, "Split", true)).inner.clicked() {
|
||||
if ui.add_enabled_ui(split_ready.is_some(), |ui| widgets::pill_button(ui, tl!("Split"), true)).inner.clicked() {
|
||||
split_now = split_ready.clone();
|
||||
close = true;
|
||||
}
|
||||
if widgets::pill_button(ui, "Cancel", false).clicked() {
|
||||
if widgets::pill_button(ui, tl!("Cancel"), false).clicked() {
|
||||
close = true;
|
||||
}
|
||||
} else if changed {
|
||||
if widgets::pill_button(ui, "OK", true).clicked() {
|
||||
if widgets::pill_button(ui, tl!("OK"), true).clicked() {
|
||||
apply = true;
|
||||
close = true;
|
||||
}
|
||||
if widgets::pill_button(ui, "Cancel", false).clicked() {
|
||||
if widgets::pill_button(ui, tl!("Cancel"), false).clicked() {
|
||||
close = true;
|
||||
}
|
||||
} else if widgets::pill_button(ui, "Close", true).clicked() {
|
||||
} else if widgets::pill_button(ui, tl!("Close"), true).clicked() {
|
||||
close = true;
|
||||
}
|
||||
});
|
||||
|
|
@ -1134,18 +1160,26 @@ pub fn show(app: &mut PdfCraftApp, ctx: &egui::Context) {
|
|||
let which: Vec<pdfcraft_engine::Hidden> = app.hidden_draft.found.iter().filter(|f| f.2 && f.1 > 0).map(|f| f.0).collect();
|
||||
let n: usize = app.hidden_draft.found.iter().filter(|f| f.2).map(|f| f.1).sum();
|
||||
if app.apply_edit(Edit::RemoveHidden { which }) {
|
||||
app.notify(format!("Removed {n} hidden item{}. Save to remove them from the file.", if n == 1 { "" } else { "s" }));
|
||||
if n == 1 {
|
||||
app.notify_tr("Removed 1 hidden item. Save to remove them from the file.");
|
||||
} else {
|
||||
app.notify_fmt("Removed {n} hidden items. Save to remove them from the file.", &[("n", &n.to_string())]);
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(Dialog::Sanitize) => {
|
||||
if app.apply_edit(Edit::Sanitize) {
|
||||
app.notify("Document sanitized. Save to finish: saving rewrites the whole file.");
|
||||
app.notify_tr("Document sanitized. Save to finish: saving rewrites the whole file.");
|
||||
}
|
||||
}
|
||||
Some(Dialog::RedactApply) => {
|
||||
let marks = app.active_ids().and_then(|(_, id)| app.session.get(id)).map_or(0, |d| d.redaction_marks());
|
||||
if app.apply_edit(Edit::ApplyRedactions { pages: None }) {
|
||||
app.notify(format!("Applied {marks} redaction mark{}. Save to remove the content from the file.", if marks == 1 { "" } else { "s" }));
|
||||
if marks == 1 {
|
||||
app.notify_tr("Applied 1 redaction mark. Save to remove the content from the file.");
|
||||
} else {
|
||||
app.notify_fmt("Applied {n} redaction marks. Save to remove the content from the file.", &[("n", &marks.to_string())]);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
|
|
@ -1188,7 +1222,7 @@ pub fn show(app: &mut PdfCraftApp, ctx: &egui::Context) {
|
|||
}
|
||||
if protect_now && app.apply_edit(app.protect_draft.edit()) {
|
||||
app.protect_draft = Default::default();
|
||||
app.notify("Password protection will be applied when you save");
|
||||
app.notify_tr("Password protection will be applied when you save");
|
||||
}
|
||||
if apply_number && let Some(edit) = number_now {
|
||||
app.apply_edit(edit);
|
||||
|
|
@ -1290,20 +1324,23 @@ fn save_prompt(app: &mut PdfCraftApp, ctx: &egui::Context) {
|
|||
ui.set_width(420.0);
|
||||
ui.horizontal(|ui| {
|
||||
ui.add(crate::icons::image("save", 22.0, t.accent));
|
||||
ui.label(egui::RichText::new(format!("Save changes to “{name}” before closing?")).font(theme::semibold(16.0)));
|
||||
ui.label(
|
||||
egui::RichText::new(crate::i18n::fmt(tl!("Save changes to “{name}” before closing?"), &[("name", &name)]))
|
||||
.font(theme::semibold(16.0)),
|
||||
);
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
ui.label(egui::RichText::new("Your changes will be lost if you don't save them.").color(t.text_muted));
|
||||
ui.label(egui::RichText::new(tl!("Your changes will be lost if you don't save them.")).color(t.text_muted));
|
||||
ui.add_space(14.0);
|
||||
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
|
||||
if widgets::pill_button(ui, "Save", true).clicked() {
|
||||
if widgets::pill_button(ui, tl!("Save"), true).clicked() {
|
||||
choice = Some(Some(true));
|
||||
}
|
||||
if widgets::pill_button(ui, "Cancel", false).clicked() {
|
||||
if widgets::pill_button(ui, tl!("Cancel"), false).clicked() {
|
||||
choice = Some(None);
|
||||
}
|
||||
ui.add_space(24.0);
|
||||
if widgets::pill_button(ui, "Don't save", false).clicked() {
|
||||
if widgets::pill_button(ui, tl!("Don't save"), false).clicked() {
|
||||
choice = Some(Some(false));
|
||||
}
|
||||
});
|
||||
|
|
@ -1323,7 +1360,7 @@ fn link_prompt(app: &mut PdfCraftApp, ctx: &egui::Context) {
|
|||
let Some(pending) = app.pending_link.clone() else { return };
|
||||
let t = Tokens::get(ctx);
|
||||
let email = pending.url.get(..7).is_some_and(|s| s.eq_ignore_ascii_case("mailto:"));
|
||||
let (title, open) = if email { ("Write this email?", "Open email app") } else { ("Open this web page?", "Open link") };
|
||||
let (title, open) = if email { (tl!("Write this email?"), tl!("Open email app")) } else { (tl!("Open this web page?"), tl!("Open link")) };
|
||||
let mut choice: Option<bool> = None;
|
||||
let modal = egui::Modal::new(egui::Id::new("link_prompt")).show(ctx, |ui| {
|
||||
ui.set_width(460.0);
|
||||
|
|
@ -1332,8 +1369,12 @@ fn link_prompt(app: &mut PdfCraftApp, ctx: &egui::Context) {
|
|||
ui.label(egui::RichText::new(title).font(theme::semibold(16.0)));
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
let what = if email { "your email app with this address" } else { "this address in your browser" };
|
||||
ui.label(format!("{} in this document wants to open {what}:", pending.origin.noun()));
|
||||
let template = if email {
|
||||
tl!("{who} in this document wants to open your email app with this address:")
|
||||
} else {
|
||||
tl!("{who} in this document wants to open this address in your browser:")
|
||||
};
|
||||
ui.label(crate::i18n::fmt(template, &[("who", tl!(pending.origin.noun()))]));
|
||||
ui.add_space(6.0);
|
||||
if let Some(host) = pdfcraft_engine::links::host(&pending.url) {
|
||||
ui.label(egui::RichText::new(host).font(theme::semibold(14.0)));
|
||||
|
|
@ -1343,15 +1384,15 @@ fn link_prompt(app: &mut PdfCraftApp, ctx: &egui::Context) {
|
|||
});
|
||||
ui.add_space(8.0);
|
||||
ui.label(
|
||||
egui::RichText::new(
|
||||
"Only continue if you trust this document. An address can carry information from the document, such as what you typed into its form.",
|
||||
)
|
||||
egui::RichText::new(tl!(
|
||||
"Only continue if you trust this document. An address can carry information from the document, such as what you typed into its form."
|
||||
))
|
||||
.color(t.text_muted)
|
||||
.small(),
|
||||
);
|
||||
ui.add_space(14.0);
|
||||
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
|
||||
if widgets::pill_button(ui, "Cancel", true).clicked() {
|
||||
if widgets::pill_button(ui, tl!("Cancel"), true).clicked() {
|
||||
choice = Some(false);
|
||||
}
|
||||
if widgets::pill_button(ui, open, false).clicked() {
|
||||
|
|
@ -1368,7 +1409,7 @@ fn link_prompt(app: &mut PdfCraftApp, ctx: &egui::Context) {
|
|||
}
|
||||
|
||||
fn yes(b: bool) -> String {
|
||||
if b { "Yes".into() } else { "No".into() }
|
||||
tl!(if b { "Yes" } else { "No" }).to_string()
|
||||
}
|
||||
|
||||
/// Password prompt for encrypted documents (Acrobat: "Password" dialog on open).
|
||||
|
|
@ -1381,12 +1422,12 @@ fn password(app: &mut PdfCraftApp, ctx: &egui::Context) {
|
|||
ui.set_width(400.0);
|
||||
ui.horizontal(|ui| {
|
||||
ui.add(crate::icons::image("lock", 22.0, t.accent));
|
||||
ui.label(egui::RichText::new("Password required").font(theme::semibold(17.0)));
|
||||
ui.label(egui::RichText::new(tl!("Password required")).font(theme::semibold(17.0)));
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
ui.label(format!("“{}” is protected. Enter a password to open it.", prompt.name));
|
||||
ui.label(crate::i18n::fmt(tl!("“{name}” is protected. Enter a password to open it."), &[("name", &prompt.name)]));
|
||||
ui.add_space(8.0);
|
||||
let r = ui.add(egui::TextEdit::singleline(&mut prompt.input).password(true).hint_text("Password").desired_width(f32::INFINITY));
|
||||
let r = ui.add(egui::TextEdit::singleline(&mut prompt.input).password(true).hint_text(tl!("Password")).desired_width(f32::INFINITY));
|
||||
// Enter submits. The field keeps focus (we request it every frame), so check the key
|
||||
// while it is focused as well as on the frame focus is lost.
|
||||
if (r.has_focus() || r.lost_focus()) && ui.input(|i| i.key_pressed(egui::Key::Enter)) {
|
||||
|
|
@ -1395,14 +1436,15 @@ fn password(app: &mut PdfCraftApp, ctx: &egui::Context) {
|
|||
r.request_focus();
|
||||
if let Some(e) = &prompt.error {
|
||||
ui.add_space(4.0);
|
||||
ui.label(egui::RichText::new(e).color(egui::Color32::from_rgb(0xD1, 0x3B, 0x3B)));
|
||||
// Known error text is translated; anything else passes through.
|
||||
ui.label(egui::RichText::new(tl!(e)).color(egui::Color32::from_rgb(0xD1, 0x3B, 0x3B)));
|
||||
}
|
||||
ui.add_space(12.0);
|
||||
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
|
||||
if widgets::pill_button(ui, "Open", true).clicked() {
|
||||
if widgets::pill_button(ui, tl!("Open"), true).clicked() {
|
||||
submit = true;
|
||||
}
|
||||
if widgets::pill_button(ui, "Cancel", false).clicked() {
|
||||
if widgets::pill_button(ui, tl!("Cancel"), false).clicked() {
|
||||
cancel = true;
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -58,10 +58,11 @@ impl Default for Extras {
|
|||
pub(crate) fn extras_panel(ui: &mut egui::Ui, e: &mut Extras) -> bool {
|
||||
let before = *e;
|
||||
ui.horizontal(|ui| {
|
||||
if crate::icons::button(ui, "underline", 26.0, e.underline, "Underline").clicked() {
|
||||
if crate::icons::button(ui, "underline", 26.0, e.underline, tl!("Underline")).clicked() {
|
||||
e.underline = !e.underline;
|
||||
}
|
||||
let label = |v: f64| if v == 0.0 { "Line spacing".to_string() } else { format!("{v:.2}×") };
|
||||
let zero = tl!("Line spacing").to_string();
|
||||
let label = |v: f64| if v == 0.0 { zero.clone() } else { format!("{v:.2}×") };
|
||||
egui::ComboBox::from_id_salt("line-spacing").selected_text(label(e.line_spacing)).width(110.0).show_ui(ui, |ui| {
|
||||
for v in [1.0, 1.15, 1.5, 2.0] {
|
||||
ui.selectable_value(&mut e.line_spacing, v, label(v));
|
||||
|
|
@ -69,11 +70,11 @@ pub(crate) fn extras_panel(ui: &mut egui::Ui, e: &mut Extras) -> bool {
|
|||
});
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
let l = ui.label("Character spacing");
|
||||
let l = ui.label(tl!("Character spacing"));
|
||||
ui.add(egui::DragValue::new(&mut e.char_spacing).range(-5.0..=50.0).speed(0.1).suffix(" pt")).labelled_by(l.id);
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
let l = ui.label("Horizontal scale");
|
||||
let l = ui.label(tl!("Horizontal scale"));
|
||||
ui.add(egui::DragValue::new(&mut e.scale).range(10.0..=400.0).speed(1.0).suffix(" %")).labelled_by(l.id);
|
||||
});
|
||||
*e != before
|
||||
|
|
@ -304,10 +305,10 @@ pub(crate) fn image_input(
|
|||
resp.context_menu(|ui| {
|
||||
use pdfcraft_engine::ImageEdit as E;
|
||||
let items: [(&str, Option<E>); 4] = [
|
||||
("Rotate Clockwise", Some(E::Rotate(1))),
|
||||
("Rotate Counterclockwise", Some(E::Rotate(3))),
|
||||
("Flip Horizontal", Some(E::Flip { horizontal: true })),
|
||||
("Flip Vertical", Some(E::Flip { horizontal: false })),
|
||||
(tl!("Rotate Clockwise"), Some(E::Rotate(1))),
|
||||
(tl!("Rotate Counterclockwise"), Some(E::Rotate(3))),
|
||||
(tl!("Flip Horizontal"), Some(E::Flip { horizontal: true })),
|
||||
(tl!("Flip Vertical"), Some(E::Flip { horizontal: false })),
|
||||
];
|
||||
for (label, change) in items {
|
||||
if ui.button(label).clicked() {
|
||||
|
|
@ -315,16 +316,16 @@ pub(crate) fn image_input(
|
|||
ui.close();
|
||||
}
|
||||
}
|
||||
if ui.button("Replace Image…").clicked() {
|
||||
if ui.button(tl!("Replace Image…")).clicked() {
|
||||
*action = Some(ImageAction::Replace(page, hit));
|
||||
ui.close();
|
||||
}
|
||||
if ui.button("Save Image As…").clicked() {
|
||||
if ui.button(tl!("Save Image As…")).clicked() {
|
||||
*action = Some(ImageAction::Save(page, hit));
|
||||
ui.close();
|
||||
}
|
||||
ui.separator();
|
||||
if ui.button("Delete").clicked() {
|
||||
if ui.button(tl!("Delete")).clicked() {
|
||||
view.image_selection = None;
|
||||
view.pending_edit = Some(Edit::EditPageImage { page, index: hit, change: E::Delete });
|
||||
ui.close();
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ impl PdfCraftApp {
|
|||
true
|
||||
}
|
||||
Err(e) => {
|
||||
self.notify(format!("{label} failed: {e}"));
|
||||
self.notify_fmt("{label} failed: {e}", &[("label", &crate::i18n::action_label(&label)), ("e", &e.to_string())]);
|
||||
false
|
||||
}
|
||||
}
|
||||
|
|
@ -89,9 +89,14 @@ impl PdfCraftApp {
|
|||
self.views[i].document_changed(&doc.info);
|
||||
}
|
||||
self.views[i].comments.selected = None;
|
||||
self.notify(format!("{} {label}", if undo { "Undid" } else { "Redid" }));
|
||||
let label = crate::i18n::action_label(&label);
|
||||
if undo {
|
||||
self.notify_fmt("Undid {label}", &[("label", &label)]);
|
||||
} else {
|
||||
self.notify_fmt("Redid {label}", &[("label", &label)]);
|
||||
}
|
||||
}
|
||||
Err(e) => self.notify(e.to_string()),
|
||||
Err(e) => self.notify_error(e),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -119,21 +124,25 @@ impl PdfCraftApp {
|
|||
let pages = self.views[i].target_pages();
|
||||
let n = doc.info.pages.len();
|
||||
if cut && pages.len() >= n {
|
||||
self.notify("A document needs at least one page: copy instead");
|
||||
self.notify_tr("A document needs at least one page: copy instead");
|
||||
return;
|
||||
}
|
||||
self.page_clipboard = Some(crate::PageClip { name: doc.name.clone(), bytes: doc.bytes.clone(), pages: pages.clone() });
|
||||
if cut {
|
||||
self.apply_edit(Edit::DeletePages { pages: pages.clone() });
|
||||
}
|
||||
let what = if pages.len() == 1 { "1 page".to_string() } else { format!("{} pages", pages.len()) };
|
||||
self.notify(format!("{} {what}", if cut { "Cut" } else { "Copied" }));
|
||||
let what = if pages.len() == 1 { tl!("1 page").to_string() } else { crate::i18n::fmt(tl!("{n} pages"), &[("n", &pages.len().to_string())]) };
|
||||
if cut {
|
||||
self.notify_fmt("Cut {what}", &[("what", &what)]);
|
||||
} else {
|
||||
self.notify_fmt("Copied {what}", &[("what", &what)]);
|
||||
}
|
||||
}
|
||||
|
||||
/// Organize ▸ Paste: insert the copied pages after the selection (or the current page).
|
||||
pub fn paste_pages(&mut self) {
|
||||
let Some(clip) = self.page_clipboard.clone() else {
|
||||
self.notify("Copy or cut pages first");
|
||||
self.notify_tr("Copy or cut pages first");
|
||||
return;
|
||||
};
|
||||
let Some(i) = self.active else { return };
|
||||
|
|
@ -160,7 +169,7 @@ impl PdfCraftApp {
|
|||
let bytes = match self.session.save_bytes(id) {
|
||||
Ok(b) => b,
|
||||
Err(e) => {
|
||||
self.notify(format!("Couldn't save {name}: {e}"));
|
||||
self.notify_fmt("Couldn't save {name}: {e}", &[("name", &name), ("e", &e.to_string())]);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
|
@ -173,7 +182,7 @@ impl PdfCraftApp {
|
|||
{
|
||||
let Some(dest) = destination else { return false };
|
||||
if let Err(e) = write_atomically(&dest, &bytes) {
|
||||
self.notify(format!("Couldn't save {dest}: {e}"));
|
||||
self.notify_fmt("Couldn't save {name}: {e}", &[("name", &dest), ("e", &e.to_string())]);
|
||||
return false;
|
||||
}
|
||||
match self.session.mark_saved(id, bytes, Some(dest.clone())) {
|
||||
|
|
@ -182,11 +191,11 @@ impl PdfCraftApp {
|
|||
if let Some(doc) = self.session.get(id) {
|
||||
self.views[index].document_changed(&doc.info);
|
||||
}
|
||||
self.notify(format!("Saved {}", short_name(&dest)));
|
||||
self.notify_fmt("Saved {name}", &[("name", &short_name(&dest))]);
|
||||
true
|
||||
}
|
||||
Err(e) => {
|
||||
self.notify(format!("Saved, but reopening failed: {e}"));
|
||||
self.notify_fmt("Saved, but reopening failed: {e}", &[("e", &e.to_string())]);
|
||||
false
|
||||
}
|
||||
}
|
||||
|
|
@ -200,11 +209,11 @@ impl PdfCraftApp {
|
|||
if let Some(doc) = self.session.get(id) {
|
||||
self.views[index].document_changed(&doc.info);
|
||||
}
|
||||
self.notify(format!("Downloaded {name}"));
|
||||
self.notify_fmt("Downloaded {name}", &[("name", &name)]);
|
||||
true
|
||||
}
|
||||
Err(e) => {
|
||||
self.notify(format!("Couldn't download {name}: {e}"));
|
||||
self.notify_fmt("Couldn't download {name}: {e}", &[("name", &name), ("e", &e.to_string())]);
|
||||
false
|
||||
}
|
||||
}
|
||||
|
|
@ -254,9 +263,9 @@ impl PdfCraftApp {
|
|||
{
|
||||
self.views[i].document_changed(&d.info);
|
||||
}
|
||||
self.notify("Reverted to the last saved version");
|
||||
self.notify_tr("Reverted to the last saved version");
|
||||
}
|
||||
Err(e) => self.notify(e.to_string()),
|
||||
Err(e) => self.notify_error(e),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -43,24 +43,26 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens, kind: E
|
|||
let d = &mut app.export_draft;
|
||||
ui.label(
|
||||
egui::RichText::new(match kind {
|
||||
ExportKind::Image => "Export to Image",
|
||||
ExportKind::Text => "Export to Text",
|
||||
ExportKind::AllImages => "Export All Images",
|
||||
ExportKind::Image => tl!("Export to Image"),
|
||||
ExportKind::Text => tl!("Export to Text"),
|
||||
ExportKind::AllImages => tl!("Export All Images"),
|
||||
})
|
||||
.font(theme::semibold(18.0)),
|
||||
);
|
||||
ui.add_space(8.0);
|
||||
if kind == ExportKind::Image {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Resolution");
|
||||
egui::ComboBox::from_id_salt("export-dpi").selected_text(format!("{} pixels/inch", d.dpi)).show_ui(ui, |ui| {
|
||||
for dpi in [72.0, 96.0, 150.0, 300.0, 600.0] {
|
||||
ui.selectable_value(&mut d.dpi, dpi, format!("{dpi} pixels/inch"));
|
||||
}
|
||||
});
|
||||
ui.label(tl!("Resolution"));
|
||||
egui::ComboBox::from_id_salt("export-dpi")
|
||||
.selected_text(crate::i18n::fmt(tl!("{dpi} pixels/inch"), &[("dpi", &d.dpi.to_string())]))
|
||||
.show_ui(ui, |ui| {
|
||||
for dpi in [72.0, 96.0, 150.0, 300.0, 600.0] {
|
||||
ui.selectable_value(&mut d.dpi, dpi, crate::i18n::fmt(tl!("{dpi} pixels/inch"), &[("dpi", &dpi.to_string())]));
|
||||
}
|
||||
});
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Format");
|
||||
ui.label(tl!("Format"));
|
||||
let mut quality = match d.format {
|
||||
ImageFormat::Jpeg { quality } => quality,
|
||||
_ => 85,
|
||||
|
|
@ -74,17 +76,21 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens, kind: E
|
|||
}
|
||||
});
|
||||
if let ImageFormat::Jpeg { .. } = d.format {
|
||||
ui.label("Quality");
|
||||
ui.label(tl!("Quality"));
|
||||
if ui.add(egui::Slider::new(&mut quality, 10..=100)).changed() {
|
||||
d.format = ImageFormat::Jpeg { quality };
|
||||
}
|
||||
}
|
||||
});
|
||||
ui.label(egui::RichText::new(format!("One {} file per page, named after the document.", d.format.label())).small().color(t.text_faint));
|
||||
ui.label(
|
||||
egui::RichText::new(crate::i18n::fmt(tl!("One {f} file per page, named after the document."), &[("f", d.format.label())]))
|
||||
.small()
|
||||
.color(t.text_faint),
|
||||
);
|
||||
} else if kind == ExportKind::AllImages {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Exclude images smaller than");
|
||||
let label = |n: u32| if n == 0 { "No limit".to_string() } else { format!("{n} pixels") };
|
||||
ui.label(tl!("Exclude images smaller than"));
|
||||
let label = move |n: u32| if n == 0 { tl!("No limit").to_string() } else { format!("{n} pixels") };
|
||||
egui::ComboBox::from_id_salt("export-min").selected_text(label(d.min_side)).show_ui(ui, |ui| {
|
||||
for n in [0, 16, 32, 64, 128, 256] {
|
||||
ui.selectable_value(&mut d.min_side, n, label(n));
|
||||
|
|
@ -92,24 +98,24 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens, kind: E
|
|||
});
|
||||
});
|
||||
ui.label(
|
||||
egui::RichText::new("Each image once, named after the document and page. JPEG images are saved unchanged; others as PNG.")
|
||||
egui::RichText::new(tl!("Each image once, named after the document and page. JPEG images are saved unchanged; others as PNG."))
|
||||
.small()
|
||||
.color(t.text_faint),
|
||||
);
|
||||
} else {
|
||||
ui.label(egui::RichText::new("Plain text in reading order; pages are separated by form feeds.").small().color(t.text_faint));
|
||||
ui.label(egui::RichText::new(tl!("Plain text in reading order; pages are separated by form feeds.")).small().color(t.text_faint));
|
||||
}
|
||||
ui.add_space(6.0);
|
||||
ui.label(egui::RichText::new("Pages").font(theme::semibold(12.5)));
|
||||
ui.label(egui::RichText::new(tl!("Pages")).font(theme::semibold(12.5)));
|
||||
d.range.ui(ui, count);
|
||||
ui.add_space(12.0);
|
||||
let (mut apply, mut cancel) = (false, false);
|
||||
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
|
||||
let ok = !d.range.pages(count).is_empty();
|
||||
if ui.add_enabled_ui(ok, |ui| widgets::pill_button(ui, "Export", true)).inner.clicked() {
|
||||
if ui.add_enabled_ui(ok, |ui| widgets::pill_button(ui, tl!("Export"), true)).inner.clicked() {
|
||||
apply = true;
|
||||
}
|
||||
if widgets::pill_button(ui, "Cancel", false).clicked() {
|
||||
if widgets::pill_button(ui, tl!("Cancel"), false).clicked() {
|
||||
cancel = true;
|
||||
}
|
||||
});
|
||||
|
|
@ -128,7 +134,10 @@ fn run(
|
|||
stem: String,
|
||||
mut sink: impl FnMut(&str, Vec<u8>) -> Result<(), String>,
|
||||
status: &ExportStatus,
|
||||
lang: crate::i18n::Lang,
|
||||
) -> String {
|
||||
// May run on a worker thread: messages are drawn in the UI's language.
|
||||
crate::i18n::set_current(lang);
|
||||
let total = pages.len();
|
||||
let set = |done: usize, msg: Option<String>| {
|
||||
if let Ok(mut s) = status.lock() {
|
||||
|
|
@ -139,17 +148,24 @@ fn run(
|
|||
set(0, None);
|
||||
let out = match pdfcraft_engine::export::extract_images(&src, &pages, min_side) {
|
||||
Ok(o) => o,
|
||||
Err(e) => return format!("Export stopped: {e}"),
|
||||
Err(e) => return crate::i18n::fmt(tl!("Export stopped: {e}"), &[("e", &e.to_string())]),
|
||||
};
|
||||
for (k, img) in out.images.iter().enumerate() {
|
||||
if let Err(e) = sink(&pdfcraft_engine::export::image_file_name(&stem, img, k + 1), img.data.clone()) {
|
||||
return format!("Export stopped: {e}");
|
||||
return crate::i18n::fmt(tl!("Export stopped: {e}"), &[("e", &e.to_string())]);
|
||||
}
|
||||
}
|
||||
let n = out.images.len();
|
||||
let mut msg = format!("Exported {n} image{}", if n == 1 { "" } else { "s" });
|
||||
let mut msg = if n == 1 {
|
||||
crate::i18n::fmt(tl!("Exported 1 image"), &[])
|
||||
} else {
|
||||
crate::i18n::fmt(tl!("Exported {n} images"), &[("n", &n.to_string())])
|
||||
};
|
||||
if !out.skipped.is_empty() {
|
||||
msg.push_str(&format!(" ({} not exported: {})", out.skipped.len(), out.skipped[0].2));
|
||||
msg.push_str(&crate::i18n::fmt(
|
||||
tl!(" ({s} not exported: {first})"),
|
||||
&[("s", &out.skipped.len().to_string()), ("first", &out.skipped[0].2)],
|
||||
));
|
||||
}
|
||||
return msg;
|
||||
}
|
||||
|
|
@ -162,16 +178,26 @@ fn run(
|
|||
set(k, None);
|
||||
let result = ex.image(*p, dpi, format).and_then(|img| sink(&format!("{stem}_page_{}.{}", p + 1, format.extension()), img));
|
||||
if let Err(e) = result {
|
||||
return format!("Export stopped: {e}");
|
||||
return crate::i18n::fmt(tl!("Export stopped: {e}"), &[("e", &e.to_string())]);
|
||||
}
|
||||
}
|
||||
format!("Exported {total} image{}", if total == 1 { "" } else { "s" })
|
||||
if total == 1 {
|
||||
crate::i18n::fmt(tl!("Exported 1 image"), &[])
|
||||
} else {
|
||||
crate::i18n::fmt(tl!("Exported {n} images"), &[("n", &total.to_string())])
|
||||
}
|
||||
}
|
||||
ExportKind::Text => {
|
||||
set(0, None);
|
||||
match ex.text_of(&pages).and_then(|text| sink(&format!("{stem}.txt"), text.into_bytes())) {
|
||||
Ok(()) => format!("Exported the text of {total} page{}", if total == 1 { "" } else { "s" }),
|
||||
Err(e) => format!("Export stopped: {e}"),
|
||||
Ok(()) => {
|
||||
if total == 1 {
|
||||
crate::i18n::fmt(tl!("Exported the text of 1 page"), &[])
|
||||
} else {
|
||||
crate::i18n::fmt(tl!("Exported the text of {n} pages"), &[("n", &total.to_string())])
|
||||
}
|
||||
}
|
||||
Err(e) => crate::i18n::fmt(tl!("Export stopped: {e}"), &[("e", &e.to_string())]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -193,19 +219,21 @@ impl PdfCraftApp {
|
|||
{
|
||||
let dir = match &self.export_dir_override {
|
||||
Some(d) => Some(std::path::PathBuf::from(d)),
|
||||
None => rfd::FileDialog::new().set_title("Choose a folder for the exported files").pick_folder(),
|
||||
None => rfd::FileDialog::new().set_title(tl!("Choose a folder for the exported files").to_string()).pick_folder(),
|
||||
};
|
||||
let Some(dir) = dir else { return };
|
||||
let st = status.clone();
|
||||
let shown = dir.display().to_string();
|
||||
let lang = crate::i18n::current();
|
||||
let work = move || {
|
||||
let sink = |name: &str, bytes: Vec<u8>| {
|
||||
crate::editing::write_atomically(&dir.join(name).to_string_lossy(), &bytes).map_err(|e| format!("{name}: {e}"))
|
||||
};
|
||||
let msg = run(src, kind, dpi, format, min_side, pages, stem, sink, &st);
|
||||
let msg = run(src, kind, dpi, format, min_side, pages, stem, sink, &st, lang);
|
||||
if let Ok(mut s) = st.lock() {
|
||||
let (done, total) = s.as_ref().map_or((0, 0), |(d, t, _)| (*d, *t));
|
||||
*s = Some((done.max(total), total, Some(format!("{msg} to {shown}"))));
|
||||
let done_msg = crate::i18n::fmt(tl!("{msg} to {dir}"), &[("msg", &msg), ("dir", &shown)]);
|
||||
*s = Some((done.max(total), total, Some(done_msg)));
|
||||
}
|
||||
};
|
||||
if self.export_dir_override.is_some() {
|
||||
|
|
@ -216,7 +244,18 @@ impl PdfCraftApp {
|
|||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
let msg = run(src, kind, dpi, format, min_side, pages, stem, |name, bytes| crate::editing::download(name, &bytes), &status);
|
||||
let msg = run(
|
||||
src,
|
||||
kind,
|
||||
dpi,
|
||||
format,
|
||||
min_side,
|
||||
pages,
|
||||
stem,
|
||||
|name, bytes| crate::editing::download(name, &bytes),
|
||||
&status,
|
||||
crate::i18n::current(),
|
||||
);
|
||||
if let Ok(mut s) = status.lock() {
|
||||
*s = Some((0, 0, Some(msg)));
|
||||
}
|
||||
|
|
@ -233,7 +272,10 @@ impl PdfCraftApp {
|
|||
self.export_status = None;
|
||||
self.notify(msg);
|
||||
}
|
||||
Some((done, total, None)) if total > 1 => self.notify(format!("Exporting… {done} of {total}")),
|
||||
Some((done, total, None)) if total > 1 => {
|
||||
let m = crate::i18n::fmt(tl!("Exporting… {d} of {t}"), &[("d", &done.to_string()), ("t", &total.to_string())]);
|
||||
self.notify(m);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
if let Some(ctx) = &self.ctx {
|
||||
|
|
|
|||
|
|
@ -109,7 +109,7 @@ impl PdfCraftApp {
|
|||
/// Ask for a PDF whose pages to insert after the selection (Organize ▸ Insert from file).
|
||||
pub fn insert_from_file_dialog(&mut self) {
|
||||
if self.active.is_none() {
|
||||
self.notify("Open a document first");
|
||||
self.notify_tr("Open a document first");
|
||||
return;
|
||||
}
|
||||
self.pick_files(FilePurpose::InsertPages, false);
|
||||
|
|
@ -146,7 +146,7 @@ impl PdfCraftApp {
|
|||
match std::fs::read(p) {
|
||||
Ok(b) => files.push((name, b)),
|
||||
Err(e) => {
|
||||
self.notify(format!("Couldn't read {name}: {e}"));
|
||||
self.notify_fmt("Couldn't read {name}: {e}", &[("name", &name), ("e", &e.to_string())]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
@ -184,7 +184,7 @@ impl PdfCraftApp {
|
|||
|
||||
pub fn replace_pages_dialog(&mut self) {
|
||||
if self.active.is_none() {
|
||||
self.notify("Open a document first");
|
||||
self.notify_tr("Open a document first");
|
||||
return;
|
||||
}
|
||||
self.pick_files(FilePurpose::ReplacePages, false);
|
||||
|
|
@ -197,7 +197,7 @@ impl PdfCraftApp {
|
|||
let src_pages = match self.session.page_count_of(&name, &bytes) {
|
||||
Ok(n) => n,
|
||||
Err(e) => {
|
||||
self.notify(format!("Couldn't use {name}: {e}"));
|
||||
self.notify_fmt("Couldn't use {name}: {e}", &[("name", &name), ("e", &e.to_string())]);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
|
@ -227,19 +227,26 @@ impl PdfCraftApp {
|
|||
match self.session.extract(id, &[p]) {
|
||||
Ok(bytes) => named.push((format!("{stem} (page {}).pdf", p + 1), bytes)),
|
||||
Err(e) => {
|
||||
self.notify(format!("Couldn't extract pages: {e}"));
|
||||
self.notify_fmt("Couldn't extract pages: {e}", &[("e", &e.to_string())]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
if self.write_files(&named, "Choose a folder for the extracted pages") == 0 {
|
||||
if self.write_files(&named, tl!("Choose a folder for the extracted pages")) == 0 {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
match self.session.extract(id, &pages) {
|
||||
Ok(bytes) => self.open_created(&format!("{stem} (extract).pdf"), bytes, &format!("Extracted {} page(s)", pages.len())),
|
||||
Ok(bytes) => {
|
||||
let message = if pages.len() == 1 {
|
||||
tl!("Extracted 1 page").to_string()
|
||||
} else {
|
||||
crate::i18n::fmt(tl!("Extracted {n} pages"), &[("n", &pages.len().to_string())])
|
||||
};
|
||||
self.open_created(&format!("{stem} (extract).pdf"), bytes, &message)
|
||||
}
|
||||
Err(e) => {
|
||||
self.notify(format!("Couldn't extract pages: {e}"));
|
||||
self.notify_fmt("Couldn't extract pages: {e}", &[("e", &e.to_string())]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
@ -262,18 +269,22 @@ impl PdfCraftApp {
|
|||
let Some(dir) = dir else { return 0 };
|
||||
for (name, bytes) in named {
|
||||
if let Err(e) = crate::editing::write_atomically(&dir.join(name).to_string_lossy(), bytes) {
|
||||
self.notify(format!("Couldn't write {name}: {e}"));
|
||||
self.notify_fmt("Couldn't write {name}: {e}", &[("name", name), ("e", &e.to_string())]);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
self.notify(format!("Wrote {} file{} to {}", named.len(), if named.len() == 1 { "" } else { "s" }, dir.display()));
|
||||
if named.len() == 1 {
|
||||
self.notify_fmt("Wrote 1 file to {dir}", &[("dir", &dir.display().to_string())]);
|
||||
} else {
|
||||
self.notify_fmt("Wrote {n} files to {dir}", &[("n", &named.len().to_string()), ("dir", &dir.display().to_string())]);
|
||||
}
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
let _ = title;
|
||||
for (name, bytes) in named {
|
||||
if let Err(e) = crate::editing::download(name, bytes) {
|
||||
self.notify(format!("Couldn't download {name}: {e}"));
|
||||
self.notify_fmt("Couldn't download {name}: {e}", &[("name", name), ("e", &e.to_string())]);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -294,7 +305,7 @@ impl PdfCraftApp {
|
|||
};
|
||||
let pages = pdfcraft_engine::filter_pages(&doc.info, &base, d.parity, d.orientation);
|
||||
if pages.is_empty() {
|
||||
self.notify("No pages match those choices");
|
||||
self.notify_tr("No pages match those choices");
|
||||
return;
|
||||
}
|
||||
self.apply_edit(pdfcraft_engine::Edit::RotatePages { pages, degrees: d.degrees });
|
||||
|
|
@ -317,7 +328,7 @@ impl PdfCraftApp {
|
|||
let parts = match parts {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
self.notify(format!("Couldn't split: {e}"));
|
||||
self.notify_fmt("Couldn't split: {e}", &[("e", &e.to_string())]);
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
|
@ -345,7 +356,7 @@ impl PdfCraftApp {
|
|||
self.session.get(id).map(|d| std::path::Path::new(&d.name).file_stem().map(|s| s.to_string_lossy().into_owned()).unwrap_or_default());
|
||||
match self.session.summarize_comments(id, self.summary_sort) {
|
||||
Ok(bytes) => self.open_created(&format!("Summary of comments on {}.pdf", stem.unwrap_or_default()), bytes, "Created a comment summary"),
|
||||
Err(e) => self.notify(format!("Couldn't summarize comments: {e}")),
|
||||
Err(e) => self.notify_fmt("Couldn't summarize comments: {e}", &[("e", &e.to_string())]),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -355,9 +366,9 @@ impl PdfCraftApp {
|
|||
let Some(doc) = self.session.get(id) else { return };
|
||||
self.views.push(crate::DocView::new(id, &doc.info));
|
||||
self.active = Some(self.views.len() - 1);
|
||||
self.notify(message);
|
||||
self.notify_tr(message);
|
||||
}
|
||||
Err(e) => self.notify(format!("Couldn't open the result: {e}")),
|
||||
Err(e) => self.notify_fmt("Couldn't open the result: {e}", &[("e", &e.to_string())]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -375,8 +386,8 @@ impl PdfCraftApp {
|
|||
Some(p) if [".xfdf", ".fdf", ".xml", ".csv", ".txt"].iter().any(|e| p.ends_with(e)) => Some(std::path::PathBuf::from(p)),
|
||||
Some(_) => None,
|
||||
None => rfd::FileDialog::new()
|
||||
.add_filter("Comment and form data", &["xfdf", "fdf", "xml", "csv", "txt"])
|
||||
.set_title("Import data")
|
||||
.add_filter(tl!("Comment and form data").to_string(), &["xfdf", "fdf", "xml", "csv", "txt"])
|
||||
.set_title(tl!("Import data").to_string())
|
||||
.pick_file(),
|
||||
};
|
||||
let Some(path) = picked else { return };
|
||||
|
|
@ -385,11 +396,11 @@ impl PdfCraftApp {
|
|||
let name = path.file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or_default();
|
||||
self.apply_edit(pdfcraft_engine::Edit::ImportData { name, bytes: std::sync::Arc::new(bytes) });
|
||||
}
|
||||
Err(e) => self.notify(format!("Couldn't read {}: {e}", path.display())),
|
||||
Err(e) => self.notify_fmt("Couldn't read {name}: {e}", &[("name", &path.display().to_string()), ("e", &e.to_string())]),
|
||||
}
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
self.notify("Importing data arrives on the web with file pickers for data files");
|
||||
self.notify_tr("Importing data arrives on the web with file pickers for data files");
|
||||
}
|
||||
|
||||
/// Export all comments / form data: the format follows the file name's extension.
|
||||
|
|
@ -405,20 +416,20 @@ impl PdfCraftApp {
|
|||
let path = match self.save_override.clone() {
|
||||
Some(p) => Some(std::path::PathBuf::from(p)),
|
||||
None => rfd::FileDialog::new()
|
||||
.set_title("Export")
|
||||
.set_title(tl!("Export").to_string())
|
||||
.add_filter(ext.to_uppercase(), &[ext])
|
||||
.set_file_name(format!("{stem}.{ext}"))
|
||||
.save_file(),
|
||||
};
|
||||
let Some(path) = path else { return };
|
||||
match crate::editing::write_atomically(&path.to_string_lossy(), &bytes) {
|
||||
Ok(()) => self.notify(format!("Exported to {}", path.display())),
|
||||
Err(e) => self.notify(format!("Couldn't write {}: {e}", path.display())),
|
||||
Ok(()) => self.notify_fmt("Exported to {path}", &[("path", &path.display().to_string())]),
|
||||
Err(e) => self.notify_fmt("Couldn't write {name}: {e}", &[("name", &path.display().to_string()), ("e", &e.to_string())]),
|
||||
}
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
if let Err(e) = crate::editing::download(&format!("{stem}.{ext}"), &bytes) {
|
||||
self.notify(e);
|
||||
self.notify_error(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -428,8 +439,8 @@ impl PdfCraftApp {
|
|||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
let paths = rfd::FileDialog::new()
|
||||
.set_title("Select data files to merge")
|
||||
.add_filter("Form data and PDF forms", &["fdf", "xfdf", "pdf"])
|
||||
.set_title(tl!("Select data files to merge").to_string())
|
||||
.add_filter(tl!("Form data and PDF forms").to_string(), &["fdf", "xfdf", "pdf"])
|
||||
.pick_files()
|
||||
.unwrap_or_default();
|
||||
let mut files = Vec::new();
|
||||
|
|
@ -437,7 +448,7 @@ impl PdfCraftApp {
|
|||
let name = p.file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or_default();
|
||||
match std::fs::read(&p) {
|
||||
Ok(b) => files.push((name, b)),
|
||||
Err(e) => return self.notify(format!("Couldn't read {name}: {e}")),
|
||||
Err(e) => return self.notify_fmt("Couldn't read {name}: {e}", &[("name", &name), ("e", &e.to_string())]),
|
||||
}
|
||||
}
|
||||
if !files.is_empty() {
|
||||
|
|
@ -445,25 +456,35 @@ impl PdfCraftApp {
|
|||
}
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
self.notify("Merging data files needs the desktop app");
|
||||
self.notify_tr("Merging data files needs the desktop app");
|
||||
}
|
||||
|
||||
/// Merge the given data files and save the spreadsheet (asks where; `save_override` in tests).
|
||||
pub fn merge_data_files(&mut self, files: Vec<(String, Vec<u8>)>) {
|
||||
let csv = match pdfcraft_engine::merge_data_files(&files) {
|
||||
Ok(c) => c,
|
||||
Err(e) => return self.notify(e),
|
||||
Err(e) => return self.notify_error(e),
|
||||
};
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
let path = match self.save_override.clone() {
|
||||
Some(p) => Some(std::path::PathBuf::from(p)),
|
||||
None => rfd::FileDialog::new().set_title("Save the spreadsheet").add_filter("CSV", &["csv"]).set_file_name("report.csv").save_file(),
|
||||
None => rfd::FileDialog::new()
|
||||
.set_title(tl!("Save the spreadsheet").to_string())
|
||||
.add_filter("CSV", &["csv"])
|
||||
.set_file_name("report.csv")
|
||||
.save_file(),
|
||||
};
|
||||
let Some(path) = path else { return };
|
||||
match crate::editing::write_atomically(&path.to_string_lossy(), csv.as_bytes()) {
|
||||
Ok(()) => self.notify(format!("Merged {} file{} into {}", files.len(), if files.len() == 1 { "" } else { "s" }, path.display())),
|
||||
Err(e) => self.notify(format!("Couldn't write {}: {e}", path.display())),
|
||||
Ok(()) => {
|
||||
if files.len() == 1 {
|
||||
self.notify_fmt("Merged 1 file into {path}", &[("path", &path.display().to_string())]);
|
||||
} else {
|
||||
self.notify_fmt("Merged {n} files into {path}", &[("n", &files.len().to_string()), ("path", &path.display().to_string())]);
|
||||
}
|
||||
}
|
||||
Err(e) => self.notify_fmt("Couldn't write {name}: {e}", &[("name", &path.display().to_string()), ("e", &e.to_string())]),
|
||||
}
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
|
|
@ -479,9 +500,8 @@ impl PdfCraftApp {
|
|||
let path = match self.save_override.clone() {
|
||||
Some(p) => Some(std::path::PathBuf::from(p)),
|
||||
None => {
|
||||
let d = rfd::FileDialog::new()
|
||||
.set_title(if comments { "Export comments" } else { "Export form data" })
|
||||
.set_file_name(format!("{stem}.xfdf"));
|
||||
let title = if comments { tl!("Export comments") } else { tl!("Export form data") };
|
||||
let d = rfd::FileDialog::new().set_title(title).set_file_name(format!("{stem}.xfdf"));
|
||||
let d = if comments {
|
||||
d.add_filter("XFDF", &["xfdf"]).add_filter("FDF", &["fdf"])
|
||||
} else {
|
||||
|
|
@ -489,7 +509,7 @@ impl PdfCraftApp {
|
|||
.add_filter("FDF", &["fdf"])
|
||||
.add_filter("XML", &["xml"])
|
||||
.add_filter("CSV", &["csv"])
|
||||
.add_filter("Text", &["txt"])
|
||||
.add_filter(tl!("Text").to_string(), &["txt"])
|
||||
};
|
||||
d.save_file()
|
||||
}
|
||||
|
|
@ -499,10 +519,10 @@ impl PdfCraftApp {
|
|||
let format = pdfcraft_engine::DataFormat::from_extension(&ext).unwrap_or(pdfcraft_engine::DataFormat::Xfdf);
|
||||
match self.session.export_data(id, format, comments, fields) {
|
||||
Ok(bytes) => match crate::editing::write_atomically(&path.to_string_lossy(), &bytes) {
|
||||
Ok(()) => self.notify(format!("Exported to {}", path.display())),
|
||||
Err(e) => self.notify(format!("Couldn't write {}: {e}", path.display())),
|
||||
Ok(()) => self.notify_fmt("Exported to {path}", &[("path", &path.display().to_string())]),
|
||||
Err(e) => self.notify_fmt("Couldn't write {name}: {e}", &[("name", &path.display().to_string()), ("e", &e.to_string())]),
|
||||
},
|
||||
Err(e) => self.notify(e.to_string()),
|
||||
Err(e) => self.notify_error(e),
|
||||
}
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
|
|
@ -510,7 +530,7 @@ impl PdfCraftApp {
|
|||
Ok(bytes) => {
|
||||
let _ = crate::editing::download(&format!("{stem}.xfdf"), &bytes);
|
||||
}
|
||||
Err(e) => self.notify(e.to_string()),
|
||||
Err(e) => self.notify_error(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -244,15 +244,17 @@ pub(crate) fn signature_entries(ui: &mut egui::Ui, app: &mut crate::PdfCraftApp,
|
|||
.into_iter()
|
||||
.enumerate()
|
||||
{
|
||||
let label = |template: &str| crate::i18n::fmt(tl!(template), &[("what", tl!(what))]);
|
||||
let Some(saved) = saved else {
|
||||
if crate::widgets::ghost_button(ui, "plus", &format!("Add {what}")).clicked() {
|
||||
if crate::widgets::ghost_button(ui, "plus", &label("Add {what}")).clicked() {
|
||||
command = Some(use_id);
|
||||
}
|
||||
continue;
|
||||
};
|
||||
ui.horizontal(|ui| {
|
||||
let (rect, response) = ui.allocate_exact_size(vec2((ui.available_width() - 68.0).max(140.0), 52.0), Sense::click());
|
||||
response.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, true, format!("Use {what}")));
|
||||
let use_label = label("Use {what}");
|
||||
response.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, true, use_label.clone()));
|
||||
let painter = ui.painter_at(rect);
|
||||
painter.rect_filled(rect, CornerRadius::same(4), Color32::WHITE);
|
||||
painter.rect_stroke(
|
||||
|
|
@ -300,13 +302,13 @@ pub(crate) fn signature_entries(ui: &mut egui::Ui, app: &mut crate::PdfCraftApp,
|
|||
}
|
||||
}
|
||||
}
|
||||
if response.on_hover_text(format!("Place saved {what}")).clicked() {
|
||||
if response.on_hover_text(label("Place saved {what}")).clicked() {
|
||||
command = Some(use_id);
|
||||
}
|
||||
if crate::icons::button(ui, "pencil", 26.0, false, &format!("Change {what}")).clicked() {
|
||||
if crate::icons::button(ui, "pencil", 26.0, false, &label("Change {what}")).clicked() {
|
||||
command = Some(change_id);
|
||||
}
|
||||
if crate::icons::button(ui, "x", 26.0, false, &format!("Remove saved {what}")).clicked() {
|
||||
if crate::icons::button(ui, "x", 26.0, false, &label("Remove saved {what}")).clicked() {
|
||||
command = Some(remove_id);
|
||||
}
|
||||
});
|
||||
|
|
@ -393,7 +395,7 @@ pub(crate) fn type_box(ctx: &egui::Context, view: &mut DocView, info: &DocInfo,
|
|||
.desired_width(width)
|
||||
.background_color(Color32::from_rgba_unmultiplied(255, 255, 255, 230))
|
||||
.text_color(Color32::BLACK)
|
||||
.hint_text("Type text")
|
||||
.hint_text(tl!("Type text"))
|
||||
.id_salt("fill-text-edit"),
|
||||
);
|
||||
if t.focus {
|
||||
|
|
@ -421,26 +423,25 @@ pub(crate) fn type_box(ctx: &egui::Context, view: &mut DocView, info: &DocInfo,
|
|||
|
||||
/// The signature pad: draw with the pointer; returns the strokes (normalised) on Apply.
|
||||
pub(crate) fn signature_pad(ui: &mut egui::Ui, t: &Tokens, d: &mut SigDraft, preview: &mut Option<(String, egui::TextureHandle)>) -> (bool, bool) {
|
||||
let what = if d.initials { "initials" } else { "signature" };
|
||||
let action = if d.editing { "Change" } else { "Create" };
|
||||
ui.label(egui::RichText::new(format!("{action} {what}")).font(crate::theme::semibold(18.0)));
|
||||
let what = if d.initials { tl!("initials") } else { tl!("signature") };
|
||||
let title = if d.editing { tl!("Change {what}") } else { tl!("Create {what}") };
|
||||
ui.label(egui::RichText::new(crate::i18n::fmt(title, &[("what", what)])).font(crate::theme::semibold(18.0)));
|
||||
ui.horizontal(|ui| {
|
||||
if crate::widgets::pill_button(ui, "Type", !d.drawing).clicked() {
|
||||
if crate::widgets::pill_button(ui, tl!("Type"), !d.drawing).clicked() {
|
||||
d.drawing = false;
|
||||
}
|
||||
if crate::widgets::pill_button(ui, "Draw", d.drawing).clicked() {
|
||||
if crate::widgets::pill_button(ui, tl!("Draw"), d.drawing).clicked() {
|
||||
d.drawing = true;
|
||||
}
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
if !d.drawing {
|
||||
let l = ui.label(egui::RichText::new(format!("Type your {what}.")).color(t.text_muted));
|
||||
let l = ui.label(egui::RichText::new(crate::i18n::fmt(tl!("Type your {what}."), &[("what", what)])).color(t.text_muted));
|
||||
ui.add(
|
||||
egui::TextEdit::singleline(&mut d.text).char_limit(pdfcraft_engine::MAX_SIGNATURE_CHARS).desired_width(460.0).hint_text(if d.initials {
|
||||
"Initials"
|
||||
} else {
|
||||
"Your name"
|
||||
}),
|
||||
egui::TextEdit::singleline(&mut d.text)
|
||||
.char_limit(pdfcraft_engine::MAX_SIGNATURE_CHARS)
|
||||
.desired_width(460.0)
|
||||
.hint_text(tl!(if d.initials { "Initials" } else { "Your name" })),
|
||||
)
|
||||
.labelled_by(l.id);
|
||||
let (rect, _) = ui.allocate_exact_size(vec2(460.0, 150.0), Sense::hover());
|
||||
|
|
@ -456,7 +457,7 @@ pub(crate) fn signature_pad(ui: &mut egui::Ui, t: &Tokens, d: &mut SigDraft, pre
|
|||
}
|
||||
return pad_buttons(ui, d);
|
||||
}
|
||||
ui.label(egui::RichText::new(format!("Draw your {what} below.")).color(t.text_muted));
|
||||
ui.label(egui::RichText::new(crate::i18n::fmt(tl!("Draw your {what} below."), &[("what", what)])).color(t.text_muted));
|
||||
let strokes = &mut d.strokes;
|
||||
let (rect, resp) = ui.allocate_exact_size(vec2(460.0, 150.0), Sense::drag());
|
||||
let painter = ui.painter_at(rect);
|
||||
|
|
@ -487,16 +488,16 @@ fn pad_buttons(ui: &mut egui::Ui, d: &mut SigDraft) -> (bool, bool) {
|
|||
ui.add_space(10.0);
|
||||
let (mut apply, mut cancel) = (false, false);
|
||||
ui.horizontal(|ui| {
|
||||
if ui.button("Clear").clicked() {
|
||||
if ui.button(tl!("Clear")).clicked() {
|
||||
d.strokes.clear();
|
||||
d.text.clear();
|
||||
}
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
let ready = d.ready();
|
||||
if ui.add_enabled_ui(ready, |ui| crate::widgets::pill_button(ui, "Apply", true)).inner.clicked() {
|
||||
if ui.add_enabled_ui(ready, |ui| crate::widgets::pill_button(ui, tl!("Apply"), true)).inner.clicked() {
|
||||
apply = true;
|
||||
}
|
||||
if crate::widgets::pill_button(ui, "Cancel", false).clicked() {
|
||||
if crate::widgets::pill_button(ui, tl!("Cancel"), false).clicked() {
|
||||
cancel = true;
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -35,11 +35,23 @@ pub struct Focus {
|
|||
pub struct FormView {
|
||||
pub focus: Option<Focus>,
|
||||
/// A message for the app to show (e.g. "buttons run JavaScript").
|
||||
pub notice: Option<String>,
|
||||
pub notice: Option<FormNotice>,
|
||||
/// A push button was clicked: (its field name, what it does).
|
||||
pub button: Option<(String, pdfcraft_engine::form_scripts::ButtonAction)>,
|
||||
}
|
||||
|
||||
/// A form message for the app to show, with document data kept separate so the visible
|
||||
/// text follows the UI language.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum FormNotice {
|
||||
/// Filling is blocked by the document's security settings.
|
||||
Security,
|
||||
/// A read-only field was clicked (its name).
|
||||
ReadOnly(String),
|
||||
/// A push button with no action was clicked (its name).
|
||||
NoAction(String),
|
||||
}
|
||||
|
||||
fn widget_rect(xf: &PageXform, info: &DocInfo, page: usize, r: [f64; 4]) -> Rect {
|
||||
xf.user_rect(info, page, [r[0] as f32, r[1] as f32, r[2] as f32, r[3] as f32])
|
||||
}
|
||||
|
|
@ -96,14 +108,14 @@ pub(crate) fn page_input(
|
|||
return resp.is_pointer_button_down_on();
|
||||
}
|
||||
if !allowed {
|
||||
view.forms.notice = Some("The document's security settings don't allow filling in form fields".into());
|
||||
view.forms.notice = Some(FormNotice::Security);
|
||||
return true;
|
||||
}
|
||||
match f.kind {
|
||||
_ if f.read_only() => view.forms.notice = Some(format!("{} is read-only", f.name)),
|
||||
_ if f.read_only() => view.forms.notice = Some(FormNotice::ReadOnly(f.name.clone())),
|
||||
FormFieldKind::PushButton => match &f.button {
|
||||
Some(a) => view.forms.button = Some((f.name.clone(), a.clone())),
|
||||
None => view.forms.notice = Some(format!("{} has no action", f.name)),
|
||||
None => view.forms.notice = Some(FormNotice::NoAction(f.name.clone())),
|
||||
},
|
||||
FormFieldKind::Signature => view.sign.field = Some(f.name.clone()),
|
||||
FormFieldKind::CheckBox => {
|
||||
|
|
@ -160,11 +172,17 @@ fn calendar(ctx: &egui::Context, view: &mut DocView, field: egui::Rect, fmt: &st
|
|||
.show(ctx, |ui| {
|
||||
egui::Frame::popup(ui.style()).show(ui, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
if ui.small_button("‹").on_hover_text("Previous month").clicked() {
|
||||
if ui.small_button("‹").on_hover_text(tl!("Previous month")).clicked() {
|
||||
(y, m) = if m == 1 { (y - 1, 12) } else { (y, m - 1) };
|
||||
}
|
||||
ui.label(egui::RichText::new(format!("{} {y}", MONTHS[(m.clamp(1, 12) - 1) as usize])).strong());
|
||||
if ui.small_button("›").on_hover_text("Next month").clicked() {
|
||||
ui.label(
|
||||
egui::RichText::new(crate::i18n::fmt(
|
||||
tl!("{month} {y}"),
|
||||
&[("month", tl!(MONTHS[(m.clamp(1, 12) - 1) as usize])), ("y", &y.to_string())],
|
||||
))
|
||||
.strong(),
|
||||
);
|
||||
if ui.small_button("›").on_hover_text(tl!("Next month")).clicked() {
|
||||
(y, m) = if m == 12 { (y + 1, 1) } else { (y, m + 1) };
|
||||
}
|
||||
});
|
||||
|
|
@ -192,7 +210,14 @@ fn calendar(ctx: &egui::Context, view: &mut DocView, field: egui::Rect, fmt: &st
|
|||
if is_today {
|
||||
text = text.strong();
|
||||
}
|
||||
if ui.selectable_label(selected, text).on_hover_text(format!("{} {day}, {y}", MONTHS[(m - 1) as usize])).clicked() {
|
||||
if ui
|
||||
.selectable_label(selected, text)
|
||||
.on_hover_text(crate::i18n::fmt(
|
||||
tl!("{month} {day}, {y}"),
|
||||
&[("month", tl!(MONTHS[(m - 1) as usize])), ("day", &day.to_string()), ("y", &y.to_string())],
|
||||
))
|
||||
.clicked()
|
||||
{
|
||||
picked = Some(format_date(DateTime { y, m, d: day, hh: 0, mm: 0, ss: 0 }, fmt));
|
||||
}
|
||||
} else {
|
||||
|
|
@ -334,7 +359,7 @@ pub(crate) fn overlay(ctx: &egui::Context, view: &mut DocView, info: &DocInfo, f
|
|||
}
|
||||
}
|
||||
});
|
||||
if multi && ui.button("Done").clicked() {
|
||||
if multi && ui.button(tl!("Done")).clicked() {
|
||||
commit(view, form);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -12,9 +12,11 @@ pub fn show(app: &mut PdfCraftApp, ui: &mut egui::Ui) {
|
|||
let t = Tokens::get(ui.ctx());
|
||||
egui::ScrollArea::vertical().auto_shrink([false, false]).show(ui, |ui| {
|
||||
egui::Frame::NONE.inner_margin(egui::Margin { left: 36, right: 36, top: 28, bottom: 28 }).show(ui, |ui| {
|
||||
ui.label(egui::RichText::new("Welcome to PdfCraft").font(theme::semibold(24.0)));
|
||||
ui.label(egui::RichText::new(tl!("Welcome to PdfCraft")).font(theme::semibold(24.0)));
|
||||
ui.label(
|
||||
egui::RichText::new("An open-source PDF workbench — local, private, and scriptable.").color(t.text_muted).font(theme::regular(14.0)),
|
||||
egui::RichText::new(tl!("An open-source PDF workbench — local, private, and scriptable."))
|
||||
.color(t.text_muted)
|
||||
.font(theme::regular(14.0)),
|
||||
);
|
||||
ui.add_space(14.0);
|
||||
egui::Frame::NONE
|
||||
|
|
@ -27,8 +29,8 @@ pub fn show(app: &mut PdfCraftApp, ui: &mut egui::Ui) {
|
|||
ui.horizontal(|ui| {
|
||||
widgets::artcraft_mark(ui, 28.0);
|
||||
ui.vertical(|ui| {
|
||||
ui.label(egui::RichText::new("Join the ArtCraft community").font(theme::semibold(15.0)));
|
||||
ui.label(egui::RichText::new("Get help, share feedback and follow development on Discord.").color(t.text_muted));
|
||||
ui.label(egui::RichText::new(tl!("Join the ArtCraft community")).font(theme::semibold(15.0)));
|
||||
ui.label(egui::RichText::new(tl!("Get help, share feedback and follow development on Discord.")).color(t.text_muted));
|
||||
});
|
||||
});
|
||||
ui.add_space(8.0);
|
||||
|
|
@ -45,30 +47,30 @@ pub fn show(app: &mut PdfCraftApp, ui: &mut egui::Ui) {
|
|||
.inner_margin(egui::Margin::same(18))
|
||||
.show(ui, |ui| {
|
||||
ui.set_width(ui.available_width());
|
||||
ui.label(egui::RichText::new("Recommended tools").font(theme::semibold(15.0)));
|
||||
ui.label(egui::RichText::new(tl!("Recommended tools")).font(theme::semibold(15.0)));
|
||||
ui.add_space(10.0);
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.spacing_mut().item_spacing = vec2(14.0, 14.0);
|
||||
for id in RECOMMENDED {
|
||||
let Some(g) = catalog::group(id) else { continue };
|
||||
let (rect, resp) = ui.allocate_exact_size(vec2(190.0, 104.0), Sense::click());
|
||||
resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, true, g.label));
|
||||
resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, true, tl!(g.label)));
|
||||
let fill = if resp.hovered() { t.hover } else { t.card };
|
||||
ui.painter().rect(rect, CornerRadius::same(10), fill, Stroke::new(1.0, t.divider), egui::StrokeKind::Inside);
|
||||
let color = egui::Color32::from_rgb(g.hue[0], g.hue[1], g.hue[2]);
|
||||
icons::paint(ui, Rect::from_min_size(rect.min + vec2(14.0, 14.0), vec2(22.0, 22.0)), g.icon, 21.0, color);
|
||||
ui.painter().text(rect.min + vec2(44.0, 25.0), Align2::LEFT_CENTER, g.label, theme::semibold(13.5), t.text);
|
||||
ui.painter().text(rect.min + vec2(44.0, 25.0), Align2::LEFT_CENTER, tl!(g.label), theme::semibold(13.5), t.text);
|
||||
let blurb = g
|
||||
.sections
|
||||
.first()
|
||||
.map(|s| s.items.iter().take(3).map(|i| i.label).collect::<Vec<_>>().join(" · "))
|
||||
.map(|s| s.items.iter().take(3).map(|i| tl!(i.label)).collect::<Vec<_>>().join(" · "))
|
||||
.unwrap_or_default();
|
||||
let galley = ui.fonts_mut(|f| f.layout(blurb, theme::regular(11.5), t.text_muted, rect.width() - 28.0));
|
||||
ui.painter().galley(rect.min + vec2(14.0, 46.0), galley, t.text_muted);
|
||||
ui.painter().text(
|
||||
rect.left_bottom() + vec2(14.0, -14.0),
|
||||
Align2::LEFT_CENTER,
|
||||
"Use now",
|
||||
tl!("Use now"),
|
||||
theme::medium(12.0),
|
||||
t.accent_text,
|
||||
);
|
||||
|
|
@ -78,7 +80,7 @@ pub fn show(app: &mut PdfCraftApp, ui: &mut egui::Ui) {
|
|||
}
|
||||
}
|
||||
let (rect, resp) = ui.allocate_exact_size(vec2(170.0, 104.0), Sense::click());
|
||||
resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, true, "Open file"));
|
||||
resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, true, tl!("Open file")));
|
||||
ui.painter().rect(
|
||||
rect,
|
||||
CornerRadius::same(10),
|
||||
|
|
@ -87,7 +89,7 @@ pub fn show(app: &mut PdfCraftApp, ui: &mut egui::Ui) {
|
|||
egui::StrokeKind::Inside,
|
||||
);
|
||||
icons::paint(ui, Rect::from_center_size(rect.center() - vec2(0.0, 16.0), vec2(28.0, 28.0)), "folder-open", 26.0, t.icon);
|
||||
ui.painter().text(rect.center() + vec2(0.0, 22.0), Align2::CENTER_CENTER, "Open file", theme::semibold(13.0), t.text);
|
||||
ui.painter().text(rect.center() + vec2(0.0, 22.0), Align2::CENTER_CENTER, tl!("Open file"), theme::semibold(13.0), t.text);
|
||||
if resp.clicked() {
|
||||
app.open_dialog();
|
||||
}
|
||||
|
|
@ -95,10 +97,10 @@ pub fn show(app: &mut PdfCraftApp, ui: &mut egui::Ui) {
|
|||
});
|
||||
|
||||
ui.add_space(26.0);
|
||||
ui.label(egui::RichText::new("Recent").font(theme::semibold(17.0)));
|
||||
ui.label(egui::RichText::new(tl!("Recent")).font(theme::semibold(17.0)));
|
||||
ui.add_space(8.0);
|
||||
if app.recent.is_empty() {
|
||||
ui.label(egui::RichText::new("Files you open in PdfCraft appear here. Drop a PDF anywhere to open it.").color(t.text_muted));
|
||||
ui.label(egui::RichText::new(tl!("Files you open in PdfCraft appear here. Drop a PDF anywhere to open it.")).color(t.text_muted));
|
||||
}
|
||||
let mut open = None;
|
||||
for r in &app.recent {
|
||||
|
|
@ -119,7 +121,7 @@ pub fn show(app: &mut PdfCraftApp, ui: &mut egui::Ui) {
|
|||
ui.painter().text(
|
||||
rect.right_center() - vec2(12.0, 0.0),
|
||||
Align2::RIGHT_CENTER,
|
||||
format!("{} pages · {}", r.pages, human_size(r.size)),
|
||||
format!("{} {} · {}", r.pages, tl!("pages"), human_size(r.size)),
|
||||
theme::regular(12.0),
|
||||
t.text_muted,
|
||||
);
|
||||
|
|
@ -136,9 +138,9 @@ pub fn show(app: &mut PdfCraftApp, ui: &mut egui::Ui) {
|
|||
}
|
||||
}
|
||||
ui.add_space(20.0);
|
||||
widgets::section_title(ui, "Privacy");
|
||||
widgets::section_title(ui, tl!("Privacy"));
|
||||
ui.label(
|
||||
egui::RichText::new("PdfCraft works offline. No telemetry, no account, and no cloud processing unless you add a provider.")
|
||||
egui::RichText::new(tl!("PdfCraft works offline. No telemetry, no account, and no cloud processing unless you add a provider."))
|
||||
.color(t.text_muted),
|
||||
);
|
||||
});
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -294,6 +294,47 @@ pub fn fmt(template: &str, args: &[(&str, &str)]) -> String {
|
|||
out
|
||||
}
|
||||
|
||||
/// A generated history label (the action part of "Undo …") in the current language. Captured
|
||||
/// values (file and field names) are inserted as they are; unknown labels are looked up whole.
|
||||
pub fn action_label(text: &str) -> String {
|
||||
for (prefix, template) in [
|
||||
("Insert pages from ", "Insert pages from {name}"),
|
||||
("Fill in ", "Fill in {name}"),
|
||||
("Set the image of ", "Set the image of {name}"),
|
||||
("Edit script of ", "Edit script of {name}"),
|
||||
("Import ", "Import {name}"),
|
||||
] {
|
||||
if let Some(value) = text.strip_prefix(prefix) {
|
||||
return fmt(t(template), &[("name", value)]);
|
||||
}
|
||||
}
|
||||
if let Some(key) = text.strip_prefix("Change ")
|
||||
&& ["Title", "Author", "Subject", "Keywords", "Creator", "Producer"].contains(&key)
|
||||
{
|
||||
return fmt(t("Change {key}"), &[("key", t(key))]);
|
||||
}
|
||||
t(text).to_owned()
|
||||
}
|
||||
|
||||
/// A command label such as "Undo Insert pages from a.pdf" in the current language.
|
||||
pub fn command_label(text: &str) -> String {
|
||||
for prefix in ["Undo", "Redo"] {
|
||||
if let Some(action) = text.strip_prefix(prefix).and_then(|tail| tail.strip_prefix(' ')) {
|
||||
return format!("{} {}", t(prefix), action_label(action));
|
||||
}
|
||||
}
|
||||
t(text).to_owned()
|
||||
}
|
||||
|
||||
/// A registered command's menu label: its `@id` catalog entry if there is one, otherwise
|
||||
/// [`command_label`] of the current English label.
|
||||
pub fn menu_label(id: &str, label: &str) -> String {
|
||||
match current().catalog().id(id) {
|
||||
Some(translated) => translated.to_owned(),
|
||||
None => command_label(label),
|
||||
}
|
||||
}
|
||||
|
||||
/// A plural-aware message: `one`/`other` are the English forms (with `{n}` where the count goes).
|
||||
pub fn trn(lang: Lang, n: u64, one: &str, other: &str) -> String {
|
||||
let idx = (lang.0.plural)(n);
|
||||
|
|
@ -461,6 +502,59 @@ mod tests {
|
|||
assert_eq!(fmt("unchanged", &[]), "unchanged");
|
||||
}
|
||||
|
||||
/// Japanese translates every registered command and every All tools group, section and item.
|
||||
#[test]
|
||||
fn japanese_covers_commands_and_catalogue() {
|
||||
for command in pdfcraft_engine::commands::COMMANDS {
|
||||
assert!(has(JA(), command.label), "missing command: {}", command.label);
|
||||
}
|
||||
for group in pdfcraft_engine::catalog::TOOL_GROUPS {
|
||||
assert!(has(JA(), group.label), "missing group: {}", group.label);
|
||||
for section in group.sections {
|
||||
assert!(has(JA(), section.title), "missing section: {}", section.title);
|
||||
for item in section.items {
|
||||
assert!(has(JA(), item.label), "missing item: {}", item.label);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// With craft-fonts, every Japanese translation has glyphs: with all interface faces (desktop)
|
||||
/// and with BIZ UDPGothic Regular alone (the web build's only Japanese face).
|
||||
#[test]
|
||||
fn japanese_labels_have_glyphs() {
|
||||
let craft = pdfcraft_fonts::ui_japanese_fonts();
|
||||
let Some(web) = craft.iter().find(|f| f.family == "BIZ UDPGothic" && f.style == "Regular") else {
|
||||
eprintln!("skipping Japanese glyph checks: build with CRAFT_FONTS_DIR to run them");
|
||||
return;
|
||||
};
|
||||
let (entries, _) = parse_entries(JA().0.source, 1);
|
||||
let labels: String = entries.iter().flat_map(|e| e.translation.chars()).chain(JA().name().chars()).filter(|c| !c.is_control()).collect();
|
||||
let web_name = web.name();
|
||||
let mut web_only = crate::theme::font_definitions();
|
||||
for family in web_only.families.values_mut() {
|
||||
family.retain(|name| !craft.iter().any(|face| face.name() == *name) || *name == web_name);
|
||||
}
|
||||
use egui::epaint::text::{Fonts, TextOptions};
|
||||
for (build, defs) in [("desktop", crate::theme::font_definitions()), ("web", web_only)] {
|
||||
let mut fonts = Fonts::new(TextOptions::default(), defs);
|
||||
for id in [egui::FontId::proportional(13.0), egui::FontId::monospace(13.0), crate::theme::medium(13.0), crate::theme::semibold(17.0)] {
|
||||
assert!(fonts.has_glyphs(&id, &labels), "{build}: {id:?} lacks a Japanese label glyph");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn history_labels_keep_captured_names() {
|
||||
set_current(JA());
|
||||
assert_eq!(action_label("Insert pages from Save {e}.pdf"), "Save {e}.pdf からページを挿入");
|
||||
assert_eq!(command_label("Undo Insert pages from Save {e}.pdf"), "取り消し Save {e}.pdf からページを挿入");
|
||||
assert_eq!(action_label("Untranslated custom action"), "Untranslated custom action");
|
||||
assert_eq!(menu_label("file.open", "Open…"), "開く…");
|
||||
set_current(Lang::EN);
|
||||
assert_eq!(command_label("Undo Fill in Save {e}"), "Undo Fill in Save {e}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn current_language_is_per_thread() {
|
||||
set_current(JA());
|
||||
|
|
|
|||
|
|
@ -44,9 +44,10 @@ impl PdfCraftApp {
|
|||
}
|
||||
}
|
||||
Request::LaunchUrl(u) => self.request_document_url(&u, crate::LinkOrigin::Script),
|
||||
Request::Submit(u) => self.notify(format!(
|
||||
"The form asks to be submitted to {u}; PdfCraft doesn't send form data. Save the document to keep your entries."
|
||||
)),
|
||||
Request::Submit(u) => self.notify_fmt(
|
||||
"The form asks to be submitted to {u}; PdfCraft doesn't send form data. Save the document to keep your entries.",
|
||||
&[("u", &u)],
|
||||
),
|
||||
Request::Focus(_) | Request::Beep | Request::Reset(_) => {}
|
||||
}
|
||||
}
|
||||
|
|
@ -67,7 +68,7 @@ impl PdfCraftApp {
|
|||
let out = JsOutput { alerts: o.alerts, console: o.console, requests: o.requests, errors: o.error.into_iter().collect() };
|
||||
self.handle_js(id, out);
|
||||
}
|
||||
Err(e) => self.notify(format!("{field}: {e}")),
|
||||
Err(e) => self.notify_fmt("{field}: {e}", &[("field", field), ("e", &e.to_string())]),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -75,14 +76,18 @@ impl PdfCraftApp {
|
|||
pub fn detect_fields(&mut self) {
|
||||
let Some((i, id)) = self.active_ids() else { return };
|
||||
match self.session.auto_detect_fields(id, &[]) {
|
||||
Ok(names) if names.is_empty() => self.notify("No form fields were detected"),
|
||||
Ok(names) if names.is_empty() => self.notify_tr("No form fields were detected"),
|
||||
Ok(names) => {
|
||||
if let Some(info) = self.session.get(id).map(|d| d.info.clone()) {
|
||||
self.views[i].document_changed(&info);
|
||||
}
|
||||
self.notify(format!("Detected {} form field{}", names.len(), if names.len() == 1 { "" } else { "s" }));
|
||||
if names.len() == 1 {
|
||||
self.notify_tr("Detected 1 form field");
|
||||
} else {
|
||||
self.notify_fmt("Detected {n} form fields", &[("n", &names.len().to_string())]);
|
||||
}
|
||||
}
|
||||
Err(e) => self.notify(e.to_string()),
|
||||
Err(e) => self.notify_error(e),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -117,11 +122,12 @@ fn buttons(ui: &mut egui::Ui, primary: &str, others: &[&str]) -> Option<String>
|
|||
ui.horizontal(|ui| {
|
||||
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
|
||||
// Stable ids: the console's output above changes how many widgets come first.
|
||||
if ui.push_id(primary, |ui| widgets::pill_button(ui, primary, true)).inner.clicked() {
|
||||
// Labels are translated for display; the returned id stays English.
|
||||
if ui.push_id(primary, |ui| widgets::pill_button(ui, tl!(primary), true)).inner.clicked() {
|
||||
clicked = Some(primary.to_string());
|
||||
}
|
||||
for o in others {
|
||||
if ui.push_id(o, |ui| widgets::pill_button(ui, o, false)).inner.clicked() {
|
||||
if ui.push_id(o, |ui| widgets::pill_button(ui, tl!(o), false)).inner.clicked() {
|
||||
clicked = Some(o.to_string());
|
||||
}
|
||||
}
|
||||
|
|
@ -132,10 +138,10 @@ fn buttons(ui: &mut egui::Ui, primary: &str, others: &[&str]) -> Option<String>
|
|||
|
||||
/// The JavaScript console. Returns `true` to close.
|
||||
pub(crate) fn console_body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> bool {
|
||||
ui.label(egui::RichText::new("JavaScript Console").font(theme::semibold(18.0)));
|
||||
ui.label(egui::RichText::new(tl!("JavaScript Console")).font(theme::semibold(18.0)));
|
||||
ui.add_space(6.0);
|
||||
if !app.session.javascript() {
|
||||
ui.label(egui::RichText::new("JavaScript is turned off (Preferences ▸ JavaScript).").small().color(t.text_muted));
|
||||
ui.label(egui::RichText::new(tl!("JavaScript is turned off (Preferences ▸ JavaScript).")).small().color(t.text_muted));
|
||||
}
|
||||
egui::Frame::new().fill(t.hover).corner_radius(egui::CornerRadius::same(6)).inner_margin(egui::Margin::same(8)).show(ui, |ui| {
|
||||
ui.set_width(ui.available_width());
|
||||
|
|
@ -143,7 +149,7 @@ pub(crate) fn console_body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens)
|
|||
ui.set_width(ui.available_width());
|
||||
ui.set_min_height(160.0);
|
||||
if app.js_console.log.is_empty() {
|
||||
ui.label(egui::RichText::new("Output appears here.").color(t.text_muted));
|
||||
ui.label(egui::RichText::new(tl!("Output appears here.")).color(t.text_muted));
|
||||
}
|
||||
for line in &app.js_console.log {
|
||||
ui.label(egui::RichText::new(line).monospace());
|
||||
|
|
@ -156,7 +162,7 @@ pub(crate) fn console_body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens)
|
|||
.code_editor()
|
||||
.desired_rows(4)
|
||||
.desired_width(f32::INFINITY)
|
||||
.hint_text("JavaScript, e.g. getField(\"total\").value")
|
||||
.hint_text(tl!("JavaScript, e.g. getField(\"total\").value"))
|
||||
.id_salt("js-console-input"),
|
||||
);
|
||||
let run_key = input.has_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter) && i.modifiers.command);
|
||||
|
|
@ -173,12 +179,12 @@ pub(crate) fn console_body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens)
|
|||
|
||||
/// Document JavaScripts: list, edit, add and delete. Returns `true` to close.
|
||||
pub(crate) fn document_js_body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> bool {
|
||||
ui.label(egui::RichText::new("Document JavaScripts").font(theme::semibold(18.0)));
|
||||
ui.label(egui::RichText::new(tl!("Document JavaScripts")).font(theme::semibold(18.0)));
|
||||
ui.add_space(6.0);
|
||||
let scripts = app.active_ids().and_then(|(_, id)| app.session.get(id)).map(|d| d.document_scripts()).unwrap_or_default();
|
||||
let mut edit: Option<Edit> = None;
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Script Name:");
|
||||
ui.label(tl!("Script Name:"));
|
||||
ui.add(egui::TextEdit::singleline(&mut app.doc_js.name).desired_width(240.0).id_salt("doc-js-name"));
|
||||
});
|
||||
ui.add_space(4.0);
|
||||
|
|
@ -187,7 +193,7 @@ pub(crate) fn document_js_body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tok
|
|||
egui::ScrollArea::vertical().max_height(120.0).id_salt("doc-js-list").show(ui, |ui| {
|
||||
ui.set_width(ui.available_width());
|
||||
if scripts.is_empty() {
|
||||
ui.label(egui::RichText::new("This document has no document-level scripts.").color(t.text_muted));
|
||||
ui.label(egui::RichText::new(tl!("This document has no document-level scripts.")).color(t.text_muted));
|
||||
}
|
||||
for (name, js) in &scripts {
|
||||
if ui.selectable_label(app.doc_js.name == *name, name).clicked() {
|
||||
|
|
@ -225,12 +231,21 @@ pub(crate) fn preferences_body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tok
|
|||
ui.horizontal(|ui| {
|
||||
ui.label(tl!("Interface language"));
|
||||
let selected = crate::i18n::Lang::from_code(&app.language).map_or(tl!("Auto"), crate::i18n::Lang::name);
|
||||
let before = app.language.clone();
|
||||
egui::ComboBox::from_id_salt("interface-language").selected_text(selected).show_ui(ui, |ui| {
|
||||
ui.selectable_value(&mut app.language, crate::i18n::AUTO.to_string(), tl!("Auto"));
|
||||
if ui.selectable_value(&mut app.language, crate::i18n::AUTO.to_string(), tl!("Auto")).clicked() {
|
||||
ui.close();
|
||||
}
|
||||
for language in crate::i18n::Lang::all() {
|
||||
ui.selectable_value(&mut app.language, language.code().to_string(), language.name());
|
||||
if ui.selectable_value(&mut app.language, language.code().to_string(), language.name()).clicked() {
|
||||
ui.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
// Relabel the rest of this dialog in the new language right away, not next frame.
|
||||
if app.language != before {
|
||||
crate::i18n::set_current(crate::i18n::Lang::from_pref(&app.language));
|
||||
}
|
||||
});
|
||||
ui.add_space(8.0);
|
||||
ui.label(egui::RichText::new(tl!("Documents and view")).font(theme::semibold(13.0)));
|
||||
|
|
@ -268,11 +283,11 @@ pub(crate) fn preferences_body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tok
|
|||
app.session.set_javascript(on);
|
||||
}
|
||||
ui.label(
|
||||
egui::RichText::new("Scripts run in a sandbox without file or network access. A script that asks to open a web page needs your permission first. With JavaScript off, Acrobat's standard format, validate and calculate functions still work.")
|
||||
egui::RichText::new(tl!("Scripts run in a sandbox without file or network access. A script that asks to open a web page needs your permission first. With JavaScript off, Acrobat's standard format, validate and calculate functions still work."))
|
||||
.small()
|
||||
.color(t.text_muted),
|
||||
);
|
||||
});
|
||||
ui.add_space(10.0);
|
||||
buttons(ui, "OK", &[]).is_some()
|
||||
buttons(ui, tl!("OK"), &[]).is_some()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -750,10 +750,10 @@ impl PdfCraftApp {
|
|||
match f.bytes() {
|
||||
Ok(bytes) => {
|
||||
if let Err(e) = self.open_bytes(&name, None, bytes) {
|
||||
self.notify(format!("Couldn't open {name}: {e}"));
|
||||
self.notify_fmt("Couldn't open {name}: {e}", &[("name", &name), ("e", &e.to_string())]);
|
||||
}
|
||||
}
|
||||
Err(e) => self.notify(format!("Couldn't read {name}: {e}")),
|
||||
Err(e) => self.notify_fmt("Couldn't read {name}: {e}", &[("name", &name), ("e", &e.to_string())]),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -779,22 +779,22 @@ impl PdfCraftApp {
|
|||
let Some(att) = d.info.attachments.get(index).cloned() else { return };
|
||||
let data = pdfcraft_render::attachment_data(&d.bytes, d.password.as_deref(), &att);
|
||||
match (data, open) {
|
||||
(Err(e), _) => self.notify(format!("Couldn't read {}: {e}", att.name)),
|
||||
(Err(e), _) => self.notify_fmt("Couldn't read {name}: {e}", &[("name", &att.name), ("e", &e.to_string())]),
|
||||
(Ok(bytes), true) => {
|
||||
if let Err(e) = self.open_bytes(&att.name, None, bytes) {
|
||||
self.notify(format!("Couldn't open {}: {e}", att.name));
|
||||
self.notify_fmt("Couldn't open {name}: {e}", &[("name", &att.name), ("e", &e.to_string())]);
|
||||
}
|
||||
}
|
||||
(Ok(bytes), false) => {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
if let Some(path) = rfd::FileDialog::new().set_file_name(&att.name).save_file() {
|
||||
match std::fs::write(&path, &bytes) {
|
||||
Ok(()) => self.notify(format!("Saved {}", path.display())),
|
||||
Err(e) => self.notify(format!("Couldn't save: {e}")),
|
||||
Ok(()) => self.notify_fmt("Saved {name}", &[("name", &path.display().to_string())]),
|
||||
Err(e) => self.notify_fmt("Couldn't save: {e}", &[("e", &e.to_string())]),
|
||||
}
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
self.notify(format!("Downloading attachments on the web arrives with M3.10 ({} bytes ready)", bytes.len()));
|
||||
self.notify_fmt("Downloading attachments on the web arrives with M3.10 ({n} bytes ready)", &[("n", &bytes.len().to_string())]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -810,7 +810,7 @@ impl PdfCraftApp {
|
|||
let Some(p) = self.password_prompt.take() else { return };
|
||||
let Some(pw) = password else { return };
|
||||
match self.try_open(&p.name, p.path, p.bytes, Some(&pw)) {
|
||||
Err(e) => self.notify(format!("Couldn't open {}: {e}", p.name)),
|
||||
Err(e) => self.notify_fmt("Couldn't open {name}: {e}", &[("name", &p.name), ("e", &e.to_string())]),
|
||||
// A recovered encrypted document is open once the prompt is gone.
|
||||
Ok(()) if self.password_prompt.is_none() => {
|
||||
if let Some(meta) = self.pending_recovered.clone() {
|
||||
|
|
@ -827,10 +827,10 @@ impl PdfCraftApp {
|
|||
match std::fs::read(path) {
|
||||
Ok(bytes) => {
|
||||
if let Err(e) = self.open_bytes(&name, Some(path.to_string()), bytes) {
|
||||
self.notify(format!("Couldn't open {name}: {e}"));
|
||||
self.notify_fmt("Couldn't open {name}: {e}", &[("name", &name), ("e", &e.to_string())]);
|
||||
}
|
||||
}
|
||||
Err(e) => self.notify(format!("Couldn't read {name}: {e}")),
|
||||
Err(e) => self.notify_fmt("Couldn't read {name}: {e}", &[("name", &name), ("e", &e.to_string())]),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -838,7 +838,7 @@ impl PdfCraftApp {
|
|||
#[cfg(not(target_arch = "wasm32"))]
|
||||
self.pick(
|
||||
pickers::PickFor::Open,
|
||||
rfd::AsyncFileDialog::new().add_filter("PDF", &["pdf"]).add_filter("Images and text (converted to PDF)", &create_ui::CONVERTIBLE),
|
||||
rfd::AsyncFileDialog::new().add_filter("PDF", &["pdf"]).add_filter(tl!("Images and text (converted to PDF)"), &create_ui::CONVERTIBLE),
|
||||
false,
|
||||
);
|
||||
// Browsers pick files asynchronously; the bytes arrive through `inbox`.
|
||||
|
|
@ -846,8 +846,11 @@ impl PdfCraftApp {
|
|||
{
|
||||
let inbox = self.inbox.clone();
|
||||
wasm_bindgen_futures::spawn_local(async move {
|
||||
if let Some(h) =
|
||||
rfd::AsyncFileDialog::new().add_filter("PDF", &["pdf"]).add_filter("Images and text", &create_ui::CONVERTIBLE).pick_file().await
|
||||
if let Some(h) = rfd::AsyncFileDialog::new()
|
||||
.add_filter("PDF", &["pdf"])
|
||||
.add_filter(tl!("Images and text"), &create_ui::CONVERTIBLE)
|
||||
.pick_file()
|
||||
.await
|
||||
{
|
||||
let bytes = h.read().await;
|
||||
if let Ok(mut q) = inbox.lock() {
|
||||
|
|
@ -904,10 +907,10 @@ impl PdfCraftApp {
|
|||
self.pending_link = Some(PendingLink { url, origin });
|
||||
}
|
||||
}
|
||||
Err(e) => self.notify(format!(
|
||||
"{} in this document tried to open an address PdfCraft won't open: {e}. Only web (http, https) and email (mailto) links open from documents.",
|
||||
origin.noun()
|
||||
)),
|
||||
Err(e) => self.notify_fmt(
|
||||
"{who} in this document tried to open an address PdfCraft won't open: {e}. Only web (http, https) and email (mailto) links open from documents.",
|
||||
&[("who", tl!(origin.noun())), ("e", &e.to_string())],
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -924,6 +927,22 @@ impl PdfCraftApp {
|
|||
self.toast = Some((msg.into(), 0.0));
|
||||
}
|
||||
|
||||
/// Notify with a static message in the UI language.
|
||||
pub fn notify_tr(&mut self, text: &str) {
|
||||
self.notify(tl!(text).to_string());
|
||||
}
|
||||
|
||||
/// Notify with an error's own text. Engine and OS messages are not translated: matching their
|
||||
/// English wording would break silently whenever it changes.
|
||||
pub fn notify_error(&mut self, error: impl std::fmt::Display) {
|
||||
self.notify(error.to_string());
|
||||
}
|
||||
|
||||
/// Notify with a `{name}`-style template in the UI language (placeholders filled once).
|
||||
pub fn notify_fmt(&mut self, template: &str, args: &[(&str, &str)]) {
|
||||
self.notify(i18n::fmt(tl!(template), args));
|
||||
}
|
||||
|
||||
pub fn set_theme(&mut self, ctx: &egui::Context, kind: ThemeKind) {
|
||||
self.theme = kind;
|
||||
theme::apply(ctx, kind);
|
||||
|
|
@ -941,12 +960,12 @@ impl PdfCraftApp {
|
|||
.flat_map(|g| g.sections.iter().flat_map(|s| s.items.iter()))
|
||||
.find(|i| i.command == command)
|
||||
.map(|i| match i.availability {
|
||||
pdfcraft_engine::catalog::Availability::Planned(m) => format!("ships in milestone {m}"),
|
||||
pdfcraft_engine::catalog::Availability::Provider => "needs an AI provider (off by default)".to_string(),
|
||||
pdfcraft_engine::catalog::Availability::Ready => "is available".to_string(),
|
||||
pdfcraft_engine::catalog::Availability::Planned(m) => crate::i18n::fmt(tl!("ships in milestone {m}"), &[("m", m)]),
|
||||
pdfcraft_engine::catalog::Availability::Provider => tl!("needs an AI provider (off by default)").to_string(),
|
||||
pdfcraft_engine::catalog::Availability::Ready => tl!("is available").to_string(),
|
||||
})
|
||||
.unwrap_or_else(|| "is not available yet".into());
|
||||
self.notify(format!("`{command}` {when}"));
|
||||
.unwrap_or_else(|| tl!("is not available yet").to_string());
|
||||
self.notify_fmt("`{command}` {when}", &[("command", command), ("when", &when)]);
|
||||
}
|
||||
|
||||
/// Select the workspace and its matching tool panel, just like the mode bar.
|
||||
|
|
@ -1274,6 +1293,8 @@ impl eframe::App for PdfCraftApp {
|
|||
|
||||
fn logic(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||
self.ctx = Some(ctx.clone());
|
||||
// Notices raised outside `ui` (opened files, OS events, the control channel) translate too.
|
||||
i18n::set_current(i18n::Lang::from_pref(&self.language));
|
||||
if !self.styled {
|
||||
egui_extras::install_image_loaders(ctx);
|
||||
theme::install_fonts(ctx);
|
||||
|
|
@ -1300,7 +1321,7 @@ impl eframe::App for PdfCraftApp {
|
|||
let arrived: Vec<(String, Vec<u8>)> = self.inbox.lock().map(|mut q| std::mem::take(&mut *q)).unwrap_or_default();
|
||||
for (name, bytes) in arrived {
|
||||
if let Err(e) = self.open_bytes(&name, None, bytes) {
|
||||
self.notify(format!("Couldn't open {name}: {e}"));
|
||||
self.notify_fmt("Couldn't open {name}: {e}", &[("name", &name), ("e", &e.to_string())]);
|
||||
}
|
||||
}
|
||||
let os_events = self.os_events.as_mut().map(|poll| poll()).unwrap_or_default();
|
||||
|
|
|
|||
|
|
@ -172,12 +172,16 @@ impl PdfCraftApp {
|
|||
let Some((_, id)) = self.active_ids() else { return };
|
||||
let found = self.session.find_urls(id);
|
||||
if found.is_empty() {
|
||||
self.notify("No unlinked web addresses were found in the text");
|
||||
self.notify_tr("No unlinked web addresses were found in the text");
|
||||
return;
|
||||
}
|
||||
let n = found.len();
|
||||
if self.apply_edit(Edit::AddLinks { links: found, style: LinkStyle::default() }) {
|
||||
self.notify(format!("Created {n} link{}", if n == 1 { "" } else { "s" }));
|
||||
if n == 1 {
|
||||
self.notify_tr("Created 1 link");
|
||||
} else {
|
||||
self.notify_fmt("Created {n} links", &[("n", &n.to_string())]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -185,43 +189,42 @@ impl PdfCraftApp {
|
|||
/// Link Properties. Returns (apply, cancel).
|
||||
pub(crate) fn body(ui: &mut egui::Ui, d: &mut LinkDraft, pages: usize, t: &Tokens) -> (bool, bool) {
|
||||
ui.set_width(460.0);
|
||||
ui.label(egui::RichText::new(if d.index.is_some() { "Link Properties" } else { "Create Link" }).font(theme::semibold(18.0)));
|
||||
ui.label(egui::RichText::new(if d.index.is_some() { tl!("Link Properties") } else { tl!("Create Link") }).font(theme::semibold(18.0)));
|
||||
ui.add_space(8.0);
|
||||
widgets::section_title(ui, "Link Appearance");
|
||||
widgets::section_title(ui, tl!("Link Appearance"));
|
||||
egui::Grid::new("link-appearance").num_columns(2).spacing([12.0, 8.0]).show(ui, |ui| {
|
||||
ui.label("Link Type:");
|
||||
egui::ComboBox::from_id_salt("link-type").selected_text(if d.style.visible { "Visible Rectangle" } else { "Invisible Rectangle" }).show_ui(
|
||||
ui,
|
||||
|ui| {
|
||||
ui.selectable_value(&mut d.style.visible, true, "Visible Rectangle");
|
||||
ui.selectable_value(&mut d.style.visible, false, "Invisible Rectangle");
|
||||
},
|
||||
);
|
||||
ui.label(tl!("Link Type:"));
|
||||
egui::ComboBox::from_id_salt("link-type")
|
||||
.selected_text(if d.style.visible { tl!("Visible Rectangle") } else { tl!("Invisible Rectangle") })
|
||||
.show_ui(ui, |ui| {
|
||||
ui.selectable_value(&mut d.style.visible, true, tl!("Visible Rectangle"));
|
||||
ui.selectable_value(&mut d.style.visible, false, tl!("Invisible Rectangle"));
|
||||
});
|
||||
ui.end_row();
|
||||
ui.label("Highlight Style:");
|
||||
egui::ComboBox::from_id_salt("link-hl").selected_text(d.style.highlight.label()).show_ui(ui, |ui| {
|
||||
ui.label(tl!("Highlight Style:"));
|
||||
egui::ComboBox::from_id_salt("link-hl").selected_text(tl!(d.style.highlight.label())).show_ui(ui, |ui| {
|
||||
for h in LinkHighlight::ALL {
|
||||
ui.selectable_value(&mut d.style.highlight, h, h.label());
|
||||
ui.selectable_value(&mut d.style.highlight, h, tl!(h.label()));
|
||||
}
|
||||
});
|
||||
ui.end_row();
|
||||
if d.style.visible {
|
||||
ui.label("Line Thickness:");
|
||||
ui.label(tl!("Line Thickness:"));
|
||||
egui::ComboBox::from_id_salt("link-w")
|
||||
.selected_text(if d.style.width <= 1.0 {
|
||||
"Thin"
|
||||
tl!("Thin")
|
||||
} else if d.style.width <= 2.0 {
|
||||
"Medium"
|
||||
tl!("Medium")
|
||||
} else {
|
||||
"Thick"
|
||||
tl!("Thick")
|
||||
})
|
||||
.show_ui(ui, |ui| {
|
||||
ui.selectable_value(&mut d.style.width, 1.0, "Thin");
|
||||
ui.selectable_value(&mut d.style.width, 2.0, "Medium");
|
||||
ui.selectable_value(&mut d.style.width, 3.0, "Thick");
|
||||
ui.selectable_value(&mut d.style.width, 1.0, tl!("Thin"));
|
||||
ui.selectable_value(&mut d.style.width, 2.0, tl!("Medium"));
|
||||
ui.selectable_value(&mut d.style.width, 3.0, tl!("Thick"));
|
||||
});
|
||||
ui.end_row();
|
||||
ui.label("Line Style:");
|
||||
ui.label(tl!("Line Style:"));
|
||||
let mut style = if d.style.dashed {
|
||||
1
|
||||
} else if d.style.underline {
|
||||
|
|
@ -229,15 +232,15 @@ pub(crate) fn body(ui: &mut egui::Ui, d: &mut LinkDraft, pages: usize, t: &Token
|
|||
} else {
|
||||
0
|
||||
};
|
||||
egui::ComboBox::from_id_salt("link-s").selected_text(["Solid", "Dashed", "Underline"][style]).show_ui(ui, |ui| {
|
||||
egui::ComboBox::from_id_salt("link-s").selected_text(tl!(["Solid", "Dashed", "Underline"][style])).show_ui(ui, |ui| {
|
||||
for (k, l) in ["Solid", "Dashed", "Underline"].iter().enumerate() {
|
||||
ui.selectable_value(&mut style, k, *l);
|
||||
ui.selectable_value(&mut style, k, tl!(l));
|
||||
}
|
||||
});
|
||||
d.style.dashed = style == 1;
|
||||
d.style.underline = style == 2;
|
||||
ui.end_row();
|
||||
ui.label("Color:");
|
||||
ui.label(tl!("Color:"));
|
||||
if let Some(c) = crate::comments::swatch_grid(ui, Some(d.style.color)) {
|
||||
d.style.color = c;
|
||||
}
|
||||
|
|
@ -245,34 +248,34 @@ pub(crate) fn body(ui: &mut egui::Ui, d: &mut LinkDraft, pages: usize, t: &Token
|
|||
}
|
||||
});
|
||||
ui.add_space(8.0);
|
||||
widgets::section_title(ui, "Link Action");
|
||||
ui.radio_value(&mut d.web, true, "Open a web page");
|
||||
widgets::section_title(ui, tl!("Link Action"));
|
||||
ui.radio_value(&mut d.web, true, tl!("Open a web page"));
|
||||
ui.add_enabled_ui(d.web, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_space(24.0);
|
||||
ui.add(egui::TextEdit::singleline(&mut d.url).desired_width(360.0).hint_text("https://example.org"));
|
||||
});
|
||||
});
|
||||
ui.radio_value(&mut d.web, false, "Go to a page view");
|
||||
ui.radio_value(&mut d.web, false, tl!("Go to a page view"));
|
||||
ui.add_enabled_ui(!d.web, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_space(24.0);
|
||||
ui.label("Page");
|
||||
ui.label(tl!("Page"));
|
||||
ui.add(egui::DragValue::new(&mut d.target).range(1..=pages.max(1)));
|
||||
ui.label(format!("of {pages}"));
|
||||
ui.label(crate::i18n::fmt(tl!("of {name}"), &[("name", &pages.to_string())]));
|
||||
});
|
||||
});
|
||||
let ok = !d.web || (d.url.trim().len() > 3 && d.url.trim() != "https://");
|
||||
if !ok {
|
||||
ui.label(egui::RichText::new("Type the web address.").small().color(t.text_faint));
|
||||
ui.label(egui::RichText::new(tl!("Type the web address.")).small().color(t.text_faint));
|
||||
}
|
||||
ui.add_space(12.0);
|
||||
let (mut a, mut c) = (false, false);
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
if ui.add_enabled_ui(ok, |ui| widgets::pill_button(ui, "OK", true)).inner.clicked() {
|
||||
if ui.add_enabled_ui(ok, |ui| widgets::pill_button(ui, tl!("OK"), true)).inner.clicked() {
|
||||
a = true;
|
||||
}
|
||||
if widgets::pill_button(ui, "Cancel", false).clicked() {
|
||||
if widgets::pill_button(ui, tl!("Cancel"), false).clicked() {
|
||||
c = true;
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -46,24 +46,24 @@ impl PageRange {
|
|||
|
||||
pub(crate) fn ui(&mut self, ui: &mut egui::Ui, count: usize) {
|
||||
ui.horizontal(|ui| {
|
||||
ui.radio_value(&mut self.all, true, "All pages");
|
||||
ui.radio_value(&mut self.all, false, "Pages from");
|
||||
ui.radio_value(&mut self.all, true, tl!("All pages"));
|
||||
ui.radio_value(&mut self.all, false, tl!("Pages from"));
|
||||
ui.add_enabled(!self.all, egui::DragValue::new(&mut self.from).range(1..=count.max(1)));
|
||||
ui.label("to");
|
||||
ui.label(tl!("to"));
|
||||
if self.to == 1 && self.from == 1 {
|
||||
self.to = count.max(1);
|
||||
}
|
||||
ui.add_enabled(!self.all, egui::DragValue::new(&mut self.to).range(1..=count.max(1)));
|
||||
ui.label("Subset");
|
||||
ui.label(tl!("Subset"));
|
||||
let label = match self.subset {
|
||||
Subset::All => "All pages in range",
|
||||
Subset::Even => "Even pages only",
|
||||
Subset::Odd => "Odd pages only",
|
||||
Subset::All => tl!("All pages in range"),
|
||||
Subset::Even => tl!("Even pages only"),
|
||||
Subset::Odd => tl!("Odd pages only"),
|
||||
};
|
||||
egui::ComboBox::from_id_salt("mark-subset").selected_text(label).show_ui(ui, |ui| {
|
||||
ui.selectable_value(&mut self.subset, Subset::All, "All pages in range");
|
||||
ui.selectable_value(&mut self.subset, Subset::Even, "Even pages only");
|
||||
ui.selectable_value(&mut self.subset, Subset::Odd, "Odd pages only");
|
||||
ui.selectable_value(&mut self.subset, Subset::All, tl!("All pages in range"));
|
||||
ui.selectable_value(&mut self.subset, Subset::Even, tl!("Even pages only"));
|
||||
ui.selectable_value(&mut self.subset, Subset::Odd, tl!("Odd pages only"));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -145,25 +145,25 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens, kind: M
|
|||
let count = doc.info.pages.len();
|
||||
let page = doc.info.pages.get(current).map(|p| (p.width as f64, p.height as f64)).unwrap_or((612.0, 792.0));
|
||||
let d = &mut app.marks_draft;
|
||||
let verb = if d.replace { "Update" } else { "Add" };
|
||||
let verb = if d.replace { tl!("Update") } else { tl!("Add") };
|
||||
let title = match kind {
|
||||
MarkKind::HeaderFooter => format!("{verb} Header and Footer"),
|
||||
MarkKind::Watermark => format!("{verb} Watermark"),
|
||||
MarkKind::Background => format!("{verb} Background"),
|
||||
MarkKind::HeaderFooter => crate::i18n::fmt(tl!("{verb} Header and Footer"), &[("verb", verb)]),
|
||||
MarkKind::Watermark => crate::i18n::fmt(tl!("{verb} Watermark"), &[("verb", verb)]),
|
||||
MarkKind::Background => crate::i18n::fmt(tl!("{verb} Background"), &[("verb", verb)]),
|
||||
};
|
||||
ui.label(egui::RichText::new(title).font(theme::semibold(18.0)));
|
||||
ui.add_space(8.0);
|
||||
match kind {
|
||||
MarkKind::HeaderFooter => {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Font: Helvetica");
|
||||
ui.label("Size");
|
||||
ui.label(tl!("Font: Helvetica"));
|
||||
ui.label(tl!("Size"));
|
||||
ui.add(egui::DragValue::new(&mut d.hf.font_size).range(4.0..=72.0).speed(0.5));
|
||||
ui.checkbox(&mut d.hf.underline, "Underline");
|
||||
ui.checkbox(&mut d.hf.underline, tl!("Underline"));
|
||||
color_button(ui, &mut d.hf.color);
|
||||
ui.add_space(16.0);
|
||||
ui.label("Margins (in)");
|
||||
for (label, k) in [("Top", 0), ("Bottom", 1), ("Left", 2), ("Right", 3)] {
|
||||
ui.label(tl!("Margins (in)"));
|
||||
for (label, k) in [(tl!("Top"), 0), (tl!("Bottom"), 1), (tl!("Left"), 2), (tl!("Right"), 3)] {
|
||||
ui.label(label);
|
||||
let mut v = d.hf.margins[k] / 72.0;
|
||||
if ui.add(egui::DragValue::new(&mut v).range(0.0..=10.0).speed(0.05).max_decimals(2)).changed() {
|
||||
|
|
@ -175,7 +175,7 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens, kind: M
|
|||
egui::Grid::new("hf-boxes").num_columns(3).spacing([10.0, 4.0]).show(ui, |ui| {
|
||||
for row in 0..2 {
|
||||
for col in 0..3 {
|
||||
ui.label(egui::RichText::new(BOX_NAMES[row * 3 + col]).small());
|
||||
ui.label(egui::RichText::new(tl!(BOX_NAMES[row * 3 + col])).small());
|
||||
}
|
||||
ui.end_row();
|
||||
for col in 0..3 {
|
||||
|
|
@ -197,7 +197,7 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens, kind: M
|
|||
}
|
||||
},
|
||||
);
|
||||
if ui.button("Insert Page Number").clicked() {
|
||||
if ui.button(tl!("Insert Page Number")).clicked() {
|
||||
d.hf.text[d.focused_box].push_str(PAGE_FORMATS[d.page_format]);
|
||||
}
|
||||
ui.add_space(12.0);
|
||||
|
|
@ -209,19 +209,19 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens, kind: M
|
|||
}
|
||||
},
|
||||
);
|
||||
if ui.button("Insert Date").clicked() {
|
||||
if ui.button(tl!("Insert Date")).clicked() {
|
||||
d.hf.text[d.focused_box].push_str(DATE_FORMATS[d.date_format]);
|
||||
}
|
||||
ui.add_space(12.0);
|
||||
ui.label("Start page number");
|
||||
ui.label(tl!("Start page number"));
|
||||
ui.add(egui::DragValue::new(&mut d.hf.start_number).range(1..=999_999));
|
||||
});
|
||||
}
|
||||
MarkKind::Watermark => {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Source");
|
||||
ui.radio_value(&mut d.use_file, false, "Text");
|
||||
ui.radio_value(&mut d.use_file, true, "File");
|
||||
ui.label(tl!("Source"));
|
||||
ui.radio_value(&mut d.use_file, false, tl!("Text"));
|
||||
ui.radio_value(&mut d.use_file, true, tl!("File"));
|
||||
});
|
||||
if d.use_file {
|
||||
file_source(ui, d, MarkKind::Watermark);
|
||||
|
|
@ -229,7 +229,7 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens, kind: M
|
|||
ui.horizontal_top(|ui| {
|
||||
ui.vertical(|ui| {
|
||||
ui.add_enabled_ui(!d.use_file, |ui| {
|
||||
ui.label("Text");
|
||||
ui.label(tl!("Text"));
|
||||
ui.add(
|
||||
egui::TextEdit::multiline(&mut d.wm.text)
|
||||
.desired_rows(2)
|
||||
|
|
@ -239,9 +239,9 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens, kind: M
|
|||
);
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_enabled_ui(!d.use_file, |ui| ui.checkbox(&mut d.fit, "Fit to page"));
|
||||
ui.add_enabled_ui(!d.use_file, |ui| ui.checkbox(&mut d.fit, tl!("Fit to page")));
|
||||
ui.add_enabled_ui(!d.fit, |ui| {
|
||||
ui.label("Size");
|
||||
ui.label(tl!("Size"));
|
||||
if d.wm.font_size == 0.0 {
|
||||
d.wm.font_size = 72.0;
|
||||
}
|
||||
|
|
@ -250,39 +250,39 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens, kind: M
|
|||
color_button(ui, &mut d.wm.color);
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Rotation");
|
||||
ui.label(tl!("Rotation"));
|
||||
for r in [-45.0, 0.0, 45.0] {
|
||||
ui.radio_value(&mut d.wm.rotation, r, if r == 0.0 { "None".to_string() } else { format!("{r}°") });
|
||||
ui.radio_value(&mut d.wm.rotation, r, if r == 0.0 { tl!("None").to_string() } else { format!("{r}°") });
|
||||
}
|
||||
ui.add(egui::DragValue::new(&mut d.wm.rotation).range(-180.0..=180.0).suffix("°"));
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Opacity");
|
||||
ui.label(tl!("Opacity"));
|
||||
let mut pct = d.wm.opacity * 100.0;
|
||||
if ui.add(egui::Slider::new(&mut pct, 0.0..=100.0).suffix("%")).changed() {
|
||||
d.wm.opacity = pct / 100.0;
|
||||
}
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Location");
|
||||
ui.radio_value(&mut d.wm.behind, true, "Appear behind page");
|
||||
ui.radio_value(&mut d.wm.behind, false, "Appear on top of page");
|
||||
ui.label(tl!("Location"));
|
||||
ui.radio_value(&mut d.wm.behind, true, tl!("Appear behind page"));
|
||||
ui.radio_value(&mut d.wm.behind, false, tl!("Appear on top of page"));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
MarkKind::Background => {
|
||||
ui.horizontal(|ui| {
|
||||
ui.radio_value(&mut d.use_file, false, "From colour");
|
||||
ui.radio_value(&mut d.use_file, false, tl!("From colour"));
|
||||
ui.add_enabled_ui(!d.use_file, |ui| color_button(ui, &mut d.bg.color));
|
||||
ui.add_space(12.0);
|
||||
ui.radio_value(&mut d.use_file, true, "File");
|
||||
ui.radio_value(&mut d.use_file, true, tl!("File"));
|
||||
});
|
||||
if d.use_file {
|
||||
file_source(ui, d, MarkKind::Background);
|
||||
}
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Opacity");
|
||||
ui.label(tl!("Opacity"));
|
||||
let mut pct = d.bg.opacity * 100.0;
|
||||
if ui.add(egui::Slider::new(&mut pct, 0.0..=100.0).suffix("%")).changed() {
|
||||
d.bg.opacity = pct / 100.0;
|
||||
|
|
@ -291,7 +291,7 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens, kind: M
|
|||
}
|
||||
}
|
||||
ui.add_space(8.0);
|
||||
ui.label(egui::RichText::new("Page Range Options").font(theme::semibold(12.5)));
|
||||
ui.label(egui::RichText::new(tl!("Page Range Options")).font(theme::semibold(12.5)));
|
||||
d.range.ui(ui, count);
|
||||
ui.add_space(8.0);
|
||||
// Preview of the current page.
|
||||
|
|
@ -360,11 +360,20 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens, kind: M
|
|||
}
|
||||
});
|
||||
ui.vertical(|ui| {
|
||||
ui.label(egui::RichText::new(format!("Preview: page {} of {count}", current + 1)).small().color(t.text_faint));
|
||||
ui.label(
|
||||
egui::RichText::new(crate::i18n::fmt(
|
||||
tl!("Preview: page {p} of {n}"),
|
||||
&[("p", &(current + 1).to_string()), ("n", &count.to_string())],
|
||||
))
|
||||
.small()
|
||||
.color(t.text_faint),
|
||||
);
|
||||
let n = d.range.pages(count).len();
|
||||
ui.label(egui::RichText::new(format!("Applies to {n} page{}", if n == 1 { "" } else { "s" })).small().color(t.text_faint));
|
||||
let applies =
|
||||
if n == 1 { tl!("Applies to 1 page").to_string() } else { crate::i18n::fmt(tl!("Applies to {n} pages"), &[("n", &n.to_string())]) };
|
||||
ui.label(egui::RichText::new(applies).small().color(t.text_faint));
|
||||
if d.replace {
|
||||
ui.label(egui::RichText::new("Replaces the existing one on those pages.").small().color(t.text_faint));
|
||||
ui.label(egui::RichText::new(tl!("Replaces the existing one on those pages.")).small().color(t.text_faint));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -382,10 +391,10 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens, kind: M
|
|||
}
|
||||
MarkKind::Background => !d.use_file || d.file.is_some(),
|
||||
} && !d.range.pages(count).is_empty();
|
||||
if ui.add_enabled_ui(ready, |ui| widgets::pill_button(ui, "OK", true)).inner.clicked() {
|
||||
if ui.add_enabled_ui(ready, |ui| widgets::pill_button(ui, tl!("OK"), true)).inner.clicked() {
|
||||
apply = true;
|
||||
}
|
||||
if widgets::pill_button(ui, "Cancel", false).clicked() {
|
||||
if widgets::pill_button(ui, tl!("Cancel"), false).clicked() {
|
||||
cancel = true;
|
||||
}
|
||||
});
|
||||
|
|
@ -401,9 +410,9 @@ fn mark_file(d: &MarksDraft) -> Option<pdfcraft_engine::MarkFile> {
|
|||
fn file_source(ui: &mut egui::Ui, d: &mut MarksDraft, kind: MarkKind) {
|
||||
ui.horizontal(|ui| {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
if ui.button("Browse…").clicked()
|
||||
if ui.button(tl!("Browse…")).clicked()
|
||||
&& let Some(p) = rfd::FileDialog::new()
|
||||
.add_filter("PDF or image", &["pdf", "png", "jpg", "jpeg", "tif", "tiff", "gif", "bmp", "jp2", "j2k", "jpx"])
|
||||
.add_filter(tl!("PDF or image"), &["pdf", "png", "jpg", "jpeg", "tif", "tiff", "gif", "bmp", "jp2", "j2k", "jpx"])
|
||||
.pick_file()
|
||||
{
|
||||
match std::fs::read(&p) {
|
||||
|
|
@ -411,14 +420,14 @@ fn file_source(ui: &mut egui::Ui, d: &mut MarksDraft, kind: MarkKind) {
|
|||
Err(_) => d.file = None,
|
||||
}
|
||||
}
|
||||
ui.label(d.file.as_ref().map(|f| f.0.clone()).unwrap_or_else(|| "No file chosen".into()));
|
||||
ui.label(d.file.as_ref().map(|f| f.0.clone()).unwrap_or_else(|| tl!("No file chosen").to_string()));
|
||||
if d.file.as_ref().is_some_and(|f| f.1.starts_with(b"%PDF")) {
|
||||
ui.label("Page number");
|
||||
ui.label(tl!("Page number"));
|
||||
ui.add(egui::DragValue::new(&mut d.file_page).range(1..=9999));
|
||||
}
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Scale relative to target page");
|
||||
ui.label(tl!("Scale relative to target page"));
|
||||
let scale = if kind == MarkKind::Background { &mut d.bg.scale } else { &mut d.wm.scale };
|
||||
let mut pct = *scale * 100.0;
|
||||
if ui.add(egui::Slider::new(&mut pct, 5.0..=100.0).suffix("%")).changed() {
|
||||
|
|
|
|||
|
|
@ -64,10 +64,10 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> (boo
|
|||
let d = &mut app.ocr_draft;
|
||||
d.to = d.to.clamp(1, pages);
|
||||
d.from = d.from.clamp(1, d.to);
|
||||
ui.label(egui::RichText::new("Recognize Text").font(theme::semibold(18.0)));
|
||||
ui.label(egui::RichText::new(tl!("Recognize Text")).font(theme::semibold(18.0)));
|
||||
ui.add_space(8.0);
|
||||
let group = |ui: &mut egui::Ui, title: &str, body: &mut dyn FnMut(&mut egui::Ui)| {
|
||||
ui.label(egui::RichText::new(title).font(theme::semibold(13.0)));
|
||||
ui.label(egui::RichText::new(tl!(title)).font(theme::semibold(13.0)));
|
||||
egui::Frame::new().fill(t.hover).corner_radius(egui::CornerRadius::same(6)).inner_margin(egui::Margin::same(10)).show(ui, |ui| {
|
||||
ui.set_width(ui.available_width());
|
||||
body(ui);
|
||||
|
|
@ -75,19 +75,19 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> (boo
|
|||
ui.add_space(8.0);
|
||||
};
|
||||
group(ui, "Pages", &mut |ui| {
|
||||
ui.radio_value(&mut d.pages, OcrPages::All, "All pages");
|
||||
ui.radio_value(&mut d.pages, OcrPages::Current, "Current page");
|
||||
ui.radio_value(&mut d.pages, OcrPages::All, tl!("All pages"));
|
||||
ui.radio_value(&mut d.pages, OcrPages::Current, tl!("Current page"));
|
||||
ui.horizontal(|ui| {
|
||||
ui.radio_value(&mut d.pages, OcrPages::Range, "From");
|
||||
ui.radio_value(&mut d.pages, OcrPages::Range, tl!("From"));
|
||||
let on = d.pages == OcrPages::Range;
|
||||
ui.add_enabled(on, egui::DragValue::new(&mut d.from).range(1..=pages));
|
||||
ui.label("to");
|
||||
ui.label(tl!("to"));
|
||||
ui.add_enabled(on, egui::DragValue::new(&mut d.to).range(1..=pages));
|
||||
});
|
||||
});
|
||||
group(ui, "Settings", &mut |ui| {
|
||||
egui::Grid::new("ocr-settings").num_columns(2).spacing([12.0, 8.0]).show(ui, |ui| {
|
||||
ui.label("Document language");
|
||||
ui.label(tl!("Document language"));
|
||||
let name = LANGUAGES.iter().find(|l| l.0 == d.language).map_or("English", |l| l.1);
|
||||
egui::ComboBox::from_id_salt("ocr-language").selected_text(name).width(220.0).show_ui(ui, |ui| {
|
||||
for (code, name) in LANGUAGES {
|
||||
|
|
@ -95,14 +95,14 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> (boo
|
|||
}
|
||||
});
|
||||
ui.end_row();
|
||||
ui.label("Output");
|
||||
egui::ComboBox::from_id_salt("ocr-output").selected_text("Searchable Image (Exact)").width(220.0).show_ui(ui, |ui| {
|
||||
ui.label(tl!("Output"));
|
||||
egui::ComboBox::from_id_salt("ocr-output").selected_text(tl!("Searchable Image (Exact)")).width(220.0).show_ui(ui, |ui| {
|
||||
let _ = ui
|
||||
.selectable_label(true, "Searchable Image (Exact)")
|
||||
.on_hover_text("Adds invisible text over each word; the page image is not changed");
|
||||
.selectable_label(true, tl!("Searchable Image (Exact)"))
|
||||
.on_hover_text(tl!("Adds invisible text over each word; the page image is not changed"));
|
||||
});
|
||||
ui.end_row();
|
||||
ui.label("Downsample to");
|
||||
ui.label(tl!("Downsample to"));
|
||||
egui::ComboBox::from_id_salt("ocr-dpi").selected_text(format!("{} dpi", d.dpi)).width(220.0).show_ui(ui, |ui| {
|
||||
for v in [600, 300, 150, 72] {
|
||||
ui.selectable_value(&mut d.dpi, v, format!("{v} dpi"));
|
||||
|
|
@ -113,7 +113,7 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> (boo
|
|||
});
|
||||
if !available {
|
||||
ui.label(
|
||||
egui::RichText::new("Text recognition isn't installed: its models are missing (run `cargo xtask models`, or set PDFCRAFT_MODELS).")
|
||||
egui::RichText::new(tl!("Text recognition isn't installed: its models are missing (run `cargo xtask models`, or set PDFCRAFT_MODELS)."))
|
||||
.small()
|
||||
.color(t.text_muted),
|
||||
);
|
||||
|
|
@ -123,10 +123,10 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> (boo
|
|||
let (mut go, mut cancel) = (false, false);
|
||||
ui.horizontal(|ui| {
|
||||
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
|
||||
if ui.add_enabled_ui(available, |ui| widgets::pill_button(ui, "Recognize text", true)).inner.clicked() {
|
||||
if ui.add_enabled_ui(available, |ui| widgets::pill_button(ui, tl!("Recognize text"), true)).inner.clicked() {
|
||||
go = true;
|
||||
}
|
||||
if widgets::pill_button(ui, "Cancel", false).clicked() {
|
||||
if widgets::pill_button(ui, tl!("Cancel"), false).clicked() {
|
||||
cancel = true;
|
||||
}
|
||||
})
|
||||
|
|
@ -139,7 +139,7 @@ impl PdfCraftApp {
|
|||
pub fn start_ocr(&mut self) {
|
||||
let Some((vi, id)) = self.active_ids() else { return };
|
||||
if self.ocr_run.is_some() {
|
||||
self.notify("Text recognition is already running");
|
||||
self.notify_tr("Text recognition is already running");
|
||||
return;
|
||||
}
|
||||
if let Some(why) = self.session.get(id).and_then(|d| d.read_only_reason.clone()) {
|
||||
|
|
@ -185,7 +185,7 @@ impl PdfCraftApp {
|
|||
/// (the export folder override in tests), under the same names.
|
||||
pub fn ocr_files(&mut self, files: Vec<(String, Vec<u8>)>) {
|
||||
if self.ocr_batch.is_some() {
|
||||
self.notify("Text recognition is already running");
|
||||
self.notify_tr("Text recognition is already running");
|
||||
return;
|
||||
}
|
||||
let settings = OcrSettings { dpi: self.ocr_draft.dpi as f32, language: self.ocr_draft.language.clone(), ..Default::default() };
|
||||
|
|
@ -193,12 +193,15 @@ impl PdfCraftApp {
|
|||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let dir = match &self.export_dir_override {
|
||||
Some(d) => Some(std::path::PathBuf::from(d)),
|
||||
None => rfd::FileDialog::new().set_title("Choose a folder for the searchable files").pick_folder(),
|
||||
None => rfd::FileDialog::new().set_title(tl!("Choose a folder for the searchable files").to_string()).pick_folder(),
|
||||
};
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let Some(dir) = dir else { return };
|
||||
let p = progress.clone();
|
||||
// The summary is written on the worker thread: draw it in the UI's language.
|
||||
let lang = crate::i18n::current();
|
||||
let work = move || {
|
||||
crate::i18n::set_current(lang);
|
||||
let (mut ok, mut words, mut failed) = (0, 0, Vec::new());
|
||||
match pdfcraft_engine::ocr::engine() {
|
||||
Err(e) => failed.push(e),
|
||||
|
|
@ -220,14 +223,19 @@ impl PdfCraftApp {
|
|||
ok += 1;
|
||||
words += n;
|
||||
}
|
||||
Err(e) => failed.push(format!("{name}: {e}")),
|
||||
Err(e) => failed.push(format!("{name}: {}", e)),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut msg = format!("Recognized {words} words in {ok} file{}", if ok == 1 { "" } else { "s" });
|
||||
let done = if ok == 1 {
|
||||
crate::i18n::fmt(tl!("Recognized {w} words in 1 file"), &[("w", &words.to_string())])
|
||||
} else {
|
||||
crate::i18n::fmt(tl!("Recognized {w} words in {n} files"), &[("w", &words.to_string()), ("n", &ok.to_string())])
|
||||
};
|
||||
let mut msg = done;
|
||||
if !failed.is_empty() {
|
||||
msg.push_str(&format!("; failed: {}", failed.join("; ")));
|
||||
msg.push_str(&crate::i18n::fmt(tl!("; failed: {list}"), &[("list", &failed.join("; "))]));
|
||||
}
|
||||
if let Ok(mut s) = p.lock() {
|
||||
s.done = s.total;
|
||||
|
|
@ -265,7 +273,10 @@ impl PdfCraftApp {
|
|||
self.notify(m);
|
||||
}
|
||||
Some(Err((done, total))) => {
|
||||
let m = format!("Recognizing text… file {} of {}", (done + 1).min(total.max(1)), total.max(1));
|
||||
let m = crate::i18n::fmt(
|
||||
tl!("Recognizing text… file {d} of {t}"),
|
||||
&[("d", &(done + 1).min(total.max(1)).to_string()), ("t", &total.max(1).to_string())],
|
||||
);
|
||||
if self.toast.as_ref().is_none_or(|t| t.0 != m) {
|
||||
self.notify(m);
|
||||
}
|
||||
|
|
@ -280,7 +291,10 @@ impl PdfCraftApp {
|
|||
let (doc, progress) = (run.doc, run.progress.clone());
|
||||
let Ok(mut s) = progress.lock() else { return };
|
||||
let Some(result) = s.result.take() else {
|
||||
let msg = format!("Recognizing text… page {} of {}", (s.done + 1).min(s.total.max(1)), s.total.max(1));
|
||||
let msg = crate::i18n::fmt(
|
||||
tl!("Recognizing text… page {d} of {t}"),
|
||||
&[("d", &(s.done + 1).min(s.total.max(1)).to_string()), ("t", &s.total.max(1).to_string())],
|
||||
);
|
||||
drop(s);
|
||||
if self.toast.as_ref().is_none_or(|t| t.0 != msg) {
|
||||
self.notify(msg);
|
||||
|
|
@ -295,7 +309,7 @@ impl PdfCraftApp {
|
|||
let found = match result {
|
||||
Ok(f) => f,
|
||||
Err(e) => {
|
||||
self.notify(e);
|
||||
self.notify_error(e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
|
@ -308,14 +322,23 @@ impl PdfCraftApp {
|
|||
{
|
||||
view.document_changed(&info);
|
||||
}
|
||||
let mut msg =
|
||||
format!("Recognized {words} word{} on {read} page{}", if words == 1 { "" } else { "s" }, if read == 1 { "" } else { "s" });
|
||||
let words_part = if words == 1 {
|
||||
crate::i18n::fmt(tl!("Recognized 1 word"), &[])
|
||||
} else {
|
||||
crate::i18n::fmt(tl!("Recognized {n} words"), &[("n", &words.to_string())])
|
||||
};
|
||||
let pages_part = if read == 1 { tl!("1 page").to_string() } else { crate::i18n::fmt(tl!("{n} pages"), &[("n", &read.to_string())]) };
|
||||
let mut msg = crate::i18n::fmt(tl!("{words} on {pages}"), &[("words", &words_part), ("pages", &pages_part)]);
|
||||
if skipped > 0 {
|
||||
msg.push_str(&format!("; {skipped} page{} already had text", if skipped == 1 { "" } else { "s" }));
|
||||
msg.push_str(&if skipped == 1 {
|
||||
crate::i18n::fmt(tl!("; 1 page already had text"), &[])
|
||||
} else {
|
||||
crate::i18n::fmt(tl!("; {n} pages already had text"), &[("n", &skipped.to_string())])
|
||||
});
|
||||
}
|
||||
self.notify(msg);
|
||||
}
|
||||
Err(e) => self.notify(e.to_string()),
|
||||
Err(e) => self.notify_error(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,20 +33,20 @@ impl Default for OptimizeDraft {
|
|||
}
|
||||
|
||||
fn image_row(ui: &mut egui::Ui, id: &str, title: &str, s: &mut ImageSettings) {
|
||||
ui.label(egui::RichText::new(title).font(theme::semibold(13.0)));
|
||||
ui.label(egui::RichText::new(tl!(title)).font(theme::semibold(13.0)));
|
||||
ui.horizontal(|ui| {
|
||||
ui.checkbox(&mut s.downsample, "Bicubic downsampling to");
|
||||
ui.checkbox(&mut s.downsample, tl!("Bicubic downsampling to"));
|
||||
ui.add_enabled(s.downsample, egui::DragValue::new(&mut s.target_ppi).range(9.0..=2400.0).suffix(" ppi"));
|
||||
ui.label("for images above");
|
||||
ui.label(tl!("for images above"));
|
||||
ui.add_enabled(s.downsample, egui::DragValue::new(&mut s.above_ppi).range(9.0..=2400.0).suffix(" ppi"));
|
||||
});
|
||||
s.above_ppi = s.above_ppi.max(s.target_ppi);
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Compression");
|
||||
ui.label(tl!("Compression"));
|
||||
let label = match s.compression {
|
||||
Compression::Jpeg(_) => "JPEG",
|
||||
Compression::Flate => "ZIP",
|
||||
Compression::Retain => "Retain existing",
|
||||
Compression::Flate => tl!("ZIP"),
|
||||
Compression::Retain => tl!("Retain existing"),
|
||||
};
|
||||
egui::ComboBox::from_id_salt((id, "compression")).selected_text(label).show_ui(ui, |ui| {
|
||||
let q = match s.compression {
|
||||
|
|
@ -54,15 +54,15 @@ fn image_row(ui: &mut egui::Ui, id: &str, title: &str, s: &mut ImageSettings) {
|
|||
_ => 60,
|
||||
};
|
||||
ui.selectable_value(&mut s.compression, Compression::Jpeg(q), "JPEG");
|
||||
ui.selectable_value(&mut s.compression, Compression::Flate, "ZIP");
|
||||
ui.selectable_value(&mut s.compression, Compression::Retain, "Retain existing");
|
||||
ui.selectable_value(&mut s.compression, Compression::Flate, tl!("ZIP"));
|
||||
ui.selectable_value(&mut s.compression, Compression::Retain, tl!("Retain existing"));
|
||||
});
|
||||
if let Compression::Jpeg(q) = &mut s.compression {
|
||||
ui.label("Quality");
|
||||
let name = QUALITIES.iter().min_by_key(|(_, v)| (*v as i32 - *q as i32).abs()).map_or("Medium", |(n, _)| n);
|
||||
ui.label(tl!("Quality"));
|
||||
let name = QUALITIES.iter().min_by_key(|(_, v)| (*v as i32 - *q as i32).abs()).map_or(tl!("Medium"), |(n, _)| tl!(n));
|
||||
egui::ComboBox::from_id_salt((id, "quality")).selected_text(name).show_ui(ui, |ui| {
|
||||
for (n, v) in QUALITIES {
|
||||
ui.selectable_value(q, v, n);
|
||||
ui.selectable_value(q, v, tl!(n));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -72,7 +72,7 @@ fn image_row(ui: &mut egui::Ui, id: &str, title: &str, s: &mut ImageSettings) {
|
|||
|
||||
fn discard_box(ui: &mut egui::Ui, list: &mut Vec<Hidden>, h: Hidden, label: &str) {
|
||||
let mut on = list.contains(&h);
|
||||
if ui.checkbox(&mut on, label).changed() {
|
||||
if ui.checkbox(&mut on, tl!(label)).changed() {
|
||||
if on {
|
||||
list.push(h);
|
||||
} else {
|
||||
|
|
@ -83,14 +83,14 @@ fn discard_box(ui: &mut egui::Ui, list: &mut Vec<Hidden>, h: Hidden, label: &str
|
|||
|
||||
/// Draw the dialog; returns (ok, cancel).
|
||||
pub(crate) fn body(ui: &mut egui::Ui, d: &mut OptimizeDraft, t: &Tokens) -> (bool, bool) {
|
||||
ui.label(egui::RichText::new("PDF Optimizer").font(theme::semibold(18.0)));
|
||||
ui.label(egui::RichText::new(tl!("PDF Optimizer")).font(theme::semibold(18.0)));
|
||||
ui.add_space(6.0);
|
||||
ui.horizontal(|ui| {
|
||||
for (tab, label) in [
|
||||
(OptimizeTab::Images, "Images"),
|
||||
(OptimizeTab::DiscardObjects, "Discard Objects"),
|
||||
(OptimizeTab::DiscardUserData, "Discard User Data"),
|
||||
(OptimizeTab::CleanUp, "Clean Up"),
|
||||
(OptimizeTab::Images, tl!("Images")),
|
||||
(OptimizeTab::DiscardObjects, tl!("Discard Objects")),
|
||||
(OptimizeTab::DiscardUserData, tl!("Discard User Data")),
|
||||
(OptimizeTab::CleanUp, tl!("Clean Up")),
|
||||
] {
|
||||
if widgets::mode_tab(ui, label, d.tab == tab).clicked() {
|
||||
d.tab = tab;
|
||||
|
|
@ -105,17 +105,17 @@ pub(crate) fn body(ui: &mut egui::Ui, d: &mut OptimizeDraft, t: &Tokens) -> (boo
|
|||
image_row(ui, "color", "Color Images", &mut s.color);
|
||||
image_row(ui, "gray", "Grayscale Images", &mut s.gray);
|
||||
ui.label(
|
||||
egui::RichText::new("Each image is measured where pages draw it; an image is replaced only if the result is smaller.")
|
||||
egui::RichText::new(tl!("Each image is measured where pages draw it; an image is replaced only if the result is smaller."))
|
||||
.small()
|
||||
.color(t.text_muted),
|
||||
);
|
||||
}
|
||||
OptimizeTab::DiscardObjects => {
|
||||
discard_box(ui, &mut d.discard, Hidden::LinksActionsScripts, "Discard all links, actions and JavaScript");
|
||||
ui.checkbox(&mut s.discard_alternate_images, "Discard alternate images");
|
||||
ui.checkbox(&mut s.discard_thumbnails, "Discard embedded page thumbnails");
|
||||
ui.checkbox(&mut s.discard_tags, "Discard document tags");
|
||||
ui.checkbox(&mut s.discard_print_settings, "Discard embedded print settings");
|
||||
ui.checkbox(&mut s.discard_alternate_images, tl!("Discard alternate images"));
|
||||
ui.checkbox(&mut s.discard_thumbnails, tl!("Discard embedded page thumbnails"));
|
||||
ui.checkbox(&mut s.discard_tags, tl!("Discard document tags"));
|
||||
ui.checkbox(&mut s.discard_print_settings, tl!("Discard embedded print settings"));
|
||||
discard_box(ui, &mut d.discard, Hidden::Bookmarks, "Discard bookmarks");
|
||||
discard_box(ui, &mut d.discard, Hidden::FormFields, "Flatten form fields");
|
||||
discard_box(ui, &mut d.discard, Hidden::HiddenLayers, "Discard hidden layer content");
|
||||
|
|
@ -128,24 +128,24 @@ pub(crate) fn body(ui: &mut egui::Ui, d: &mut OptimizeDraft, t: &Tokens) -> (boo
|
|||
discard_box(ui, &mut d.discard, Hidden::HiddenText, "Discard hidden text");
|
||||
}
|
||||
OptimizeTab::CleanUp => {
|
||||
ui.checkbox(&mut s.flate_unencoded, "Use Flate to encode streams that are not encoded");
|
||||
ui.checkbox(&mut s.remove_invalid_links, "Remove invalid links and bookmarks");
|
||||
ui.checkbox(&mut s.remove_unreferenced_dests, "Remove unreferenced named destinations");
|
||||
ui.add_enabled(false, egui::Checkbox::new(&mut true, "Compress document structure (object streams)"));
|
||||
ui.add_enabled(false, egui::Checkbox::new(&mut true, "Remove unused objects and merge identical ones"));
|
||||
ui.checkbox(&mut s.flate_unencoded, tl!("Use Flate to encode streams that are not encoded"));
|
||||
ui.checkbox(&mut s.remove_invalid_links, tl!("Remove invalid links and bookmarks"));
|
||||
ui.checkbox(&mut s.remove_unreferenced_dests, tl!("Remove unreferenced named destinations"));
|
||||
ui.add_enabled(false, egui::Checkbox::new(&mut true, tl!("Compress document structure (object streams)")));
|
||||
ui.add_enabled(false, egui::Checkbox::new(&mut true, tl!("Remove unused objects and merge identical ones")));
|
||||
}
|
||||
}
|
||||
ui.add_space(12.0);
|
||||
let (mut ok, mut cancel) = (false, false);
|
||||
ui.horizontal(|ui| {
|
||||
if widgets::pill_button(ui, "Audit space usage…", false).clicked() {
|
||||
if widgets::pill_button(ui, tl!("Audit space usage…"), false).clicked() {
|
||||
d.audit = true;
|
||||
}
|
||||
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
|
||||
if widgets::pill_button(ui, "OK", true).clicked() {
|
||||
if widgets::pill_button(ui, tl!("OK"), true).clicked() {
|
||||
ok = true;
|
||||
}
|
||||
if widgets::pill_button(ui, "Cancel", false).clicked() {
|
||||
if widgets::pill_button(ui, tl!("Cancel"), false).clicked() {
|
||||
cancel = true;
|
||||
}
|
||||
});
|
||||
|
|
@ -155,28 +155,35 @@ pub(crate) fn body(ui: &mut egui::Ui, d: &mut OptimizeDraft, t: &Tokens) -> (boo
|
|||
|
||||
/// Audit Space Usage: bytes and share of the file per kind of content.
|
||||
pub(crate) fn audit_body(ui: &mut egui::Ui, rows: &[pdfcraft_engine::optimize::SpaceUse], t: &Tokens) -> bool {
|
||||
ui.label(egui::RichText::new("Space Audit").font(crate::theme::semibold(18.0)));
|
||||
ui.label(egui::RichText::new(tl!("Space Audit")).font(crate::theme::semibold(18.0)));
|
||||
ui.add_space(8.0);
|
||||
egui::Grid::new("space-audit").num_columns(3).striped(true).spacing([24.0, 4.0]).show(ui, |ui| {
|
||||
for h in ["Description", "Bytes", "Percentage"] {
|
||||
for h in [tl!("Description"), tl!("Bytes"), tl!("Percentage")] {
|
||||
ui.label(egui::RichText::new(h).color(t.text_muted));
|
||||
}
|
||||
ui.end_row();
|
||||
let total: u64 = rows.iter().map(|r| r.bytes).sum();
|
||||
for r in rows.iter().filter(|r| r.bytes > 0) {
|
||||
ui.label(r.category.label());
|
||||
// "Patterns" also names the redaction search patterns ("模式"); the audit's PDF
|
||||
// graphics objects need their own key.
|
||||
let category = if r.category == pdfcraft_engine::optimize::SpaceCategory::Patterns {
|
||||
tl!("Patterns (graphics objects)").to_string()
|
||||
} else {
|
||||
tl!(r.category.label()).to_string()
|
||||
};
|
||||
ui.label(category);
|
||||
ui.with_layout(Layout::right_to_left(Align::Center), |ui| ui.label(r.bytes.to_string()));
|
||||
ui.with_layout(Layout::right_to_left(Align::Center), |ui| ui.label(format!("{:.2}%", r.percent)));
|
||||
ui.end_row();
|
||||
}
|
||||
ui.label(egui::RichText::new("Total").strong());
|
||||
ui.label(egui::RichText::new(tl!("Total")).strong());
|
||||
ui.with_layout(Layout::right_to_left(Align::Center), |ui| ui.label(egui::RichText::new(total.to_string()).strong()));
|
||||
ui.with_layout(Layout::right_to_left(Align::Center), |ui| ui.label(egui::RichText::new("100.00%").strong()));
|
||||
ui.end_row();
|
||||
});
|
||||
ui.add_space(12.0);
|
||||
let mut ok = false;
|
||||
ui.horizontal(|ui| ui.with_layout(Layout::right_to_left(Align::Center), |ui| ok = widgets::pill_button(ui, "OK", true).clicked()));
|
||||
ui.horizontal(|ui| ui.with_layout(Layout::right_to_left(Align::Center), |ui| ok = widgets::pill_button(ui, tl!("OK"), true).clicked()));
|
||||
ok
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -106,12 +106,12 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> (boo
|
|||
};
|
||||
d.seeded = Some((current, d.which));
|
||||
}
|
||||
ui.label(egui::RichText::new("Set Page Boxes").font(theme::semibold(18.0)));
|
||||
ui.label(egui::RichText::new(tl!("Set Page Boxes")).font(theme::semibold(18.0)));
|
||||
ui.add_space(8.0);
|
||||
ui.horizontal_top(|ui| {
|
||||
ui.vertical(|ui| {
|
||||
egui::Grid::new("boxes-grid").num_columns(2).spacing([12.0, 8.0]).show(ui, |ui| {
|
||||
ui.label("Box");
|
||||
ui.label(tl!("Box"));
|
||||
let names = [
|
||||
(PageBox::Crop, "CropBox"),
|
||||
(PageBox::Trim, "TrimBox"),
|
||||
|
|
@ -126,7 +126,7 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> (boo
|
|||
}
|
||||
});
|
||||
ui.end_row();
|
||||
ui.label("Units");
|
||||
ui.label(tl!("Units"));
|
||||
egui::ComboBox::from_id_salt("boxes-unit").selected_text(d.unit.label()).show_ui(ui, |ui| {
|
||||
for u in [Unit::Inches, Unit::Millimetres, Unit::Points] {
|
||||
ui.selectable_value(&mut d.unit, u, u.label());
|
||||
|
|
@ -134,7 +134,7 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> (boo
|
|||
});
|
||||
ui.end_row();
|
||||
let k = d.unit.per_point();
|
||||
for (label, idx) in [("Top", 3), ("Bottom", 1), ("Left", 0), ("Right", 2)] {
|
||||
for (label, idx) in [(tl!("Top"), 3), (tl!("Bottom"), 1), (tl!("Left"), 0), (tl!("Right"), 2)] {
|
||||
let l = ui.label(label);
|
||||
let mut v = d.margins[idx] * k;
|
||||
let speed = if d.unit == Unit::Inches { 0.01 } else { 0.5 };
|
||||
|
|
@ -147,25 +147,25 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> (boo
|
|||
ui.end_row();
|
||||
}
|
||||
ui.label("");
|
||||
if ui.button("Set to zero").clicked() {
|
||||
if ui.button(tl!("Set to zero")).clicked() {
|
||||
d.margins = [0.0; 4];
|
||||
}
|
||||
ui.end_row();
|
||||
ui.label("Pages");
|
||||
ui.label(tl!("Pages"));
|
||||
ui.vertical(|ui| {
|
||||
ui.radio_value(&mut d.range, Range::All, "All");
|
||||
ui.radio_value(&mut d.range, Range::Current, format!("Current page ({})", current + 1));
|
||||
ui.radio_value(&mut d.range, Range::All, tl!("All"));
|
||||
ui.radio_value(&mut d.range, Range::Current, crate::i18n::fmt(tl!("Current page ({n})"), &[("n", &(current + 1).to_string())]));
|
||||
let (mut a, mut b) = match d.range {
|
||||
Range::Pages(a, b) => (a, b),
|
||||
_ => (1, count),
|
||||
};
|
||||
ui.horizontal(|ui| {
|
||||
let on = matches!(d.range, Range::Pages(..));
|
||||
if ui.radio(on, "From").clicked() {
|
||||
if ui.radio(on, tl!("From")).clicked() {
|
||||
d.range = Range::Pages(a, b);
|
||||
}
|
||||
let ra = ui.add(egui::DragValue::new(&mut a).range(1..=count));
|
||||
ui.label("to");
|
||||
ui.label(tl!("to"));
|
||||
let rb = ui.add(egui::DragValue::new(&mut b).range(1..=count));
|
||||
if ra.changed() || rb.changed() {
|
||||
d.range = Range::Pages(a.min(b), b.max(a));
|
||||
|
|
@ -202,10 +202,10 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> (boo
|
|||
ui.add_space(12.0);
|
||||
let (mut apply, mut cancel) = (false, false);
|
||||
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
|
||||
if widgets::pill_button(ui, "OK", true).clicked() {
|
||||
if widgets::pill_button(ui, tl!("OK"), true).clicked() {
|
||||
apply = true;
|
||||
}
|
||||
if widgets::pill_button(ui, "Cancel", false).clicked() {
|
||||
if widgets::pill_button(ui, tl!("Cancel"), false).clicked() {
|
||||
cancel = true;
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -45,13 +45,14 @@ pub fn show(app: &mut PdfCraftApp, ctx: &egui::Context) {
|
|||
let active = app.active_ids().map(|(_, id)| id);
|
||||
for spec in pdfcraft_engine::commands::COMMANDS {
|
||||
let label = pdfcraft_engine::commands::current_label(spec, &app.session, active);
|
||||
if let Some(s) = score(&label, &q).or_else(|| score(spec.id, &q).map(|s| s + 50)) {
|
||||
let translated = crate::i18n::command_label(&label);
|
||||
if let Some(s) = score(&translated, &q).or_else(|| score(&label, &q)).or_else(|| score(spec.id, &q).map(|s| s + 50)) {
|
||||
hits.push((
|
||||
s,
|
||||
Hit {
|
||||
group: None,
|
||||
label,
|
||||
detail: spec.shortcut.map(|k| k.label(mac)).unwrap_or_else(|| spec.menu.unwrap_or("Command").to_string()),
|
||||
label: translated,
|
||||
detail: spec.shortcut.map(|k| k.label(mac)).unwrap_or_else(|| tl!(spec.menu.unwrap_or("Command")).to_string()),
|
||||
icon: spec.icon,
|
||||
command: Some(spec.id),
|
||||
ready: app.command_enabled(spec),
|
||||
|
|
@ -60,13 +61,13 @@ pub fn show(app: &mut PdfCraftApp, ctx: &egui::Context) {
|
|||
}
|
||||
}
|
||||
for g in TOOL_GROUPS {
|
||||
if let Some(s) = score(g.label, &q) {
|
||||
if let Some(s) = score(tl!(g.label), &q).or_else(|| score(g.label, &q)) {
|
||||
hits.push((
|
||||
s,
|
||||
Hit {
|
||||
group: Some(g.id),
|
||||
label: g.label.to_string(),
|
||||
detail: "Tool".into(),
|
||||
label: tl!(g.label).to_string(),
|
||||
detail: tl!("Tool").into(),
|
||||
icon: g.icon,
|
||||
command: None,
|
||||
ready: g.availability == Availability::Ready,
|
||||
|
|
@ -78,13 +79,13 @@ pub fn show(app: &mut PdfCraftApp, ctx: &egui::Context) {
|
|||
if pdfcraft_engine::commands::command(i.command).is_some() {
|
||||
continue; // listed above as a command
|
||||
}
|
||||
if let Some(s) = score(i.label, &q).or_else(|| score(i.command, &q).map(|s| s + 50)) {
|
||||
if let Some(s) = score(tl!(i.label), &q).or_else(|| score(i.label, &q)).or_else(|| score(i.command, &q).map(|s| s + 50)) {
|
||||
hits.push((
|
||||
s + 1,
|
||||
Hit {
|
||||
group: Some(g.id),
|
||||
label: i.label.to_string(),
|
||||
detail: g.label.into(),
|
||||
label: tl!(i.label).to_string(),
|
||||
detail: tl!(g.label).into(),
|
||||
icon: i.icon,
|
||||
command: Some(i.command),
|
||||
ready: i.availability == Availability::Ready,
|
||||
|
|
@ -110,7 +111,7 @@ pub fn show(app: &mut PdfCraftApp, ctx: &egui::Context) {
|
|||
ui.add(icons::image("search", 18.0, t.text_muted));
|
||||
let r = ui.add(
|
||||
egui::TextEdit::singleline(&mut app.palette_query)
|
||||
.hint_text("Search tools and commands…")
|
||||
.hint_text(tl!("Search tools and commands…"))
|
||||
.frame(egui::Frame::NONE)
|
||||
.font(theme::regular(15.0))
|
||||
.desired_width(f32::INFINITY),
|
||||
|
|
@ -132,16 +133,19 @@ pub fn show(app: &mut PdfCraftApp, ctx: &egui::Context) {
|
|||
ui.painter().rect_filled(rect, CornerRadius::same(6), t.hover);
|
||||
}
|
||||
icons::paint(ui, Rect::from_min_size(rect.min + vec2(8.0, 9.0), vec2(18.0, 18.0)), h.icon, 17.0, t.icon);
|
||||
resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, h.ready, &h.label));
|
||||
// Display is translated; matching above already considered both languages.
|
||||
let shown = crate::i18n::command_label(&h.label);
|
||||
let shown_detail = tl!(&h.detail).to_string();
|
||||
resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, h.ready, &shown));
|
||||
let fg = if h.ready { t.text } else { t.text_faint };
|
||||
ui.painter().text(rect.left_center() + vec2(36.0, 0.0), Align2::LEFT_CENTER, &h.label, theme::regular(13.5), fg);
|
||||
ui.painter().text(rect.right_center() - vec2(10.0, 0.0), Align2::RIGHT_CENTER, &h.detail, theme::regular(12.0), t.text_faint);
|
||||
ui.painter().text(rect.left_center() + vec2(36.0, 0.0), Align2::LEFT_CENTER, shown, theme::regular(13.5), fg);
|
||||
ui.painter().text(rect.right_center() - vec2(10.0, 0.0), Align2::RIGHT_CENTER, shown_detail, theme::regular(12.0), t.text_faint);
|
||||
if resp.clicked() {
|
||||
chosen = Some((h.command, h.group));
|
||||
}
|
||||
}
|
||||
if hits.is_empty() {
|
||||
ui.label(egui::RichText::new("No matching tools").color(t.text_muted));
|
||||
ui.label(egui::RichText::new(tl!("No matching tools")).color(t.text_muted));
|
||||
}
|
||||
ui.add_space(2.0);
|
||||
let _ = Stroke::NONE;
|
||||
|
|
|
|||
|
|
@ -38,12 +38,12 @@ fn panel_header(ui: &mut egui::Ui, t: &Tokens, title: &str, back: bool) -> (bool
|
|||
let mut go_back = false;
|
||||
let mut close = false;
|
||||
ui.horizontal(|ui| {
|
||||
if back && icons::button(ui, "chevron-left", 26.0, false, "Back to all tools").clicked() {
|
||||
if back && icons::button(ui, "chevron-left", 26.0, false, tl!("Back to all tools")).clicked() {
|
||||
go_back = true;
|
||||
}
|
||||
ui.label(egui::RichText::new(title).font(theme::semibold(15.5)).color(t.text));
|
||||
ui.label(egui::RichText::new(tl!(title)).font(theme::semibold(15.5)).color(t.text));
|
||||
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
|
||||
if icons::button(ui, "x", 26.0, false, "Close panel").clicked() {
|
||||
if icons::button(ui, "x", 26.0, false, tl!("Close panel")).clicked() {
|
||||
close = true;
|
||||
}
|
||||
});
|
||||
|
|
@ -66,7 +66,7 @@ fn all_tools(app: &mut PdfCraftApp, ui: &mut egui::Ui, t: &Tokens) {
|
|||
}
|
||||
ui.add_space(4.0);
|
||||
let more = if app.all_tools_expanded { "View less" } else { "View more" };
|
||||
if ui.add(egui::Label::new(egui::RichText::new(more).color(t.accent_text).font(theme::medium(13.0))).sense(Sense::click())).clicked() {
|
||||
if ui.add(egui::Label::new(egui::RichText::new(tl!(more)).color(t.accent_text).font(theme::medium(13.0))).sense(Sense::click())).clicked() {
|
||||
app.all_tools_expanded = !app.all_tools_expanded;
|
||||
}
|
||||
});
|
||||
|
|
@ -74,26 +74,27 @@ fn all_tools(app: &mut PdfCraftApp, ui: &mut egui::Ui, t: &Tokens) {
|
|||
|
||||
fn tool_row(ui: &mut egui::Ui, t: &Tokens, g: &ToolGroup) -> egui::Response {
|
||||
let (rect, resp) = ui.allocate_exact_size(vec2(ui.available_width(), 34.0), Sense::click());
|
||||
resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, true, g.label));
|
||||
resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, true, tl!(g.label)));
|
||||
if resp.hovered() {
|
||||
ui.painter().rect_filled(rect, CornerRadius::same(6), t.hover);
|
||||
}
|
||||
icons::paint(ui, Rect::from_min_size(rect.min + vec2(6.0, 7.0), vec2(20.0, 20.0)), g.icon, 19.0, hue(g));
|
||||
ui.painter().text(rect.left_center() + vec2(36.0, 0.0), Align2::LEFT_CENTER, g.label, theme::regular(13.5), t.text);
|
||||
ui.painter().text(rect.left_center() + vec2(36.0, 0.0), Align2::LEFT_CENTER, tl!(g.label), theme::regular(13.5), t.text);
|
||||
match (g.badge, g.availability) {
|
||||
(Some(b), _) => {
|
||||
let font = theme::semibold(9.5);
|
||||
let w = ui.fonts_mut(|f| f.layout_no_wrap(b.to_string(), font.clone(), Color32::WHITE).size().x);
|
||||
let badge = tl!(b);
|
||||
let w = ui.fonts_mut(|f| f.layout_no_wrap(badge.to_string(), font.clone(), Color32::WHITE).size().x);
|
||||
let r = Rect::from_center_size(rect.right_center() - vec2(w / 2.0 + 10.0, 0.0), vec2(w + 10.0, 16.0));
|
||||
ui.painter().rect_filled(r, CornerRadius::same(4), t.badge_new);
|
||||
ui.painter().text(r.center(), Align2::CENTER_CENTER, b, font, Color32::WHITE);
|
||||
ui.painter().text(r.center(), Align2::CENTER_CENTER, badge, font, Color32::WHITE);
|
||||
}
|
||||
// Planned tools: a quiet milestone hint instead of a chip, so the list stays calm.
|
||||
(None, Availability::Planned(m)) if resp.hovered() => {
|
||||
ui.painter().text(
|
||||
rect.right_center() - vec2(10.0, 0.0),
|
||||
Align2::RIGHT_CENTER,
|
||||
format!("Planned · {m}"),
|
||||
format!("{} · {m}", tl!("Planned")),
|
||||
theme::medium(10.5),
|
||||
t.text_faint,
|
||||
);
|
||||
|
|
@ -101,9 +102,9 @@ fn tool_row(ui: &mut egui::Ui, t: &Tokens, g: &ToolGroup) -> egui::Response {
|
|||
_ => {}
|
||||
}
|
||||
let tip = match g.availability {
|
||||
Availability::Ready => "Available".to_string(),
|
||||
Availability::Planned(m) => format!("Planned for milestone {m} — open to see what it will include"),
|
||||
Availability::Provider => "Optional: needs an AI provider you configure".into(),
|
||||
Availability::Ready => tl!("Available").to_string(),
|
||||
Availability::Planned(m) => format!("{} {m} — {}", tl!("Planned for milestone"), tl!("open to see what it will include")),
|
||||
Availability::Provider => tl!("Optional: needs an AI provider you configure").into(),
|
||||
};
|
||||
resp.on_hover_text(tip)
|
||||
}
|
||||
|
|
@ -121,7 +122,9 @@ fn tool_detail(app: &mut PdfCraftApp, ui: &mut egui::Ui, t: &Tokens, g: &'static
|
|||
egui::Frame::NONE.fill(t.accent_soft).corner_radius(CornerRadius::same(8)).inner_margin(egui::Margin::same(10)).show(ui, |ui| {
|
||||
ui.set_width(ui.available_width());
|
||||
ui.label(
|
||||
egui::RichText::new(format!("Coming in milestone {m}. Items marked Ready work today.")).color(t.text).font(theme::regular(12.0)),
|
||||
egui::RichText::new(format!("{} {m}. {}", tl!("Coming in milestone"), tl!("Items marked Ready work today.")))
|
||||
.color(t.text)
|
||||
.font(theme::regular(12.0)),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
@ -134,7 +137,8 @@ fn tool_detail(app: &mut PdfCraftApp, ui: &mut egui::Ui, t: &Tokens, g: &'static
|
|||
if g.id == "form" {
|
||||
ui.horizontal(|ui| {
|
||||
let label = if app.form_preview { "Edit fields" } else { "Preview" };
|
||||
if widgets::pill_button(ui, label, app.form_preview).on_hover_text("Try the form as people filling it in will see it").clicked() {
|
||||
if widgets::pill_button(ui, tl!(label), app.form_preview).on_hover_text(tl!("Try the form as people filling it in will see it")).clicked()
|
||||
{
|
||||
app.form_preview = !app.form_preview;
|
||||
if app.form_preview {
|
||||
app.quick_tool = crate::QuickTool::Select;
|
||||
|
|
@ -154,13 +158,13 @@ fn tool_detail(app: &mut PdfCraftApp, ui: &mut egui::Ui, t: &Tokens, g: &'static
|
|||
let list_h = if footer { (ui.available_height() - 52.0).max(80.0) } else { ui.available_height() };
|
||||
egui::ScrollArea::vertical().auto_shrink([false, false]).max_height(list_h).show(ui, |ui| {
|
||||
for s in g.sections {
|
||||
widgets::section_title(ui, s.title);
|
||||
widgets::section_title(ui, tl!(s.title));
|
||||
for item in s.items {
|
||||
if g.id == "fill_sign" && (item.command.starts_with("sign.fill.signature") || item.command.starts_with("sign.fill.initials")) {
|
||||
continue;
|
||||
}
|
||||
let (rect, resp) = ui.allocate_exact_size(vec2(ui.available_width(), 34.0), Sense::click());
|
||||
resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, true, item.label));
|
||||
resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, true, tl!(item.label)));
|
||||
let ready = item.availability == Availability::Ready;
|
||||
if resp.hovered() {
|
||||
ui.painter().rect_filled(rect, CornerRadius::same(6), t.hover);
|
||||
|
|
@ -173,24 +177,24 @@ fn tool_detail(app: &mut PdfCraftApp, ui: &mut egui::Ui, t: &Tokens, g: &'static
|
|||
17.0,
|
||||
if ready { hue(g) } else { t.text_faint },
|
||||
);
|
||||
ui.painter().text(rect.left_center() + vec2(34.0, 0.0), Align2::LEFT_CENTER, item.label, theme::regular(13.0), fg);
|
||||
ui.painter().text(rect.left_center() + vec2(34.0, 0.0), Align2::LEFT_CENTER, tl!(item.label), theme::regular(13.0), fg);
|
||||
let (chip, fill, cfg) = match item.availability {
|
||||
Availability::Ready => ("Ready", Color32::from_rgb(0xDD, 0xF3, 0xE4), Color32::from_rgb(0x1E, 0x7B, 0x43)),
|
||||
Availability::Ready => (tl!("Ready"), Color32::from_rgb(0xDD, 0xF3, 0xE4), Color32::from_rgb(0x1E, 0x7B, 0x43)),
|
||||
Availability::Planned(m) => (m, t.pressed, t.text_muted),
|
||||
Availability::Provider => ("AI", t.pressed, t.text_muted),
|
||||
};
|
||||
let font = theme::semibold(9.5);
|
||||
let w = ui.fonts_mut(|f| f.layout_no_wrap(chip.to_string(), font.clone(), cfg).size().x);
|
||||
let w = ui.fonts_mut(|f| f.layout_no_wrap(tl!(chip).to_string(), font.clone(), cfg).size().x);
|
||||
let r = Rect::from_center_size(rect.right_center() - vec2(w / 2.0 + 10.0, 0.0), vec2(w + 10.0, 16.0));
|
||||
ui.painter().rect_filled(r, CornerRadius::same(4), fill);
|
||||
ui.painter().text(r.center(), Align2::CENTER_CENTER, chip, font, cfg);
|
||||
ui.painter().text(r.center(), Align2::CENTER_CENTER, tl!(chip), font, cfg);
|
||||
if resp.on_hover_text(item.command).clicked() {
|
||||
run = Some(item.command);
|
||||
}
|
||||
}
|
||||
}
|
||||
if g.id == "fill_sign" {
|
||||
widgets::section_title(ui, "Sign yourself");
|
||||
widgets::section_title(ui, tl!("Sign yourself"));
|
||||
if let Some(cmd) = crate::fill_sign::signature_entries(ui, app, t) {
|
||||
run = Some(cmd);
|
||||
}
|
||||
|
|
@ -200,13 +204,20 @@ fn tool_detail(app: &mut PdfCraftApp, ui: &mut egui::Ui, t: &Tokens, g: &'static
|
|||
ui.add_space(8.0);
|
||||
ui.horizontal(|ui| {
|
||||
if marks > 0 {
|
||||
ui.label(egui::RichText::new(format!("{marks} mark{}", if marks == 1 { "" } else { "s" })).color(t.text_muted));
|
||||
ui.label(
|
||||
egui::RichText::new(if marks == 1 {
|
||||
crate::i18n::fmt(tl!("1 mark"), &[])
|
||||
} else {
|
||||
crate::i18n::fmt(tl!("{n} marks"), &[("n", &marks.to_string())])
|
||||
})
|
||||
.color(t.text_muted),
|
||||
);
|
||||
}
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
if ui.add_enabled_ui(marks > 0, |ui| widgets::pill_button(ui, "Redact all", true)).inner.clicked() {
|
||||
if ui.add_enabled_ui(marks > 0, |ui| widgets::pill_button(ui, tl!("Redact all"), true)).inner.clicked() {
|
||||
run = Some("redact.apply");
|
||||
}
|
||||
if ui.add_enabled_ui(marks > 0, |ui| widgets::pill_button(ui, "Clear all", false)).inner.clicked() {
|
||||
if ui.add_enabled_ui(marks > 0, |ui| widgets::pill_button(ui, tl!("Clear all"), false)).inner.clicked() {
|
||||
run = Some("redact.clear");
|
||||
}
|
||||
});
|
||||
|
|
@ -221,11 +232,13 @@ fn tool_detail(app: &mut PdfCraftApp, ui: &mut egui::Ui, t: &Tokens, g: &'static
|
|||
/// page to place it.
|
||||
fn stamp_palette(app: &mut PdfCraftApp, ui: &mut egui::Ui, t: &Tokens) {
|
||||
use pdfcraft_engine::{StampGroup, StampKind};
|
||||
ui.label(egui::RichText::new("Choose a stamp, then click on the page to place it.").small().color(t.text_faint));
|
||||
ui.label(egui::RichText::new(tl!("Choose a stamp, then click on the page to place it.")).small().color(t.text_faint));
|
||||
egui::ScrollArea::vertical().auto_shrink([false, false]).show(ui, |ui| {
|
||||
for (group, title) in
|
||||
[(StampGroup::Dynamic, "Dynamic"), (StampGroup::SignHere, "Sign Here"), (StampGroup::StandardBusiness, "Standard Business")]
|
||||
{
|
||||
for (group, title) in [
|
||||
(StampGroup::Dynamic, tl!("Dynamic")),
|
||||
(StampGroup::SignHere, tl!("Sign Here")),
|
||||
(StampGroup::StandardBusiness, tl!("Standard Business")),
|
||||
] {
|
||||
widgets::section_title(ui, title);
|
||||
for kind in StampKind::ALL.into_iter().filter(|k| k.group() == group) {
|
||||
let active = app.quick_tool == crate::QuickTool::Stamp(kind);
|
||||
|
|
@ -416,12 +429,12 @@ pub fn right_panel(app: &mut PdfCraftApp, ui: &mut egui::Ui) {
|
|||
RightPanel::Compare => ("Compare", compare.as_ref().filter(|c| c.new == id).map(|c| c.result.changes.len())),
|
||||
};
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(egui::RichText::new(title).font(theme::semibold(15.5)));
|
||||
ui.label(egui::RichText::new(tl!(title)).font(theme::semibold(15.5)));
|
||||
if let Some(c) = count {
|
||||
ui.label(egui::RichText::new(c.to_string()).font(theme::medium(13.0)).color(t.text_faint));
|
||||
}
|
||||
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
|
||||
if icons::button(ui, "x", 26.0, false, "Close").clicked() {
|
||||
if icons::button(ui, "x", 26.0, false, tl!("Close")).clicked() {
|
||||
close = true;
|
||||
}
|
||||
// Right to left: close, "…", filter, search (Acrobat's order left to right).
|
||||
|
|
@ -429,7 +442,7 @@ pub fn right_panel(app: &mut PdfCraftApp, ui: &mut egui::Ui) {
|
|||
panel_command = crate::comments_panel::header_controls(ui, info, view, doc.comments_hidden());
|
||||
}
|
||||
if panel == RightPanel::Comments
|
||||
&& icons::button(ui, "search", 26.0, view.comments.search.is_some(), "Search comments").clicked()
|
||||
&& icons::button(ui, "search", 26.0, view.comments.search.is_some(), tl!("Search comments")).clicked()
|
||||
{
|
||||
view.comments.search = match view.comments.search {
|
||||
Some(_) => None,
|
||||
|
|
@ -438,12 +451,14 @@ pub fn right_panel(app: &mut PdfCraftApp, ui: &mut egui::Ui) {
|
|||
view.comments.search_focus = true;
|
||||
}
|
||||
if panel == RightPanel::Bookmarks && !info.outline.is_empty() {
|
||||
let more = icons::button(ui, "ellipsis", 26.0, false, "Bookmark options");
|
||||
let more = icons::button(ui, "ellipsis", 26.0, false, tl!("Bookmark options"));
|
||||
egui::Popup::menu(&more).show(|ui| {
|
||||
ui.set_min_width(200.0);
|
||||
for (levels, label) in
|
||||
[(usize::MAX, "Expand all bookmarks"), (1, "Expand top-level bookmarks"), (0, "Collapse all bookmarks")]
|
||||
{
|
||||
for (levels, label) in [
|
||||
(usize::MAX, tl!("Expand all bookmarks")),
|
||||
(1, tl!("Expand top-level bookmarks")),
|
||||
(0, tl!("Collapse all bookmarks")),
|
||||
] {
|
||||
if ui.button(label).clicked() {
|
||||
bm_expand = Some(levels);
|
||||
ui.close();
|
||||
|
|
@ -453,7 +468,7 @@ pub fn right_panel(app: &mut PdfCraftApp, ui: &mut egui::Ui) {
|
|||
}
|
||||
if panel == RightPanel::Bookmarks
|
||||
&& bm_editable
|
||||
&& icons::button(ui, "bookmark-plus", 26.0, false, "New bookmark (⌘B)").clicked()
|
||||
&& icons::button(ui, "bookmark-plus", 26.0, false, tl!("New bookmark (⌘B)")).clicked()
|
||||
{
|
||||
bm_action = Some(BmAction::New);
|
||||
}
|
||||
|
|
@ -503,7 +518,7 @@ pub fn right_panel(app: &mut PdfCraftApp, ui: &mut egui::Ui) {
|
|||
);
|
||||
let fg = if l.visible { t.text } else { t.text_faint };
|
||||
ui.painter().text(rect.left_center() + vec2(32.0, 0.0), Align2::LEFT_CENTER, &l.name, theme::regular(13.0), fg);
|
||||
if resp.on_hover_text(if l.visible { "Hide layer" } else { "Show layer" }).clicked() {
|
||||
if resp.on_hover_text(if l.visible { tl!("Hide layer") } else { tl!("Show layer") }).clicked() {
|
||||
toggle_layer = Some((li, !l.visible));
|
||||
}
|
||||
}
|
||||
|
|
@ -529,7 +544,11 @@ pub fn right_panel(app: &mut PdfCraftApp, ui: &mut egui::Ui) {
|
|||
ui.add(egui::Label::new(egui::RichText::new(&a.name).font(theme::medium(13.0))).truncate());
|
||||
let mut meta = a.size.map(human_size).unwrap_or_default();
|
||||
if let pdfcraft_render::AttachmentSource::Annotation { page, .. } = a.source {
|
||||
meta = format!("{meta} · on page {}", info.pages.get(page).map(|p| p.label.as_str()).unwrap_or("?"));
|
||||
let on_page = crate::i18n::fmt(
|
||||
tl!("on page {label}"),
|
||||
&[("label", info.pages.get(page).map(|p| p.label.as_str()).unwrap_or("?"))],
|
||||
);
|
||||
meta = format!("{meta} · {on_page}");
|
||||
}
|
||||
if let Some(d) = &a.description {
|
||||
meta = format!("{meta} · {d}");
|
||||
|
|
@ -537,11 +556,11 @@ pub fn right_panel(app: &mut PdfCraftApp, ui: &mut egui::Ui) {
|
|||
ui.add(egui::Label::new(egui::RichText::new(meta).color(t.text_faint).small()).truncate());
|
||||
});
|
||||
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
|
||||
if icons::button(ui, "file-down", 28.0, false, "Save attachment…").clicked() {
|
||||
if icons::button(ui, "file-down", 28.0, false, tl!("Save attachment…")).clicked() {
|
||||
attachment_action = Some((ai, false));
|
||||
}
|
||||
if a.name.to_lowercase().ends_with(".pdf")
|
||||
&& icons::button(ui, "file-input", 28.0, false, "Open in a new tab").clicked()
|
||||
&& icons::button(ui, "file-input", 28.0, false, tl!("Open in a new tab")).clicked()
|
||||
{
|
||||
attachment_action = Some((ai, true));
|
||||
}
|
||||
|
|
@ -621,7 +640,8 @@ fn empty(ui: &mut egui::Ui, t: &Tokens, icon: &str, text: &str) {
|
|||
ui.vertical_centered(|ui| {
|
||||
ui.add(icons::image(icon, 36.0, t.text_faint));
|
||||
ui.add_space(8.0);
|
||||
ui.label(egui::RichText::new(text).color(t.text_muted));
|
||||
// Every empty-panel message goes through here.
|
||||
ui.label(egui::RichText::new(tl!(text)).color(t.text_muted));
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -727,20 +747,21 @@ fn outline_item(ui: &mut egui::Ui, t: &Tokens, info: &DocInfo, item: &OutlineIte
|
|||
let current = cx.current;
|
||||
resp.context_menu(|ui| {
|
||||
let mut pick = |ui: &mut egui::Ui, label: &str, enabled: bool, a: BmAction| {
|
||||
if ui.add_enabled(enabled, egui::Button::new(label)).clicked() {
|
||||
if ui.add_enabled(enabled, egui::Button::new(tl!(label))).clicked() {
|
||||
*cx.action = Some(a);
|
||||
ui.close();
|
||||
}
|
||||
};
|
||||
pick(ui, "Rename", true, BmAction::StartRename(path.to_vec()));
|
||||
pick(ui, &format!("Set to current page ({})", current + 1), true, BmAction::SetToCurrentPage(path.to_vec()));
|
||||
pick(ui, tl!("Rename"), true, BmAction::StartRename(path.to_vec()));
|
||||
let set_page = crate::i18n::fmt(tl!("Set to current page ({n})"), &[("n", &(current + 1).to_string())]);
|
||||
pick(ui, &set_page, true, BmAction::SetToCurrentPage(path.to_vec()));
|
||||
ui.separator();
|
||||
pick(ui, "Move up", i > 0, BmAction::MoveUp(path.to_vec()));
|
||||
pick(ui, "Move down", i + 1 < siblings, BmAction::MoveDown(path.to_vec()));
|
||||
pick(ui, "Indent", i > 0, BmAction::Indent(path.to_vec()));
|
||||
pick(ui, "Outdent", depth > 0, BmAction::Outdent(path.to_vec()));
|
||||
pick(ui, tl!("Move up"), i > 0, BmAction::MoveUp(path.to_vec()));
|
||||
pick(ui, tl!("Move down"), i + 1 < siblings, BmAction::MoveDown(path.to_vec()));
|
||||
pick(ui, tl!("Indent"), i > 0, BmAction::Indent(path.to_vec()));
|
||||
pick(ui, tl!("Outdent"), depth > 0, BmAction::Outdent(path.to_vec()));
|
||||
ui.separator();
|
||||
pick(ui, "Delete", true, BmAction::Delete(path.to_vec()));
|
||||
pick(ui, tl!("Delete"), true, BmAction::Delete(path.to_vec()));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -759,7 +780,8 @@ fn pages(ui: &mut egui::Ui, t: &Tokens, info: &DocInfo, view: &crate::DocView, n
|
|||
ui.vertical_centered(|ui| {
|
||||
let h = w * p.height / p.width.max(1.0);
|
||||
let (rect, resp) = ui.allocate_exact_size(vec2(w + 16.0, h + 16.0), Sense::click());
|
||||
resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, true, format!("Page {}", p.label)));
|
||||
let info = crate::i18n::fmt(tl!("Page {label}"), &[("label", &p.label)]);
|
||||
resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, true, info.clone()));
|
||||
let selected = i == view.current;
|
||||
if selected {
|
||||
ui.painter().rect_filled(rect, CornerRadius::same(8), t.accent_soft);
|
||||
|
|
@ -805,13 +827,14 @@ fn fields(
|
|||
let mut ordered: Vec<_> = info.fields.iter().collect();
|
||||
ordered.sort_by_key(|f| (f.page, rank(&f.name)));
|
||||
if preparing {
|
||||
ui.label(egui::RichText::new("Tab order: move a field with its arrows.").small().color(t.text_muted));
|
||||
ui.label(egui::RichText::new(tl!("Tab order: move a field with its arrows.")).small().color(t.text_muted));
|
||||
}
|
||||
let mut pages: Vec<Option<usize>> = info.fields.iter().map(|f| f.page).collect();
|
||||
pages.sort();
|
||||
pages.dedup();
|
||||
for p in pages {
|
||||
let label = p.map(|p| format!("Page {}", info.pages[p].label)).unwrap_or_else(|| "Unplaced".into());
|
||||
let label =
|
||||
p.map(|p| crate::i18n::fmt(tl!("Page {label}"), &[("label", &info.pages[p].label)])).unwrap_or_else(|| tl!("Unplaced").to_string());
|
||||
ui.add_space(4.0);
|
||||
ui.label(egui::RichText::new(label).font(theme::semibold(12.5)).color(t.text_muted));
|
||||
for f in ordered.iter().copied().filter(|f| f.page == p) {
|
||||
|
|
@ -836,17 +859,19 @@ fn fields(
|
|||
let v: String = if v.chars().count() > 18 { format!("{}…", v.chars().take(17).collect::<String>()) } else { v.clone() };
|
||||
ui.painter().text(rect.right_center() - vec2(8.0, 0.0), Align2::RIGHT_CENTER, v, theme::regular(11.5), t.text_faint);
|
||||
}
|
||||
let mut tip = format!("{:?} field", f.kind);
|
||||
let kind = format!("{:?}", f.kind);
|
||||
let mut tip = crate::i18n::fmt(tl!("{kind} field"), &[("kind", tl!(&kind))]);
|
||||
if let Some(tt) = &f.tooltip {
|
||||
tip.push_str(&format!(" — {tt}"));
|
||||
}
|
||||
if f.has_actions {
|
||||
tip.push_str("\nHas JavaScript actions (run in M6)");
|
||||
tip.push_str(&format!("\n{}", tl!("Has JavaScript actions (run in M6)")));
|
||||
}
|
||||
if preparing && f.page.is_some() {
|
||||
let up = Rect::from_center_size(rect.right_center() - vec2(44.0, 0.0), vec2(22.0, 22.0));
|
||||
let down = Rect::from_center_size(rect.right_center() - vec2(20.0, 0.0), vec2(22.0, 22.0));
|
||||
for (r, icon, earlier, tip) in [(up, "chevron-up", true, "Earlier in tab order"), (down, "chevron-down", false, "Later in tab order")]
|
||||
for (r, icon, earlier, tip) in
|
||||
[(up, "chevron-up", true, tl!("Earlier in tab order")), (down, "chevron-down", false, tl!("Later in tab order"))]
|
||||
{
|
||||
let b = ui.interact(r, ui.id().with((&f.name, earlier)), Sense::click());
|
||||
b.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, true, format!("{tip}: {}", f.name)));
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@ impl PdfCraftApp {
|
|||
};
|
||||
if !started {
|
||||
self.pickers.showing.store(false, Ordering::SeqCst);
|
||||
self.notify("Couldn't show the file picker. Please try again.");
|
||||
self.notify_tr("Couldn't show the file picker. Please try again.");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -125,7 +125,7 @@ impl PdfCraftApp {
|
|||
continue;
|
||||
}
|
||||
if target.is_some() && self.active_ids().map(|(_, id)| id) != target {
|
||||
self.notify("The document changed while you were choosing a file, so nothing was added.");
|
||||
self.notify_tr("The document changed while you were choosing a file, so nothing was added.");
|
||||
continue;
|
||||
}
|
||||
match pick_for {
|
||||
|
|
|
|||
|
|
@ -812,11 +812,11 @@ fn flag_box(ui: &mut egui::Ui, flags: &mut u32, bit: u32, inverted: bool, label:
|
|||
pub(crate) fn body(ui: &mut egui::Ui, d: &mut FieldDraft, t: &crate::theme::Tokens) -> (bool, bool) {
|
||||
use crate::widgets;
|
||||
ui.set_width(600.0);
|
||||
ui.label(egui::RichText::new(d.title()).font(theme::semibold(18.0)));
|
||||
ui.label(egui::RichText::new(tl!(d.title())).font(theme::semibold(18.0)));
|
||||
ui.add_space(6.0);
|
||||
ui.horizontal(|ui| {
|
||||
for (tab, label) in d.tabs() {
|
||||
if widgets::mode_tab(ui, label, d.tab == tab).clicked() {
|
||||
if widgets::mode_tab(ui, tl!(label), d.tab == tab).clicked() {
|
||||
d.tab = tab;
|
||||
}
|
||||
}
|
||||
|
|
@ -827,7 +827,7 @@ pub(crate) fn body(ui: &mut egui::Ui, d: &mut FieldDraft, t: &crate::theme::Toke
|
|||
// A locked field keeps its properties until it is unlocked (Acrobat greys them out).
|
||||
let was_locked = d.original.as_ref().as_ref().is_some_and(|o| o.locked) && d.locked;
|
||||
if was_locked {
|
||||
ui.label(egui::RichText::new("This field is locked. Unlock it to change its properties.").small().color(t.text_muted));
|
||||
ui.label(egui::RichText::new(tl!("This field is locked. Unlock it to change its properties.")).small().color(t.text_muted));
|
||||
}
|
||||
ui.scope(|ui| {
|
||||
if was_locked {
|
||||
|
|
@ -836,39 +836,39 @@ pub(crate) fn body(ui: &mut egui::Ui, d: &mut FieldDraft, t: &crate::theme::Toke
|
|||
match d.tab {
|
||||
FieldTab::General => {
|
||||
egui::Grid::new("field-general").num_columns(2).spacing([12.0, 10.0]).show(ui, |ui| {
|
||||
let l = ui.label("Name:");
|
||||
let l = ui.label(tl!("Name:"));
|
||||
let r = ui.add(egui::TextEdit::singleline(&mut d.name).desired_width(340.0)).labelled_by(l.id);
|
||||
enter |= r.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter));
|
||||
ui.end_row();
|
||||
let l = ui.label("Tooltip:");
|
||||
let l = ui.label(tl!("Tooltip:"));
|
||||
ui.add(egui::TextEdit::singleline(&mut d.tooltip).desired_width(340.0)).labelled_by(l.id);
|
||||
ui.end_row();
|
||||
});
|
||||
ui.add_space(12.0);
|
||||
widgets::section_title(ui, "Common Properties");
|
||||
ui.checkbox(&mut d.read_only, "Read Only");
|
||||
ui.checkbox(&mut d.required, "Required");
|
||||
widgets::section_title(ui, tl!("Common Properties"));
|
||||
ui.checkbox(&mut d.read_only, tl!("Read Only"));
|
||||
ui.checkbox(&mut d.required, tl!("Required"));
|
||||
}
|
||||
FieldTab::Appearance => {
|
||||
egui::Grid::new("field-appearance").num_columns(2).spacing([12.0, 10.0]).show(ui, |ui| {
|
||||
ui.label("Font Size:");
|
||||
ui.label(tl!("Font Size:"));
|
||||
let mut auto = d.font_size == 0.0;
|
||||
ui.horizontal(|ui| {
|
||||
if ui.checkbox(&mut auto, "Auto").changed() {
|
||||
if ui.checkbox(&mut auto, tl!("Auto")).changed() {
|
||||
d.font_size = if auto { 0.0 } else { 12.0 };
|
||||
}
|
||||
ui.add_enabled(!auto, egui::DragValue::new(&mut d.font_size).range(2.0..=100.0).speed(0.25).suffix(" pt"));
|
||||
});
|
||||
ui.end_row();
|
||||
if let Some(l) = d.look.as_mut() {
|
||||
ui.label("Font:");
|
||||
ui.label(tl!("Font:"));
|
||||
egui::ComboBox::from_id_salt("field-font").selected_text(l.font.label()).show_ui(ui, |ui| {
|
||||
for f in FieldFont::ALL {
|
||||
ui.selectable_value(&mut l.font, f, f.label());
|
||||
}
|
||||
});
|
||||
ui.end_row();
|
||||
ui.label("Text Color:");
|
||||
ui.label(tl!("Text Color:"));
|
||||
if let Some(c) = crate::comments::swatch_grid(ui, Some(l.text)) {
|
||||
l.text = c;
|
||||
}
|
||||
|
|
@ -876,13 +876,13 @@ pub(crate) fn body(ui: &mut egui::Ui, d: &mut FieldDraft, t: &crate::theme::Toke
|
|||
}
|
||||
});
|
||||
if let Some(l) = d.look.as_mut() {
|
||||
widgets::section_title(ui, "Borders and Colors");
|
||||
widgets::section_title(ui, tl!("Borders and Colors"));
|
||||
egui::Grid::new("field-borders").num_columns(2).spacing([12.0, 8.0]).show(ui, |ui| {
|
||||
for (label, slot) in [("Border Color:", &mut l.border), ("Fill Color:", &mut l.fill)] {
|
||||
for (label, slot) in [(tl!("Border Color:"), &mut l.border), (tl!("Fill Color:"), &mut l.fill)] {
|
||||
ui.label(label);
|
||||
ui.horizontal(|ui| {
|
||||
let mut none = slot.is_none();
|
||||
if ui.checkbox(&mut none, "No color").changed() {
|
||||
if ui.checkbox(&mut none, tl!("No color")).changed() {
|
||||
*slot = if none { None } else { Some([0.0; 3]) };
|
||||
}
|
||||
if let Some(c) = crate::comments::swatch_grid(ui, *slot) {
|
||||
|
|
@ -891,26 +891,26 @@ pub(crate) fn body(ui: &mut egui::Ui, d: &mut FieldDraft, t: &crate::theme::Toke
|
|||
});
|
||||
ui.end_row();
|
||||
}
|
||||
ui.label("Line Thickness:");
|
||||
ui.label(tl!("Line Thickness:"));
|
||||
let thick = |w: f64| {
|
||||
if w <= 1.0 {
|
||||
"Thin"
|
||||
tl!("Thin")
|
||||
} else if w <= 2.0 {
|
||||
"Medium"
|
||||
tl!("Medium")
|
||||
} else {
|
||||
"Thick"
|
||||
tl!("Thick")
|
||||
}
|
||||
};
|
||||
egui::ComboBox::from_id_salt("field-width").selected_text(thick(l.width)).show_ui(ui, |ui| {
|
||||
for (w, label) in [(1.0, "Thin"), (2.0, "Medium"), (3.0, "Thick")] {
|
||||
for (w, label) in [(1.0, tl!("Thin")), (2.0, tl!("Medium")), (3.0, tl!("Thick"))] {
|
||||
ui.selectable_value(&mut l.width, w, label);
|
||||
}
|
||||
});
|
||||
ui.end_row();
|
||||
ui.label("Line Style:");
|
||||
egui::ComboBox::from_id_salt("field-style").selected_text(l.style.label()).show_ui(ui, |ui| {
|
||||
ui.label(tl!("Line Style:"));
|
||||
egui::ComboBox::from_id_salt("field-style").selected_text(tl!(l.style.label())).show_ui(ui, |ui| {
|
||||
for s in BorderStyle::ALL {
|
||||
ui.selectable_value(&mut l.style, s, s.label());
|
||||
ui.selectable_value(&mut l.style, s, tl!(s.label()));
|
||||
}
|
||||
});
|
||||
ui.end_row();
|
||||
|
|
@ -919,7 +919,7 @@ pub(crate) fn body(ui: &mut egui::Ui, d: &mut FieldDraft, t: &crate::theme::Toke
|
|||
}
|
||||
FieldTab::Position => {
|
||||
egui::Grid::new("field-position").num_columns(4).spacing([12.0, 10.0]).show(ui, |ui| {
|
||||
for (i, label) in ["Left:", "Bottom:", "Width:", "Height:"].into_iter().enumerate() {
|
||||
for (i, label) in [tl!("Left:"), tl!("Bottom:"), tl!("Width:"), tl!("Height:")].into_iter().enumerate() {
|
||||
ui.label(label);
|
||||
let range = if i < 2 { -14_400.0..=14_400.0 } else { 4.0..=14_400.0 };
|
||||
ui.add(egui::DragValue::new(&mut d.position[i]).range(range).speed(0.5).suffix(" pt"));
|
||||
|
|
@ -928,7 +928,7 @@ pub(crate) fn body(ui: &mut egui::Ui, d: &mut FieldDraft, t: &crate::theme::Toke
|
|||
}
|
||||
}
|
||||
});
|
||||
ui.label(egui::RichText::new("Points from the page's bottom-left corner.").small().color(t.text_faint));
|
||||
ui.label(egui::RichText::new(tl!("Points from the page's bottom-left corner.")).small().color(t.text_faint));
|
||||
}
|
||||
FieldTab::Format => format_tab(ui, d, t),
|
||||
FieldTab::Validate => validate_tab(ui, d),
|
||||
|
|
@ -938,34 +938,33 @@ pub(crate) fn body(ui: &mut egui::Ui, d: &mut FieldDraft, t: &crate::theme::Toke
|
|||
FormFieldKind::Text => {
|
||||
use pdfcraft_engine::field_flags as ff;
|
||||
egui::Grid::new("text-options").num_columns(2).spacing([12.0, 8.0]).show(ui, |ui| {
|
||||
ui.label("Alignment:");
|
||||
egui::ComboBox::from_id_salt("quadding").selected_text(["Left", "Center", "Right"][d.quadding.clamp(0, 2) as usize]).show_ui(
|
||||
ui,
|
||||
|ui| {
|
||||
for (q, l) in [(0, "Left"), (1, "Center"), (2, "Right")] {
|
||||
ui.label(tl!("Alignment:"));
|
||||
egui::ComboBox::from_id_salt("quadding")
|
||||
.selected_text(tl!(["Left", "Center", "Right"][d.quadding.clamp(0, 2) as usize]))
|
||||
.show_ui(ui, |ui| {
|
||||
for (q, l) in [(0, tl!("Left")), (1, tl!("Center")), (2, tl!("Right"))] {
|
||||
ui.selectable_value(&mut d.quadding, q, l);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
ui.end_row();
|
||||
let l = ui.label("Default Value:");
|
||||
let l = ui.label(tl!("Default Value:"));
|
||||
ui.add(egui::TextEdit::singleline(&mut d.default).desired_width(260.0)).labelled_by(l.id);
|
||||
ui.end_row();
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
ui.checkbox(&mut d.multiline, "Multi-line");
|
||||
flag_box(ui, &mut d.flags, ff::DO_NOT_SCROLL, true, "Scroll long text");
|
||||
flag_box(ui, &mut d.flags, ff::RICH_TEXT, false, "Allow Rich Text Formatting");
|
||||
ui.checkbox(&mut d.multiline, tl!("Multi-line"));
|
||||
flag_box(ui, &mut d.flags, ff::DO_NOT_SCROLL, true, tl!("Scroll long text"));
|
||||
flag_box(ui, &mut d.flags, ff::RICH_TEXT, false, tl!("Allow Rich Text Formatting"));
|
||||
ui.horizontal(|ui| {
|
||||
ui.checkbox(&mut d.limit, "Limit of");
|
||||
ui.checkbox(&mut d.limit, tl!("Limit of"));
|
||||
ui.add_enabled(d.limit, egui::DragValue::new(&mut d.max_len).range(0..=10_000));
|
||||
ui.label("characters");
|
||||
ui.label(tl!("characters"));
|
||||
});
|
||||
flag_box(ui, &mut d.flags, ff::PASSWORD, false, "Password");
|
||||
flag_box(ui, &mut d.flags, ff::FILE_SELECT, false, "Field is used for file selection");
|
||||
flag_box(ui, &mut d.flags, ff::DO_NOT_SPELL_CHECK, true, "Check spelling");
|
||||
flag_box(ui, &mut d.flags, ff::PASSWORD, false, tl!("Password"));
|
||||
flag_box(ui, &mut d.flags, ff::FILE_SELECT, false, tl!("Field is used for file selection"));
|
||||
flag_box(ui, &mut d.flags, ff::DO_NOT_SPELL_CHECK, true, tl!("Check spelling"));
|
||||
ui.horizontal(|ui| {
|
||||
flag_box(ui, &mut d.flags, ff::COMB, false, "Comb of");
|
||||
flag_box(ui, &mut d.flags, ff::COMB, false, tl!("Comb of"));
|
||||
if d.flags & ff::COMB != 0 {
|
||||
d.limit = true;
|
||||
d.multiline = false;
|
||||
|
|
@ -975,26 +974,27 @@ pub(crate) fn body(ui: &mut egui::Ui, d: &mut FieldDraft, t: &crate::theme::Toke
|
|||
}
|
||||
}
|
||||
ui.add_enabled(d.flags & ff::COMB != 0, egui::DragValue::new(&mut d.max_len).range(1..=500));
|
||||
ui.label("characters");
|
||||
ui.label(tl!("characters"));
|
||||
});
|
||||
}
|
||||
FormFieldKind::CheckBox | FormFieldKind::Radio => {
|
||||
use pdfcraft_engine::field_flags as ff;
|
||||
let on = d.on_state.clone();
|
||||
ui.label(format!("Export Value: {on}"));
|
||||
ui.label(crate::i18n::fmt(tl!("Export Value: {on}"), &[("on", &on)]));
|
||||
let mut checked = !d.default.is_empty() && d.default == on;
|
||||
let label = if d.kind == FormFieldKind::CheckBox { "Check box is checked by default" } else { "Button is checked by default" };
|
||||
let label =
|
||||
if d.kind == FormFieldKind::CheckBox { tl!("Check box is checked by default") } else { tl!("Button is checked by default") };
|
||||
if ui.checkbox(&mut checked, label).changed() {
|
||||
d.default = if checked { on } else { String::new() };
|
||||
}
|
||||
if let Some(style) = d.check_style.as_mut() {
|
||||
ui.horizontal(|ui| {
|
||||
let l = ui.label(if d.kind == FormFieldKind::CheckBox { "Check Box Style:" } else { "Button Style:" });
|
||||
let l = ui.label(tl!(if d.kind == FormFieldKind::CheckBox { "Check Box Style:" } else { "Button Style:" }));
|
||||
egui::ComboBox::from_id_salt("check-style")
|
||||
.selected_text(style.label())
|
||||
.selected_text(tl!(style.label()))
|
||||
.show_ui(ui, |ui| {
|
||||
for s in pdfcraft_engine::CheckStyle::ALL {
|
||||
ui.selectable_value(style, s, s.label());
|
||||
ui.selectable_value(style, s, tl!(s.label()));
|
||||
}
|
||||
})
|
||||
.response
|
||||
|
|
@ -1002,21 +1002,21 @@ pub(crate) fn body(ui: &mut egui::Ui, d: &mut FieldDraft, t: &crate::theme::Toke
|
|||
});
|
||||
}
|
||||
if d.kind == FormFieldKind::Radio {
|
||||
flag_box(ui, &mut d.flags, ff::RADIOS_IN_UNISON, false, "Buttons with the same name and value are selected in unison");
|
||||
flag_box(ui, &mut d.flags, ff::RADIOS_IN_UNISON, false, tl!("Buttons with the same name and value are selected in unison"));
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
ui.horizontal(|ui| {
|
||||
let l = ui.label("Item:");
|
||||
let l = ui.label(tl!("Item:"));
|
||||
let r = ui.add(egui::TextEdit::singleline(&mut d.new_option).desired_width(240.0)).labelled_by(l.id);
|
||||
let typed = r.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter));
|
||||
if (widgets::pill_button(ui, "Add", false).clicked() || typed) && !d.new_option.trim().is_empty() {
|
||||
if (widgets::pill_button(ui, tl!("Add"), false).clicked() || typed) && !d.new_option.trim().is_empty() {
|
||||
d.options.push(d.new_option.trim().to_string());
|
||||
d.new_option.clear();
|
||||
}
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
widgets::section_title(ui, "Item List");
|
||||
widgets::section_title(ui, tl!("Item List"));
|
||||
let mut remove = None;
|
||||
let mut up = None;
|
||||
egui::ScrollArea::vertical().max_height(140.0).show(ui, |ui| {
|
||||
|
|
@ -1024,10 +1024,10 @@ pub(crate) fn body(ui: &mut egui::Ui, d: &mut FieldDraft, t: &crate::theme::Toke
|
|||
ui.horizontal(|ui| {
|
||||
ui.label(o);
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
if ui.small_button("Delete").on_hover_text(format!("Delete {o}")).clicked() {
|
||||
if ui.small_button(tl!("Delete")).on_hover_text(crate::i18n::fmt(tl!("Delete {o}"), &[("o", o)])).clicked() {
|
||||
remove = Some(i);
|
||||
}
|
||||
if i > 0 && ui.small_button("Up").clicked() {
|
||||
if i > 0 && ui.small_button(tl!("Up")).clicked() {
|
||||
up = Some(i);
|
||||
}
|
||||
});
|
||||
|
|
@ -1041,41 +1041,41 @@ pub(crate) fn body(ui: &mut egui::Ui, d: &mut FieldDraft, t: &crate::theme::Toke
|
|||
d.options.swap(i - 1, i);
|
||||
}
|
||||
if d.options.is_empty() {
|
||||
ui.label(egui::RichText::new("Add the choices people pick from.").color(t.text_muted));
|
||||
ui.label(egui::RichText::new(tl!("Add the choices people pick from.")).color(t.text_muted));
|
||||
}
|
||||
use pdfcraft_engine::field_flags as ff;
|
||||
ui.add_space(6.0);
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Default:");
|
||||
let shown = if d.default.is_empty() { "(none)".to_string() } else { d.default.clone() };
|
||||
ui.label(tl!("Default:"));
|
||||
let shown = if d.default.is_empty() { tl!("(none)").to_string() } else { d.default.clone() };
|
||||
egui::ComboBox::from_id_salt("choice-default").selected_text(shown).show_ui(ui, |ui| {
|
||||
ui.selectable_value(&mut d.default, String::new(), "(none)");
|
||||
ui.selectable_value(&mut d.default, String::new(), tl!("(none)"));
|
||||
for o in d.options.clone() {
|
||||
ui.selectable_value(&mut d.default, o.clone(), o);
|
||||
}
|
||||
});
|
||||
});
|
||||
flag_box(ui, &mut d.flags, ff::SORT, false, "Sort items");
|
||||
flag_box(ui, &mut d.flags, ff::SORT, false, tl!("Sort items"));
|
||||
if d.kind == FormFieldKind::Combo {
|
||||
flag_box(ui, &mut d.flags, ff::EDIT, false, "Allow user to enter custom text");
|
||||
flag_box(ui, &mut d.flags, ff::DO_NOT_SPELL_CHECK, true, "Check spelling");
|
||||
flag_box(ui, &mut d.flags, ff::EDIT, false, tl!("Allow user to enter custom text"));
|
||||
flag_box(ui, &mut d.flags, ff::DO_NOT_SPELL_CHECK, true, tl!("Check spelling"));
|
||||
} else {
|
||||
flag_box(ui, &mut d.flags, ff::MULTI_SELECT, false, "Multiple selection");
|
||||
flag_box(ui, &mut d.flags, ff::MULTI_SELECT, false, tl!("Multiple selection"));
|
||||
}
|
||||
flag_box(ui, &mut d.flags, ff::COMMIT_ON_SEL_CHANGE, false, "Commit selected value immediately");
|
||||
flag_box(ui, &mut d.flags, ff::COMMIT_ON_SEL_CHANGE, false, tl!("Commit selected value immediately"));
|
||||
}
|
||||
},
|
||||
}
|
||||
});
|
||||
ui.add_space(8.0);
|
||||
ui.checkbox(&mut d.locked, "Locked");
|
||||
ui.checkbox(&mut d.locked, tl!("Locked"));
|
||||
ui.add_space(6.0);
|
||||
let (mut apply, mut cancel) = (enter, false);
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
if widgets::pill_button(ui, "OK", true).clicked() {
|
||||
if widgets::pill_button(ui, tl!("OK"), true).clicked() {
|
||||
apply = true;
|
||||
}
|
||||
if widgets::pill_button(ui, "Cancel", false).clicked() {
|
||||
if widgets::pill_button(ui, tl!("Cancel"), false).clicked() {
|
||||
cancel = true;
|
||||
}
|
||||
});
|
||||
|
|
@ -1112,24 +1112,24 @@ fn format_tab(ui: &mut egui::Ui, d: &mut FieldDraft, t: &crate::theme::Tokens) {
|
|||
let cat = category(&d.format);
|
||||
let mut picked = cat;
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Select format category:");
|
||||
ui.label(tl!("Select format category:"));
|
||||
egui::ComboBox::from_id_salt("format-cat")
|
||||
.selected_text(match cat {
|
||||
.selected_text(tl!(match cat {
|
||||
Category::None => "None",
|
||||
Category::Number => "Number",
|
||||
Category::Percent => "Percentage",
|
||||
Category::Date => "Date",
|
||||
Category::Time => "Time",
|
||||
Category::Special => "Special",
|
||||
})
|
||||
}))
|
||||
.show_ui(ui, |ui| {
|
||||
for (c, l) in [
|
||||
(Category::None, "None"),
|
||||
(Category::Number, "Number"),
|
||||
(Category::Percent, "Percentage"),
|
||||
(Category::Date, "Date"),
|
||||
(Category::Time, "Time"),
|
||||
(Category::Special, "Special"),
|
||||
(Category::None, tl!("None")),
|
||||
(Category::Number, tl!("Number")),
|
||||
(Category::Percent, tl!("Percentage")),
|
||||
(Category::Date, tl!("Date")),
|
||||
(Category::Time, tl!("Time")),
|
||||
(Category::Special, tl!("Special")),
|
||||
] {
|
||||
ui.selectable_value(&mut picked, c, l);
|
||||
}
|
||||
|
|
@ -1157,34 +1157,34 @@ fn format_tab(ui: &mut egui::Ui, d: &mut FieldDraft, t: &crate::theme::Tokens) {
|
|||
};
|
||||
match &mut d.format {
|
||||
Format::None => {
|
||||
ui.label(egui::RichText::new("The value is shown as typed.").color(t.text_muted));
|
||||
ui.label(egui::RichText::new(tl!("The value is shown as typed.")).color(t.text_muted));
|
||||
}
|
||||
Format::Number { decimals, sep, neg, currency, prepend } => {
|
||||
egui::Grid::new("fmt-number").num_columns(2).spacing([12.0, 8.0]).show(ui, |ui| {
|
||||
ui.label("Decimal places:");
|
||||
ui.label(tl!("Decimal places:"));
|
||||
ui.add(egui::DragValue::new(decimals).range(0..=10));
|
||||
ui.end_row();
|
||||
ui.label("Separator style:");
|
||||
ui.label(tl!("Separator style:"));
|
||||
egui::ComboBox::from_id_salt("fmt-sep").selected_text(SEPARATORS[(*sep).min(4) as usize]).show_ui(ui, |ui| {
|
||||
for (i, s) in SEPARATORS.iter().enumerate() {
|
||||
ui.selectable_value(sep, i as u8, *s);
|
||||
}
|
||||
});
|
||||
ui.end_row();
|
||||
ui.label("Currency symbol:");
|
||||
ui.label(tl!("Currency symbol:"));
|
||||
ui.horizontal(|ui| {
|
||||
egui::ComboBox::from_id_salt("fmt-cur")
|
||||
.selected_text(if currency.is_empty() { "None" } else { currency.as_str() })
|
||||
.selected_text(if currency.is_empty() { tl!("None") } else { currency.as_str() })
|
||||
.width(70.0)
|
||||
.show_ui(ui, |ui| {
|
||||
for c in ["", "$", "€", "£", "¥", "CHF "] {
|
||||
ui.selectable_value(currency, c.to_string(), if c.is_empty() { "None" } else { c });
|
||||
ui.selectable_value(currency, c.to_string(), if c.is_empty() { tl!("None") } else { c });
|
||||
}
|
||||
});
|
||||
ui.checkbox(prepend, "Before the number");
|
||||
ui.checkbox(prepend, tl!("Before the number"));
|
||||
});
|
||||
ui.end_row();
|
||||
ui.label("Negative number style:");
|
||||
ui.label(tl!("Negative number style:"));
|
||||
egui::ComboBox::from_id_salt("fmt-neg").selected_text(NEGATIVES[(*neg).min(3) as usize]).show_ui(ui, |ui| {
|
||||
for (i, s) in NEGATIVES.iter().enumerate() {
|
||||
ui.selectable_value(neg, i as u8, *s);
|
||||
|
|
@ -1195,10 +1195,10 @@ fn format_tab(ui: &mut egui::Ui, d: &mut FieldDraft, t: &crate::theme::Tokens) {
|
|||
}
|
||||
Format::Percent { decimals, sep } => {
|
||||
egui::Grid::new("fmt-pct").num_columns(2).spacing([12.0, 8.0]).show(ui, |ui| {
|
||||
ui.label("Decimal places:");
|
||||
ui.label(tl!("Decimal places:"));
|
||||
ui.add(egui::DragValue::new(decimals).range(0..=10));
|
||||
ui.end_row();
|
||||
ui.label("Separator style:");
|
||||
ui.label(tl!("Separator style:"));
|
||||
egui::ComboBox::from_id_salt("fmt-psep").selected_text(SEPARATORS[(*sep).min(4) as usize]).show_ui(ui, |ui| {
|
||||
for (i, s) in SEPARATORS.iter().enumerate() {
|
||||
ui.selectable_value(sep, i as u8, *s);
|
||||
|
|
@ -1214,21 +1214,21 @@ fn format_tab(ui: &mut egui::Ui, d: &mut FieldDraft, t: &crate::theme::Tokens) {
|
|||
DATE_PRESETS.iter().copied().chain(["m/d/yyyy", "mm/dd/yyyy", "yyyy-mm-dd", "dd/mm/yyyy"]).collect()
|
||||
};
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Format:");
|
||||
ui.label(tl!("Format:"));
|
||||
egui::ComboBox::from_id_salt("fmt-date").selected_text(p.as_str()).width(170.0).show_ui(ui, |ui| {
|
||||
for pr in presets {
|
||||
ui.selectable_value(p, pr.to_string(), pr);
|
||||
}
|
||||
});
|
||||
ui.label("Custom:");
|
||||
ui.label(tl!("Custom:"));
|
||||
ui.add(egui::TextEdit::singleline(p).desired_width(120.0));
|
||||
});
|
||||
}
|
||||
Format::Special(n) => {
|
||||
let mut idx = *n as usize;
|
||||
egui::ComboBox::from_id_salt("fmt-special").selected_text(SPECIALS[idx.min(4)]).show_ui(ui, |ui| {
|
||||
egui::ComboBox::from_id_salt("fmt-special").selected_text(tl!(SPECIALS[idx.min(4)])).show_ui(ui, |ui| {
|
||||
for (i, s) in SPECIALS.iter().enumerate() {
|
||||
ui.selectable_value(&mut idx, i, *s);
|
||||
ui.selectable_value(&mut idx, i, tl!(s));
|
||||
}
|
||||
});
|
||||
if idx == 4 {
|
||||
|
|
@ -1239,16 +1239,16 @@ fn format_tab(ui: &mut egui::Ui, d: &mut FieldDraft, t: &crate::theme::Tokens) {
|
|||
}
|
||||
Format::Mask(m) => {
|
||||
let mut idx = 4usize;
|
||||
egui::ComboBox::from_id_salt("fmt-special").selected_text(SPECIALS[4]).show_ui(ui, |ui| {
|
||||
egui::ComboBox::from_id_salt("fmt-special").selected_text(tl!(SPECIALS[4])).show_ui(ui, |ui| {
|
||||
for (i, s) in SPECIALS.iter().enumerate() {
|
||||
ui.selectable_value(&mut idx, i, *s);
|
||||
ui.selectable_value(&mut idx, i, tl!(s));
|
||||
}
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Mask:");
|
||||
ui.label(tl!("Mask:"));
|
||||
ui.add(egui::TextEdit::singleline(m).desired_width(160.0));
|
||||
});
|
||||
ui.label(egui::RichText::new("9 digit, A letter, O letter or digit, X any character").small().color(t.text_faint));
|
||||
ui.label(egui::RichText::new(tl!("9 digit, A letter, O letter or digit, X any character")).small().color(t.text_faint));
|
||||
if idx < 4 {
|
||||
d.format = Format::Special(idx as u8);
|
||||
}
|
||||
|
|
@ -1256,14 +1256,16 @@ fn format_tab(ui: &mut egui::Ui, d: &mut FieldDraft, t: &crate::theme::Tokens) {
|
|||
}
|
||||
if !sample.is_empty() && d.format != Format::None {
|
||||
ui.add_space(6.0);
|
||||
ui.label(egui::RichText::new(format!("Example: {}", format_value(&d.format, sample))).color(t.text_muted));
|
||||
ui.label(
|
||||
egui::RichText::new(crate::i18n::fmt(tl!("Example: {sample}"), &[("sample", &format_value(&d.format, sample))])).color(t.text_muted),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_tab(ui: &mut egui::Ui, d: &mut FieldDraft) {
|
||||
let mut ranged = matches!(d.validate, Validate::Range { .. });
|
||||
ui.radio_value(&mut ranged, false, "Field value is not validated");
|
||||
ui.radio_value(&mut ranged, true, "Field value is in range:");
|
||||
ui.radio_value(&mut ranged, false, tl!("Field value is not validated"));
|
||||
ui.radio_value(&mut ranged, true, tl!("Field value is in range:"));
|
||||
if !ranged {
|
||||
d.validate = Validate::None;
|
||||
return;
|
||||
|
|
@ -1274,12 +1276,12 @@ fn validate_tab(ui: &mut egui::Ui, d: &mut FieldDraft) {
|
|||
if let Validate::Range { min, max } = &mut d.validate {
|
||||
ui.horizontal(|ui| {
|
||||
let mut has = min.is_some();
|
||||
ui.checkbox(&mut has, "From:");
|
||||
ui.checkbox(&mut has, tl!("From:"));
|
||||
let mut v = min.unwrap_or(0.0);
|
||||
ui.add_enabled(has, egui::DragValue::new(&mut v).speed(1.0));
|
||||
*min = has.then_some(v);
|
||||
let mut has = max.is_some();
|
||||
ui.checkbox(&mut has, "To:");
|
||||
ui.checkbox(&mut has, tl!("To:"));
|
||||
let mut v = max.unwrap_or(100.0);
|
||||
ui.add_enabled(has, egui::DragValue::new(&mut v).speed(1.0));
|
||||
*max = has.then_some(v);
|
||||
|
|
@ -1289,33 +1291,33 @@ fn validate_tab(ui: &mut egui::Ui, d: &mut FieldDraft) {
|
|||
|
||||
fn actions_tab(ui: &mut egui::Ui, d: &mut FieldDraft, t: &crate::theme::Tokens) {
|
||||
use pdfcraft_engine::FieldTrigger as T;
|
||||
ui.label(egui::RichText::new("Add an Action").font(theme::semibold(13.0)));
|
||||
ui.label(egui::RichText::new(tl!("Add an Action")).font(theme::semibold(13.0)));
|
||||
let a = &mut d.new_action;
|
||||
egui::Grid::new("field-actions-add").num_columns(2).spacing([12.0, 8.0]).show(ui, |ui| {
|
||||
ui.label("Select Trigger:");
|
||||
egui::ComboBox::from_id_salt("action-trigger").selected_text(a.trigger.label()).width(220.0).show_ui(ui, |ui| {
|
||||
ui.label(tl!("Select Trigger:"));
|
||||
egui::ComboBox::from_id_salt("action-trigger").selected_text(tl!(a.trigger.label())).width(220.0).show_ui(ui, |ui| {
|
||||
for tr in T::ALL {
|
||||
ui.selectable_value(&mut a.trigger, tr, tr.label());
|
||||
ui.selectable_value(&mut a.trigger, tr, tl!(tr.label()));
|
||||
}
|
||||
});
|
||||
ui.end_row();
|
||||
ui.label("Select Action:");
|
||||
egui::ComboBox::from_id_salt("action-kind").selected_text(ACTION_KINDS[a.kind]).width(220.0).show_ui(ui, |ui| {
|
||||
ui.label(tl!("Select Action:"));
|
||||
egui::ComboBox::from_id_salt("action-kind").selected_text(tl!(ACTION_KINDS[a.kind])).width(220.0).show_ui(ui, |ui| {
|
||||
for (i, k) in ACTION_KINDS.iter().enumerate() {
|
||||
ui.selectable_value(&mut a.kind, i, *k);
|
||||
ui.selectable_value(&mut a.kind, i, tl!(k));
|
||||
}
|
||||
});
|
||||
ui.end_row();
|
||||
let hint = match a.kind {
|
||||
0 => "JavaScript",
|
||||
1 => "https://…",
|
||||
2 => "Fields to reset (comma-separated; empty: all)",
|
||||
3 => "Print, NextPage, PrevPage, FirstPage or LastPage",
|
||||
4 => "Page number",
|
||||
5 | 6 => "Field names (comma-separated)",
|
||||
_ => "URL to submit to",
|
||||
2 => tl!("Fields to reset (comma-separated; empty: all)"),
|
||||
3 => tl!("Print, NextPage, PrevPage, FirstPage or LastPage"),
|
||||
4 => tl!("Page number"),
|
||||
5 | 6 => tl!("Field names (comma-separated)"),
|
||||
_ => tl!("URL to submit to"),
|
||||
};
|
||||
ui.label(if a.kind == 0 { "Script:" } else { "Value:" });
|
||||
ui.label(if a.kind == 0 { tl!("Script:") } else { tl!("Value:") });
|
||||
if a.kind == 0 {
|
||||
ui.add(egui::TextEdit::multiline(&mut a.text).code_editor().desired_rows(3).desired_width(340.0).hint_text(hint).id_salt("action-text"));
|
||||
} else {
|
||||
|
|
@ -1324,7 +1326,7 @@ fn actions_tab(ui: &mut egui::Ui, d: &mut FieldDraft, t: &crate::theme::Tokens)
|
|||
ui.end_row();
|
||||
});
|
||||
let mut error = None;
|
||||
if ui.button("Add").clicked() {
|
||||
if ui.button(tl!("Add")).clicked() {
|
||||
match d.new_action.action() {
|
||||
Ok(act) => {
|
||||
let tr = d.new_action.trigger;
|
||||
|
|
@ -1340,19 +1342,19 @@ fn actions_tab(ui: &mut egui::Ui, d: &mut FieldDraft, t: &crate::theme::Tokens)
|
|||
ui.label(egui::RichText::new(e).small().color(t.text_muted));
|
||||
}
|
||||
ui.add_space(8.0);
|
||||
ui.label(egui::RichText::new("Actions").font(theme::semibold(13.0)));
|
||||
ui.label(egui::RichText::new(tl!("Actions")).font(theme::semibold(13.0)));
|
||||
egui::Frame::new().fill(t.hover).corner_radius(egui::CornerRadius::same(6)).inner_margin(egui::Margin::same(8)).show(ui, |ui| {
|
||||
ui.set_width(ui.available_width());
|
||||
if d.actions.is_empty() {
|
||||
ui.label(egui::RichText::new("No actions").color(t.text_muted));
|
||||
ui.label(egui::RichText::new(tl!("No actions")).color(t.text_muted));
|
||||
}
|
||||
let mut remove = None;
|
||||
for (i, (tr, act)) in d.actions.iter().enumerate() {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(egui::RichText::new(tr.label()).strong());
|
||||
ui.label(egui::RichText::new(tl!(tr.label())).strong());
|
||||
ui.label(act.describe());
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
if ui.push_id(i, |ui| ui.button("Delete")).inner.clicked() {
|
||||
if ui.push_id(i, |ui| ui.button(tl!("Delete"))).inner.clicked() {
|
||||
remove = Some(i);
|
||||
}
|
||||
});
|
||||
|
|
@ -1377,8 +1379,8 @@ fn calculate_tab(ui: &mut egui::Ui, d: &mut FieldDraft, t: &crate::theme::Tokens
|
|||
Calculate::Notation(_) => Mode::Notation,
|
||||
};
|
||||
let mut m = mode;
|
||||
ui.radio_value(&mut m, Mode::None, "Value is not calculated");
|
||||
ui.radio_value(&mut m, Mode::Simple, "Value is the");
|
||||
ui.radio_value(&mut m, Mode::None, tl!("Value is not calculated"));
|
||||
ui.radio_value(&mut m, Mode::Simple, tl!("Value is the"));
|
||||
if m != mode {
|
||||
d.calculate = match m {
|
||||
Mode::None => Calculate::None,
|
||||
|
|
@ -1389,12 +1391,12 @@ fn calculate_tab(ui: &mut egui::Ui, d: &mut FieldDraft, t: &crate::theme::Tokens
|
|||
if let Calculate::Simple { op, fields } = &mut d.calculate {
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_space(24.0);
|
||||
egui::ComboBox::from_id_salt("calc-op").selected_text(op.label()).show_ui(ui, |ui| {
|
||||
egui::ComboBox::from_id_salt("calc-op").selected_text(tl!(op.label())).show_ui(ui, |ui| {
|
||||
for o in CalcOp::ALL {
|
||||
ui.selectable_value(op, o, o.label());
|
||||
ui.selectable_value(op, o, tl!(o.label()));
|
||||
}
|
||||
});
|
||||
ui.label("of the following fields:");
|
||||
ui.label(tl!("of the following fields:"));
|
||||
});
|
||||
egui::ScrollArea::vertical().max_height(110.0).show(ui, |ui| {
|
||||
for name in &d.others {
|
||||
|
|
@ -1413,11 +1415,11 @@ fn calculate_tab(ui: &mut egui::Ui, d: &mut FieldDraft, t: &crate::theme::Tokens
|
|||
Calculate::Notation(_) => Mode::Notation,
|
||||
_ => m,
|
||||
};
|
||||
if ui.radio_value(&mut m2, Mode::Notation, "Simplified field notation:").clicked() && !matches!(d.calculate, Calculate::Notation(_)) {
|
||||
if ui.radio_value(&mut m2, Mode::Notation, tl!("Simplified field notation:")).clicked() && !matches!(d.calculate, Calculate::Notation(_)) {
|
||||
d.calculate = Calculate::Notation(String::new());
|
||||
}
|
||||
if let Calculate::Notation(expr) = &mut d.calculate {
|
||||
ui.add(egui::TextEdit::multiline(expr).hint_text("Price * Quantity").desired_rows(2).desired_width(420.0));
|
||||
ui.label(egui::RichText::new("Field names with + - * / and parentheses; put \\ before spaces in names.").small().color(t.text_faint));
|
||||
ui.label(egui::RichText::new(tl!("Field names with + - * / and parentheses; put \\ before spaces in names.")).small().color(t.text_faint));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -151,7 +151,7 @@ impl PdfCraftApp {
|
|||
let settings = match self.print_draft.settings(doc.info.pages.len(), &labels) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
self.notify(e);
|
||||
self.notify_error(e);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
|
@ -159,18 +159,22 @@ impl PdfCraftApp {
|
|||
let bytes = match self.session.print_pdf(id, &settings) {
|
||||
Ok(b) => b,
|
||||
Err(e) => {
|
||||
self.notify(e.to_string());
|
||||
self.notify_error(e);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
match self.print_draft.printer.clone() {
|
||||
Some(printer) => match spool::submit(&bytes, &self.print_draft.job(&name)) {
|
||||
Ok(msg) => {
|
||||
self.notify(if msg.is_empty() { format!("Sent to {printer}") } else { format!("Sent to {printer}: {msg}") });
|
||||
self.notify(if msg.is_empty() {
|
||||
crate::i18n::fmt(tl!("Sent to {printer}"), &[("printer", &printer)])
|
||||
} else {
|
||||
crate::i18n::fmt(tl!("Sent to {printer}: {msg}"), &[("printer", &printer), ("msg", &msg)])
|
||||
});
|
||||
true
|
||||
}
|
||||
Err(e) => {
|
||||
self.notify(e.to_string());
|
||||
self.notify_error(e);
|
||||
false
|
||||
}
|
||||
},
|
||||
|
|
@ -188,11 +192,11 @@ impl PdfCraftApp {
|
|||
let Some(path) = path else { return false };
|
||||
match std::fs::write(&path, &bytes) {
|
||||
Ok(()) => {
|
||||
self.notify(format!("Saved the print-ready PDF to {}", path.display()));
|
||||
self.notify_fmt("Saved the print-ready PDF to {path}", &[("path", &path.display().to_string())]);
|
||||
true
|
||||
}
|
||||
Err(e) => {
|
||||
self.notify(format!("Could not save: {e}"));
|
||||
self.notify_fmt("Could not save: {e}", &[("e", &e.to_string())]);
|
||||
false
|
||||
}
|
||||
}
|
||||
|
|
@ -221,7 +225,7 @@ pub(crate) fn body(
|
|||
thumb: &dyn Fn(usize) -> Option<egui::TextureId>,
|
||||
) -> (bool, bool) {
|
||||
ui.set_width(820.0);
|
||||
ui.label(egui::RichText::new("Print").font(theme::semibold(18.0)));
|
||||
ui.label(egui::RichText::new(tl!("Print")).font(theme::semibold(18.0)));
|
||||
ui.add_space(8.0);
|
||||
let settings = d.settings(sizes.len(), labels);
|
||||
let sheets = settings.as_ref().ok().and_then(|s| print::layout(sizes, s).ok()).unwrap_or_default();
|
||||
|
|
@ -231,33 +235,37 @@ pub(crate) fn body(
|
|||
ui.vertical(|ui| {
|
||||
ui.set_width(470.0);
|
||||
egui::Grid::new("print-top").num_columns(2).spacing([10.0, 8.0]).show(ui, |ui| {
|
||||
ui.label("Printer:");
|
||||
let shown = d.printer.clone().unwrap_or_else(|| "Save as PDF".into());
|
||||
ui.label(tl!("Printer:"));
|
||||
let shown = d.printer.clone().unwrap_or_else(|| tl!("Save as PDF").to_string());
|
||||
egui::ComboBox::from_id_salt("printer").selected_text(shown).width(260.0).show_ui(ui, |ui| {
|
||||
for p in &d.printers {
|
||||
let label = if p.default { format!("{} (default)", p.name) } else { p.name.clone() };
|
||||
let label = if p.default { crate::i18n::fmt(tl!("{name} (default)"), &[("name", &p.name)]) } else { p.name.clone() };
|
||||
ui.selectable_value(&mut d.printer, Some(p.name.clone()), label);
|
||||
}
|
||||
ui.selectable_value(&mut d.printer, None, "Save as PDF");
|
||||
ui.selectable_value(&mut d.printer, None, tl!("Save as PDF"));
|
||||
});
|
||||
ui.end_row();
|
||||
ui.label("Copies:");
|
||||
ui.label(tl!("Copies:"));
|
||||
ui.horizontal(|ui| {
|
||||
ui.add(egui::DragValue::new(&mut d.copies).range(1..=999));
|
||||
ui.checkbox(&mut d.collate, "Collate");
|
||||
ui.checkbox(&mut d.grayscale, "Print in grayscale");
|
||||
ui.checkbox(&mut d.collate, tl!("Collate"));
|
||||
ui.checkbox(&mut d.grayscale, tl!("Print in grayscale"));
|
||||
});
|
||||
ui.end_row();
|
||||
ui.label("Two-sided:");
|
||||
ui.label(tl!("Two-sided:"));
|
||||
combo(
|
||||
ui,
|
||||
"duplex",
|
||||
&mut d.duplex,
|
||||
&[(spool::Duplex::Off, "Off"), (spool::Duplex::LongEdge, "Flip on long edge"), (spool::Duplex::ShortEdge, "Flip on short edge")],
|
||||
&[
|
||||
(spool::Duplex::Off, tl!("Off")),
|
||||
(spool::Duplex::LongEdge, tl!("Flip on long edge")),
|
||||
(spool::Duplex::ShortEdge, tl!("Flip on short edge")),
|
||||
],
|
||||
160.0,
|
||||
);
|
||||
ui.end_row();
|
||||
ui.label("Paper:");
|
||||
ui.label(tl!("Paper:"));
|
||||
egui::ComboBox::from_id_salt("paper").selected_text(PAPERS[d.paper].0).width(160.0).show_ui(ui, |ui| {
|
||||
for (i, (name, _)) in PAPERS.iter().enumerate() {
|
||||
ui.selectable_value(&mut d.paper, i, *name);
|
||||
|
|
@ -266,11 +274,11 @@ pub(crate) fn body(
|
|||
ui.end_row();
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
widgets::section_title(ui, "Pages to Print");
|
||||
widgets::section_title(ui, tl!("Pages to Print"));
|
||||
ui.horizontal(|ui| {
|
||||
ui.radio_value(&mut d.which, Which::All, "All");
|
||||
ui.radio_value(&mut d.which, Which::Current, "Current page");
|
||||
ui.radio_value(&mut d.which, Which::Range, "Pages");
|
||||
ui.radio_value(&mut d.which, Which::All, tl!("All"));
|
||||
ui.radio_value(&mut d.which, Which::Current, tl!("Current page"));
|
||||
ui.radio_value(&mut d.which, Which::Range, tl!("Pages"));
|
||||
let r = ui.add_enabled(
|
||||
d.which == Which::Range,
|
||||
egui::TextEdit::singleline(&mut d.range).hint_text(format!("1-{}", sizes.len())).desired_width(110.0),
|
||||
|
|
@ -280,22 +288,25 @@ pub(crate) fn body(
|
|||
}
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("More options:");
|
||||
ui.label(tl!("More options:"));
|
||||
combo(
|
||||
ui,
|
||||
"subset",
|
||||
&mut d.subset,
|
||||
&[(Subset::All, "All pages in range"), (Subset::Odd, "Odd pages only"), (Subset::Even, "Even pages only")],
|
||||
&[(Subset::All, tl!("All pages in range")), (Subset::Odd, tl!("Odd pages only")), (Subset::Even, tl!("Even pages only"))],
|
||||
150.0,
|
||||
);
|
||||
ui.checkbox(&mut d.reverse, "Reverse pages");
|
||||
ui.checkbox(&mut d.reverse, tl!("Reverse pages"));
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
widgets::section_title(ui, "Page Sizing & Handling");
|
||||
widgets::section_title(ui, tl!("Page Sizing & Handling"));
|
||||
ui.horizontal(|ui| {
|
||||
for (h, label) in
|
||||
[(Handling::Size, "Size"), (Handling::Poster, "Poster"), (Handling::Multiple, "Multiple"), (Handling::Booklet, "Booklet")]
|
||||
{
|
||||
for (h, label) in [
|
||||
(Handling::Size, tl!("Size")),
|
||||
(Handling::Poster, tl!("Poster")),
|
||||
(Handling::Multiple, tl!("Multiple")),
|
||||
(Handling::Booklet, tl!("Booklet")),
|
||||
] {
|
||||
if widgets::mode_tab(ui, label, d.handling == h).clicked() {
|
||||
d.handling = h;
|
||||
d.sheet = 0;
|
||||
|
|
@ -306,13 +317,13 @@ pub(crate) fn body(
|
|||
match d.handling {
|
||||
Handling::Size => {
|
||||
ui.horizontal(|ui| {
|
||||
ui.radio_value(&mut d.size, SizeMode::Fit, "Fit");
|
||||
ui.radio_value(&mut d.size, SizeMode::Actual, "Actual size");
|
||||
ui.radio_value(&mut d.size, SizeMode::Shrink, "Shrink oversized pages");
|
||||
ui.radio_value(&mut d.size, SizeMode::Fit, tl!("Fit"));
|
||||
ui.radio_value(&mut d.size, SizeMode::Actual, tl!("Actual size"));
|
||||
ui.radio_value(&mut d.size, SizeMode::Shrink, tl!("Shrink oversized pages"));
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
let custom = matches!(d.size, SizeMode::Custom(_));
|
||||
if ui.radio(custom, "Custom scale:").clicked() {
|
||||
if ui.radio(custom, tl!("Custom scale:")).clicked() {
|
||||
d.size = SizeMode::Custom(d.custom_scale);
|
||||
}
|
||||
ui.add_enabled(custom, egui::DragValue::new(&mut d.custom_scale).range(1.0..=1000.0).suffix(" %"));
|
||||
|
|
@ -320,79 +331,79 @@ pub(crate) fn body(
|
|||
}
|
||||
Handling::Poster => {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Tile scale:");
|
||||
ui.label(tl!("Tile scale:"));
|
||||
ui.add(egui::DragValue::new(&mut d.poster_scale).range(10.0..=1000.0).suffix(" %"));
|
||||
ui.label("Overlap:");
|
||||
ui.label(tl!("Overlap:"));
|
||||
ui.add(egui::DragValue::new(&mut d.overlap).range(0.0..=144.0).suffix(" pt"));
|
||||
ui.checkbox(&mut d.cut_marks, "Cut marks");
|
||||
ui.checkbox(&mut d.cut_marks, tl!("Cut marks"));
|
||||
});
|
||||
}
|
||||
Handling::Multiple => {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Pages per sheet:");
|
||||
ui.label(tl!("Pages per sheet:"));
|
||||
combo(ui, "per-sheet", &mut d.per_sheet, &[(2, "2"), (4, "4"), (6, "6"), (9, "9"), (16, "16")], 60.0);
|
||||
ui.label("Page order:");
|
||||
ui.label(tl!("Page order:"));
|
||||
combo(
|
||||
ui,
|
||||
"order",
|
||||
&mut d.order,
|
||||
&[
|
||||
(PageOrder::Horizontal, "Horizontal"),
|
||||
(PageOrder::HorizontalReversed, "Horizontal reversed"),
|
||||
(PageOrder::Vertical, "Vertical"),
|
||||
(PageOrder::VerticalReversed, "Vertical reversed"),
|
||||
(PageOrder::CutStack, "Cut and stack"),
|
||||
(PageOrder::Horizontal, tl!("Horizontal")),
|
||||
(PageOrder::HorizontalReversed, tl!("Horizontal reversed")),
|
||||
(PageOrder::Vertical, tl!("Vertical")),
|
||||
(PageOrder::VerticalReversed, tl!("Vertical reversed")),
|
||||
(PageOrder::CutStack, tl!("Cut and stack")),
|
||||
],
|
||||
150.0,
|
||||
);
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.checkbox(&mut d.border, "Print page border");
|
||||
ui.checkbox(&mut d.auto_rotate, "Auto-rotate pages");
|
||||
ui.checkbox(&mut d.border, tl!("Print page border"));
|
||||
ui.checkbox(&mut d.auto_rotate, tl!("Auto-rotate pages"));
|
||||
});
|
||||
if d.order == PageOrder::CutStack {
|
||||
ui.label(
|
||||
"Print single-sided. Keep the sheets in order, cut at the marks, then stack the piles left to right, top to bottom.",
|
||||
);
|
||||
ui.label(tl!(
|
||||
"Print single-sided. Keep the sheets in order, cut at the marks, then stack the piles left to right, top to bottom."
|
||||
));
|
||||
}
|
||||
}
|
||||
Handling::Booklet => {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Booklet subset:");
|
||||
ui.label(tl!("Booklet subset:"));
|
||||
combo(
|
||||
ui,
|
||||
"booklet",
|
||||
&mut d.booklet_subset,
|
||||
&[
|
||||
(BookletSubset::BothSides, "Both sides"),
|
||||
(BookletSubset::FrontOnly, "Front side only"),
|
||||
(BookletSubset::BackOnly, "Back side only"),
|
||||
(BookletSubset::BothSides, tl!("Both sides")),
|
||||
(BookletSubset::FrontOnly, tl!("Front side only")),
|
||||
(BookletSubset::BackOnly, tl!("Back side only")),
|
||||
],
|
||||
130.0,
|
||||
);
|
||||
ui.label("Binding:");
|
||||
combo(ui, "binding", &mut d.binding, &[(Binding::Left, "Left"), (Binding::Right, "Right")], 80.0);
|
||||
ui.label(tl!("Binding:"));
|
||||
combo(ui, "binding", &mut d.binding, &[(Binding::Left, tl!("Left")), (Binding::Right, tl!("Right"))], 80.0);
|
||||
});
|
||||
}
|
||||
}
|
||||
ui.add_space(6.0);
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Orientation:");
|
||||
ui.radio_value(&mut d.orientation, Orientation::Auto, "Auto portrait/landscape");
|
||||
ui.radio_value(&mut d.orientation, Orientation::Portrait, "Portrait");
|
||||
ui.radio_value(&mut d.orientation, Orientation::Landscape, "Landscape");
|
||||
ui.label(tl!("Orientation:"));
|
||||
ui.radio_value(&mut d.orientation, Orientation::Auto, tl!("Auto portrait/landscape"));
|
||||
ui.radio_value(&mut d.orientation, Orientation::Portrait, tl!("Portrait"));
|
||||
ui.radio_value(&mut d.orientation, Orientation::Landscape, tl!("Landscape"));
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
widgets::section_title(ui, "Comments & Forms");
|
||||
widgets::section_title(ui, tl!("Comments & Forms"));
|
||||
combo(
|
||||
ui,
|
||||
"content",
|
||||
&mut d.content,
|
||||
&[
|
||||
(Content::Document, "Document"),
|
||||
(Content::DocumentAndMarkups, "Document and markups"),
|
||||
(Content::DocumentAndStamps, "Document and stamps"),
|
||||
(Content::FormFieldsOnly, "Form fields only"),
|
||||
(Content::Document, tl!("Document")),
|
||||
(Content::DocumentAndMarkups, tl!("Document and markups")),
|
||||
(Content::DocumentAndStamps, tl!("Document and stamps")),
|
||||
(Content::FormFieldsOnly, tl!("Form fields only")),
|
||||
],
|
||||
220.0,
|
||||
);
|
||||
|
|
@ -470,11 +481,15 @@ pub(crate) fn body(
|
|||
}
|
||||
ui.horizontal(|ui| {
|
||||
let n = sheets.len();
|
||||
if ui.add_enabled(d.sheet > 0, egui::Button::new("‹")).on_hover_text("Previous sheet").clicked() {
|
||||
if ui.add_enabled(d.sheet > 0, egui::Button::new("‹")).on_hover_text(tl!("Previous sheet")).clicked() {
|
||||
d.sheet -= 1;
|
||||
}
|
||||
ui.label(if n == 0 { "No sheets".to_string() } else { format!("Sheet {} of {n}", d.sheet + 1) });
|
||||
if ui.add_enabled(d.sheet + 1 < n, egui::Button::new("›")).on_hover_text("Next sheet").clicked() {
|
||||
ui.label(if n == 0 {
|
||||
tl!("No sheets").to_string()
|
||||
} else {
|
||||
crate::i18n::fmt(tl!("Sheet {s} of {n}"), &[("s", &(d.sheet + 1).to_string()), ("n", &n.to_string())])
|
||||
});
|
||||
if ui.add_enabled(d.sheet + 1 < n, egui::Button::new("›")).on_hover_text(tl!("Next sheet")).clicked() {
|
||||
d.sheet += 1;
|
||||
}
|
||||
});
|
||||
|
|
@ -491,7 +506,7 @@ pub(crate) fn body(
|
|||
if ui.add_enabled_ui(settings.is_ok() && !sheets.is_empty(), |ui| widgets::pill_button(ui, label, true)).inner.clicked() {
|
||||
go = true;
|
||||
}
|
||||
if widgets::pill_button(ui, "Cancel", false).clicked() {
|
||||
if widgets::pill_button(ui, tl!("Cancel"), false).clicked() {
|
||||
cancel = true;
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -84,8 +84,9 @@ fn radio(ui: &mut egui::Ui, t: &Tokens, on: bool, label: &str) -> egui::Response
|
|||
} else {
|
||||
ui.painter().circle_stroke(c, 7.5, egui::Stroke::new(1.5, t.text_muted));
|
||||
}
|
||||
ui.painter().text(egui::pos2(rect.left() + 26.0, rect.center().y), egui::Align2::LEFT_CENTER, label, theme::regular(14.0), t.text);
|
||||
resp.widget_info(|| egui::WidgetInfo::selected(egui::WidgetType::RadioButton, true, on, label));
|
||||
ui.painter().text(egui::pos2(rect.left() + 26.0, rect.center().y), egui::Align2::LEFT_CENTER, tl!(label), theme::regular(14.0), t.text);
|
||||
let info = tl!(label).to_string();
|
||||
resp.widget_info(|| egui::WidgetInfo::selected(egui::WidgetType::RadioButton, true, on, info.clone()));
|
||||
resp.on_hover_cursor(egui::CursorIcon::PointingHand)
|
||||
}
|
||||
|
||||
|
|
@ -93,11 +94,11 @@ fn radio(ui: &mut egui::Ui, t: &Tokens, on: bool, label: &str) -> egui::Response
|
|||
pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> (bool, bool) {
|
||||
use pdfcraft_engine::Algorithm as A;
|
||||
let d = &mut app.protect_draft;
|
||||
ui.label(egui::RichText::new("Protect Using Password").font(theme::semibold(18.0)));
|
||||
ui.label(egui::RichText::new(tl!("Protect Using Password")).font(theme::semibold(18.0)));
|
||||
ui.add_space(4.0);
|
||||
ui.separator();
|
||||
ui.add_space(6.0);
|
||||
ui.label("Requires user to enter a password for:");
|
||||
ui.label(tl!("Requires user to enter a password for:"));
|
||||
ui.add_space(4.0);
|
||||
if radio(ui, t, d.viewing, "Viewing").clicked() {
|
||||
d.viewing = true;
|
||||
|
|
@ -108,7 +109,7 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> (boo
|
|||
ui.add_space(10.0);
|
||||
// Bordered fields (Acrobat: 1 pt #B1B1B1, radius 4).
|
||||
let field = |ui: &mut egui::Ui, label: &str, text: &mut String, id: &str| {
|
||||
let l = ui.label(label);
|
||||
let l = ui.label(tl!(label));
|
||||
egui::Frame::new()
|
||||
.stroke(egui::Stroke::new(1.0, t.border))
|
||||
.corner_radius(egui::CornerRadius::same(4))
|
||||
|
|
@ -122,25 +123,33 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> (boo
|
|||
if r.changed() || !d.password.is_empty() {
|
||||
let (s, c) = strength(&d.password);
|
||||
if !d.password.is_empty() {
|
||||
ui.label(egui::RichText::new(format!("Strength: {s}")).font(theme::medium(11.5)).color(c));
|
||||
ui.label(egui::RichText::new(crate::i18n::fmt(tl!("Strength: {s}"), &[("s", tl!(s))])).font(theme::medium(11.5)).color(c));
|
||||
}
|
||||
}
|
||||
ui.add_space(6.0);
|
||||
field(ui, "Re-type Password", &mut d.confirm, "protect-pw2");
|
||||
ui.add_space(6.0);
|
||||
let chevron = if d.advanced { "⌃" } else { "⌄" };
|
||||
if ui.add(egui::Button::new(egui::RichText::new(format!("Advanced Options {chevron}")).font(theme::semibold(13.0))).frame(false)).clicked() {
|
||||
if ui
|
||||
.add(
|
||||
egui::Button::new(
|
||||
egui::RichText::new(crate::i18n::fmt(tl!("Advanced Options {chevron}"), &[("chevron", chevron)])).font(theme::semibold(13.0)),
|
||||
)
|
||||
.frame(false),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
d.advanced = !d.advanced;
|
||||
}
|
||||
if d.advanced {
|
||||
let p = &mut d.protection;
|
||||
egui::Grid::new("protect-advanced").num_columns(2).spacing([12.0, 8.0]).show(ui, |ui| {
|
||||
ui.label("Compatibility");
|
||||
ui.label(tl!("Compatibility"));
|
||||
let algos = [
|
||||
(A::Aes256, "Acrobat X and later (256-bit AES)"),
|
||||
(A::Aes128, "Acrobat 7.0 and later (128-bit AES)"),
|
||||
(A::Rc4_128, "Acrobat 5.0 and later (128-bit RC4)"),
|
||||
(A::Rc4_40, "Acrobat 3.0 and later (40-bit RC4)"),
|
||||
(A::Aes256, tl!("Acrobat X and later (256-bit AES)")),
|
||||
(A::Aes128, tl!("Acrobat 7.0 and later (128-bit AES)")),
|
||||
(A::Rc4_128, tl!("Acrobat 5.0 and later (128-bit RC4)")),
|
||||
(A::Rc4_40, tl!("Acrobat 3.0 and later (40-bit RC4)")),
|
||||
];
|
||||
let cur = algos.iter().find(|(a, _)| *a == p.algorithm).map_or(algos[0].1, |(_, l)| *l);
|
||||
egui::ComboBox::from_id_salt("protect-algo").width(300.0).selected_text(cur).show_ui(ui, |ui| {
|
||||
|
|
@ -149,34 +158,35 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> (boo
|
|||
}
|
||||
});
|
||||
ui.end_row();
|
||||
ui.label("Encrypt");
|
||||
ui.label(tl!("Encrypt"));
|
||||
ui.vertical(|ui| {
|
||||
ui.radio_value(&mut p.encrypt_metadata, true, "All document contents");
|
||||
ui.radio_value(&mut p.encrypt_metadata, true, tl!("All document contents"));
|
||||
// Unencrypted metadata needs crypt filters (Acrobat 6.0 and later).
|
||||
ui.add_enabled_ui(p.algorithm != A::Rc4_40 && p.algorithm != A::Rc4_128, |ui| {
|
||||
ui.radio_value(&mut p.encrypt_metadata, false, "All document contents except metadata");
|
||||
ui.radio_value(&mut p.encrypt_metadata, false, tl!("All document contents except metadata"));
|
||||
});
|
||||
});
|
||||
ui.end_row();
|
||||
if !d.viewing {
|
||||
ui.label("Printing allowed");
|
||||
let opts = [(Printing::None, "None"), (Printing::Low, "Low Resolution (150 dpi)"), (Printing::High, "High Resolution")];
|
||||
let cur = opts.iter().find(|(o, _)| *o == p.printing).map_or("High Resolution", |(_, l)| *l);
|
||||
ui.label(tl!("Printing allowed"));
|
||||
let opts =
|
||||
[(Printing::None, tl!("None")), (Printing::Low, tl!("Low Resolution (150 dpi)")), (Printing::High, tl!("High Resolution"))];
|
||||
let cur = opts.iter().find(|(o, _)| *o == p.printing).map_or(tl!("High Resolution"), |(_, l)| *l);
|
||||
egui::ComboBox::from_id_salt("protect-print").width(300.0).selected_text(cur).show_ui(ui, |ui| {
|
||||
for (o, l) in opts {
|
||||
ui.selectable_value(&mut p.printing, o, l);
|
||||
}
|
||||
});
|
||||
ui.end_row();
|
||||
ui.label("Changes allowed");
|
||||
ui.label(tl!("Changes allowed"));
|
||||
let opts = [
|
||||
(Changes::None, "None"),
|
||||
(Changes::Pages, "Inserting, deleting, and rotating pages"),
|
||||
(Changes::FillSign, "Filling in form fields and signing existing signature fields"),
|
||||
(Changes::CommentFillSign, "Commenting, filling in form fields, and signing existing signature fields"),
|
||||
(Changes::AnyExceptExtract, "Any except extracting pages"),
|
||||
(Changes::None, tl!("None")),
|
||||
(Changes::Pages, tl!("Inserting, deleting, and rotating pages")),
|
||||
(Changes::FillSign, tl!("Filling in form fields and signing existing signature fields")),
|
||||
(Changes::CommentFillSign, tl!("Commenting, filling in form fields, and signing existing signature fields")),
|
||||
(Changes::AnyExceptExtract, tl!("Any except extracting pages")),
|
||||
];
|
||||
let cur = opts.iter().find(|(o, _)| *o == p.changes).map_or("None", |(_, l)| *l);
|
||||
let cur = opts.iter().find(|(o, _)| *o == p.changes).map_or(tl!("None"), |(_, l)| *l);
|
||||
egui::ComboBox::from_id_salt("protect-changes").width(300.0).selected_text(cur).show_ui(ui, |ui| {
|
||||
for (o, l) in opts {
|
||||
ui.selectable_value(&mut p.changes, o, l);
|
||||
|
|
@ -185,8 +195,8 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> (boo
|
|||
ui.end_row();
|
||||
ui.label("");
|
||||
ui.vertical(|ui| {
|
||||
ui.checkbox(&mut p.copy, "Enable copying of text, images, and other content");
|
||||
ui.checkbox(&mut p.accessibility, "Enable text access for screen reader devices for the visually impaired");
|
||||
ui.checkbox(&mut p.copy, tl!("Enable copying of text, images, and other content"));
|
||||
ui.checkbox(&mut p.accessibility, tl!("Enable text access for screen reader devices for the visually impaired"));
|
||||
});
|
||||
ui.end_row();
|
||||
}
|
||||
|
|
@ -197,13 +207,14 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> (boo
|
|||
}
|
||||
ui.add_space(8.0);
|
||||
if let Some(problem) = d.problem().filter(|_| !d.password.is_empty() || !d.confirm.is_empty()) {
|
||||
ui.label(egui::RichText::new(problem).color(t.text_muted).font(theme::regular(12.0)));
|
||||
// problem() stays English (tested); only the display translates.
|
||||
ui.label(egui::RichText::new(tl!(problem)).color(t.text_muted).font(theme::regular(12.0)));
|
||||
}
|
||||
ui.label(
|
||||
egui::RichText::new(if d.viewing {
|
||||
"Anyone opening the document will need this password. It's applied when you save."
|
||||
tl!("Anyone opening the document will need this password. It's applied when you save.")
|
||||
} else {
|
||||
"The document opens without a password; this password is needed to change the restrictions. It's applied when you save."
|
||||
tl!("The document opens without a password; this password is needed to change the restrictions. It's applied when you save.")
|
||||
})
|
||||
.color(t.text_faint)
|
||||
.font(theme::regular(11.5)),
|
||||
|
|
@ -212,10 +223,10 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> (boo
|
|||
let (mut apply, mut cancel) = (false, false);
|
||||
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
|
||||
let ok = d.problem().is_none();
|
||||
if ui.add_enabled_ui(ok, |ui| widgets::pill_button(ui, "Apply", true)).inner.clicked() {
|
||||
if ui.add_enabled_ui(ok, |ui| widgets::pill_button(ui, tl!("Apply"), true)).inner.clicked() {
|
||||
apply = true;
|
||||
}
|
||||
if widgets::pill_button(ui, "Cancel", false).clicked() {
|
||||
if widgets::pill_button(ui, tl!("Cancel"), false).clicked() {
|
||||
cancel = true;
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -162,7 +162,7 @@ impl PdfCraftApp {
|
|||
let bytes = match store.read(key) {
|
||||
Ok(b) => b,
|
||||
Err(e) => {
|
||||
self.notify(format!("Couldn't recover {}: {e}", meta.name));
|
||||
self.notify_fmt("Couldn't recover {name}: {e}", &[("name", &meta.name), ("e", &e.to_string())]);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
|
@ -173,7 +173,7 @@ impl PdfCraftApp {
|
|||
match self.open_bytes(&meta.name, None, bytes) {
|
||||
Ok(()) if self.password_prompt.is_none() => self.finish_recovery(&meta),
|
||||
Ok(()) => {}
|
||||
Err(e) => self.notify(format!("Couldn't recover {}: {e}", meta.name)),
|
||||
Err(e) => self.notify_fmt("Couldn't recover {name}: {e}", &[("name", &meta.name), ("e", &e.to_string())]),
|
||||
}
|
||||
}
|
||||
self.recoverable.retain(|m| !keys.contains(&m.key));
|
||||
|
|
|
|||
|
|
@ -201,15 +201,15 @@ impl PdfCraftApp {
|
|||
|
||||
/// Redact pages ("Mark Page Range"). Returns (apply, cancel).
|
||||
pub(crate) fn pages_body(ui: &mut egui::Ui, d: &mut PagesDraft, pages: usize, _t: &Tokens) -> (bool, bool) {
|
||||
ui.label(egui::RichText::new("Mark Page Range").font(crate::theme::semibold(18.0)));
|
||||
ui.label(egui::RichText::new(tl!("Mark Page Range")).font(crate::theme::semibold(18.0)));
|
||||
ui.add_space(8.0);
|
||||
ui.radio_value(&mut d.current, true, "Current page");
|
||||
ui.radio_value(&mut d.current, true, tl!("Current page"));
|
||||
ui.horizontal(|ui| {
|
||||
ui.radio_value(&mut d.current, false, "Pages from");
|
||||
ui.radio_value(&mut d.current, false, tl!("Pages from"));
|
||||
ui.add_enabled(!d.current, egui::DragValue::new(&mut d.from).range(1..=pages));
|
||||
ui.label("to");
|
||||
ui.label(tl!("to"));
|
||||
ui.add_enabled(!d.current, egui::DragValue::new(&mut d.to).range(1..=pages));
|
||||
ui.label(format!("of {pages}"));
|
||||
ui.label(crate::i18n::fmt(tl!("of {name}"), &[("name", &pages.to_string())]));
|
||||
});
|
||||
d.to = d.to.max(d.from);
|
||||
ui.add_space(12.0);
|
||||
|
|
@ -219,28 +219,33 @@ pub(crate) fn pages_body(ui: &mut egui::Ui, d: &mut PagesDraft, pages: usize, _t
|
|||
/// Find text and redact. Returns (search, cancel).
|
||||
pub(crate) fn search_body(ui: &mut egui::Ui, d: &mut SearchDraft, t: &Tokens) -> (bool, bool) {
|
||||
ui.set_width(420.0);
|
||||
ui.label(egui::RichText::new("Find text and redact").font(crate::theme::semibold(18.0)));
|
||||
ui.label(egui::RichText::new(tl!("Find text and redact")).font(crate::theme::semibold(18.0)));
|
||||
ui.add_space(8.0);
|
||||
ui.radio_value(&mut d.patterns, false, "Single word or phrase");
|
||||
ui.radio_value(&mut d.patterns, false, tl!("Single word or phrase"));
|
||||
let mut enter = false;
|
||||
ui.add_enabled_ui(!d.patterns, |ui| {
|
||||
let r = ui.add(egui::TextEdit::singleline(&mut d.text).hint_text("Text to find").desired_width(380.0));
|
||||
let r = ui.add(egui::TextEdit::singleline(&mut d.text).hint_text(tl!("Text to find")).desired_width(380.0));
|
||||
enter = r.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter));
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
ui.radio_value(&mut d.patterns, true, "Patterns");
|
||||
ui.radio_value(&mut d.patterns, true, tl!("Patterns"));
|
||||
ui.add_enabled_ui(d.patterns, |ui| {
|
||||
egui::ComboBox::from_id_salt("redact-pattern").selected_text(d.pattern.label()).width(240.0).show_ui(ui, |ui| {
|
||||
egui::ComboBox::from_id_salt("redact-pattern").selected_text(tl!(d.pattern.label())).width(240.0).show_ui(ui, |ui| {
|
||||
for p in REDACT_PATTERNS {
|
||||
ui.selectable_value(&mut d.pattern, p, p.label());
|
||||
ui.selectable_value(&mut d.pattern, p, tl!(p.label()));
|
||||
}
|
||||
});
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
ui.label(egui::RichText::new("Text in images isn't found; recognise text (OCR) first.").small().color(t.text_faint));
|
||||
ui.label(egui::RichText::new(tl!("Text in images isn't found; recognise text (OCR) first.")).small().color(t.text_faint));
|
||||
if let Some(n) = d.found {
|
||||
ui.label(
|
||||
egui::RichText::new(if n == 0 { "No matches.".to_string() } else { format!("{n} match(es) marked for redaction.") }).color(t.text_muted),
|
||||
egui::RichText::new(if n == 0 {
|
||||
tl!("No matches.").to_string()
|
||||
} else {
|
||||
crate::i18n::fmt(tl!("{n} match(es) marked for redaction."), &[("n", &n.to_string())])
|
||||
})
|
||||
.color(t.text_muted),
|
||||
);
|
||||
}
|
||||
ui.add_space(12.0);
|
||||
|
|
@ -251,13 +256,13 @@ pub(crate) fn search_body(ui: &mut egui::Ui, d: &mut SearchDraft, t: &Tokens) ->
|
|||
/// Redaction Tool Properties. Returns (apply, cancel).
|
||||
pub(crate) fn props_body(ui: &mut egui::Ui, d: &mut RedactPrefs, _t: &Tokens) -> (bool, bool) {
|
||||
ui.set_width(380.0);
|
||||
ui.label(egui::RichText::new("Redaction Tool Properties").font(crate::theme::semibold(18.0)));
|
||||
ui.label(egui::RichText::new(tl!("Redaction Tool Properties")).font(crate::theme::semibold(18.0)));
|
||||
ui.add_space(8.0);
|
||||
egui::Grid::new("redact-props").num_columns(2).spacing([12.0, 10.0]).show(ui, |ui| {
|
||||
ui.label("Redacted area fill colour:");
|
||||
ui.label(tl!("Redacted area fill colour:"));
|
||||
ui.horizontal(|ui| {
|
||||
let mut none = d.fill.is_none();
|
||||
if ui.checkbox(&mut none, "No colour").changed() {
|
||||
if ui.checkbox(&mut none, tl!("No colour")).changed() {
|
||||
d.fill = if none { None } else { Some([0.0, 0.0, 0.0]) };
|
||||
}
|
||||
});
|
||||
|
|
@ -268,14 +273,14 @@ pub(crate) fn props_body(ui: &mut egui::Ui, d: &mut RedactPrefs, _t: &Tokens) ->
|
|||
}
|
||||
ui.end_row();
|
||||
ui.label("");
|
||||
ui.checkbox(&mut d.use_overlay, "Use overlay text");
|
||||
ui.checkbox(&mut d.use_overlay, tl!("Use overlay text"));
|
||||
ui.end_row();
|
||||
let l = ui.label("Custom text:");
|
||||
let l = ui.label(tl!("Custom text:"));
|
||||
ui.add_enabled(d.use_overlay, egui::TextEdit::singleline(&mut d.overlay).desired_width(220.0)).labelled_by(l.id);
|
||||
ui.end_row();
|
||||
let on = d.use_overlay;
|
||||
let look = &mut d.look;
|
||||
ui.label("Font:");
|
||||
ui.label(tl!("Font:"));
|
||||
ui.add_enabled_ui(on, |ui| {
|
||||
egui::ComboBox::from_id_salt("overlay-font").selected_text(look.font.name()).show_ui(ui, |ui| {
|
||||
for f in pdfcraft_engine::OverlayFont::ALL {
|
||||
|
|
@ -284,11 +289,11 @@ pub(crate) fn props_body(ui: &mut egui::Ui, d: &mut RedactPrefs, _t: &Tokens) ->
|
|||
});
|
||||
});
|
||||
ui.end_row();
|
||||
ui.label("Font size:");
|
||||
ui.label(tl!("Font size:"));
|
||||
ui.add_enabled_ui(on, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
let mut auto = look.size <= 0.0;
|
||||
if ui.checkbox(&mut auto, "Auto-size text to fit redaction region").changed() {
|
||||
if ui.checkbox(&mut auto, tl!("Auto-size text to fit redaction region")).changed() {
|
||||
look.size = if auto { 0.0 } else { 10.0 };
|
||||
}
|
||||
if !auto {
|
||||
|
|
@ -297,7 +302,7 @@ pub(crate) fn props_body(ui: &mut egui::Ui, d: &mut RedactPrefs, _t: &Tokens) ->
|
|||
});
|
||||
});
|
||||
ui.end_row();
|
||||
ui.label("Font colour:");
|
||||
ui.label(tl!("Font colour:"));
|
||||
ui.add_enabled_ui(on, |ui| {
|
||||
if let Some(c) = crate::comments::swatch_grid(ui, Some(look.color)) {
|
||||
look.color = c;
|
||||
|
|
@ -305,12 +310,12 @@ pub(crate) fn props_body(ui: &mut egui::Ui, d: &mut RedactPrefs, _t: &Tokens) ->
|
|||
});
|
||||
ui.end_row();
|
||||
ui.label("");
|
||||
ui.add_enabled(on, egui::Checkbox::new(&mut look.repeat, "Repeat overlay text"));
|
||||
ui.add_enabled(on, egui::Checkbox::new(&mut look.repeat, tl!("Repeat overlay text")));
|
||||
ui.end_row();
|
||||
ui.label("Text alignment:");
|
||||
ui.label(tl!("Text alignment:"));
|
||||
ui.add_enabled_ui(on, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
for (a, label) in [(0u8, "Left"), (1, "Center"), (2, "Right")] {
|
||||
for (a, label) in [(0u8, tl!("Left")), (1, tl!("Center")), (2, tl!("Right"))] {
|
||||
ui.radio_value(&mut look.align, a, label);
|
||||
}
|
||||
});
|
||||
|
|
@ -324,15 +329,16 @@ pub(crate) fn props_body(ui: &mut egui::Ui, d: &mut RedactPrefs, _t: &Tokens) ->
|
|||
/// Apply redactions confirmation. Returns (apply, cancel).
|
||||
pub(crate) fn apply_body(ui: &mut egui::Ui, marks: usize, t: &Tokens) -> (bool, bool) {
|
||||
ui.set_width(420.0);
|
||||
ui.label(egui::RichText::new("Apply redactions").font(crate::theme::semibold(18.0)));
|
||||
ui.label(egui::RichText::new(tl!("Apply redactions")).font(crate::theme::semibold(18.0)));
|
||||
ui.add_space(8.0);
|
||||
ui.label(format!(
|
||||
"You are about to apply {marks} redaction mark{}. Text, images and drawings under the marks, and comments and form fields that overlap them, are removed permanently.",
|
||||
if marks == 1 { "" } else { "s" }
|
||||
));
|
||||
ui.label(if marks == 1 {
|
||||
tl!("You are about to apply 1 redaction mark. Text, images and drawings under the marks, and comments and form fields that overlap them, are removed permanently.").to_string()
|
||||
} else {
|
||||
crate::i18n::fmt(tl!("You are about to apply {n} redaction marks. Text, images and drawings under the marks, and comments and form fields that overlap them, are removed permanently."), &[("n", &marks.to_string())])
|
||||
});
|
||||
ui.add_space(4.0);
|
||||
ui.label(
|
||||
egui::RichText::new("Save the document afterwards: saving rewrites the whole file so no trace of the removed content stays in it.")
|
||||
egui::RichText::new(tl!("Save the document afterwards: saving rewrites the whole file so no trace of the removed content stays in it."))
|
||||
.small()
|
||||
.color(t.text_muted),
|
||||
);
|
||||
|
|
@ -343,10 +349,11 @@ pub(crate) fn apply_body(ui: &mut egui::Ui, marks: usize, t: &Tokens) -> (bool,
|
|||
fn buttons(ui: &mut egui::Ui, ok: &str, primary: bool) -> (bool, bool) {
|
||||
let (mut a, mut c) = (false, false);
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
if widgets::pill_button(ui, ok, primary).clicked() {
|
||||
// `ok` stays English internally; only the display is translated.
|
||||
if widgets::pill_button(ui, tl!(ok), primary).clicked() {
|
||||
a = true;
|
||||
}
|
||||
if widgets::pill_button(ui, "Cancel", false).clicked() {
|
||||
if widgets::pill_button(ui, tl!("Cancel"), false).clicked() {
|
||||
c = true;
|
||||
}
|
||||
});
|
||||
|
|
@ -372,33 +379,33 @@ impl PdfCraftApp {
|
|||
/// Returns (remove, cancel).
|
||||
pub(crate) fn hidden_body(ui: &mut egui::Ui, d: &mut HiddenDraft, t: &Tokens) -> (bool, bool) {
|
||||
ui.set_width(440.0);
|
||||
ui.label(egui::RichText::new("Remove hidden information").font(crate::theme::semibold(18.0)));
|
||||
ui.label(egui::RichText::new(tl!("Remove hidden information")).font(crate::theme::semibold(18.0)));
|
||||
ui.add_space(4.0);
|
||||
ui.label(egui::RichText::new("Select the items to remove from this document.").color(t.text_muted));
|
||||
ui.label(egui::RichText::new(tl!("Select the items to remove from this document.")).color(t.text_muted));
|
||||
ui.add_space(8.0);
|
||||
let total: usize = d.found.iter().map(|f| f.1).sum();
|
||||
for (h, n, on) in d.found.iter_mut() {
|
||||
ui.add_enabled_ui(*n > 0, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.checkbox(on, h.label());
|
||||
ui.checkbox(on, tl!(h.label()));
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
ui.label(egui::RichText::new(if *n == 0 { "None found".to_string() } else { n.to_string() }).color(t.text_muted));
|
||||
ui.label(egui::RichText::new(if *n == 0 { tl!("None found").to_string() } else { n.to_string() }).color(t.text_muted));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
ui.add_space(6.0);
|
||||
ui.label(
|
||||
egui::RichText::new("Form fields are flattened: their values stay visible. Saving rewrites the whole file.").small().color(t.text_faint),
|
||||
egui::RichText::new(tl!("Form fields are flattened: their values stay visible. Saving rewrites the whole file.")).small().color(t.text_faint),
|
||||
);
|
||||
ui.add_space(12.0);
|
||||
let any = d.found.iter().any(|f| f.2 && f.1 > 0);
|
||||
let (mut a, mut c) = (false, false);
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
if ui.add_enabled_ui(any && total > 0, |ui| widgets::pill_button(ui, "Remove", true)).inner.clicked() {
|
||||
if ui.add_enabled_ui(any && total > 0, |ui| widgets::pill_button(ui, tl!("Remove"), true)).inner.clicked() {
|
||||
a = true;
|
||||
}
|
||||
if widgets::pill_button(ui, "Cancel", false).clicked() {
|
||||
if widgets::pill_button(ui, tl!("Cancel"), false).clicked() {
|
||||
c = true;
|
||||
}
|
||||
});
|
||||
|
|
@ -408,12 +415,12 @@ pub(crate) fn hidden_body(ui: &mut egui::Ui, d: &mut HiddenDraft, t: &Tokens) ->
|
|||
/// Sanitize Document confirmation. Returns (sanitize, cancel).
|
||||
pub(crate) fn sanitize_body(ui: &mut egui::Ui, t: &Tokens) -> (bool, bool) {
|
||||
ui.set_width(440.0);
|
||||
ui.label(egui::RichText::new("Sanitize document").font(crate::theme::semibold(18.0)));
|
||||
ui.label(egui::RichText::new(tl!("Sanitize document")).font(crate::theme::semibold(18.0)));
|
||||
ui.add_space(8.0);
|
||||
ui.label("Sanitizing removes hidden information from the document: metadata, file attachments, comments, form fields (flattened), hidden text and layers, bookmarks, links, actions and scripts, and private application data.");
|
||||
ui.label(tl!("Sanitizing removes hidden information from the document: metadata, file attachments, comments, form fields (flattened), hidden text and layers, bookmarks, links, actions and scripts, and private application data."));
|
||||
ui.add_space(4.0);
|
||||
ui.label(
|
||||
egui::RichText::new("Save the document afterwards; saving rewrites the whole file so nothing removed stays in it.")
|
||||
egui::RichText::new(tl!("Save the document afterwards; saving rewrites the whole file so nothing removed stays in it."))
|
||||
.small()
|
||||
.color(t.text_muted),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -14,9 +14,11 @@ pub(crate) fn panel(ui: &mut egui::Ui, t: &Tokens, view: &mut DocView, pages: us
|
|||
let searched = view.texts.len() + view.text_failed.len();
|
||||
let Some(find) = view.find.as_mut() else { return };
|
||||
find.in_panel = true;
|
||||
let l = ui.label(egui::RichText::new("What word or phrase would you like to search for?").color(t.text_muted));
|
||||
let l = ui.label(egui::RichText::new(tl!("What word or phrase would you like to search for?")).color(t.text_muted));
|
||||
let r = ui
|
||||
.add(egui::TextEdit::singleline(&mut find.query).id(egui::Id::new("search-panel-input")).hint_text("Search").desired_width(f32::INFINITY))
|
||||
.add(
|
||||
egui::TextEdit::singleline(&mut find.query).id(egui::Id::new("search-panel-input")).hint_text(tl!("Search")).desired_width(f32::INFINITY),
|
||||
)
|
||||
.labelled_by(l.id);
|
||||
if find.focus {
|
||||
r.request_focus();
|
||||
|
|
@ -27,8 +29,8 @@ pub(crate) fn panel(ui: &mut egui::Ui, t: &Tokens, view: &mut DocView, pages: us
|
|||
let w = &mut ui.visuals_mut().widgets;
|
||||
w.inactive.bg_stroke = egui::Stroke::new(1.0, t.border);
|
||||
w.inactive.bg_fill = t.hover;
|
||||
let a = ui.checkbox(&mut find.whole_words, "Whole words only").changed();
|
||||
let b = ui.checkbox(&mut find.case_sensitive, "Case-sensitive").changed();
|
||||
let a = ui.checkbox(&mut find.whole_words, tl!("Whole words only")).changed();
|
||||
let b = ui.checkbox(&mut find.case_sensitive, tl!("Case-sensitive")).changed();
|
||||
if a || b {
|
||||
find.case_query.clear();
|
||||
}
|
||||
|
|
@ -41,11 +43,17 @@ pub(crate) fn panel(ui: &mut egui::Ui, t: &Tokens, view: &mut DocView, pages: us
|
|||
let status = if find.query.trim().is_empty() {
|
||||
String::new()
|
||||
} else if find.matches.is_empty() && searched < pages {
|
||||
format!("Searching… {searched} of {pages} pages")
|
||||
crate::i18n::fmt(tl!("Searching… {s} of {p} pages"), &[("s", &searched.to_string()), ("p", &pages.to_string())])
|
||||
} else {
|
||||
let n = find.matches.len();
|
||||
let more = if searched < pages { format!(" (searching… {searched} of {pages} pages)") } else { String::new() };
|
||||
format!("{n} instance{}{more}", if n == 1 { "" } else { "s" })
|
||||
let more = if searched < pages {
|
||||
crate::i18n::fmt(tl!(" (searching… {s} of {p} pages)"), &[("s", &searched.to_string()), ("p", &pages.to_string())])
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
// English keeps its singular; Chinese has no plural.
|
||||
let count = if n == 1 { crate::i18n::fmt(tl!("1 instance"), &[]) } else { crate::i18n::fmt(tl!("{n} instances"), &[("n", &n.to_string())]) };
|
||||
format!("{count}{more}")
|
||||
};
|
||||
ui.label(egui::RichText::new(status).font(theme::semibold(12.5)));
|
||||
ui.add_space(4.0);
|
||||
|
|
@ -54,7 +62,7 @@ pub(crate) fn panel(ui: &mut egui::Ui, t: &Tokens, view: &mut DocView, pages: us
|
|||
for (i, (p, range)) in find.matches.iter().enumerate().take(2000) {
|
||||
if last_page != Some(*p) {
|
||||
ui.add_space(4.0);
|
||||
ui.label(egui::RichText::new(format!("Page {}", p + 1)).small().color(t.text_faint));
|
||||
ui.label(egui::RichText::new(crate::i18n::fmt(tl!("Page {label}"), &[("label", &(p + 1).to_string())])).small().color(t.text_faint));
|
||||
last_page = Some(*p);
|
||||
}
|
||||
let Some(text) = view.texts.get(p) else { continue };
|
||||
|
|
@ -80,7 +88,7 @@ pub(crate) fn panel(ui: &mut egui::Ui, t: &Tokens, view: &mut DocView, pages: us
|
|||
job.append(&format!(" {}…", flat(after)), 0.0, plain);
|
||||
job.wrap.max_width = ui.available_width();
|
||||
let selected = find.current == Some(i);
|
||||
let resp = ui.add(egui::Button::selectable(selected, job).wrap()).on_hover_text("Go to this result");
|
||||
let resp = ui.add(egui::Button::selectable(selected, job).wrap()).on_hover_text(tl!("Go to this result"));
|
||||
if resp.clicked() {
|
||||
go = Some(i);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -199,7 +199,7 @@ impl PdfCraftApp {
|
|||
self.digital_ids.push(entry_for(&sign::keychain::reference(&id.certificate), &id.certificate));
|
||||
}
|
||||
}
|
||||
Err(e) => self.notify(format!("The Keychain's digital IDs couldn't be listed: {e}")),
|
||||
Err(e) => self.notify_fmt("The Keychain's digital IDs couldn't be listed: {e}", &[("e", &e.to_string())]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -309,7 +309,7 @@ impl PdfCraftApp {
|
|||
self.views[i].invalidate_content();
|
||||
}
|
||||
self.right = Some(crate::RightPanel::Signatures);
|
||||
self.notify(format!("Signed and saved to {}", path.display()));
|
||||
self.notify_fmt("Signed and saved to {path}", &[("path", &path.display().to_string())]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -331,7 +331,7 @@ impl PdfCraftApp {
|
|||
self.views.push(DocView::new(new, &doc.info));
|
||||
self.active = Some(self.views.len() - 1);
|
||||
}
|
||||
Err(e) => self.notify(format!("Couldn't open revision {n}: {e}")),
|
||||
Err(e) => self.notify_fmt("Couldn't open revision {n}: {e}", &[("n", &n.to_string()), ("e", &e.to_string())]),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -347,7 +347,7 @@ impl PdfCraftApp {
|
|||
self.views.push(DocView::new(new, &doc.info));
|
||||
self.active = Some(self.views.len() - 1);
|
||||
}
|
||||
Err(e) => self.notify(format!("Couldn't open the signed version: {e}")),
|
||||
Err(e) => self.notify_fmt("Couldn't open the signed version: {e}", &[("e", &e.to_string())]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -363,7 +363,7 @@ pub(crate) fn dialog(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> bo
|
|||
}
|
||||
|
||||
fn title(ui: &mut egui::Ui, text: &str) {
|
||||
ui.label(egui::RichText::new(text).font(theme::semibold(18.0)));
|
||||
ui.label(egui::RichText::new(tl!(text)).font(theme::semibold(18.0)));
|
||||
ui.add_space(4.0);
|
||||
ui.separator();
|
||||
ui.add_space(8.0);
|
||||
|
|
@ -378,7 +378,7 @@ fn error(ui: &mut egui::Ui, err: &Option<String>) {
|
|||
|
||||
fn choose(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> bool {
|
||||
title(ui, "Sign with a Digital ID");
|
||||
ui.label("Choose the digital ID that you want to use for signing:");
|
||||
ui.label(tl!("Choose the digital ID that you want to use for signing:"));
|
||||
ui.add_space(6.0);
|
||||
let ids = app.digital_ids.clone();
|
||||
let Some(d) = app.sign_draft.as_mut() else { return true };
|
||||
|
|
@ -399,11 +399,13 @@ fn choose(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> bool {
|
|||
icons::paint(ui, Rect::from_min_size(rect.min + vec2(10.0, 15.0), vec2(20.0, 20.0)), "badge-check", 18.0, t.accent);
|
||||
ui.painter().text(rect.min + vec2(40.0, 9.0), egui::Align2::LEFT_TOP, &e.name, theme::semibold(13.0), t.text);
|
||||
let sub = format!(
|
||||
"{}{}Issued by: {}, Expires: {}",
|
||||
if e.path.starts_with("keychain:") { "Keychain · " } else { "" },
|
||||
if e.email.is_empty() { String::new() } else { format!("{} · ", e.email) },
|
||||
e.issuer,
|
||||
e.expires
|
||||
"{keychain}{email}{issued}{issuer}{expires}{date}",
|
||||
keychain = if e.path.starts_with("keychain:") { tl!("Keychain · ").to_string() } else { String::new() },
|
||||
email = if e.email.is_empty() { String::new() } else { format!("{} · ", e.email) },
|
||||
issued = tl!("Issued by: "),
|
||||
issuer = e.issuer,
|
||||
expires = tl!(", Expires: "),
|
||||
date = e.expires,
|
||||
);
|
||||
ui.painter().text(rect.min + vec2(40.0, 28.0), egui::Align2::LEFT_TOP, sub, theme::regular(11.5), t.text_muted);
|
||||
if resp.clicked() {
|
||||
|
|
@ -414,21 +416,21 @@ fn choose(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> bool {
|
|||
}
|
||||
}
|
||||
if ids.is_empty() {
|
||||
ui.label(egui::RichText::new("No digital IDs yet.").color(t.text_muted));
|
||||
ui.label(egui::RichText::new(tl!("No digital IDs yet.")).color(t.text_muted));
|
||||
}
|
||||
});
|
||||
ui.add_space(10.0);
|
||||
ui.horizontal(|ui| {
|
||||
if widgets::pill_button(ui, "Configure New Digital ID", false).clicked() {
|
||||
if widgets::pill_button(ui, tl!("Configure New Digital ID"), false).clicked() {
|
||||
d.step = SignStep::Configure;
|
||||
d.error = None;
|
||||
}
|
||||
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
|
||||
if ui.add_enabled_ui(d.selected.is_some(), |ui| widgets::pill_button(ui, "Continue", true)).inner.clicked() {
|
||||
if ui.add_enabled_ui(d.selected.is_some(), |ui| widgets::pill_button(ui, tl!("Continue"), true)).inner.clicked() {
|
||||
d.step = SignStep::SignAs;
|
||||
d.error = None;
|
||||
}
|
||||
if widgets::pill_button(ui, "Cancel", false).clicked() {
|
||||
if widgets::pill_button(ui, tl!("Cancel"), false).clicked() {
|
||||
close = true;
|
||||
}
|
||||
});
|
||||
|
|
@ -441,58 +443,58 @@ fn configure(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> bool {
|
|||
let Some(d) = app.sign_draft.as_mut() else { return true };
|
||||
let mut close = false;
|
||||
let mut go = false;
|
||||
ui.radio_value(&mut d.new_id.create, false, "Use a Digital ID from a file");
|
||||
ui.radio_value(&mut d.new_id.create, true, "Create a new Digital ID (self-signed, saved to a password-protected file)");
|
||||
ui.radio_value(&mut d.new_id.create, false, tl!("Use a Digital ID from a file"));
|
||||
ui.radio_value(&mut d.new_id.create, true, tl!("Create a new Digital ID (self-signed, saved to a password-protected file)"));
|
||||
ui.add_space(8.0);
|
||||
let n = &mut d.new_id;
|
||||
if n.create {
|
||||
egui::Grid::new("new-id").num_columns(2).spacing([12.0, 8.0]).show(ui, |ui| {
|
||||
for (label, value) in [
|
||||
("Name", &mut n.name),
|
||||
("Organizational Unit", &mut n.unit),
|
||||
("Organization Name", &mut n.organization),
|
||||
("Email Address", &mut n.email),
|
||||
(tl!("Name"), &mut n.name),
|
||||
(tl!("Organizational Unit"), &mut n.unit),
|
||||
(tl!("Organization Name"), &mut n.organization),
|
||||
(tl!("Email Address"), &mut n.email),
|
||||
] {
|
||||
let l = ui.label(label);
|
||||
ui.add(egui::TextEdit::singleline(value).desired_width(260.0)).labelled_by(l.id);
|
||||
ui.end_row();
|
||||
}
|
||||
let l = ui.label("Country/Region");
|
||||
let l = ui.label(tl!("Country/Region"));
|
||||
ui.add(egui::TextEdit::singleline(&mut n.country).hint_text("US").char_limit(2).desired_width(60.0)).labelled_by(l.id);
|
||||
ui.end_row();
|
||||
ui.label("Key Algorithm");
|
||||
ui.label(tl!("Key Algorithm"));
|
||||
egui::ComboBox::from_id_salt("key-alg").selected_text(KEY_ALGORITHMS[n.key].0).show_ui(ui, |ui| {
|
||||
for (i, (label, _)) in KEY_ALGORITHMS.iter().enumerate() {
|
||||
ui.selectable_value(&mut n.key, i, *label);
|
||||
}
|
||||
});
|
||||
ui.end_row();
|
||||
let l = ui.label("Password");
|
||||
let l = ui.label(tl!("Password"));
|
||||
ui.add(egui::TextEdit::singleline(&mut n.password).password(true).desired_width(200.0)).labelled_by(l.id);
|
||||
ui.end_row();
|
||||
let l = ui.label("Confirm Password");
|
||||
let l = ui.label(tl!("Confirm Password"));
|
||||
ui.add(egui::TextEdit::singleline(&mut n.confirm).password(true).desired_width(200.0)).labelled_by(l.id);
|
||||
ui.end_row();
|
||||
});
|
||||
ui.label(
|
||||
egui::RichText::new("Valid for 5 years, for digital signatures. Self-signed IDs are usually not trusted by others.")
|
||||
egui::RichText::new(tl!("Valid for 5 years, for digital signatures. Self-signed IDs are usually not trusted by others."))
|
||||
.small()
|
||||
.color(t.text_muted),
|
||||
);
|
||||
} else {
|
||||
egui::Grid::new("id-file").num_columns(2).spacing([12.0, 8.0]).show(ui, |ui| {
|
||||
let l = ui.label("File (.p12, .pfx)");
|
||||
let l = ui.label(tl!("File (.p12, .pfx)"));
|
||||
ui.horizontal(|ui| {
|
||||
ui.add(egui::TextEdit::singleline(&mut n.file).desired_width(220.0)).labelled_by(l.id);
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
if ui.button("Browse…").clicked()
|
||||
&& let Some(p) = rfd::FileDialog::new().add_filter("Digital ID", &["p12", "pfx"]).pick_file()
|
||||
if ui.button(tl!("Browse…")).clicked()
|
||||
&& let Some(p) = rfd::FileDialog::new().add_filter(tl!("Digital ID"), &["p12", "pfx"]).pick_file()
|
||||
{
|
||||
n.file = p.to_string_lossy().into_owned();
|
||||
}
|
||||
});
|
||||
ui.end_row();
|
||||
let l = ui.label("Password");
|
||||
let l = ui.label(tl!("Password"));
|
||||
ui.add(egui::TextEdit::singleline(&mut n.file_password).password(true).desired_width(200.0)).labelled_by(l.id);
|
||||
ui.end_row();
|
||||
});
|
||||
|
|
@ -500,15 +502,15 @@ fn configure(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> bool {
|
|||
ui.add_space(6.0);
|
||||
error(ui, &d.error);
|
||||
ui.horizontal(|ui| {
|
||||
if !app.digital_ids.is_empty() && widgets::pill_button(ui, "Back", false).clicked() {
|
||||
if !app.digital_ids.is_empty() && widgets::pill_button(ui, tl!("Back"), false).clicked() {
|
||||
d.step = SignStep::Choose;
|
||||
d.error = None;
|
||||
}
|
||||
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
|
||||
if widgets::pill_button(ui, if d.new_id.create { "Save" } else { "Continue" }, true).clicked() {
|
||||
if widgets::pill_button(ui, if d.new_id.create { tl!("Save") } else { tl!("Continue") }, true).clicked() {
|
||||
go = true;
|
||||
}
|
||||
if widgets::pill_button(ui, "Cancel", false).clicked() {
|
||||
if widgets::pill_button(ui, tl!("Cancel"), false).clicked() {
|
||||
close = true;
|
||||
}
|
||||
});
|
||||
|
|
@ -567,34 +569,40 @@ fn sign_as(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> bool {
|
|||
d.step = SignStep::Choose;
|
||||
return false;
|
||||
};
|
||||
title(ui, &format!("{} as \"{}\"", if d.certify.is_some() { "Certify" } else { "Sign" }, entry.name));
|
||||
title(
|
||||
ui,
|
||||
&crate::i18n::fmt(
|
||||
tl!("{verb} as \"{name}\""),
|
||||
&[("verb", if d.certify.is_some() { tl!("Certify") } else { tl!("Sign") }), ("name", &entry.name)],
|
||||
),
|
||||
);
|
||||
let in_keychain = entry.path.starts_with("keychain:");
|
||||
let mut close = false;
|
||||
let mut go = false;
|
||||
if d.rect.is_some() || d.field.is_some() {
|
||||
ui.label(egui::RichText::new("Appearance").font(theme::semibold(12.5)));
|
||||
ui.label(egui::RichText::new(tl!("Appearance")).font(theme::semibold(12.5)));
|
||||
preview(ui, t, &entry.name, d);
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
let a = &mut d.appearance;
|
||||
ui.checkbox(&mut a.name, "Name");
|
||||
ui.checkbox(&mut a.date, "Date");
|
||||
ui.checkbox(&mut a.reason, "Reason");
|
||||
ui.checkbox(&mut a.location, "Location");
|
||||
ui.checkbox(&mut a.distinguished_name, "Distinguished name");
|
||||
ui.checkbox(&mut a.labels, "Labels");
|
||||
ui.checkbox(&mut a.name, tl!("Name"));
|
||||
ui.checkbox(&mut a.date, tl!("Date"));
|
||||
ui.checkbox(&mut a.reason, tl!("Reason"));
|
||||
ui.checkbox(&mut a.location, tl!("Location"));
|
||||
ui.checkbox(&mut a.distinguished_name, tl!("Distinguished name"));
|
||||
ui.checkbox(&mut a.labels, tl!("Labels"));
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
} else {
|
||||
ui.label(egui::RichText::new("The signature will be invisible.").color(t.text_muted));
|
||||
ui.label(egui::RichText::new(tl!("The signature will be invisible.")).color(t.text_muted));
|
||||
}
|
||||
egui::Grid::new("sign-as").num_columns(2).spacing([12.0, 8.0]).show(ui, |ui| {
|
||||
if d.certify.is_some() {
|
||||
ui.label("Permitted actions after certifying");
|
||||
ui.label(tl!("Permitted actions after certifying"));
|
||||
let mut p = d.certify.unwrap_or(2);
|
||||
let label = |p: u8| match p {
|
||||
1 => "No changes allowed",
|
||||
3 => "Annotations, form fill-in, and digital signatures",
|
||||
_ => "Form fill-in and digital signatures",
|
||||
1 => tl!("No changes allowed"),
|
||||
3 => tl!("Annotations, form fill-in, and digital signatures"),
|
||||
_ => tl!("Form fill-in and digital signatures"),
|
||||
};
|
||||
egui::ComboBox::from_id_salt("certify-p").selected_text(label(p)).width(300.0).show_ui(ui, |ui| {
|
||||
for v in [1u8, 2, 3] {
|
||||
|
|
@ -604,17 +612,19 @@ fn sign_as(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> bool {
|
|||
d.certify = Some(p);
|
||||
ui.end_row();
|
||||
}
|
||||
let l = ui.label("Reason");
|
||||
ui.add(egui::TextEdit::singleline(&mut d.reason).hint_text("Optional").desired_width(260.0)).labelled_by(l.id);
|
||||
let l = ui.label(tl!("Reason"));
|
||||
ui.add(egui::TextEdit::singleline(&mut d.reason).hint_text(tl!("Optional")).desired_width(260.0)).labelled_by(l.id);
|
||||
ui.end_row();
|
||||
let l = ui.label("Location");
|
||||
ui.add(egui::TextEdit::singleline(&mut d.location).hint_text("Optional").desired_width(260.0)).labelled_by(l.id);
|
||||
let l = ui.label(tl!("Location"));
|
||||
ui.add(egui::TextEdit::singleline(&mut d.location).hint_text(tl!("Optional")).desired_width(260.0)).labelled_by(l.id);
|
||||
ui.end_row();
|
||||
if in_keychain {
|
||||
ui.label("");
|
||||
ui.label(egui::RichText::new("The key is in the macOS Keychain, which may ask to allow PdfCraft to use it.").small().color(t.text_muted));
|
||||
ui.label(
|
||||
egui::RichText::new(tl!("The key is in the macOS Keychain, which may ask to allow PdfCraft to use it.")).small().color(t.text_muted),
|
||||
);
|
||||
} else {
|
||||
let l = ui.label("Digital ID password");
|
||||
let l = ui.label(tl!("Digital ID password"));
|
||||
let r = ui.add(egui::TextEdit::singleline(&mut d.password).password(true).desired_width(200.0)).labelled_by(l.id);
|
||||
if r.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) {
|
||||
go = true;
|
||||
|
|
@ -625,15 +635,15 @@ fn sign_as(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> bool {
|
|||
ui.add_space(6.0);
|
||||
error(ui, &d.error);
|
||||
ui.horizontal(|ui| {
|
||||
if widgets::pill_button(ui, "Back", false).clicked() {
|
||||
if widgets::pill_button(ui, tl!("Back"), false).clicked() {
|
||||
d.step = SignStep::Choose;
|
||||
d.error = None;
|
||||
}
|
||||
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
|
||||
if widgets::pill_button(ui, "Sign", true).clicked() {
|
||||
if widgets::pill_button(ui, tl!("Sign"), true).clicked() {
|
||||
go = true;
|
||||
}
|
||||
if widgets::pill_button(ui, "Cancel", false).clicked() {
|
||||
if widgets::pill_button(ui, tl!("Cancel"), false).clicked() {
|
||||
close = true;
|
||||
}
|
||||
});
|
||||
|
|
@ -654,8 +664,10 @@ fn sign_as(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> bool {
|
|||
close
|
||||
}
|
||||
|
||||
/// The message bar for signed documents: (icon, colour, text, certified).
|
||||
pub(crate) fn banner(sigs: &[SignatureInfo]) -> Option<(&'static str, Color32, String)> {
|
||||
/// The message bar for signed documents: (icon, colour, template, argument).
|
||||
/// The template renders through [`crate::i18n::tr_fmt`] at the call site, so the bar follows
|
||||
/// the UI language; names inside `argument` stay as they are.
|
||||
pub(crate) fn banner(sigs: &[SignatureInfo]) -> Option<(&'static str, Color32, &'static str, String)> {
|
||||
let signed: Vec<&SignatureInfo> = sigs.iter().filter(|s| s.signed).collect();
|
||||
if signed.is_empty() {
|
||||
return None;
|
||||
|
|
@ -667,14 +679,14 @@ pub(crate) fn banner(sigs: &[SignatureInfo]) -> Option<(&'static str, Color32, S
|
|||
let org = x.subject.organization().map(|o| format!(", {o}")).unwrap_or_default();
|
||||
format!("{}{org}, certificate issued by {}.", x.display_name(), x.issuer.common_name().unwrap_or("an unknown issuer"))
|
||||
});
|
||||
return Some(("badge-check", Color32::from_rgb(0x14, 0x73, 0xE6), format!("Certified by {}", by.unwrap_or_default())));
|
||||
return Some(("badge-check", Color32::from_rgb(0x14, 0x73, 0xE6), "Certified by {by}", by.unwrap_or_default()));
|
||||
}
|
||||
Some(if signed.iter().any(|s| s.status == SignatureStatus::Invalid) {
|
||||
("circle-x", Color32::from_rgb(0xD7, 0x37, 0x3F), "At least one signature is invalid.".into())
|
||||
("circle-x", Color32::from_rgb(0xD7, 0x37, 0x3F), "At least one signature is invalid.", String::new())
|
||||
} else if signed.iter().all(|s| s.status == SignatureStatus::Valid) {
|
||||
("circle-check", Color32::from_rgb(0x2D, 0x9D, 0x78), "Signed and all signatures are valid.".into())
|
||||
("circle-check", Color32::from_rgb(0x2D, 0x9D, 0x78), "Signed and all signatures are valid.", String::new())
|
||||
} else {
|
||||
("triangle-alert", Color32::from_rgb(0xE6, 0x86, 0x19), "At least one signature has problems.".into())
|
||||
("triangle-alert", Color32::from_rgb(0xE6, 0x86, 0x19), "At least one signature has problems.", String::new())
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -707,7 +719,7 @@ pub enum PanelAction {
|
|||
pub(crate) fn panel(ui: &mut egui::Ui, t: &Tokens, sigs: &[SignatureInfo], expanded: &mut Vec<String>) -> Option<PanelAction> {
|
||||
let mut action = None;
|
||||
ui.horizontal(|ui| {
|
||||
if widgets::pill_button(ui, "Validate all", false).clicked() {
|
||||
if widgets::pill_button(ui, tl!("Validate all"), false).clicked() {
|
||||
action = Some(PanelAction::Validate);
|
||||
}
|
||||
});
|
||||
|
|
@ -717,7 +729,7 @@ pub(crate) fn panel(ui: &mut egui::Ui, t: &Tokens, sigs: &[SignatureInfo], expan
|
|||
ui.vertical_centered(|ui| {
|
||||
ui.add(icons::image("signature", 32.0, t.text_faint));
|
||||
ui.add_space(6.0);
|
||||
ui.label(egui::RichText::new("This document has no signatures.").color(t.text_muted));
|
||||
ui.label(egui::RichText::new(tl!("This document has no signatures.")).color(t.text_muted));
|
||||
});
|
||||
return action;
|
||||
}
|
||||
|
|
@ -758,16 +770,16 @@ pub(crate) fn panel(ui: &mut egui::Ui, t: &Tokens, sigs: &[SignatureInfo], expan
|
|||
egui::Frame::NONE.inner_margin(egui::Margin { left: 44, right: 4, top: 2, bottom: 8 }).show(ui, |ui| {
|
||||
ui.spacing_mut().item_spacing.y = 3.0;
|
||||
if !s.signed {
|
||||
if widgets::pill_button(ui, "Sign this field…", true).clicked() {
|
||||
if widgets::pill_button(ui, tl!("Sign this field…"), true).clicked() {
|
||||
action = Some(PanelAction::Sign(s.field.clone()));
|
||||
}
|
||||
return;
|
||||
}
|
||||
ui.label(
|
||||
egui::RichText::new(match s.status {
|
||||
SignatureStatus::Valid => "Signature is valid:",
|
||||
SignatureStatus::Unknown => "Signature validity is UNKNOWN:",
|
||||
SignatureStatus::Invalid => "Signature is INVALID:",
|
||||
SignatureStatus::Valid => tl!("Signature is valid:"),
|
||||
SignatureStatus::Unknown => tl!("Signature validity is UNKNOWN:"),
|
||||
SignatureStatus::Invalid => tl!("Signature is INVALID:"),
|
||||
})
|
||||
.font(theme::semibold(12.0)),
|
||||
);
|
||||
|
|
@ -775,9 +787,9 @@ pub(crate) fn panel(ui: &mut egui::Ui, t: &Tokens, sigs: &[SignatureInfo], expan
|
|||
ui.add(egui::Label::new(egui::RichText::new(format!("• {line}")).font(theme::regular(11.5)).color(t.text_muted)).wrap());
|
||||
}
|
||||
ui.add_space(4.0);
|
||||
ui.label(egui::RichText::new("Signature Details").font(theme::semibold(12.0)));
|
||||
ui.label(egui::RichText::new(tl!("Signature Details")).font(theme::semibold(12.0)));
|
||||
let row = |ui: &mut egui::Ui, k: &str, v: &str| {
|
||||
ui.add(egui::Label::new(egui::RichText::new(format!("{k}: {v}")).font(theme::regular(11.5))).wrap());
|
||||
ui.add(egui::Label::new(egui::RichText::new(format!("{}: {v}", tl!(k))).font(theme::regular(11.5))).wrap());
|
||||
};
|
||||
if let Some(r) = &s.reason {
|
||||
row(ui, "Reason", r);
|
||||
|
|
@ -800,10 +812,13 @@ pub(crate) fn panel(ui: &mut egui::Ui, t: &Tokens, sigs: &[SignatureInfo], expan
|
|||
Some(p) => {
|
||||
let link = ui.add(
|
||||
egui::Label::new(
|
||||
egui::RichText::new(format!("Field: {} on page {}", s.field, p + 1))
|
||||
.font(theme::regular(11.5))
|
||||
.color(t.accent_text)
|
||||
.underline(),
|
||||
egui::RichText::new(crate::i18n::fmt(
|
||||
tl!("Field: {field} on page {p}"),
|
||||
&[("field", &s.field), ("p", &(p + 1).to_string())],
|
||||
))
|
||||
.font(theme::regular(11.5))
|
||||
.color(t.accent_text)
|
||||
.underline(),
|
||||
)
|
||||
.sense(egui::Sense::click()),
|
||||
);
|
||||
|
|
@ -817,22 +832,22 @@ pub(crate) fn panel(ui: &mut egui::Ui, t: &Tokens, sigs: &[SignatureInfo], expan
|
|||
ui.horizontal_wrapped(|ui| {
|
||||
if let Some(c) = s.chain.last().or(s.certificate.as_ref())
|
||||
&& s.status == SignatureStatus::Unknown
|
||||
&& ui.button("Add to trusted certificates").clicked()
|
||||
&& ui.button(tl!("Add to trusted certificates")).clicked()
|
||||
{
|
||||
action = Some(PanelAction::Trust(Box::new(c.clone())));
|
||||
}
|
||||
if ui.button("View signed version").clicked() {
|
||||
if ui.button(tl!("View signed version")).clicked() {
|
||||
action = Some(PanelAction::ViewSigned(s.signed_len));
|
||||
}
|
||||
if let Some(c) = &s.certificate
|
||||
&& ui.button("Show certificate…").clicked()
|
||||
&& ui.button(tl!("Show certificate…")).clicked()
|
||||
{
|
||||
let mut chain = vec![c.clone()];
|
||||
chain.extend(s.chain.iter().filter(|x| x.raw != c.raw).cloned());
|
||||
action = Some(PanelAction::ViewCertificate(chain));
|
||||
}
|
||||
if let Some(c) = &s.certificate
|
||||
&& ui.button("Export certificate…").clicked()
|
||||
&& ui.button(tl!("Export certificate…")).clicked()
|
||||
{
|
||||
action = Some(PanelAction::ExportCertificate(Box::new(c.clone())));
|
||||
}
|
||||
|
|
@ -886,7 +901,7 @@ pub(crate) enum CertAction {
|
|||
/// The Certificate Viewer dialog: the chain on the left, the selected certificate's tabs.
|
||||
pub(crate) fn cert_viewer(ui: &mut egui::Ui, v: &mut CertViewer, trusted: &[Certificate], t: &Tokens) -> (bool, Option<CertAction>) {
|
||||
title(ui, "Certificate Viewer");
|
||||
ui.label(egui::RichText::new("This dialog shows the details of a certificate and its chain.").color(t.text_muted));
|
||||
ui.label(egui::RichText::new(tl!("This dialog shows the details of a certificate and its chain.")).color(t.text_muted));
|
||||
ui.add_space(8.0);
|
||||
let mut action = None;
|
||||
v.selected = v.selected.min(v.chain.len().saturating_sub(1));
|
||||
|
|
@ -909,7 +924,7 @@ pub(crate) fn cert_viewer(ui: &mut egui::Ui, v: &mut CertViewer, trusted: &[Cert
|
|||
ui.set_width(440.0);
|
||||
let Some(c) = v.chain.get(v.selected).cloned() else { return };
|
||||
ui.horizontal(|ui| {
|
||||
for (tab, label) in [(CertTab::Summary, "Summary"), (CertTab::Details, "Details"), (CertTab::Trust, "Trust")] {
|
||||
for (tab, label) in [(CertTab::Summary, tl!("Summary")), (CertTab::Details, tl!("Details")), (CertTab::Trust, tl!("Trust"))] {
|
||||
if widgets::pill_button(ui, label, v.tab == tab).clicked() {
|
||||
v.tab = tab;
|
||||
}
|
||||
|
|
@ -919,7 +934,7 @@ pub(crate) fn cert_viewer(ui: &mut egui::Ui, v: &mut CertViewer, trusted: &[Cert
|
|||
let grid = |ui: &mut egui::Ui, rows: Vec<(&str, String)>| {
|
||||
egui::Grid::new(("cert-rows", v.tab as u8)).num_columns(2).spacing([12.0, 5.0]).show(ui, |ui| {
|
||||
for (k, val) in rows {
|
||||
ui.label(egui::RichText::new(k).color(t.text_muted));
|
||||
ui.label(egui::RichText::new(tl!(k)).color(t.text_muted));
|
||||
ui.add(egui::Label::new(val).wrap());
|
||||
ui.end_row();
|
||||
}
|
||||
|
|
@ -933,7 +948,7 @@ pub(crate) fn cert_viewer(ui: &mut egui::Ui, v: &mut CertViewer, trusted: &[Cert
|
|||
("Issued by", c.issuer.display()),
|
||||
("Valid from", c.not_before.to_string()),
|
||||
("Valid to", c.not_after.to_string()),
|
||||
("Intended usage", c.key_usage.map(key_usage).unwrap_or_else(|| "Any".into())),
|
||||
("Intended usage", c.key_usage.map(key_usage).unwrap_or_else(|| tl!("Any").to_string())),
|
||||
],
|
||||
),
|
||||
CertTab::Details => grid(
|
||||
|
|
@ -947,8 +962,8 @@ pub(crate) fn cert_viewer(ui: &mut egui::Ui, v: &mut CertViewer, trusted: &[Cert
|
|||
("Validity ends", c.not_after.to_string()),
|
||||
("Public key", c.public_key.describe()),
|
||||
("Basic constraints", if c.is_ca { "Certificate authority".into() } else { "End entity".into() }),
|
||||
("Key usage", c.key_usage.map(key_usage).unwrap_or_else(|| "Not present".into())),
|
||||
("Self-signed", if c.is_self_signed() { "Yes".into() } else { "No".into() }),
|
||||
("Key usage", c.key_usage.map(key_usage).unwrap_or_else(|| tl!("Not present").to_string())),
|
||||
("Self-signed", if c.is_self_signed() { tl!("Yes").to_string() } else { tl!("No").to_string() }),
|
||||
("SHA-1 digest", hex(&sign::keys::DigestAlg::Sha1.digest(&[&c.raw]))),
|
||||
("SHA-256 digest", hex(&sign::keys::DigestAlg::Sha256.digest(&[&c.raw]))),
|
||||
],
|
||||
|
|
@ -957,27 +972,27 @@ pub(crate) fn cert_viewer(ui: &mut egui::Ui, v: &mut CertViewer, trusted: &[Cert
|
|||
let is_trusted = trusted.iter().any(|x| x.raw == c.raw);
|
||||
let anchored = v.chain.iter().any(|x| trusted.iter().any(|y| y.raw == x.raw));
|
||||
ui.label(if is_trusted {
|
||||
"This certificate is in your list of trusted certificates."
|
||||
tl!("This certificate is in your list of trusted certificates.")
|
||||
} else if anchored {
|
||||
"This certificate is trusted through a certificate above it in the chain."
|
||||
tl!("This certificate is trusted through a certificate above it in the chain.")
|
||||
} else {
|
||||
"This certificate is not trusted. Signatures made with it show an unknown identity."
|
||||
tl!("This certificate is not trusted. Signatures made with it show an unknown identity.")
|
||||
});
|
||||
ui.add_space(8.0);
|
||||
if !is_trusted && widgets::pill_button(ui, "Add to Trusted Certificates", false).clicked() {
|
||||
if !is_trusted && widgets::pill_button(ui, tl!("Add to Trusted Certificates"), false).clicked() {
|
||||
action = Some(CertAction::Trust(Box::new(c.clone())));
|
||||
}
|
||||
}
|
||||
}
|
||||
ui.add_space(8.0);
|
||||
if widgets::pill_button(ui, "Export…", false).clicked() {
|
||||
if widgets::pill_button(ui, tl!("Export…"), false).clicked() {
|
||||
action = Some(CertAction::Export(Box::new(c.clone())));
|
||||
}
|
||||
});
|
||||
});
|
||||
ui.add_space(10.0);
|
||||
let mut close = false;
|
||||
ui.horizontal(|ui| ui.with_layout(Layout::right_to_left(Align::Center), |ui| close = widgets::pill_button(ui, "OK", true).clicked()));
|
||||
ui.horizontal(|ui| ui.with_layout(Layout::right_to_left(Align::Center), |ui| close = widgets::pill_button(ui, tl!("OK"), true).clicked()));
|
||||
(close, action)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -58,14 +58,14 @@ pub(crate) fn create_body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens)
|
|||
c
|
||||
};
|
||||
let d = &mut app.stamp_draft;
|
||||
ui.label(egui::RichText::new("Create Custom Stamp").font(theme::semibold(18.0)));
|
||||
ui.label(egui::RichText::new(tl!("Create Custom Stamp")).font(theme::semibold(18.0)));
|
||||
ui.add_space(8.0);
|
||||
ui.label(egui::RichText::new(format!("From {}", d.file)).color(t.text_muted));
|
||||
ui.label(egui::RichText::new(crate::i18n::fmt(tl!("From {file}"), &[("file", &d.file)])).color(t.text_muted));
|
||||
ui.add_space(8.0);
|
||||
egui::Grid::new("stamp-create").num_columns(2).spacing([10.0, 8.0]).show(ui, |ui| {
|
||||
let l = ui.label("Category:");
|
||||
let l = ui.label(tl!("Category:"));
|
||||
ui.horizontal(|ui| {
|
||||
ui.add(egui::TextEdit::singleline(&mut d.category).desired_width(200.0).hint_text("e.g. My stamps")).labelled_by(l.id);
|
||||
ui.add(egui::TextEdit::singleline(&mut d.category).desired_width(200.0).hint_text(tl!("e.g. My stamps"))).labelled_by(l.id);
|
||||
if !categories.is_empty() {
|
||||
egui::ComboBox::from_id_salt("stamp-categories").selected_text("").width(24.0).show_ui(ui, |ui| {
|
||||
for c in &categories {
|
||||
|
|
@ -77,7 +77,7 @@ pub(crate) fn create_body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens)
|
|||
}
|
||||
});
|
||||
ui.end_row();
|
||||
let l = ui.label("Name:");
|
||||
let l = ui.label(tl!("Name:"));
|
||||
ui.add(egui::TextEdit::singleline(&mut d.name).desired_width(200.0)).labelled_by(l.id);
|
||||
ui.end_row();
|
||||
});
|
||||
|
|
@ -85,10 +85,10 @@ pub(crate) fn create_body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens)
|
|||
let ok = !d.category.trim().is_empty() && !d.name.trim().is_empty();
|
||||
let (mut save, mut cancel) = (false, false);
|
||||
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
|
||||
if ui.add_enabled_ui(ok, |ui| widgets::pill_button(ui, "OK", true)).inner.clicked() {
|
||||
if ui.add_enabled_ui(ok, |ui| widgets::pill_button(ui, tl!("OK"), true)).inner.clicked() {
|
||||
save = true;
|
||||
}
|
||||
if widgets::pill_button(ui, "Cancel", false).clicked() {
|
||||
if widgets::pill_button(ui, tl!("Cancel"), false).clicked() {
|
||||
cancel = true;
|
||||
}
|
||||
});
|
||||
|
|
@ -97,7 +97,7 @@ pub(crate) fn create_body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens)
|
|||
|
||||
/// The Custom stamps section of the stamps palette.
|
||||
pub(crate) fn palette_section(app: &mut PdfCraftApp, ui: &mut egui::Ui, t: &Tokens) {
|
||||
widgets::section_title(ui, "Custom");
|
||||
widgets::section_title(ui, tl!("Custom"));
|
||||
let mut remove = None;
|
||||
let mut categories: Vec<String> = app.custom_stamps.iter().map(|s| s.category.clone()).collect();
|
||||
categories.sort();
|
||||
|
|
@ -111,7 +111,7 @@ pub(crate) fn palette_section(app: &mut PdfCraftApp, ui: &mut egui::Ui, t: &Toke
|
|||
app.quick_tool = QuickTool::CustomStamp(i);
|
||||
}
|
||||
resp.context_menu(|ui| {
|
||||
if ui.button("Delete stamp").clicked() {
|
||||
if ui.button(tl!("Delete stamp")).clicked() {
|
||||
remove = Some(i);
|
||||
ui.close();
|
||||
}
|
||||
|
|
@ -123,7 +123,7 @@ pub(crate) fn palette_section(app: &mut PdfCraftApp, ui: &mut egui::Ui, t: &Toke
|
|||
app.quick_tool = QuickTool::Select;
|
||||
}
|
||||
ui.add_space(4.0);
|
||||
if widgets::pill_button(ui, "Create custom stamp…", false).on_hover_text("From a PDF page or an image").clicked() {
|
||||
if widgets::pill_button(ui, tl!("Create custom stamp…"), false).on_hover_text(tl!("From a PDF page or an image")).clicked() {
|
||||
app.pick_stamp_file();
|
||||
}
|
||||
}
|
||||
|
|
@ -137,8 +137,8 @@ impl PdfCraftApp {
|
|||
Some(p) if [".png", ".jpg", ".pdf"].iter().any(|e| p.ends_with(e)) => Some(std::path::PathBuf::from(p)),
|
||||
Some(_) => None,
|
||||
None => rfd::FileDialog::new()
|
||||
.add_filter("PDF or image", &["pdf", "png", "jpg", "jpeg", "tif", "tiff", "gif", "bmp", "jp2", "j2k", "jpx"])
|
||||
.set_title("Select a file for the stamp")
|
||||
.add_filter(tl!("PDF or image"), &["pdf", "png", "jpg", "jpeg", "tif", "tiff", "gif", "bmp", "jp2", "j2k", "jpx"])
|
||||
.set_title(tl!("Select a file for the stamp"))
|
||||
.pick_file(),
|
||||
};
|
||||
let Some(path) = picked else { return };
|
||||
|
|
@ -147,17 +147,17 @@ impl PdfCraftApp {
|
|||
let name = path.file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or_default();
|
||||
self.start_custom_stamp(name, bytes);
|
||||
}
|
||||
Err(e) => self.notify(format!("Couldn't read {}: {e}", path.display())),
|
||||
Err(e) => self.notify_fmt("Couldn't read {name}: {e}", &[("name", &path.display().to_string()), ("e", &e.to_string())]),
|
||||
}
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
self.notify("Custom stamps arrive on the web with file pickers for images");
|
||||
self.notify_tr("Custom stamps arrive on the web with file pickers for images");
|
||||
}
|
||||
|
||||
/// Open the Create Custom Stamp dialog for a file.
|
||||
pub fn start_custom_stamp(&mut self, file: String, bytes: Vec<u8>) {
|
||||
if bytes.len() > MAX_STAMP_BYTES {
|
||||
self.notify(format!("{file} is too large for a stamp (at most {} MB)", MAX_STAMP_BYTES >> 20));
|
||||
self.notify_fmt("{file} is too large for a stamp (at most {n} MB)", &[("file", &file), ("n", &(MAX_STAMP_BYTES >> 20).to_string())]);
|
||||
return;
|
||||
}
|
||||
let name = file.rsplit_once('.').map_or(file.as_str(), |(s, _)| s).to_string();
|
||||
|
|
|
|||
|
|
@ -35,9 +35,11 @@ impl PdfCraftApp {
|
|||
self.pdfa_verify();
|
||||
let left = self.pdfa.issues.as_ref().map_or(0, Vec::len);
|
||||
self.notify(if left == 0 {
|
||||
format!("The document now conforms to {}; save it to keep the changes", level.label())
|
||||
crate::i18n::fmt(tl!("The document now conforms to {level}; save it to keep the changes"), &[("level", level.label())])
|
||||
} else if left == 1 {
|
||||
crate::i18n::fmt(tl!("Fixed what could be fixed; 1 problem remains"), &[])
|
||||
} else {
|
||||
format!("Fixed what could be fixed; {left} problem{} remain", if left == 1 { "" } else { "s" })
|
||||
crate::i18n::fmt(tl!("Fixed what could be fixed; {n} problems remain"), &[("n", &left.to_string())])
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -48,15 +50,17 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> bool
|
|||
ui.label(egui::RichText::new("PDF/A").font(theme::semibold(18.0)));
|
||||
ui.add_space(6.0);
|
||||
let declared = app.active_ids().and_then(|(_, id)| app.session.get(id)).map(|d| d.standards()).unwrap_or_default();
|
||||
let shown = declared.pdfa.as_ref().map_or("none".to_string(), |(p, c)| format!("PDF/A-{p}{}", c.to_lowercase()));
|
||||
ui.label(format!("Declared conformance: {shown}"));
|
||||
let shown = declared.pdfa.as_ref().map_or(tl!("none").to_string(), |(p, c)| format!("PDF/A-{p}{}", c.to_lowercase()));
|
||||
ui.label(crate::i18n::fmt(tl!("Declared conformance: {shown}"), &[("shown", &shown)]));
|
||||
if !declared.output_intents.is_empty() {
|
||||
ui.label(egui::RichText::new(format!("Output intent: {}", declared.output_intents.join(", "))).color(t.text_muted));
|
||||
ui.label(
|
||||
egui::RichText::new(crate::i18n::fmt(tl!("Output intent: {list}"), &[("list", &declared.output_intents.join(", "))])).color(t.text_muted),
|
||||
);
|
||||
}
|
||||
ui.add_space(6.0);
|
||||
let before = app.pdfa.level;
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Conformance level:");
|
||||
ui.label(tl!("Conformance level:"));
|
||||
egui::ComboBox::from_id_salt("pdfa-level").selected_text(app.pdfa.level.label()).show_ui(ui, |ui| {
|
||||
for l in [Level::A2b, Level::A3b] {
|
||||
ui.selectable_value(&mut app.pdfa.level, l, l.label());
|
||||
|
|
@ -73,15 +77,15 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> bool
|
|||
ui.set_min_height(120.0);
|
||||
match &app.pdfa.issues {
|
||||
None => {
|
||||
ui.label(egui::RichText::new("Verify to see what the document needs.").color(t.text_muted));
|
||||
ui.label(egui::RichText::new(tl!("Verify to see what the document needs.")).color(t.text_muted));
|
||||
}
|
||||
Some(v) if v.is_empty() => {
|
||||
ui.label(format!("No problems found: the document conforms to {}.", app.pdfa.level.label()));
|
||||
ui.label(crate::i18n::fmt(tl!("No problems found: the document conforms to {level}."), &[("level", app.pdfa.level.label())]));
|
||||
}
|
||||
Some(v) => {
|
||||
for i in v {
|
||||
let page = i.page.map(|p| format!(" (page {})", p + 1)).unwrap_or_default();
|
||||
let fix = if i.fixable { "" } else { " · not fixable here" };
|
||||
let page = i.page.map(|p| crate::i18n::fmt(tl!(" (page {p})"), &[("p", &(p + 1).to_string())])).unwrap_or_default();
|
||||
let fix = if i.fixable { String::new() } else { tl!(" · not fixable here").to_string() };
|
||||
ui.label(format!("{}{page}", i.message));
|
||||
ui.label(egui::RichText::new(format!("ISO 19005 {}{fix}", i.clause)).small().color(t.text_muted));
|
||||
}
|
||||
|
|
@ -94,13 +98,13 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> bool
|
|||
let mut action = None;
|
||||
ui.horizontal(|ui| {
|
||||
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
|
||||
if widgets::pill_button(ui, "Save as PDF/A", true).clicked() {
|
||||
if widgets::pill_button(ui, tl!("Save as PDF/A"), true).clicked() {
|
||||
action = Some(true);
|
||||
}
|
||||
if widgets::pill_button(ui, "Verify", false).clicked() {
|
||||
if widgets::pill_button(ui, tl!("Verify"), false).clicked() {
|
||||
action = Some(false);
|
||||
}
|
||||
if widgets::pill_button(ui, "Close", false).clicked() {
|
||||
if widgets::pill_button(ui, tl!("Close"), false).clicked() {
|
||||
close = true;
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -120,48 +120,60 @@ pub(crate) fn dialog(app: &mut PdfCraftApp, ctx: &egui::Context) {
|
|||
ui.set_width(420.0);
|
||||
ui.horizontal(|ui| {
|
||||
ui.add(crate::icons::image("cloud", 22.0, t.accent));
|
||||
ui.label(egui::RichText::new("Check for updates").font(theme::semibold(16.0)));
|
||||
ui.label(egui::RichText::new(tl!("Check for updates")).font(theme::semibold(16.0)));
|
||||
});
|
||||
ui.add_space(8.0);
|
||||
match &app.updates.check {
|
||||
Check::Idle => {
|
||||
ui.label("No check has run yet.");
|
||||
ui.label(tl!("No check has run yet."));
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
Check::Running(_) => {
|
||||
ui.horizontal(|ui| {
|
||||
ui.spinner();
|
||||
ui.label("Checking for a newer version…");
|
||||
ui.label(tl!("Checking for a newer version…"));
|
||||
});
|
||||
}
|
||||
Check::Done(Ok(r)) if is_newer(&r.version, current) => {
|
||||
let version = r.version.trim_start_matches(['v', 'V']);
|
||||
ui.label(egui::RichText::new(format!("PdfCraft {version} is available.")).strong());
|
||||
ui.label(egui::RichText::new(crate::i18n::fmt(tl!("PdfCraft {v} is available."), &[("v", version)])).strong());
|
||||
ui.label(
|
||||
egui::RichText::new(format!("You have version {current}. Download the new version from its release page.")).color(t.text_muted),
|
||||
egui::RichText::new(crate::i18n::fmt(
|
||||
tl!("You have version {c}. Download the new version from its release page."),
|
||||
&[("c", current)],
|
||||
))
|
||||
.color(t.text_muted),
|
||||
);
|
||||
download = Some(r.url.clone());
|
||||
}
|
||||
Check::Done(Ok(_)) => {
|
||||
ui.label(format!("PdfCraft {current} is up to date."));
|
||||
ui.label(crate::i18n::fmt(tl!("PdfCraft {c} is up to date."), &[("c", current)]));
|
||||
}
|
||||
Check::Done(Err(e)) => {
|
||||
ui.label(format!("Couldn't check for updates: {e}"));
|
||||
ui.label(egui::RichText::new(format!("You have version {current}. All releases are listed at {RELEASES_PAGE}.")).color(t.text_muted));
|
||||
ui.label(crate::i18n::fmt(tl!("Couldn't check for updates: {e}"), &[("e", &e.to_string())]));
|
||||
ui.label(
|
||||
egui::RichText::new(crate::i18n::fmt(
|
||||
tl!("You have version {c}. All releases are listed at {page}."),
|
||||
&[("c", current), ("page", RELEASES_PAGE)],
|
||||
))
|
||||
.color(t.text_muted),
|
||||
);
|
||||
}
|
||||
}
|
||||
ui.add_space(10.0);
|
||||
ui.label(
|
||||
egui::RichText::new("Asks GitHub for the latest release. Nothing is downloaded or installed automatically.").color(t.text_muted).small(),
|
||||
egui::RichText::new(tl!("Asks GitHub for the latest release. Nothing is downloaded or installed automatically."))
|
||||
.color(t.text_muted)
|
||||
.small(),
|
||||
);
|
||||
ui.add_space(12.0);
|
||||
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
|
||||
if let Some(url) = download.take() {
|
||||
let get = widgets::pill_button(ui, "Download", true).clicked();
|
||||
let later = widgets::pill_button(ui, "Later", false).clicked();
|
||||
let get = widgets::pill_button(ui, tl!("Download"), true).clicked();
|
||||
let later = widgets::pill_button(ui, tl!("Later"), false).clicked();
|
||||
close = get || later;
|
||||
download = get.then_some(url);
|
||||
} else if widgets::pill_button(ui, "Close", true).clicked() {
|
||||
} else if widgets::pill_button(ui, tl!("Close"), true).clicked() {
|
||||
close = true;
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -75,7 +75,8 @@ pub fn menu_item(ui: &mut egui::Ui, label: &str, shortcut: &str) -> Response {
|
|||
pub fn section_title(ui: &mut egui::Ui, text: &str) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
ui.add_space(10.0);
|
||||
ui.label(egui::RichText::new(text.to_uppercase()).font(theme::semibold(10.5)).color(t.text_faint).extra_letter_spacing(0.6));
|
||||
// Section titles across every panel go through here, so one translation point covers them.
|
||||
ui.label(egui::RichText::new(tl!(text).to_uppercase()).font(theme::semibold(10.5)).color(t.text_faint).extra_letter_spacing(0.6));
|
||||
ui.add_space(2.0);
|
||||
}
|
||||
|
||||
|
|
@ -134,7 +135,7 @@ pub fn community_links(ui: &mut egui::Ui) -> Option<&'static str> {
|
|||
ui.horizontal_wrapped(|ui| {
|
||||
ui.spacing_mut().item_spacing = vec2(8.0, 8.0);
|
||||
for (i, l) in pdfcraft_engine::links::LINKS.iter().enumerate() {
|
||||
let resp = if i == 0 { icon_pill(ui, l.icon, "Join our Discord", true) } else { icon_pill(ui, l.icon, l.label, false) };
|
||||
let resp = if i == 0 { icon_pill(ui, l.icon, tl!("Join our Discord"), true) } else { icon_pill(ui, l.icon, tl!(l.label), false) };
|
||||
if resp.on_hover_text(l.url).clicked() {
|
||||
clicked = Some(l.command);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,8 +64,10 @@ impl PdfCraftApp {
|
|||
return;
|
||||
}
|
||||
match self.snapshot(index, page, view_rect) {
|
||||
Ok((w, h)) => self.notify(format!("The selected area has been copied ({w} × {h} pixels)")),
|
||||
Err(e) => self.notify(format!("Couldn't take the snapshot: {e}")),
|
||||
Ok((w, h)) => {
|
||||
self.notify_fmt("The selected area has been copied ({w} × {h} pixels)", &[("w", &w.to_string()), ("h", &h.to_string())])
|
||||
}
|
||||
Err(e) => self.notify_fmt("Couldn't take the snapshot: {e}", &[("e", &e.to_string())]),
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
|
|
|
|||
|
|
@ -422,6 +422,107 @@ fn state_and_view_options() {
|
|||
assert!(call(&mut h, &c, "ui.set", json!({ "key": "panel", "value": "nonsense" })).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn japanese_controls_and_search_keep_command_ids() {
|
||||
let (mut h, c) = harness();
|
||||
ok(&mut h, &c, "ui.set", json!({ "key": "language", "value": "ja" }));
|
||||
assert_eq!(ok(&mut h, &c, "ui.state", json!({}))["language"], "ja");
|
||||
ok(&mut h, &c, "ui.click", json!({ "label": "閲覧" }));
|
||||
assert_eq!(ok(&mut h, &c, "ui.state", json!({}))["mode"], "Read");
|
||||
|
||||
ok(&mut h, &c, "ui.command", json!({ "id": "app.preferences" }));
|
||||
let prefs = ok(&mut h, &c, "ui.inspect", json!({ "query": "表示言語" }));
|
||||
assert!(prefs["count"].as_u64().unwrap() > 0, "{prefs}");
|
||||
ok(&mut h, &c, "ui.click", json!({ "label": "OK" }));
|
||||
|
||||
for (query, translated) in [("整理", "ページを整理"), ("Split document", "文書を分割…"), ("page.split", "文書を分割…")] {
|
||||
ok(&mut h, &c, "ui.command", json!({ "id": "view.palette" }));
|
||||
ok(&mut h, &c, "ui.type", json!({ "text": query }));
|
||||
let hits = ok(&mut h, &c, "ui.inspect", json!({ "query": translated }));
|
||||
assert!(hits["count"].as_u64().unwrap() > 0, "{query}: {hits}");
|
||||
ok(&mut h, &c, "ui.key", json!({ "key": "Escape" }));
|
||||
h.state_mut().palette_query.clear();
|
||||
}
|
||||
assert_eq!(ok(&mut h, &c, "ui.state", json!({}))["documents"][0]["name"], "doc.pdf");
|
||||
ok(&mut h, &c, "ui.set", json!({ "key": "language", "value": "en" }));
|
||||
ok(&mut h, &c, "ui.click", json!({ "label": "All tools" }));
|
||||
assert_eq!(ok(&mut h, &c, "ui.state", json!({}))["mode"], "AllTools");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preferences_menu_and_shortcut_allow_switching_interface_languages() {
|
||||
let (mut h, c) = harness();
|
||||
ok(&mut h, &c, "ui.set", json!({ "key": "language", "value": "ja" }));
|
||||
ok(&mut h, &c, "ui.click", json!({ "label": "メニュー" }));
|
||||
ok(&mut h, &c, "ui.click", json!({ "label": "編集 ⏵" }));
|
||||
let menu = ok(&mut h, &c, "ui.inspect", json!({ "query": "環境設定…" }));
|
||||
let prefs = menu["widgets"].as_array().unwrap().iter().find(|w| w["clickable"] == true).expect("Preferences menu item");
|
||||
ok(&mut h, &c, "ui.click", json!({ "id": prefs["id"] }));
|
||||
for (current, next, code) in [("日本語", "English", "en"), ("English", "日本語", "ja")] {
|
||||
let selector = ok(&mut h, &c, "ui.inspect", json!({ "query": current }));
|
||||
let combo = selector["widgets"].as_array().unwrap().iter().find(|w| w["role"] == "ComboBox").expect("language selector");
|
||||
ok(&mut h, &c, "ui.click", json!({ "id": combo["id"] }));
|
||||
ok(&mut h, &c, "ui.click", json!({ "label": next }));
|
||||
assert_eq!(ok(&mut h, &c, "ui.state", json!({}))["language"], code);
|
||||
}
|
||||
ok(&mut h, &c, "ui.click", json!({ "label": "OK" }));
|
||||
ok(&mut h, &c, "ui.key", json!({ "key": ",", "modifiers": ["command"] }));
|
||||
let prefs = ok(&mut h, &c, "ui.inspect", json!({ "query": "表示言語" }));
|
||||
assert!(prefs["count"].as_u64().unwrap() > 0, "{prefs}");
|
||||
ok(&mut h, &c, "ui.click", json!({ "label": "OK" }));
|
||||
|
||||
ok(&mut h, &c, "ui.command", json!({ "id": "help.shortcuts" }));
|
||||
for label in ["キーボードショートカット", "開く", "環境設定", "次/前の検索結果", "ダブルクリック", "閉じる"]
|
||||
{
|
||||
let found = ok(&mut h, &c, "ui.inspect", json!({ "query": label }));
|
||||
assert!(found["count"].as_u64().unwrap() > 0, "{label}: {found}");
|
||||
}
|
||||
let english = ok(&mut h, &c, "ui.inspect", json!({ "query": "Next / previous match" }));
|
||||
assert_eq!(english["count"], 0);
|
||||
ok(&mut h, &c, "ui.click", json!({ "label": "閉じる" }));
|
||||
assert_eq!(ok(&mut h, &c, "ui.state", json!({}))["documents"][0]["name"], "doc.pdf");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn japanese_dialogs_errors_and_custom_action_names() {
|
||||
let (mut h, c) = harness();
|
||||
ok(&mut h, &c, "ui.set", json!({"key": "language", "value": "ja"}));
|
||||
for (dialog, label) in [
|
||||
("properties", "文書のプロパティ"),
|
||||
("protect", "パスワードで保護"),
|
||||
("export-image", "画像に書き出し"),
|
||||
("optimize", "PDF の最適化"),
|
||||
("recognize-text", "テキストを認識"),
|
||||
("accessibility-options", "アクセシビリティチェックのオプション"),
|
||||
("js-console", "JavaScript コンソール"),
|
||||
("compare-files", "ファイルを比較"),
|
||||
("sign", "署名用のデジタル ID を設定"),
|
||||
] {
|
||||
ok(&mut h, &c, "ui.set", json!({"key": "dialog", "value": dialog}));
|
||||
let found = ok(&mut h, &c, "ui.inspect", json!({"query": label}));
|
||||
assert!(found["count"].as_u64().unwrap() > 0, "{dialog}: {found}");
|
||||
}
|
||||
ok(&mut h, &c, "ui.set", json!({"key": "dialog", "value": "none"}));
|
||||
assert!(!h.state_mut().apply_edit(pdfcraft_engine::Edit::DeletePages { pages: vec![0, 1, 2, 3, 4] }));
|
||||
let state = ok(&mut h, &c, "ui.state", json!({}));
|
||||
// The frame is translated; the engine's own error text is shown as it is.
|
||||
assert_eq!(state["notice"], "操作「ページを削除」に失敗しました: a document must keep at least one page");
|
||||
assert_eq!(ok(&mut h, &c, "ui.state", json!({}))["documents"][0]["name"], "doc.pdf");
|
||||
|
||||
h.state_mut().custom_actions.push(pdfcraft_engine::actions::Action {
|
||||
name: "Save".to_string(),
|
||||
description: "Print".to_string(),
|
||||
steps: Vec::new(),
|
||||
builtin: false,
|
||||
});
|
||||
h.state_mut().wizard.selected = Some("Save".to_string());
|
||||
ok(&mut h, &c, "ui.set", json!({"key": "dialog", "value": "action-wizard"}));
|
||||
for label in ["アクションウィザード", "Save", "Print"] {
|
||||
let found = ok(&mut h, &c, "ui.inspect", json!({"query": label}));
|
||||
assert!(found["widgets"].as_array().unwrap().iter().any(|w| w["label"] == label || w["value"] == label), "{found}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inspect_and_click_by_label_and_id() {
|
||||
let (mut h, c) = harness();
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Interface language
|
||||
|
||||
Choose **Edit > Preferences > Interface language** and select **Auto**, **English**, **日本語**, **Čeština** or **Português (Brasil)**. 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** or **Português (Brasil)**. 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. A system language without a catalog, such as French, shows English. Windows has no system-language detection yet, so **Auto** shows English there unless `LANG` is set.
|
||||
|
||||
|
|
@ -12,7 +12,7 @@ The control channel exposes the setting through `ui.set`:
|
|||
|
||||
The value is `auto` or a language code (`en`, `ja`, `cs`, `pt-br`), in any case. `ui.state` reports `language` as written in the preferences (`auto`, `en`, `ja`, `cs` or `pt-br`). Unknown values return an error without changing the current setting. Preferences saved before this setting existed follow the system language.
|
||||
|
||||
This first translation pass covers the main menu and core registered menu commands. Untranslated labels use English. 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 translates them (about 1,800 entries); 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. Czech and Brazilian Portuguese need only the bundled Latin faces (tested in `tests/fonts.rs`).
|
||||
|
||||
|
|
|
|||
|
|
@ -9804,10 +9804,10 @@ area = "N"
|
|||
tier = "P1"
|
||||
milestone = "M14"
|
||||
status = "partial"
|
||||
commands = []
|
||||
commands = ["app.preferences"]
|
||||
tools = []
|
||||
evidence = ["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/tests/control.rs::language_switch_preserves_document_and_command_ids"]
|
||||
notes = "Preferences switches Auto (system language)/English/Japanese/Czech/Brazilian Portuguese and persists; ui.set language auto|en|ja|cs|pt-br and ui.state expose it. TSV catalogs per language (same system as PhotoCraft). Core menu labels translated (Czech: every registered menu command, tested); dialogs, panels, Windows system-language detection and more languages remain open."
|
||||
evidence = ["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/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/Czech/Brazilian Portuguese and persists; ui.set language auto|en|ja|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 covers commands, dialogs, panels, shortcuts and history labels; Czech and Brazilian Portuguese the menus. Engine/OS error details stay untranslated. Missing: the other catalogs filled in, Windows system-language detection, a full localization audit."
|
||||
|
||||
[[feature]]
|
||||
id = "misc.prefs-reading"
|
||||
|
|
|
|||
Loading…
Reference in New Issue