M14: refine German labels and prevent translated UI text overlap (#515)
* M14: refine German labels and keep translated text within UI bounds Co-authored-by: Codex <codex@openai.com> * Tool names inside notices stay lowercase in English; German keeps its capitalised nouns --------- Co-authored-by: Codex <codex@openai.com> Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
parent
63363ec438
commit
f626db25cc
|
|
@ -85,7 +85,7 @@ licence = "MIT OR Apache-2.0"
|
|||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "German UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "f46a3815d45c4ffba9690dd8c30c1f799bca1620dc33f242e2d27e805f9747a6"
|
||||
sha256 = "ef0c8f5830ecb2950b6956eff50f963282bc607f4aedd98d89b9020a73db53b7"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/te.tsv"
|
||||
|
|
|
|||
|
|
@ -139,6 +139,8 @@ M0 → M1 → M2 → M3 → M4 must happen in order. After M4, M5–M12 can run
|
|||
|
||||
Newest first. One line per session: the date, what moved, and the new overall percentage.
|
||||
|
||||
- **2026-10-09 (M14, German interface follow-up):** Follow the merged German catalog with shorter search/action labels, corrected callout/accessibility wording and a distinct certificate Subject context. Localized tool names retain their capitalization and comment composer titles go through the catalog. Search hints reserve shortcut space and Home cards bound text with full hover text; layout, context and German Preferences/search control regression tests cover these changes. Overall effort estimate remains about 30–35%.
|
||||
|
||||
- **2026-10-09 (community, destinations):** Bookmarks and links go to their destination's position, not just its page (ISO 32000-2 §12.3.2.2): `/XYZ` puts (left, top) at the window's top-left and applies a non-zero zoom; `/Fit`, `/FitH` and `/FitV` switch to fit page, width and height with `top` or `left` at the window's edge; `/FitR` zooms to fit the rectangle, centred. Named destinations resolve the same way; crop-box origin, `/Rotate` and view rotation are accounted for; malformed or non-finite operands fall back to the top of the page. Not yet: the `/FitB*` content box (read as the page). Overall estimate unchanged (about 30–35%).
|
||||
|
||||
- **2026-10-09 (community, M14, Hungarian):** Magyar (`hu`) is a complete interface catalog (2,036 entries) covering every command, All tools entry, dialog, panel and notice, selected automatically for `hu` and `hu_HU`, and from Preferences. Informal "te"; commands are nouns (Mentés, Megnyitás). After a numeral the noun stays singular, so both plural forms match. Coverage tests check commands, All tools labels and every `tl!` literal; history labels keep captured names; the bundled Latin faces cover Hungarian diacritics including ő and ű. Overall estimate unchanged (about 30–35%).
|
||||
|
|
|
|||
|
|
@ -466,7 +466,10 @@ impl PdfCraftApp {
|
|||
if let Some(i) = active {
|
||||
self.views[i].forms.focus = None;
|
||||
}
|
||||
self.notify_fmt("Click on the page to add a {tool}, or drag to set its size", &[("tool", &tl!(tool.label()).to_lowercase())]);
|
||||
self.notify_fmt(
|
||||
"Click on the page to add a {tool}, or drag to set its size",
|
||||
&[("tool", &crate::i18n::in_sentence(tl!(tool.label())))],
|
||||
);
|
||||
}
|
||||
"sign.fill.signature.remove" => self.signature = None,
|
||||
"sign.fill.initials.remove" => self.initials = None,
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ impl PdfCraftApp {
|
|||
style.width = w;
|
||||
}
|
||||
self.comment_prefs.set_style(tool, style);
|
||||
self.notify_fmt("New {tool} comments will look like this one", &[("tool", &tl!(tool.label()).to_lowercase())]);
|
||||
self.notify_fmt("New {tool} comments will look like this one", &[("tool", &crate::i18n::in_sentence(tl!(tool.label())))]);
|
||||
}
|
||||
|
||||
/// Open Comment Properties for the comment at `(page, index)` of the active document.
|
||||
|
|
|
|||
|
|
@ -1074,7 +1074,7 @@ pub(crate) fn composer(ctx: &egui::Context, view: &mut DocView, info: &DocInfo,
|
|||
ui.set_width(260.0);
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(egui::RichText::new(&prefs.author).font(theme::semibold(13.0)));
|
||||
ui.label(egui::RichText::new(title).font(theme::regular(11.5)).color(t.text_faint));
|
||||
ui.label(egui::RichText::new(tl!(title)).font(theme::regular(11.5)).color(t.text_faint));
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
let hint = match c.kind {
|
||||
|
|
|
|||
|
|
@ -60,13 +60,22 @@ pub fn show(app: &mut PdfCraftApp, ui: &mut egui::Ui) {
|
|||
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, tl!(g.label), theme::semibold(13.5), t.text);
|
||||
let mut title_job = egui::text::LayoutJob::simple_singleline(tl!(g.label).to_owned(), theme::semibold(13.5), t.text);
|
||||
title_job.wrap =
|
||||
egui::text::TextWrapping { max_width: rect.width() - 58.0, max_rows: 1, break_anywhere: true, ..Default::default() };
|
||||
let title = ui.fonts_mut(|f| f.layout_job(title_job));
|
||||
let title_elided = title.elided;
|
||||
ui.painter().galley(rect.min + vec2(44.0, 25.0 - title.size().y * 0.5), title, t.text);
|
||||
let blurb = g
|
||||
.sections
|
||||
.first()
|
||||
.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));
|
||||
// Fixed-height cards must leave room for their action in every language.
|
||||
let mut job = egui::text::LayoutJob::simple(blurb.clone(), theme::regular(11.5), t.text_muted, rect.width() - 28.0);
|
||||
job.wrap.max_rows = 2;
|
||||
let galley = ui.fonts_mut(|f| f.layout_job(job));
|
||||
let blurb_elided = galley.elided;
|
||||
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),
|
||||
|
|
@ -75,6 +84,7 @@ pub fn show(app: &mut PdfCraftApp, ui: &mut egui::Ui) {
|
|||
theme::medium(12.0),
|
||||
t.accent_text,
|
||||
);
|
||||
let resp = if title_elided || blurb_elided { resp.on_hover_text(format!("{}\n{blurb}", tl!(g.label))) } else { resp };
|
||||
if resp.clicked() {
|
||||
app.left = LeftPanel::Tool(g.id);
|
||||
app.left_open = true;
|
||||
|
|
@ -182,3 +192,46 @@ pub fn show(app: &mut PdfCraftApp, ui: &mut egui::Ui) {
|
|||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn translated_card_description_leaves_room_for_the_action() {
|
||||
let de = crate::i18n::Lang::from_code("de").unwrap();
|
||||
crate::i18n::set_current(de);
|
||||
let ctx = egui::Context::default();
|
||||
ctx.set_fonts(theme::font_definitions());
|
||||
let mut app = PdfCraftApp::new();
|
||||
let blurb = catalog::group("form")
|
||||
.unwrap()
|
||||
.sections
|
||||
.first()
|
||||
.unwrap()
|
||||
.items
|
||||
.iter()
|
||||
.take(3)
|
||||
.map(|item| crate::i18n::tr(de, item.label))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" · ");
|
||||
let output = ctx
|
||||
.run_ui(egui::RawInput { screen_rect: Some(Rect::from_min_size(egui::Pos2::ZERO, vec2(1440.0, 900.0))), ..Default::default() }, |ui| {
|
||||
show(&mut app, ui)
|
||||
});
|
||||
crate::i18n::set_current(crate::i18n::Lang::EN);
|
||||
let text_shapes: Vec<_> =
|
||||
output.shapes.iter().filter_map(|shape| if let egui::Shape::Text(text) = &shape.shape { Some(text) } else { None }).collect();
|
||||
let description = text_shapes.iter().find(|text| text.galley.job.text == blurb).unwrap();
|
||||
let action = text_shapes
|
||||
.iter()
|
||||
.find(|text| text.galley.job.text == crate::i18n::tr(de, "Use now") && (text.pos.x - description.pos.x).abs() < 0.1)
|
||||
.unwrap();
|
||||
assert!(description.galley.rows.len() <= 2);
|
||||
assert!(
|
||||
description.pos.y + description.galley.size().y + 4.0 <= action.pos.y,
|
||||
"the translated description must leave a visible gap before its action"
|
||||
);
|
||||
output.drop_without_applying_deltas();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@
|
|||
Comments panel Kommentarbereich
|
||||
Form fields panel Formularfeldbereich
|
||||
Clear form Formular zurücksetzen
|
||||
Find tools and commands… Werkzeuge und Befehle suchen…
|
||||
Find tools and commands… Tools und Befehle suchen…
|
||||
Zoom Zoom
|
||||
Actual size Originalgröße
|
||||
Zoom to page level Ganze Seite
|
||||
|
|
@ -84,7 +84,7 @@
|
|||
Print (⌘P) Drucken (⌘P)
|
||||
Save (M4) Speichern (M4)
|
||||
Document properties (⌘D) Dokumenteigenschaften (⌘D)
|
||||
Find tools and commands Werkzeuge und Befehle suchen
|
||||
Find tools and commands Tools und Befehle suchen
|
||||
Fit to height Seitenhöhe
|
||||
Zoom in Vergrößern
|
||||
Zoom out Verkleinern
|
||||
|
|
@ -94,7 +94,7 @@
|
|||
Previous view Vorherige Ansicht
|
||||
Next view Nächste Ansicht
|
||||
Page display Seitenanzeige
|
||||
Continuous scrolling Fortlaufend blättern
|
||||
Continuous scrolling Fortlaufendes Scrollen
|
||||
Two-page view Doppelseitenansicht
|
||||
Single page Einzelseite
|
||||
Show cover page in two-page view Deckblatt in Doppelseitenansicht anzeigen
|
||||
|
|
@ -137,7 +137,7 @@
|
|||
View more Mehr anzeigen
|
||||
Available Verfügbar
|
||||
Ready Bereit
|
||||
Search tools and commands… Werkzeuge und Befehle suchen…
|
||||
Search tools and commands… Tools und Befehle suchen…
|
||||
No matching tools Keine passenden Werkzeuge
|
||||
Tool Werkzeug
|
||||
Command Befehl
|
||||
|
|
@ -155,7 +155,7 @@
|
|||
Draw a polygon Polygon zeichnen
|
||||
Draw connected lines Verbundene Linien zeichnen
|
||||
Draw a cloud Wolke zeichnen
|
||||
Add a callout Legende hinzufügen
|
||||
Add a callout Text mit Hinweislinie hinzufügen
|
||||
Insert text Text einfügen
|
||||
Replace text Text ersetzen
|
||||
Attach a file Datei anhängen
|
||||
|
|
@ -324,7 +324,7 @@
|
|||
Set page boxes Seitenrahmen festlegen
|
||||
Page labels Seitenbeschriftungen
|
||||
Page transitions Seitenübergänge
|
||||
Add comments Kommentare hinzufügen
|
||||
Add comments Kommentieren
|
||||
Markup Markierungen
|
||||
Sticky note Notiz
|
||||
Underline Unterstreichen
|
||||
|
|
@ -840,7 +840,7 @@
|
|||
View signed version Signierte Version anzeigen
|
||||
Show certificate… Zertifikat anzeigen…
|
||||
Export certificate… Zertifikat exportieren…
|
||||
Certificate Viewer Zertifikatanzeige
|
||||
Certificate Viewer Zertifikatsanzeige
|
||||
This dialog shows the details of a certificate and its chain. Dieser Dialog zeigt die Details eines Zertifikats und seiner Kette.
|
||||
Trust Vertrauen
|
||||
Issued to Ausgestellt für
|
||||
|
|
@ -884,6 +884,7 @@
|
|||
Remove hidden information, metadata, scripts and embedded content, and rewrite the files. Ausgeblendete Informationen, Metadaten, Skripte und eingebettete Inhalte entfernen und die Dateien neu schreiben.
|
||||
Type text Text eingeben
|
||||
Text to insert Einzufügender Text
|
||||
Inserted text Eingefügter Text
|
||||
Replacement text Ersatztext
|
||||
Edit text… Text bearbeiten…
|
||||
Add a sticky note here Hier eine Notiz hinzufügen
|
||||
|
|
@ -924,7 +925,7 @@
|
|||
Save & Close Speichern und schließen
|
||||
Accessibility Checker Options Optionen der Barrierefreiheitsprüfung
|
||||
Report Options Berichtsoptionen
|
||||
Create accessibility report Bericht zur Barrierefreiheit erstellen
|
||||
Create accessibility report Barrierefreiheitsbericht erstellen
|
||||
Page Range Seitenbereich
|
||||
All pages in document Alle Seiten im Dokument
|
||||
Checking Options ({on} of 32 in all categories) Prüfoptionen ({on} von 32 in allen Kategorien)
|
||||
|
|
@ -940,7 +941,7 @@
|
|||
{label} ({n} issues) {label} ({n} Probleme)
|
||||
Check again Erneut prüfen
|
||||
Report Bericht
|
||||
Show the accessibility report Bericht zur Barrierefreiheit anzeigen
|
||||
Show the accessibility report Barrierefreiheitsbericht anzeigen
|
||||
Collapse Einklappen
|
||||
Expand Ausklappen
|
||||
{rule} - {status} {rule} – {status}
|
||||
|
|
@ -1002,8 +1003,8 @@
|
|||
Animation that flashes can cause seizures. Check scripts and media for flicker. Blinkende Animationen können Anfälle auslösen. Prüfe Skripte und Medien auf Flackern.
|
||||
Scripts Skripte
|
||||
No inaccessible scripts Keine nicht barrierefreien Skripte
|
||||
Timed responses Zeitgesteuerte Antworten
|
||||
Page does not require timed responses Seite erfordert keine zeitgesteuerten Antworten
|
||||
Timed responses Zeitbegrenzte Antworten
|
||||
Page does not require timed responses Die Seite erfordert keine zeitbegrenzten Antworten
|
||||
People may need more time to respond. Check that nothing has a time limit. Manche Menschen brauchen mehr Zeit zum Reagieren. Prüfe, dass nichts ein Zeitlimit hat.
|
||||
Navigation links Navigationslinks
|
||||
Navigation links are not repetitive Navigationslinks wiederholen sich nicht
|
||||
|
|
@ -1179,12 +1180,12 @@
|
|||
{n} marks {n} Markierungen
|
||||
Dynamic Dynamisch
|
||||
Sign Here Hier unterschreiben
|
||||
Standard Business Standard-Geschäftsstempel
|
||||
Standard Business Geschäftlich
|
||||
Choose a stamp, then click on the page to place it. Wähle einen Stempel und klicke dann auf die Seite, um ihn zu platzieren.
|
||||
Note Notiz
|
||||
Polyline Polylinie
|
||||
Attachment Anhang
|
||||
Callout Legende
|
||||
Callout Text mit Hinweislinie
|
||||
Typewriter Schreibmaschine
|
||||
Checked Abgehakt
|
||||
Unchecked Nicht abgehakt
|
||||
|
|
@ -1207,7 +1208,7 @@
|
|||
Set status Status festlegen
|
||||
Copy text Text kopieren
|
||||
Properties… Eigenschaften…
|
||||
Make Current Properties Default Aktuelle Eigenschaften als Standard festlegen
|
||||
Make Current Properties Default Als Standard festlegen
|
||||
More options Weitere Optionen
|
||||
Sort by Sortieren nach
|
||||
Author Verfasser
|
||||
|
|
@ -1326,7 +1327,7 @@
|
|||
Choose a layout: Layout wählen:
|
||||
Comments only Nur Kommentare
|
||||
Sort comments by: Kommentare sortieren nach:
|
||||
Create PDF Comment Summary PDF-Kommentarzusammenfassung erstellen
|
||||
Create PDF Comment Summary Kommentarübersicht erstellen
|
||||
Revert to the previously saved version of “{name}”? Changes since then can't be undone afterwards. Zur zuletzt gespeicherten Version von „{name}“ zurückkehren? Seitdem vorgenommene Änderungen lassen sich danach nicht wiederherstellen.
|
||||
Number pages Seiten nummerieren
|
||||
Style Stil
|
||||
|
|
@ -1812,7 +1813,7 @@
|
|||
Add cloud Wolke hinzufügen
|
||||
Add polygon Polygon hinzufügen
|
||||
Add connected lines Verbundene Linien hinzufügen
|
||||
Add callout Legende hinzufügen
|
||||
Add callout Text mit Hinweislinie hinzufügen
|
||||
Add inserted text Eingefügten Text hinzufügen
|
||||
Add file attachment Dateianhang hinzufügen
|
||||
Set status Accepted Status „Angenommen“ festlegen
|
||||
|
|
@ -2043,6 +2044,7 @@ comment menu Edit Bearbeiten
|
|||
Undo {action} {action} rückgängig machen
|
||||
Redo {action} {action} wiederholen
|
||||
signature pad Type Tippen
|
||||
certificate Subject Zertifikatsinhaber
|
||||
action wizard Start Starten
|
||||
Reopen the files that were open when PdfCraft last closed Beim Start die Dateien wieder öffnen, die beim letzten Beenden von PdfCraft geöffnet waren
|
||||
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 51.
|
|
|
@ -290,6 +290,12 @@ pub fn set_current(lang: Lang) {
|
|||
}
|
||||
|
||||
/// The language the UI is drawn in.
|
||||
/// `label` as it reads inside a sentence ("add a text box"): lowercased, except in languages
|
||||
/// that capitalise nouns (German), where lowercasing would misspell it.
|
||||
pub fn in_sentence(label: &str) -> std::borrow::Cow<'_, str> {
|
||||
if current().code() == "de" { std::borrow::Cow::Borrowed(label) } else { std::borrow::Cow::Owned(label.to_lowercase()) }
|
||||
}
|
||||
|
||||
pub fn current() -> Lang {
|
||||
CURRENT.get()
|
||||
}
|
||||
|
|
@ -1216,6 +1222,24 @@ mod tests {
|
|||
assert!(missing.is_empty(), "untranslated German UI literals: {missing:#?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_names_inside_sentences_are_lowercased_except_in_german() {
|
||||
set_current(Lang::EN);
|
||||
assert_eq!(in_sentence("Text Box"), "text box");
|
||||
set_current(Lang::from_code("de").expect("de registered"));
|
||||
assert_eq!(in_sentence("Textfeld"), "Textfeld");
|
||||
set_current(Lang::EN);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn german_certificate_subject_has_a_distinct_context() {
|
||||
let de = Lang::from_code("de").expect("de registered");
|
||||
assert_eq!(tr(de, "Subject"), "Thema");
|
||||
assert_eq!(tr_ctx(de, "certificate", "Subject"), "Zertifikatsinhaber");
|
||||
assert_eq!(tr_ctx(de, "certificate", "Issuer"), tr(de, "Issuer"));
|
||||
assert_eq!(tr_ctx(Lang::EN, "certificate", "Subject"), "Subject");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn german_history_and_diagnostics_preserve_user_values() {
|
||||
let de = Lang::from_code("de").expect("de registered");
|
||||
|
|
|
|||
|
|
@ -1044,7 +1044,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(tl!(k)).color(t.text_muted));
|
||||
ui.label(egui::RichText::new(tl_ctx!("certificate", k)).color(t.text_muted));
|
||||
ui.add(egui::Label::new(val).wrap());
|
||||
ui.end_row();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,10 +63,22 @@ pub fn search_box(ui: &mut egui::Ui, placeholder: &str, width: f32) -> Response
|
|||
let fill = if resp.hovered() { t.hover } else { t.field };
|
||||
ui.painter().rect(rect, CornerRadius::same(16), fill, Stroke::new(1.0, t.border), egui::StrokeKind::Inside);
|
||||
icons::paint(ui, Rect::from_min_size(rect.min + vec2(10.0, 8.0), vec2(16.0, 16.0)), "search", 15.0, t.text_muted);
|
||||
ui.painter().text(rect.left_center() + vec2(34.0, 0.0), Align2::LEFT_CENTER, placeholder, theme::regular(13.0), t.text_faint);
|
||||
let shortcut = ui.ctx().format_shortcut(&egui::KeyboardShortcut::new(egui::Modifiers::COMMAND, egui::Key::K));
|
||||
ui.painter().text(rect.right_center() - vec2(12.0, 0.0), Align2::RIGHT_CENTER, shortcut, theme::regular(11.5), t.text_faint);
|
||||
resp.on_hover_cursor(egui::CursorIcon::Text)
|
||||
let shortcut = ui.painter().layout_no_wrap(shortcut, theme::regular(11.5), t.text_faint);
|
||||
let shortcut_pos = rect.right_center() - vec2(12.0 + shortcut.size().x, shortcut.size().y * 0.5);
|
||||
// Reserve the measured shortcut width in every language; long translations are elided
|
||||
// within the remaining space rather than painted underneath the shortcut.
|
||||
let text_right = (shortcut_pos.x - 10.0).max(rect.left());
|
||||
let text_left = (rect.left() + 34.0).min(text_right);
|
||||
let text_rect = Rect::from_min_max(egui::pos2(text_left, rect.top()), egui::pos2(text_right, rect.bottom()));
|
||||
let mut job = egui::text::LayoutJob::simple_singleline(placeholder.to_owned(), theme::regular(13.0), t.text_faint);
|
||||
job.wrap = egui::text::TextWrapping { max_width: text_rect.width(), max_rows: 1, break_anywhere: true, ..Default::default() };
|
||||
let text = ui.fonts_mut(|f| f.layout_job(job));
|
||||
let elided = text.elided;
|
||||
ui.painter().with_clip_rect(text_rect).galley(egui::pos2(text_left, rect.center().y - text.size().y * 0.5), text, t.text_faint);
|
||||
ui.painter().with_clip_rect(rect).galley(shortcut_pos, shortcut, t.text_faint);
|
||||
let resp = resp.on_hover_cursor(egui::CursorIcon::Text);
|
||||
if elided { resp.on_hover_text(placeholder) } else { resp }
|
||||
}
|
||||
|
||||
pub fn menu_item(ui: &mut egui::Ui, label: &str, shortcut: &str) -> Response {
|
||||
|
|
@ -298,6 +310,29 @@ mod tests {
|
|||
use super::*;
|
||||
use egui::Pos2;
|
||||
|
||||
#[test]
|
||||
fn search_placeholder_never_overlaps_the_shortcut() {
|
||||
for width in [260.0, 160.0, 90.0] {
|
||||
let ctx = egui::Context::default();
|
||||
ctx.set_fonts(theme::font_definitions());
|
||||
let placeholder = "Werkzeuge und Befehle suchen";
|
||||
let output = ctx.run_ui(egui::RawInput::default(), |ui| {
|
||||
search_box(ui, placeholder, width);
|
||||
});
|
||||
let text_shapes: Vec<_> = output
|
||||
.shapes
|
||||
.iter()
|
||||
.filter_map(|shape| if let egui::Shape::Text(text) = &shape.shape { Some((shape.clip_rect, text)) } else { None })
|
||||
.collect();
|
||||
let (clip, hint) = text_shapes.iter().find(|(_, text)| text.galley.job.text == placeholder).unwrap();
|
||||
let (_, shortcut) = text_shapes.iter().find(|(_, text)| text.galley.job.text.contains('K')).unwrap();
|
||||
assert!(clip.right() <= shortcut.pos.x - 9.0, "hint must leave a gap before the shortcut at width {width}");
|
||||
assert!(hint.galley.elided, "the long original German hint must be elided at width {width}");
|
||||
assert_eq!(hint.galley.rows.len(), 1);
|
||||
output.drop_without_applying_deltas();
|
||||
}
|
||||
}
|
||||
|
||||
fn toast_rect(app: &mut PdfCraftApp, ctx: &egui::Context, width: f32, now: &mut f64) -> Rect {
|
||||
// Let the Area settle after a notice/viewport change, without advancing to
|
||||
// expiry. No native rendering or renderer worker is needed for this layout.
|
||||
|
|
|
|||
|
|
@ -502,6 +502,37 @@ fn japanese_controls_and_search_keep_command_ids() {
|
|||
assert_eq!(ok(&mut h, &c, "ui.state", json!({}))["mode"], "AllTools");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn german_preferences_and_search_keep_command_ids() {
|
||||
let (mut h, c) = harness();
|
||||
ok(&mut h, &c, "ui.set", json!({ "key": "language", "value": "de" }));
|
||||
ok(&mut h, &c, "ui.click", json!({ "label": "Lesen" }));
|
||||
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": "Sprache der Oberfläche" }));
|
||||
assert!(prefs["count"].as_u64().unwrap() > 0, "{prefs}");
|
||||
let selector = ok(&mut h, &c, "ui.inspect", json!({ "query": "Deutsch" }));
|
||||
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": "English" }));
|
||||
assert_eq!(ok(&mut h, &c, "ui.state", json!({}))["language"], "en");
|
||||
let selector = ok(&mut h, &c, "ui.inspect", json!({ "query": "English" }));
|
||||
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": "Deutsch" }));
|
||||
assert_eq!(ok(&mut h, &c, "ui.state", json!({}))["language"], "de");
|
||||
ok(&mut h, &c, "ui.click", json!({ "label": "OK" }));
|
||||
for query in ["Dokument teilen", "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": "Dokument teilen…" }));
|
||||
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");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preferences_menu_and_shortcut_allow_switching_interface_languages() {
|
||||
let (mut h, c) = harness();
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ There is no right-to-left interface language yet. File names and document titles
|
|||
|
||||
Simplified Chinese, French and German coverage tests, following PhotoCraft's complete-catalog approach, check every registered command, All tools label and `tl!("literal")` in the UI source. Simplified Chinese also checks direct `i18n::t` calls and multiline literals, plus the About tabs, contributor name modes, sort options, table headers and model columns. Generated history labels, contributor summaries and diagnostics are tested with user-supplied names and error details. Catalog coverage does not guarantee glyph coverage: the release-pinned craft-fonts input currently has no `Hans` face, and the web build embeds only BIZ UDPGothic Regular. Chinese font delivery remains separate work, as discussed in [#112](https://github.com/storytold/pdfcraft/pull/112) and [#140](https://github.com/storytold/pdfcraft/pull/140).
|
||||
|
||||
German labels keep nouns capitalized, including tool names inserted into notices. The certificate viewer uses the `certificate` context for `Subject` (the certificate holder), while document properties keep the plain translation (the topic). Search hints reserve room for the shortcut, and Home cards limit their title and description to the available rows; hovering shows the full text when it is shortened.
|
||||
|
||||
## How translations work
|
||||
|
||||
PdfCraft uses the same system as PhotoCraft (`crates/ui-egui/src/i18n/`):
|
||||
|
|
|
|||
|
|
@ -9818,8 +9818,8 @@ milestone = "M14"
|
|||
status = "partial"
|
||||
commands = ["app.preferences"]
|
||||
tools = []
|
||||
evidence = ["crates/ui-egui/src/i18n/mod.rs::windows_display_language_resolves_chinese_scripts", "crates/ui-egui/src/i18n/mod.rs::windows_display_language_query_returns_a_locale_tag", "crates/ui-egui/src/i18n/mod.rs::language_persists_and_invalid_input_keeps_current_language", "crates/ui-egui/src/i18n/mod.rs::bundled_catalogs_are_consistent", "crates/ui-egui/src/i18n/mod.rs::japanese_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::simplified_chinese_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::simplified_chinese_covers_ui_literals", "crates/ui-egui/src/i18n/mod.rs::simplified_chinese_history_and_diagnostics_preserve_user_values", "crates/ui-egui/tests/control.rs::simplified_chinese_signature_prompts_and_errors_keep_document_state", "crates/ui-egui/tests/control.rs::language_switch_preserves_document_and_command_ids", "crates/ui-egui/tests/control.rs::japanese_controls_and_search_keep_command_ids", "crates/ui-egui/tests/control.rs::japanese_dialogs_errors_and_custom_action_names", "crates/ui-egui/src/i18n/mod.rs::french_is_registered", "crates/ui-egui/src/i18n/mod.rs::french_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::french_covers_ui_literals", "crates/ui-egui/src/i18n/mod.rs::french_history_and_diagnostics_preserve_user_values", "crates/ui-egui/src/i18n/mod.rs::telugu_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::telugu_labels_have_glyphs", "crates/ui-egui/src/i18n/mod.rs::hungarian_is_registered", "crates/ui-egui/src/i18n/mod.rs::hungarian_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::hungarian_covers_ui_literals", "crates/ui-egui/src/i18n/mod.rs::hungarian_history_and_diagnostics_preserve_user_values"]
|
||||
notes = "Preferences switches Auto (system language)/English/Japanese/Simplified Chinese/Traditional Chinese/Czech/Brazilian Portuguese/German/Spanish/French/Telugu/Hungarian and persists; ui.set language auto|en|ja|zh-hans|zh-hant|cs|pt-br|de|es|fr|te|hu and ui.state expose it. TSV catalogs per language (same system as PhotoCraft); every dialog, panel and notice goes through tl!. Japanese, Traditional Chinese, Simplified Chinese, German, Spanish, French and Hungarian cover commands, dialogs, panels, shortcuts and history labels; Czech and Brazilian Portuguese the menus. French locales (fr, fr_FR, fr_CA, fr_BE) select the French catalog, German locales (de, de_DE, de_AT, de_CH) the German one, Hungarian locales (hu, hu_HU) the Hungarian one. Engine/OS error details stay untranslated. Auto detects the Windows display language after locale environment overrides, with Chinese script/region mapping and English fallback. Missing: Czech and Brazilian Portuguese dialogs, Chinese and Telugu font delivery in releases, a full localization audit."
|
||||
evidence = ["crates/ui-egui/src/i18n/mod.rs::windows_display_language_resolves_chinese_scripts", "crates/ui-egui/src/i18n/mod.rs::windows_display_language_query_returns_a_locale_tag", "crates/ui-egui/src/i18n/mod.rs::language_persists_and_invalid_input_keeps_current_language", "crates/ui-egui/src/i18n/mod.rs::bundled_catalogs_are_consistent", "crates/ui-egui/src/i18n/mod.rs::japanese_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::simplified_chinese_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::simplified_chinese_covers_ui_literals", "crates/ui-egui/src/i18n/mod.rs::simplified_chinese_history_and_diagnostics_preserve_user_values", "crates/ui-egui/tests/control.rs::simplified_chinese_signature_prompts_and_errors_keep_document_state", "crates/ui-egui/tests/control.rs::language_switch_preserves_document_and_command_ids", "crates/ui-egui/tests/control.rs::japanese_controls_and_search_keep_command_ids", "crates/ui-egui/tests/control.rs::japanese_dialogs_errors_and_custom_action_names", "crates/ui-egui/src/i18n/mod.rs::french_is_registered", "crates/ui-egui/src/i18n/mod.rs::french_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::french_covers_ui_literals", "crates/ui-egui/src/i18n/mod.rs::french_history_and_diagnostics_preserve_user_values", "crates/ui-egui/src/i18n/mod.rs::german_certificate_subject_has_a_distinct_context", "crates/ui-egui/src/widgets.rs::search_placeholder_never_overlaps_the_shortcut", "crates/ui-egui/src/home.rs::translated_card_description_leaves_room_for_the_action", "crates/ui-egui/tests/control.rs::german_preferences_and_search_keep_command_ids", "crates/ui-egui/src/i18n/mod.rs::telugu_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::telugu_labels_have_glyphs", "crates/ui-egui/src/i18n/mod.rs::hungarian_is_registered", "crates/ui-egui/src/i18n/mod.rs::hungarian_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::hungarian_covers_ui_literals", "crates/ui-egui/src/i18n/mod.rs::hungarian_history_and_diagnostics_preserve_user_values"]
|
||||
notes = "Preferences switches Auto (system language)/English/Japanese/Simplified Chinese/Traditional Chinese/Czech/Brazilian Portuguese/German/Spanish/French/Telugu/Hungarian and persists; ui.set language auto|en|ja|zh-hans|zh-hant|cs|pt-br|de|es|fr|te|hu and ui.state expose it. TSV catalogs per language (same system as PhotoCraft); every dialog, panel and notice goes through tl!. Japanese, Traditional Chinese, Simplified Chinese, German, Spanish, French and Hungarian cover commands, dialogs, panels, shortcuts and history labels; Czech and Brazilian Portuguese the menus. French locales (fr, fr_FR, fr_CA, fr_BE) select the French catalog, German locales (de, de_DE, de_AT, de_CH) the German one, Hungarian locales (hu, hu_HU) the Hungarian one. German search hints reserve shortcut space, Home cards bound translated text, certificate Subject has its own context, and inserted tool names keep their capitalization. Engine/OS error details stay untranslated. Auto detects the Windows display language after locale environment overrides, with Chinese script/region mapping and English fallback. Missing: Czech and Brazilian Portuguese dialogs, Chinese and Telugu font delivery in releases, a full localization audit."
|
||||
|
||||
[[feature]]
|
||||
id = "misc.prefs-reading"
|
||||
|
|
|
|||
Loading…
Reference in New Issue