fix(ui): prevent translated tool labels from overflowing and align German terminology (#939)

* fix(ui): bound translated tool labels and polish German terminology (#828)

* style(ui): keep bounded-label layout readable

* German: keep „Ausfüllen und unterschreiben“ (Fill & Sign isn't a certificate signature) and „Für Barrierefreiheit vorbereiten“

* Refresh catalog hashes

* Test: find the German Fill & Sign heading whole or elided

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
Nigel Ihza Firdausy 2026-10-12 01:36:41 +07:00 committed by GitHub
parent 21d5323132
commit a3d30d5866
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 61 additions and 17 deletions

View File

@ -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 = "386d5e5e5dcdf80487d304f914a0ac0f7e17c5d2134ff5df37b477b8cb5bf3ca"
sha256 = "6bb45afb959816bc6c69a1b262d568d49fe8c9a3dcec8a321231196b0831251f"
[[asset]]
path = "crates/ui-egui/src/i18n/te.tsv"

View File

@ -29,7 +29,7 @@
Create PDF from images… PDF aus Bildern erstellen…
Create PDF from multiple files… PDF aus mehreren Dateien erstellen…
Create PDF from clipboard PDF aus Zwischenablage erstellen
Combine files… Dateien kombinieren…
Combine files… Dateien zusammenführen…
Save Speichern
Save as… Speichern unter…
Close file Datei schließen
@ -54,7 +54,7 @@
Comments panel Kommentarbereich
Form fields panel Formularfeldbereich
Clear form Formular zurücksetzen
Find tools and commands… Tools und Befehle suchen…
Find tools and commands… Werkzeuge und Befehle suchen…
Zoom Zoom
Actual size Originalgröße
Zoom to page level Ganze Seite
@ -96,7 +96,7 @@
Print ({key}) Drucken ({key})
Save ({key}) Speichern ({key})
Document properties ({key}) Dokumenteigenschaften ({key})
Find tools and commands Tools und Befehle suchen
Find tools and commands Werkzeuge und Befehle suchen
Fit to height Seitenhöhe
Zoom in Vergrößern
Zoom out Verkleinern
@ -322,9 +322,9 @@
Images Bilder
Clipboard Zwischenablage
Blank page Leere Seite
Combine files Dateien kombinieren
Combine Kombinieren
Add files to combine Dateien zum Kombinieren hinzufügen
Combine files Dateien zusammenführen
Combine Zusammenführen
Add files to combine Dateien zum Zusammenführen hinzufügen
Page options Seitenoptionen
Page grid Seitenraster
Rotate Drehen
@ -337,7 +337,7 @@
Set page boxes Seitenrahmen festlegen
Page labels Seitenbeschriftungen
Page transitions Seitenübergänge
Add comments Kommentieren
Add comments Kommentare hinzufügen
Markup Markierungen
Sticky note Notiz
Underline Unterstreichen

Can't render this file because it contains an unexpected character in line 6 and column 51.

View File

@ -35,12 +35,14 @@ fn panel_header(ui: &mut egui::Ui, t: &Tokens, title: &str, back: bool) -> (bool
if back && icons::button(ui, "chevron-left", 26.0, false, tl!("Back to all tools")).clicked() {
go_back = true;
}
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, tl!("Close panel")).clicked() {
close = true;
}
});
// Reserve space for Close before laying out the translated title. Long tool names
// must never expand the fixed-width sidebar or push the Close control off-screen.
let title_width = (ui.available_width() - 26.0 - ui.spacing().item_spacing.x).max(1.0);
ui.add_sized([title_width, 26.0], egui::Label::new(egui::RichText::new(tl!(title)).font(theme::semibold(15.5)).color(t.text)).truncate())
.on_hover_text(tl!(title));
if icons::button(ui, "x", 26.0, false, tl!("Close panel")).clicked() {
close = true;
}
});
ui.add_space(6.0);
(go_back, close)
@ -66,6 +68,17 @@ fn all_tools(app: &mut PdfCraftApp, ui: &mut egui::Ui, t: &Tokens) {
});
}
/// Draw a one-line row label without painting into the status chip or beyond the sidebar.
/// Accessibility retains the complete name through the row's WidgetInfo.
fn row_label(ui: &mut egui::Ui, text: &str, font: egui::FontId, color: Color32, left_center: egui::Pos2, max_width: f32) -> bool {
let mut job = egui::text::LayoutJob::simple_singleline(text.to_owned(), font, color);
job.wrap = egui::text::TextWrapping { max_width: max_width.max(1.0), max_rows: 1, break_anywhere: true, ..Default::default() };
let galley = ui.fonts_mut(|f| f.layout_job(job));
let elided = galley.elided;
ui.painter().galley(left_center - vec2(0.0, galley.size().y / 2.0), galley, color);
elided
}
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, tl!(g.label)));
@ -73,7 +86,16 @@ fn tool_row(ui: &mut egui::Ui, t: &Tokens, g: &ToolGroup) -> egui::Response {
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, tl!(g.label), theme::regular(13.5), t.text);
let right_reserved = match (g.badge, g.availability) {
(Some(b), _) => ui.fonts_mut(|f| f.layout_no_wrap(tl!(b).to_owned(), theme::semibold(9.5), Color32::WHITE).size().x) + 26.0,
(None, Availability::Planned(m)) if resp.hovered() => {
let hint = format!("{} · {m}", tl!("Planned"));
ui.fonts_mut(|f| f.layout_no_wrap(hint, theme::medium(10.5), t.text_faint).size().x) + 18.0
}
_ => 8.0,
};
let clipped =
row_label(ui, tl!(g.label), theme::regular(13.5), t.text, rect.left_center() + vec2(36.0, 0.0), rect.width() - 36.0 - right_reserved);
match (g.badge, g.availability) {
(Some(b), _) => {
let font = theme::semibold(9.5);
@ -100,6 +122,7 @@ fn tool_row(ui: &mut egui::Ui, t: &Tokens, g: &ToolGroup) -> egui::Response {
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(),
};
let tip = if clipped { format!("{} — {tip}", tl!(g.label)) } else { tip };
resp.on_hover_text(tip)
}
@ -185,7 +208,6 @@ 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, tl!(item.label), theme::regular(13.0), fg);
let (chip, fill, cfg) = match item.availability {
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),
@ -194,9 +216,10 @@ fn tool_detail(app: &mut PdfCraftApp, ui: &mut egui::Ui, t: &Tokens, g: &'static
let font = theme::semibold(9.5);
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));
row_label(ui, tl!(item.label), theme::regular(13.0), fg, rect.left_center() + vec2(34.0, 0.0), r.left() - rect.left() - 42.0);
ui.painter().rect_filled(r, CornerRadius::same(4), fill);
ui.painter().text(r.center(), Align2::CENTER_CENTER, tl!(chip), font, cfg);
if resp.on_hover_text(item.command).clicked() {
if resp.on_hover_text(format!("{} — {}", tl!(item.label), item.command)).clicked() {
run = Some(item.command);
}
}

View File

@ -937,6 +937,27 @@ fn cancelling_then_discarding_an_earlier_dirty_tab_preserves_the_selected_docume
}
}
#[test]
fn long_german_tool_labels_stay_within_the_sidebar() {
use pdfcraft_ui_egui::{LeftPanel, Mode};
let mut h = harness(|app| app.open_bytes("fixture.pdf", None, FIXTURE.to_vec()).expect("fixture opens"));
h.state_mut().set_option("language", "de").unwrap();
h.state_mut().mode = Mode::AllTools;
h.state_mut().left_open = true;
// The tool heading has to leave room for Back and Close even when translated.
h.state_mut().left = LeftPanel::Tool("fill_sign");
h.run_steps(4);
let heading =
painted_text(&h).into_iter().find(|(s, ..)| s.starts_with("Ausfüllen und")).expect("translated heading is painted, whole or elided");
assert!(heading.2.right() <= 272.0, "translated heading runs past the sidebar: {:?}", heading.2);
// A long sub-tool name must stop before its status chip, not paint over the PDF.
h.state_mut().left = LeftPanel::Tool("protect");
h.run_steps(4);
let item =
painted_text(&h).into_iter().find(|(s, ..)| s == "Ausgeblendete Informationen entfernen").expect("long translated sub-tool is painted");
assert!(item.2.right() <= 272.0, "translated tool item runs past the sidebar: {:?}", item.2);
}
#[test]
fn document_tabs_expose_selection_and_activate_from_keyboard() {
use egui::accesskit::Role;