From 033248d5d8f2d46ebdeea4fd6e1c27d2585564a3 Mon Sep 17 00:00:00 2001 From: Dmytro Matviichuk Date: Fri, 9 Oct 2026 19:59:09 +0200 Subject: [PATCH 1/6] i18n #30: add Ukrainian interface and language preference Co-authored-by: Codex --- ATTRIBUTION.md | 1 + Cargo.lock | 10 + ROADMAP.md | 15 +- apps/deckcraft-web/Cargo.toml | 2 +- apps/deckcraft-web/src/web.rs | 9 +- apps/deckcraft/Cargo.toml | 1 + apps/deckcraft/src/main.rs | 14 +- crates/render/src/lib.rs | 9 +- crates/render/src/tests.rs | 22 + crates/ui-egui/src/canvas.rs | 30 +- crates/ui-egui/src/credits.rs | 58 +- crates/ui-egui/src/dialogs.rs | 214 +++--- crates/ui-egui/src/fillui.rs | 22 +- crates/ui-egui/src/i18n.rs | 358 +++++++++ crates/ui-egui/src/i18n/uk.tsv | 1234 ++++++++++++++++++++++++++++++++ crates/ui-egui/src/lib.rs | 29 +- crates/ui-egui/src/media.rs | 4 +- crates/ui-egui/src/menus.rs | 13 +- crates/ui-egui/src/panes.rs | 136 ++-- crates/ui-egui/src/ribbon.rs | 310 ++++---- crates/ui-egui/src/show.rs | 40 +- crates/ui-egui/src/sorter.rs | 6 +- crates/ui-egui/src/status.rs | 21 +- crates/ui-egui/src/textures.rs | 58 +- crates/ui-egui/src/thumbs.rs | 32 +- crates/ui-egui/src/widgets.rs | 29 +- docs/gaps.md | 12 +- docs/localization-parity.md | 28 +- docs/roadmap.md | 7 +- docs/target-app-parity.md | 5 +- 30 files changed, 2288 insertions(+), 441 deletions(-) create mode 100644 crates/ui-egui/src/i18n.rs create mode 100644 crates/ui-egui/src/i18n/uk.tsv diff --git a/ATTRIBUTION.md b/ATTRIBUTION.md index c8137dfbf..7af26b760 100644 --- a/ATTRIBUTION.md +++ b/ATTRIBUTION.md @@ -27,6 +27,7 @@ deck and UI icons are original work of the DeckCraft contributors (drawn or gene | `crates/media/tests/fixtures/tone*` (`tone.mp3`, `.m4a`, `.aac`, `.ogg`, `.opus`, `.flac`, `.wma`, `tone-alac.m4a`) | DeckCraft contributors | synthesised 0.5 s 440 Hz sine, encoded by `crates/media/tests/fixtures/make.sh` (ffmpeg as the encoder only) | CC0-1.0 | Audio decoding tests | | `crates/media/tests/fixtures/clip-*` (`clip-h264.mp4`, `clip-h264.mov`, `clip-hevc.mp4`, `clip-vp9.webm`, `clip-av1.mkv`) | DeckCraft contributors | 6 frames of a colour gradient generated by `make.sh` (python3), encoded by ffmpeg (x264 / x265 / libvpx / SVT-AV1 as encoders only) | CC0-1.0 | Video decoding tests | | `crates/media/tests/fixtures/make.sh` | DeckCraft contributors | this repository | MIT OR Apache-2.0 | Regenerates the media fixtures | +| `crates/ui-egui/src/i18n/uk.tsv` | @dmatviichuk, assisted by Codex | Contributor-original Ukrainian translation in this repository; terminology checked against VESUM 6.8.6 and ULIF (validation tools/data are not bundled) | MIT OR Apache-2.0 (`LICENSE-MIT`, `LICENSE-APACHE`) | Ukrainian interface text | ## Bundled through dependencies diff --git a/Cargo.lock b/Cargo.lock index e50d814e4..a397e870b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -890,6 +890,7 @@ dependencies = [ "log", "rfd", "serde_json", + "sys-locale", "winit", "winresource", ] @@ -4278,6 +4279,15 @@ dependencies = [ "syn 3.0.6", ] +[[package]] +name = "sys-locale" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4" +dependencies = [ + "libc", +] + [[package]] name = "tempfile" version = "3.27.0" diff --git a/ROADMAP.md b/ROADMAP.md index 801ddd843..d49ff8386 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -2,7 +2,7 @@ **Stage: alpha** · next: beta, ~20 points and ~120–210 h away -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** minor (hours to ~95% per audience added; ready 56% → 55%; stage stays alpha; previous: major, full re-measure against PowerPoint for Mac 16.113.4; restructured to the craftrules progress-docs standard) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** minor (Ukrainian UI catalog and language selection) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) DeckCraft aims at full PowerPoint parity, and to be better: faster, open (a documented zip+JSON format plus PPTX), scriptable by agents (CLI, JSON control channel, MCP), and available everywhere @@ -84,7 +84,10 @@ EMF/SVG pictures, the data-loss and startup-crash bugs (#60, #40, #59) and the b ## Languages -English only today; no string catalog yet (Ukrainian is in review as PR #34). Detail: +English and Ukrainian: Preferences offers System language, English and Українська. Ukrainian +OS/browser locales select Ukrainian automatically; explicit desktop choices persist in `ui.json`. +Menus, ribbon, command palette, panels, dialogs and presenter controls use the interface catalog. +Presentation content and command IDs retain their original values. Detail: [docs/localization-parity.md](docs/localization-parity.md). | Language | Code | UI strings | Status | Hours to full | @@ -102,8 +105,8 @@ English only today; no string catalog yet (Ukrainian is in review as PR #34). De | Korean | ko | 0% | none | 4–7 | | Vietnamese | vi | 0% | none | 3–5 | -Plus 8–14 h once for the catalog infrastructure. Other languages shipped: none. PowerPoint for Mac -ships 26. +Other languages shipped: **Ukrainian (uk)**, with English fallback and bundled Cyrillic glyph +coverage. PowerPoint for Mac ships 26 languages; additional catalogs and native review remain. ## Upcoming @@ -113,7 +116,7 @@ Ranked; detail and the full beta plan in [docs/roadmap.md](docs/roadmap.md). image paste (#53), editor soak tests. 10–20 h. 2. **PPTX on real decks:** owned corpus, round-trip checks, fixes; #70, #74, #77; EMF/WMF and SVG. 36–64 h. -3. **Localization:** catalog, system language (#15), zh-Hans, es, fr, de, ja with IME. 27–46 h. +3. **Localization:** additional catalogs for zh-Hans, es, fr, de, ja, native review and IME. 27–46 h. 4. **Output:** vector PDF, printing. 10–16 h. 5. **Depth that users hit:** font weights (#69), chart data and elements, Animation Pane timeline, presenter view on the right display, native macOS menu bar, Format Shape pane. 33–56 h. @@ -128,6 +131,7 @@ app's JSON control channel (`deckcraft --control PORT`). | Date | What landed | |---|---| +| 2026-10-10 | Ukrainian UI catalog, system-language detection and persisted language choice (#15, #30); translated placeholder prompts preserve presentation content | | 2026-10-10 | Progress docs restructured to the craftrules standard; full re-measure (breadth 79%, ready ~55%, mainstream ~53%, essentials ~62%, alpha). Landed: Animation Painter (#54), Morph geometry outlines (#27), animated GIF playback (#39), hyperlink colour and following links in the show (#37, #45), date fields in their own format (#36), flipped shapes keep readable text (#35), show transition fix (#46), start screen from the Home button (#49), MCP export lists PDF (#65), OmaStore manifest (#57) | | 2026-10-09 | v0.4.0 released. UAX #9 bidi and Arabic shaping (#4), Morph with Words and Characters (#5), tables grow to fit and PDF cell text (#14), rotating log file (#7), DX12 default on Windows (#16), H.264/HEVC overflow hardening (#2), PPTX sniffing anywhere in the zip (#19) | | 2026-10-08 | v0.2.0 and v0.3.0 released: Windows arm64, Flatpak aarch64, AppImage zsync updates, branded DMG | @@ -139,6 +143,7 @@ app's JSON control channel (`deckcraft --control PORT`). | Date | Change | Summary | |---|---|---| +| 2026-10-10 | minor | Added Ukrainian UI and language selection to the reorganized language summary | | 2026-10-10 | minor | Readiness-by-audience table with hours to ~95% for full, mainstream and essentials | | 2026-10-10 | minor | Added mainstream practitioner (~53%) and essentials user (~62%); features recomputed from written area weights, ready ~56% → ~55% | | 2026-10-10 | minor | Stage checked against the new core-workflow gate: passes, stays alpha | diff --git a/apps/deckcraft-web/Cargo.toml b/apps/deckcraft-web/Cargo.toml index 7014421d8..fa1171ed3 100644 --- a/apps/deckcraft-web/Cargo.toml +++ b/apps/deckcraft-web/Cargo.toml @@ -27,4 +27,4 @@ rfd = { workspace = true } wasm-bindgen = "0.2" wasm-bindgen-futures = "0.4" js-sys = "0.3" -web-sys = { version = "0.3", features = ["Window", "Document", "Element", "HtmlElement", "HtmlCanvasElement", "HtmlAnchorElement", "Blob", "BlobPropertyBag", "Url", "Location", "CssStyleDeclaration"] } +web-sys = { version = "0.3", features = ["Window", "Navigator", "Document", "Element", "HtmlElement", "HtmlCanvasElement", "HtmlAnchorElement", "Blob", "BlobPropertyBag", "Url", "Location", "CssStyleDeclaration"] } diff --git a/apps/deckcraft-web/src/web.rs b/apps/deckcraft-web/src/web.rs index f0199a558..014af2412 100644 --- a/apps/deckcraft-web/src/web.rs +++ b/apps/deckcraft-web/src/web.rs @@ -38,6 +38,7 @@ pub fn start() { } let inbox: Inbox = Inbox::default(); let mut app = SlideApp::new(Session::new(), services(inbox.clone(), cc.egui_ctx.clone())); + app.services.system_locale = web_sys::window().and_then(|w| w.navigator().language()); let q = query(); if !q.contains("blank") { if let Err(e) = deckcraft_engine::sample::open_sample(&mut app.session) { @@ -111,10 +112,10 @@ fn services(inbox: Inbox, ctx: egui::Context) -> Services { let inbox = open_inbox.clone(); let ctx = ctx.clone(); let dialog = match purpose { - "picture" => rfd::AsyncFileDialog::new().add_filter("Pictures", PICTURE_EXTS), - "audio" => rfd::AsyncFileDialog::new().add_filter("Audio", AUDIO_EXTS), - "video" => rfd::AsyncFileDialog::new().add_filter("Video", VIDEO_EXTS), - _ => rfd::AsyncFileDialog::new().add_filter("Presentations", PRESENTATION_EXTS), + "picture" => rfd::AsyncFileDialog::new().add_filter(deckcraft_ui_egui::i18n::tr("Pictures"), PICTURE_EXTS), + "audio" => rfd::AsyncFileDialog::new().add_filter(deckcraft_ui_egui::i18n::tr("Audio"), AUDIO_EXTS), + "video" => rfd::AsyncFileDialog::new().add_filter(deckcraft_ui_egui::i18n::tr("Video"), VIDEO_EXTS), + _ => rfd::AsyncFileDialog::new().add_filter(deckcraft_ui_egui::i18n::tr("Presentations"), PRESENTATION_EXTS), }; wasm_bindgen_futures::spawn_local(async move { let Some(file) = dialog.pick_file().await else { diff --git a/apps/deckcraft/Cargo.toml b/apps/deckcraft/Cargo.toml index 26ea30812..bbfaf9db0 100644 --- a/apps/deckcraft/Cargo.toml +++ b/apps/deckcraft/Cargo.toml @@ -11,6 +11,7 @@ default-run = "deckcraft" workspace = true [dependencies] +sys-locale = "0.3.2" deckcraft-engine = { workspace = true } deckcraft-ui-egui = { workspace = true } deckcraft-media = { workspace = true } diff --git a/apps/deckcraft/src/main.rs b/apps/deckcraft/src/main.rs index 7b5e48bbe..7bb70469b 100644 --- a/apps/deckcraft/src/main.rs +++ b/apps/deckcraft/src/main.rs @@ -91,20 +91,21 @@ fn save_prefs(app: &SlideApp) { fn services() -> Services { Services { + system_locale: sys_locale::get_locale(), pick_open: Some(Box::new(|purpose: &str| { let d = rfd::FileDialog::new(); let d = match purpose { - "picture" => d.add_filter("Pictures", &["png", "jpg", "jpeg", "gif", "webp", "bmp", "tif", "tiff"]), + "picture" => d.add_filter(deckcraft_ui_egui::i18n::tr("Pictures"), &["png", "jpg", "jpeg", "gif", "webp", "bmp", "tif", "tiff"]), "audio" => d.add_filter( - "Audio", + deckcraft_ui_egui::i18n::tr("Audio"), &["wav", "mp3", "m4a", "m4b", "aac", "flac", "ogg", "oga", "opus", "aif", "aiff", "aifc", "caf", "wma", "weba", "mka"], ), - "video" => d.add_filter("Video", &["mp4", "m4v", "mov", "webm", "mkv", "wmv"]), + "video" => d.add_filter(deckcraft_ui_egui::i18n::tr("Video"), &["mp4", "m4v", "mov", "webm", "mkv", "wmv"]), _ => d - .add_filter("Presentations", &["deckcraft", "pptx", "potx", "ppsx"]) + .add_filter(deckcraft_ui_egui::i18n::tr("Presentations"), &["deckcraft", "pptx", "potx", "ppsx"]) .add_filter("DeckCraft", &["deckcraft"]) .add_filter("PowerPoint", &["pptx", "potx", "ppsx"]) - .add_filter("Outline", &["txt", "md"]), + .add_filter(deckcraft_ui_egui::i18n::tr("Outline"), &["txt", "md"]), }; d.pick_file().map(|p| p.to_string_lossy().to_string()) })), @@ -205,7 +206,8 @@ fn main() -> eframe::Result { if let Ok(v) = app.session.execute("file.recovery.open", &serde_json::json!({})) && let Some(n) = v.get("opened").and_then(|o| o.as_array()).map(Vec::len).filter(|n| *n > 0) { - app.set_status(format!("Recovered {n} unsaved presentation{} from the last session", if n == 1 { "" } else { "s" })); + deckcraft_ui_egui::i18n::set_current(app.ui.interface_language.resolve(app.services.system_locale.as_deref())); + app.set_status(deckcraft_ui_egui::i18n::recovered(n)); } } app.integrated_titlebar = cfg!(target_os = "macos"); diff --git a/crates/render/src/lib.rs b/crates/render/src/lib.rs index 6dde577a0..3e80d735c 100644 --- a/crates/render/src/lib.rs +++ b/crates/render/src/lib.rs @@ -138,6 +138,8 @@ pub struct RenderOpts<'a> { pub scale: f64, /// Editor view: placeholder prompts and dashed placeholder outlines. pub edit: bool, + /// Optional interface prompt provider; custom layout prompts and document text stay intact. + pub placeholder_prompt: Option &'static str>, /// Draw shapes marked hidden (Selection pane) — the editor hides them like the show does. pub show_hidden: bool, /// Per-shape animation state. @@ -166,6 +168,7 @@ impl Default for RenderOpts<'_> { RenderOpts { scale: 1.0, edit: false, + placeholder_prompt: None, show_hidden: false, state: None, skip: &[], @@ -773,7 +776,7 @@ impl Renderer { draw_layout_with(ctx, &l, tr, m, &st.paras); } else if f.opts.edit && s.ph.is_some() { let kind = s.ph_type().unwrap_or(PhType::Body); - let prompt_body = prompt_body(rctx, s, kind); + let prompt_body = prompt_body(rctx, s, kind, f.opts.placeholder_prompt.map(|prompt| prompt(kind))); if !prompt_body.is_empty() { let grey = Rgba::rgb(0x59, 0x59, 0x59); draw_text(ctx, rctx, s, &prompt_body, tr, m, &f.fields, Some(grey)); @@ -1009,7 +1012,7 @@ fn arrowheads(ctx: &mut RenderContext, rctx: &Ctx, line: &Line, ph: Option } /// The prompt shown in an empty placeholder, formatted like its content would be. -fn prompt_body(rctx: &Ctx, s: &Shape, kind: PhType) -> TextBody { +fn prompt_body(rctx: &Ctx, s: &Shape, kind: PhType, interface_prompt: Option<&str>) -> TextBody { // Layout/master prompt text when the layout supplies a custom one. let (lp, _) = resolve::parents(rctx, s); let custom = lp.and_then(|l| l.ph.as_ref().filter(|p| p.has_custom_prompt).and(l.text.as_ref())).filter(|t| !t.is_empty()); @@ -1019,7 +1022,7 @@ fn prompt_body(rctx: &Ctx, s: &Shape, kind: PhType) -> TextBody { if matches!(rctx.owner, resolve::Owner::Layout | resolve::Owner::Master) { return TextBody::default(); } - let txt = kind.prompt(); + let txt = interface_prompt.unwrap_or_else(|| kind.prompt()); if txt.is_empty() || kind.is_footer_kind() { return TextBody::default(); } diff --git a/crates/render/src/tests.rs b/crates/render/src/tests.rs index ccb4472fa..17bb7d863 100644 --- a/crates/render/src/tests.rs +++ b/crates/render/src/tests.rs @@ -523,3 +523,25 @@ fn hyperlinks_draw_in_the_hyperlink_colour_underlined() { let own = px(&run_deck(RunProps { link, fill: red.clone(), ..Default::default() })); assert_eq!(diff(&own, &px(&run_deck(RunProps { fill: red, underline: Some("sng".into()), ..Default::default() }))), 0); } + +#[test] +fn interface_placeholder_prompts_preserve_custom_prompts_and_document_data() { + let mut p = Presentation::default(); + let slide = p.slides.first().unwrap(); + let shape = slide.shapes.iter().find(|shape| shape.ph_type() == Some(PhType::Title) || shape.ph_type() == Some(PhType::CtrTitle)).unwrap(); + let c = Ctx::for_slide(&p, slide).unwrap(); + let before = p.clone(); + assert_eq!(prompt_body(&c, shape, PhType::Title, Some("Натисніть, щоб додати заголовок")).text(), "Натисніть, щоб додати заголовок"); + assert_eq!(p, before); + + let layout_id = slide.layout; + let master = Arc::make_mut(p.masters.first_mut().unwrap()); + let layout = master.layouts.iter_mut().find(|layout| layout.id == layout_id).unwrap(); + let shape = layout.shapes.iter_mut().find(|shape| shape.ph_type() == Some(PhType::Title) || shape.ph_type() == Some(PhType::CtrTitle)).unwrap(); + shape.ph.as_mut().unwrap().has_custom_prompt = true; + shape.text = Some(TextBody::from_text("Click to add title")); + let slide = p.slides.first().unwrap(); + let shape = slide.shapes.iter().find(|shape| shape.ph_type() == Some(PhType::Title) || shape.ph_type() == Some(PhType::CtrTitle)).unwrap(); + let c = Ctx::for_slide(&p, slide).unwrap(); + assert_eq!(prompt_body(&c, shape, PhType::Title, Some("Натисніть, щоб додати заголовок")).text(), "Click to add title"); +} diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index d267f76cb..69a261efb 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -56,7 +56,7 @@ pub fn show(app: &mut SlideApp, ui: &mut Ui) { let Some(st) = app.session.active() else { return }; if st.doc.slides.is_empty() && st.selection.target == Target::Slides { let resp = ui.allocate_rect(avail, Sense::click()); - ui.painter().text(avail.center(), Align2::CENTER_CENTER, "Click to add first slide", theme::font(22.0), t.text_dim); + ui.painter().text(avail.center(), Align2::CENTER_CENTER, crate::i18n::tr("Click to add first slide"), theme::font(22.0), t.text_dim); if resp.clicked() { let _ = app.run("slide.new", json!({"layout": "title"})); } @@ -271,7 +271,7 @@ fn ruler(p: &egui::Painter, avail: Rect, slide: Rect, scale: f32, t: &Tokens) { }; p.line_segment([pos2(x, top.max.y - h), pos2(x, top.max.y)], Stroke::new(1.0, t.text_faint)); if k % 4 == 0 { - p.text(pos2(x + 2.0, top.min.y + 1.0), Align2::LEFT_TOP, format!("{}", k / 4), theme::font(8.0), t.text_dim); + p.text(pos2(x + 2.0, top.min.y + 1.0), Align2::LEFT_TOP, (k / 4).to_string(), theme::font(8.0), t.text_dim); } k += 1; } @@ -540,47 +540,47 @@ fn context_menu(app: &mut SlideApp, resp: &egui::Response) { resp.context_menu(|ui| { let has_sel = app.session.active().is_some_and(|d| !d.selection.shapes.is_empty()); for (l, id) in [("Cut", "edit.cut"), ("Copy", "edit.copy"), ("Paste", "edit.paste")] { - if ui.add_enabled(crate::ribbon::enabled(app, id), egui::Button::new(l)).clicked() { + if ui.add_enabled(crate::ribbon::enabled(app, id), egui::Button::new(crate::i18n::tr(l))).clicked() { let _ = app.run(id, json!({})); ui.close(); } } ui.separator(); if has_sel { - if ui.button("Edit Text").clicked() { + if ui.button(crate::i18n::tr("Edit Text")).clicked() { let _ = app.run("text.edit", json!({})); ui.close(); } - ui.menu_button("Bring to Front", |ui| { + ui.menu_button(crate::i18n::tr("Bring to Front"), |ui| { crate::ribbon::menu_item(app, ui, crate::icons::Icon::BringToFront, "Bring to Front", "arrange.bringToFront", json!({})); crate::ribbon::menu_item(app, ui, crate::icons::Icon::BringForward, "Bring Forward", "arrange.bringForward", json!({})); }); - ui.menu_button("Send to Back", |ui| { + ui.menu_button(crate::i18n::tr("Send to Back"), |ui| { crate::ribbon::menu_item(app, ui, crate::icons::Icon::SendToBack, "Send to Back", "arrange.sendToBack", json!({})); crate::ribbon::menu_item(app, ui, crate::icons::Icon::SendBackward, "Send Backward", "arrange.sendBackward", json!({})); }); - ui.menu_button("Group", |ui| { + ui.menu_button(crate::i18n::tr("Group"), |ui| { crate::ribbon::menu_item(app, ui, crate::icons::Icon::Group, "Group", "arrange.group", json!({})); crate::ribbon::menu_item(app, ui, crate::icons::Icon::Ungroup, "Ungroup", "arrange.ungroup", json!({})); }); - if ui.button("Link…").clicked() { + if ui.button(crate::i18n::tr("Link…")).clicked() { app.dialog = Some(crate::dialogs::Dialog::new("hyperlink")); ui.close(); } - if ui.button("Edit Alt Text…").clicked() { + if ui.button(crate::i18n::tr("Edit Alt Text…")).clicked() { app.dialog = Some(crate::dialogs::Dialog::new("altText")); ui.close(); } - if ui.button("Set as Default Shape").clicked() { + if ui.button(crate::i18n::tr("Set as Default Shape")).clicked() { let _ = app.run("shape.setDefault", json!({})); ui.close(); } - if ui.button("Format Shape…").clicked() { + if ui.button(crate::i18n::tr("Format Shape…")).clicked() { let _ = app.run("view.pane", json!({"pane": "format"})); ui.close(); } } else { - ui.menu_button("Layout", |ui| { + ui.menu_button(crate::i18n::tr("Layout"), |ui| { let layouts: Vec<(String, u32)> = app .session .active() @@ -593,15 +593,15 @@ fn context_menu(app: &mut SlideApp, resp: &egui::Response) { } } }); - if ui.button("Reset Slide").clicked() { + if ui.button(crate::i18n::tr("Reset Slide")).clicked() { let _ = app.run("slide.reset", json!({})); ui.close(); } - if ui.button("Format Background…").clicked() { + if ui.button(crate::i18n::tr("Format Background…")).clicked() { let _ = app.run("view.pane", json!({"pane": "background"})); ui.close(); } - if ui.button("New Comment").clicked() { + if ui.button(crate::i18n::tr("New Comment")).clicked() { app.dialog = Some(crate::dialogs::Dialog::new("comment")); ui.close(); } diff --git a/crates/ui-egui/src/credits.rs b/crates/ui-egui/src/credits.rs index 765dec19d..b7f01233b 100644 --- a/crates/ui-egui/src/credits.rs +++ b/crates/ui-egui/src/credits.rs @@ -123,18 +123,20 @@ impl Contributor { /// One line with everything we know, for tooltips. pub fn summary(&self) -> String { - format!( + crate::i18n::format( "@{}: {} PRs, {} commits, +{} / −{} lines (Δ {}), +{} / −{} binary assets, {} – {}", - self.login, - self.prs, - self.commits, - group(self.lines_added), - group(self.lines_deleted), - signed(self.lines_delta()), - self.binary_added, - self.binary_deleted, - day(self.first_commit), - day(self.last_commit), + &[ + (self.login).to_string(), + (self.prs).to_string(), + (self.commits).to_string(), + (group(self.lines_added)).to_string(), + (group(self.lines_deleted)).to_string(), + (signed(self.lines_delta())).to_string(), + (self.binary_added).to_string(), + (self.binary_deleted).to_string(), + (day(self.first_commit)).to_string(), + (day(self.last_commit)).to_string(), + ], ) } } @@ -215,39 +217,43 @@ pub fn contributors_ui(ui: &mut egui::Ui) { let id = egui::Id::new("credits_view"); let mut v = ui.data_mut(|d| d.get_temp::(id)).unwrap_or_default(); ui.horizontal_wrapped(|ui| { - ui.label("Show"); + ui.label(crate::i18n::tr("Show")); for m in NameMode::ALL { - if ui.selectable_label(v.names == m, m.label()).clicked() { + if ui.selectable_label(v.names == m, crate::i18n::tr(m.label())).clicked() { v.names = m; } } ui.separator(); - ui.label("Sort"); - egui::ComboBox::from_id_salt("credits_sort").selected_text(v.key.label().0).show_ui(ui, |ui| { + ui.label(crate::i18n::tr("Sort")); + egui::ComboBox::from_id_salt("credits_sort").selected_text(crate::i18n::tr(v.key.label().0)).show_ui(ui, |ui| { for k in SortKey::ALL { - if ui.selectable_label(v.key == k, k.label().0).clicked() { + if ui.selectable_label(v.key == k, crate::i18n::tr(k.label().0)).clicked() { v.key = k; v.ascending = k.default_ascending(); } } }); - if ui.button(if v.ascending { "▲" } else { "▼" }).on_hover_text("Reverse the order").clicked() { + if ui.button(if v.ascending { "▲" } else { "▼" }).on_hover_text(crate::i18n::tr("Reverse the order")).clicked() { v.ascending = !v.ascending; } ui.separator(); - if ui.selectable_label(!v.table, "Grab bag").clicked() { + if ui.selectable_label(!v.table, crate::i18n::tr("Grab bag")).clicked() { v.table = false; } - if ui.selectable_label(v.table, "Table").clicked() { + if ui.selectable_label(v.table, crate::i18n::tr("Table")).clicked() { v.table = true; } }); let list = sorted(CONTRIBUTORS, v.names, v.key, v.ascending); - ui.label(RichText::new(format!("{} contributors · {} commits", list.len(), group(TOTAL_COMMITS))).small().weak()); + ui.label( + RichText::new(crate::i18n::format("{} contributors · {} commits", &[(list.len()).to_string(), (group(TOTAL_COMMITS)).to_string()])) + .small() + .weak(), + ); ui.separator(); egui::ScrollArea::both().auto_shrink([false, false]).show(ui, |ui| { if list.is_empty() { - ui.label("No contributor data was built into this copy."); + ui.label(crate::i18n::tr("No contributor data was built into this copy.")); } else if v.table { table(ui, &list, &mut v); } else { @@ -268,7 +274,11 @@ fn table(ui: &mut egui::Ui, list: &[&Contributor], v: &mut View) { egui::Grid::new("credits_table").striped(true).num_columns(SortKey::ALL.len()).show(ui, |ui| { for k in SortKey::ALL { let arrow = if v.key == k { if v.ascending { " ▲" } else { " ▼" } } else { "" }; - if ui.button(RichText::new(format!("{}{arrow}", k.label().1)).strong()).on_hover_text(k.label().0).clicked() { + if ui + .button(RichText::new(format!("{}{arrow}", crate::i18n::tr(k.label().1))).strong()) + .on_hover_text(crate::i18n::tr(k.label().0)) + .clicked() + { if v.key == k { v.ascending = !v.ascending; } else { @@ -297,14 +307,14 @@ fn table(ui: &mut egui::Ui, list: &[&Contributor], v: &mut View) { /// About ▸ Models: AI models credited in Co-Authored-By trailers. pub fn models_ui(ui: &mut egui::Ui) { if MODELS.is_empty() { - ui.label("No model credits were built into this copy."); + ui.label(crate::i18n::tr("No model credits were built into this copy.")); return; } let assisted: u64 = MODELS.iter().map(|m| m.commits).max().unwrap_or(0).max(1); egui::ScrollArea::both().auto_shrink([false, false]).show(ui, |ui| { egui::Grid::new("credits_models").striped(true).num_columns(6).show(ui, |ui| { for h in ["Company", "Model", "Version", "Commits", "% of all commits", "Lines +/−"] { - ui.label(RichText::new(h).strong()); + ui.label(RichText::new(crate::i18n::tr(h)).strong()); } ui.end_row(); for m in MODELS { diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index 73ce180b4..affbd6d55 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -64,7 +64,7 @@ pub fn show(app: &mut SlideApp, ctx: &egui::Context) { let Some(mut d) = app.dialog.take() else { return }; let mut open = true; let mut close = false; - egui::Window::new(title(&d.id)) + egui::Window::new(crate::i18n::tr(title(&d.id))) .id(egui::Id::new(("dialog", d.id.clone()))) .collapsible(false) .resizable(false) @@ -85,12 +85,14 @@ fn buttons(ui: &mut Ui, ok_label: &str) -> (bool, bool) { ui.add_space(8.0); ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { let t = Tokens::get(ui.ctx()); - if ui.add(egui::Button::new(egui::RichText::new(ok_label).color(t.accent_text)).fill(t.accent).min_size(vec2(72.0, 24.0))).clicked() + if ui + .add(egui::Button::new(egui::RichText::new(crate::i18n::tr(ok_label)).color(t.accent_text)).fill(t.accent).min_size(vec2(72.0, 24.0))) + .clicked() || ui.input(|i| i.key_pressed(egui::Key::Enter)) { ok = true; } - if ui.add(egui::Button::new("Cancel").min_size(vec2(72.0, 24.0))).clicked() { + if ui.add(egui::Button::new(crate::i18n::tr("Cancel")).min_size(vec2(72.0, 24.0))).clicked() { cancel = true; } }); @@ -100,7 +102,7 @@ fn buttons(ui: &mut Ui, ok_label: &str) -> (bool, bool) { fn field(ui: &mut Ui, d: &mut Dialog, label: &str, key: &str, default: &str) { let mut v = d.get(key, default); ui.horizontal(|ui| { - ui.add_sized(vec2(120.0, 20.0), egui::Label::new(label)); + ui.add_sized(vec2(120.0, 20.0), egui::Label::new(crate::i18n::tr(label))); ui.add(egui::TextEdit::singleline(&mut v).desired_width(180.0)); }); d.fields.insert(key.into(), v); @@ -108,7 +110,7 @@ fn field(ui: &mut Ui, d: &mut Dialog, label: &str, key: &str, default: &str) { fn check(ui: &mut Ui, d: &mut Dialog, label: &str, key: &str, default: bool) -> bool { let mut v = d.get(key, if default { "1" } else { "0" }) == "1"; - ui.checkbox(&mut v, label); + ui.checkbox(&mut v, crate::i18n::tr(label)); d.fields.insert(key.into(), if v { "1".into() } else { "0".into() }); v } @@ -135,17 +137,20 @@ fn body(app: &mut SlideApp, ui: &mut Ui, d: &mut Dialog) -> bool { } "slideSize" => { let cur = app.session.active().map(|s| s.doc.slide_size).unwrap_or(deckcraft_model::defaults::WIDE); - ui.label("Slides sized for:"); + ui.label(crate::i18n::tr("Slides sized for:")); let preset = d.get("preset", ""); - egui::ComboBox::from_id_salt("sizes").selected_text(if preset.is_empty() { "Custom" } else { &preset }).show_ui(ui, |ui| { - for (label, w, h) in deckcraft_model::defaults::SLIDE_SIZES { - if ui.selectable_label(preset == *label, *label).clicked() { - d.fields.insert("preset".into(), label.to_string()); - d.fields.insert("w".into(), format!("{:.2}", w / 72.0)); - d.fields.insert("h".into(), format!("{:.2}", h / 72.0)); + egui::ComboBox::from_id_salt("sizes").selected_text(crate::i18n::tr(if preset.is_empty() { "Custom" } else { &preset })).show_ui( + ui, + |ui| { + for (label, w, h) in deckcraft_model::defaults::SLIDE_SIZES { + if ui.selectable_label(preset == *label, crate::i18n::tr(label)).clicked() { + d.fields.insert("preset".into(), label.to_string()); + d.fields.insert("w".into(), format!("{:.2}", w / 72.0)); + d.fields.insert("h".into(), format!("{:.2}", h / 72.0)); + } } - } - }); + }, + ); field(ui, d, "Width (in):", "w", &format!("{:.2}", cur.width / 72.0)); field(ui, d, "Height (in):", "h", &format!("{:.2}", cur.height / 72.0)); let maximize = check(ui, d, "Maximize content (else ensure fit)", "max", false); @@ -168,9 +173,9 @@ fn body(app: &mut SlideApp, ui: &mut Ui, d: &mut Dialog) -> bool { let mut apply_all = false; let mut apply = false; ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - apply_all = ui.button("Apply to All").clicked(); - apply = ui.button("Apply").clicked(); - if ui.button("Cancel").clicked() { + apply_all = ui.button(crate::i18n::tr("Apply to All")).clicked(); + apply = ui.button(crate::i18n::tr("Apply")).clicked(); + if ui.button(crate::i18n::tr("Cancel")).clicked() { d.fields.insert("cancel".into(), "1".into()); } }); @@ -188,13 +193,13 @@ fn body(app: &mut SlideApp, ui: &mut Ui, d: &mut Dialog) -> bool { "hyperlink" => { let mode = d.get("mode", "url"); ui.horizontal(|ui| { - if ui.selectable_label(mode == "url", "Web Page or File").clicked() { + if ui.selectable_label(mode == "url", crate::i18n::tr("Web Page or File")).clicked() { d.fields.insert("mode".into(), "url".into()); } - if ui.selectable_label(mode == "slide", "This Document").clicked() { + if ui.selectable_label(mode == "slide", crate::i18n::tr("This Document")).clicked() { d.fields.insert("mode".into(), "slide".into()); } - if ui.selectable_label(mode == "email", "Email Address").clicked() { + if ui.selectable_label(mode == "email", crate::i18n::tr("Email Address")).clicked() { d.fields.insert("mode".into(), "email".into()); } }); @@ -233,13 +238,13 @@ fn body(app: &mut SlideApp, ui: &mut Ui, d: &mut Dialog) -> bool { "setupShow" => { let sh = app.session.active().map(|s| s.doc.show.clone()).unwrap_or_default(); let ty = d.get("type", &sh.show_type); - ui.label("Show type"); + ui.label(crate::i18n::tr("Show type")); for (l, v) in [ ("Presented by a speaker (full screen)", "speaker"), ("Browsed by an individual (window)", "browsed"), ("Browsed at a kiosk (full screen)", "kiosk"), ] { - if ui.radio(ty == v, l).clicked() { + if ui.radio(ty == v, crate::i18n::tr(l)).clicked() { d.fields.insert("type".into(), v.into()); } } @@ -261,21 +266,31 @@ fn body(app: &mut SlideApp, ui: &mut Ui, d: &mut Dialog) -> bool { ui.ctx().send_viewport_cmd(egui::ViewportCommand::Close); return true; } - let what = if names.len() == 1 { format!("“{}”", names[0]) } else { format!("{} presentations", names.len()) }; - ui.label(egui::RichText::new(format!("Do you want to save the changes you made to {what}?")).strong()); - ui.label("Your changes will be lost if you don't save them."); + let what = if names.len() == 1 { + format!("“{}”", names[0]) + } else { + crate::i18n::count(names.len(), "presentations", "презентації", "презентацій", "презентацій") + }; + ui.label( + egui::RichText::new(crate::i18n::format("Do you want to save the changes you made to {what}?", std::slice::from_ref(&what))).strong(), + ); + ui.label(crate::i18n::tr("Your changes will be lost if you don't save them.")); ui.add_space(8.0); let mut done = false; ui.horizontal(|ui| { let t = Tokens::get(ui.ctx()); - if ui.add(egui::Button::new("Don't Save").min_size(vec2(84.0, 24.0))).clicked() { + if ui.add(egui::Button::new(crate::i18n::tr("Don't Save")).min_size(vec2(84.0, 24.0))).clicked() { let _ = app.session.execute("file.recovery.discard", &json!({})); app.quit_confirmed = true; ui.ctx().send_viewport_cmd(egui::ViewportCommand::Close); done = true; } ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - let save = ui.add(egui::Button::new(egui::RichText::new("Save").color(t.accent_text)).fill(t.accent).min_size(vec2(72.0, 24.0))); + let save = ui.add( + egui::Button::new(egui::RichText::new(crate::i18n::tr("Save")).color(t.accent_text)) + .fill(t.accent) + .min_size(vec2(72.0, 24.0)), + ); if save.clicked() || ui.input(|i| i.key_pressed(egui::Key::Enter)) { if app.save_all() { app.quit_confirmed = true; @@ -283,7 +298,7 @@ fn body(app: &mut SlideApp, ui: &mut Ui, d: &mut Dialog) -> bool { } done = true; } - if ui.add(egui::Button::new("Cancel").min_size(vec2(72.0, 24.0))).clicked() { + if ui.add(egui::Button::new(crate::i18n::tr("Cancel")).min_size(vec2(72.0, 24.0))).clicked() { done = true; } }); @@ -296,7 +311,7 @@ fn body(app: &mut SlideApp, ui: &mut Ui, d: &mut Dialog) -> bool { let _ = app.run("view.zoom", json!({"percent": pct})); } } - if ui.button("Fit").clicked() { + if ui.button(crate::i18n::tr("Fit")).clicked() { app.ui.zoom = None; } let (ok, cancel) = buttons(ui, "OK"); @@ -304,7 +319,7 @@ fn body(app: &mut SlideApp, ui: &mut Ui, d: &mut Dialog) -> bool { } "comment" => { let mut v = d.get("text", ""); - ui.add(egui::TextEdit::multiline(&mut v).hint_text("Start a conversation").desired_rows(4).desired_width(320.0)); + ui.add(egui::TextEdit::multiline(&mut v).hint_text(crate::i18n::tr("Start a conversation")).desired_rows(4).desired_width(320.0)); d.fields.insert("text".into(), v.clone()); let (ok, cancel) = buttons(ui, "Post"); if ok && !v.trim().is_empty() { @@ -315,7 +330,7 @@ fn body(app: &mut SlideApp, ui: &mut Ui, d: &mut Dialog) -> bool { } "altText" => { let cur = app.session.active().and_then(|s| s.selected_shapes().first().map(|x| x.descr.clone())).unwrap_or_default(); - ui.label("How would you describe this object and its context to someone who is blind or has low vision?"); + ui.label(crate::i18n::tr("How would you describe this object and its context to someone who is blind or has low vision?")); let mut v = d.get("text", &cur); ui.add(egui::TextEdit::multiline(&mut v).desired_rows(4).desired_width(340.0)); d.fields.insert("text".into(), v.clone()); @@ -327,7 +342,7 @@ fn body(app: &mut SlideApp, ui: &mut Ui, d: &mut Dialog) -> bool { ok || cancel } "outline" => { - ui.label("Paste or type an outline. Unindented lines become slide titles; tab-indented lines become bullets."); + ui.label(crate::i18n::tr("Paste or type an outline. Unindented lines become slide titles; tab-indented lines become bullets.")); let mut v = d.get("text", ""); ui.add(egui::TextEdit::multiline(&mut v).desired_rows(10).desired_width(380.0).code_editor()); d.fields.insert("text".into(), v.clone()); @@ -368,7 +383,7 @@ fn body(app: &mut SlideApp, ui: &mut Ui, d: &mut Dialog) -> bool { ok || cancel } "export" => { - ui.label("Export the presentation as:"); + ui.label(crate::i18n::tr("Export the presentation as:")); let fmt = d.get("fmt", "pdf"); for (l, f) in [ ("PDF document (.pdf)", "pdf"), @@ -378,7 +393,7 @@ fn body(app: &mut SlideApp, ui: &mut Ui, d: &mut Dialog) -> bool { ("PowerPoint Presentation (.pptx)", "pptx"), ("Outline (.txt)", "outline"), ] { - if ui.radio(fmt == f, l).clicked() { + if ui.radio(fmt == f, crate::i18n::tr(l)).clicked() { d.fields.insert("fmt".into(), f.into()); } } @@ -386,7 +401,7 @@ fn body(app: &mut SlideApp, ui: &mut Ui, d: &mut Dialog) -> bool { if fmt == "pdf" { ui.add_space(4.0); ui.horizontal(|ui| { - ui.label("Print layout:"); + ui.label(crate::i18n::tr("Print layout:")); egui::ComboBox::from_id_salt("pdf_layout") .selected_text(match pdf_layout.as_str() { "notes" => "Notes Pages", @@ -409,14 +424,14 @@ fn body(app: &mut SlideApp, ui: &mut Ui, d: &mut Dialog) -> bool { ("h6", "Handouts (6 slides per page)"), ("h9", "Handouts (9 slides per page)"), ] { - if ui.selectable_label(pdf_layout == k, l).clicked() { + if ui.selectable_label(pdf_layout == k, crate::i18n::tr(l)).clicked() { d.fields.insert("pdfLayout".into(), k.into()); } } }); }); let mut hidden = d.get("pdfHidden", "false") == "true"; - if ui.checkbox(&mut hidden, "Include hidden slides").changed() { + if ui.checkbox(&mut hidden, crate::i18n::tr("Include hidden slides")).changed() { d.fields.insert("pdfHidden".into(), hidden.to_string()); } } @@ -450,7 +465,7 @@ fn body(app: &mut SlideApp, ui: &mut Ui, d: &mut Dialog) -> bool { } let r = app.session.execute("file.export", ¶ms); match r { - Ok(_) => app.set_status(format!("Exported {path}")), + Ok(_) => app.set_status(crate::i18n::format("Exported {path}", std::slice::from_ref(&path))), Err(e) => app.set_status(e.to_string()), } } @@ -473,7 +488,7 @@ fn body(app: &mut SlideApp, ui: &mut Ui, d: &mut Dialog) -> bool { ok || cancel } "symbol" => { - ui.label("Click a symbol to insert it at the insertion point."); + ui.label(crate::i18n::tr("Click a symbol to insert it at the insertion point.")); let groups: [(&str, &str); 6] = [ ("Punctuation", "–—‘’“”…•·§¶†‡‰′″‹›«»¡¿"), ("Currency", "$€£¥¢₹₩₽₺₿"), @@ -483,7 +498,7 @@ fn body(app: &mut SlideApp, ui: &mut Ui, d: &mut Dialog) -> bool { ("Shapes", "■□▪▫▲△▶▷▼▽◀◁◆◇○●◎★☆✓✔✗✘♠♣♥♦"), ]; for (g, chars) in groups { - ui.label(egui::RichText::new(g).font(theme::bold(12.0))); + ui.label(egui::RichText::new(crate::i18n::tr(g)).font(theme::bold(12.0))); ui.horizontal_wrapped(|ui| { for c in chars.chars() { if ui.add(egui::Button::new(egui::RichText::new(c.to_string()).size(16.0)).min_size(vec2(26.0, 26.0))).clicked() { @@ -499,7 +514,7 @@ fn body(app: &mut SlideApp, ui: &mut Ui, d: &mut Dialog) -> bool { ok || cancel } "equation" => { - ui.label("Type an equation in linear form (e.g. a^2+b^2=c^2, x=(-b±√(b^2-4ac))/2a)."); + ui.label(crate::i18n::tr("Type an equation in linear form (e.g. a^2+b^2=c^2, x=(-b±√(b^2-4ac))/2a).")); let mut v = d.get("eq", ""); ui.add(egui::TextEdit::singleline(&mut v).desired_width(340.0).font(theme::font(15.0))); d.fields.insert("eq".into(), v.clone()); @@ -519,13 +534,16 @@ fn body(app: &mut SlideApp, ui: &mut Ui, d: &mut Dialog) -> bool { let v = app.session.execute("review.spelling", &json!({})).unwrap_or_default(); let list = v.as_array().cloned().unwrap_or_default(); if list.is_empty() { - ui.label("The spelling check is complete. No issues found."); + ui.label(crate::i18n::tr("The spelling check is complete. No issues found.")); } for item in &list { ui.horizontal(|ui| { let w = item.get("word").and_then(Value::as_str).unwrap_or(""); - ui.label(format!("Slide {}: “{w}”", item.get("slide").and_then(Value::as_u64).unwrap_or(0) + 1)); - if ui.small_button("Go to").clicked() { + ui.label(crate::i18n::format( + "Slide {}: “{w}”", + &[(item.get("slide").and_then(Value::as_u64).unwrap_or(0) + 1).to_string(), (w).to_string()], + )); + if ui.small_button(crate::i18n::tr("Go to")).clicked() { let _ = app.run("slide.go", json!({"index": item.get("slide").cloned().unwrap_or_default()})); } }); @@ -537,18 +555,22 @@ fn body(app: &mut SlideApp, ui: &mut Ui, d: &mut Dialog) -> bool { let v = app.session.execute("review.accessibility", &json!({})).unwrap_or_default(); let list = v.as_array().cloned().unwrap_or_default(); if list.is_empty() { - ui.label("No accessibility issues found. People with disabilities should not have difficulty reading this document."); + ui.label(crate::i18n::tr( + "No accessibility issues found. People with disabilities should not have difficulty reading this document.", + )); } egui::ScrollArea::vertical().max_height(300.0).show(ui, |ui| { for item in &list { ui.horizontal(|ui| { - ui.label(format!( + ui.label(crate::i18n::format( "Slide {}: {}{}", - item.get("slide").and_then(Value::as_u64).unwrap_or(0) + 1, - item.get("issue").and_then(Value::as_str).unwrap_or(""), - item.get("name").and_then(Value::as_str).map(|n| format!(" — {n}")).unwrap_or_default() + &[ + (item.get("slide").and_then(Value::as_u64).unwrap_or(0) + 1).to_string(), + (item.get("issue").and_then(Value::as_str).unwrap_or("")).to_string(), + (item.get("name").and_then(Value::as_str).map(|n| format!(" — {n}")).unwrap_or_default()).to_string(), + ], )); - if ui.small_button("Go to").clicked() { + if ui.small_button(crate::i18n::tr("Go to")).clicked() { let _ = app.run("slide.go", json!({"index": item.get("slide").cloned().unwrap_or_default()})); if let Some(id) = item.get("shape") { let _ = app.run("edit.select", json!({"ids": [id]})); @@ -561,7 +583,8 @@ fn body(app: &mut SlideApp, ui: &mut Ui, d: &mut Dialog) -> bool { ok || cancel } "language" => { - ui.label("Mark selected text as:"); + ui.label("Українська"); + ui.label(crate::i18n::tr("Mark selected text as:")); for l in [ "English (United States)", "English (United Kingdom)", @@ -574,7 +597,7 @@ fn body(app: &mut SlideApp, ui: &mut Ui, d: &mut Dialog) -> bool { "中文", "العربية", ] { - let _ = ui.selectable_label(l.starts_with("English (United States)"), l); + let _ = ui.selectable_label(l.starts_with("English (United States)"), crate::i18n::tr(l)); } let (ok, cancel) = buttons(ui, "OK"); ok || cancel @@ -583,8 +606,12 @@ fn body(app: &mut SlideApp, ui: &mut Ui, d: &mut Dialog) -> bool { let shows = app.session.active().map(|s| s.doc.custom_shows.clone()).unwrap_or_default(); for s in &shows { ui.horizontal(|ui| { - ui.label(format!("{} ({} slides)", s.name, s.slides.len())); - if ui.small_button("Delete").clicked() { + ui.label(format!( + "{} ({})", + s.name, + crate::i18n::count(s.slides.len(), if s.slides.len() == 1 { "slide" } else { "slides" }, "слайд", "слайди", "слайдів") + )); + if ui.small_button(crate::i18n::tr("Delete")).clicked() { let _ = app.run("show.customShow", json!({"name": s.name, "delete": true})); } }); @@ -606,11 +633,11 @@ fn body(app: &mut SlideApp, ui: &mut Ui, d: &mut Dialog) -> bool { .find_map(|x| if let deckcraft_model::ShapeKind::Chart(c) = &x.kind { Some(((**c).clone(), x.id)) } else { None }) }); let Some((c, id)) = ch else { - ui.label("Select a chart first."); + ui.label(crate::i18n::tr("Select a chart first.")); let (ok, cancel) = buttons(ui, "Close"); return ok || cancel; }; - ui.label("Edit the data: first row = series names, first column = categories."); + ui.label(crate::i18n::tr("Edit the data: first row = series names, first column = categories.")); let mut grid: Vec> = d.params.get("grid").and_then(|g| serde_json::from_value(g.clone()).ok()).unwrap_or_else(|| { let mut g = vec![std::iter::once(String::new()).chain(c.series.iter().map(|s| s.name.clone())).collect::>()]; for (i, cat) in c.categories.iter().enumerate() { @@ -631,11 +658,11 @@ fn body(app: &mut SlideApp, ui: &mut Ui, d: &mut Dialog) -> bool { } }); ui.horizontal(|ui| { - if ui.button("+ Row").clicked() { + if ui.button(crate::i18n::tr("+ Row")).clicked() { let n = grid.first().map(|r| r.len()).unwrap_or(2); grid.push(vec![String::new(); n]); } - if ui.button("+ Series").clicked() { + if ui.button(crate::i18n::tr("+ Series")).clicked() { for r in grid.iter_mut() { r.push(String::new()); } @@ -667,7 +694,7 @@ fn body(app: &mut SlideApp, ui: &mut Ui, d: &mut Dialog) -> bool { ok || cancel } "smartart" => { - ui.label("Pick a graphic; type one item per line."); + ui.label(crate::i18n::tr("Pick a graphic; type one item per line.")); let kind = d.get("kind", "process"); ui.horizontal_wrapped(|ui| { for (k, l) in [ @@ -678,7 +705,7 @@ fn body(app: &mut SlideApp, ui: &mut Ui, d: &mut Dialog) -> bool { ("pyramid", "Pyramid"), ("matrix", "Matrix"), ] { - if ui.selectable_label(kind == k, l).clicked() { + if ui.selectable_label(kind == k, crate::i18n::tr(l)).clicked() { d.fields.insert("kind".into(), k.into()); } } @@ -696,7 +723,7 @@ fn body(app: &mut SlideApp, ui: &mut Ui, d: &mut Dialog) -> bool { let tab = d.get("tab", "about"); ui.horizontal(|ui| { for (k, l) in [("about", "About"), ("contributors", "Contributors"), ("models", "Models")] { - if ui.selectable_label(tab == k, l).clicked() { + if ui.selectable_label(tab == k, crate::i18n::tr(l)).clicked() { d.fields.insert("tab".into(), k.into()); } } @@ -715,29 +742,44 @@ fn body(app: &mut SlideApp, ui: &mut Ui, d: &mut Dialog) -> bool { ok } "preferences" => { - ui.label(egui::RichText::new("General").font(theme::bold(13.0))); + ui.horizontal(|ui| { + ui.label(crate::i18n::tr("Interface language:")); + egui::ComboBox::from_id_salt("interface_language").selected_text(app.ui.interface_language.label()).show_ui(ui, |ui| { + for language in crate::i18n::Language::ALL { + if ui.selectable_label(app.ui.interface_language == language, crate::i18n::tr(language.label())).clicked() { + let code = match language { + crate::i18n::Language::System => "system", + crate::i18n::Language::En => "en", + crate::i18n::Language::Uk => "uk", + }; + let _ = app.run("app.language", json!({"language": code})); + } + } + }); + }); + ui.label(egui::RichText::new(crate::i18n::tr("General")).font(theme::bold(13.0))); let mut dark = app.ui.brightness == theme::Brightness::Dark; - if ui.checkbox(&mut dark, "Dark appearance").changed() { + if ui.checkbox(&mut dark, crate::i18n::tr("Dark appearance")).changed() { let _ = app.run("view.dark", json!({"on": dark})); } let mut scale = app.ui.ui_scale; - if ui.add(egui::Slider::new(&mut scale, 0.75..=1.75).text("Interface size")).changed() { + if ui.add(egui::Slider::new(&mut scale, 0.75..=1.75).text(crate::i18n::tr("Interface size"))).changed() { app.ui.ui_scale = scale; } - ui.label(egui::RichText::new("Editing").font(theme::bold(13.0))); - ui.checkbox(&mut app.session.prefs.smart_guides, "Show smart guides"); - ui.checkbox(&mut app.session.prefs.snap_to_grid, "Snap objects to grid"); - ui.checkbox(&mut app.session.prefs.smart_quotes, "Replace straight quotes with smart quotes"); - ui.checkbox(&mut app.session.prefs.autocorrect, "AutoCorrect as you type"); + ui.label(egui::RichText::new(crate::i18n::tr("Editing")).font(theme::bold(13.0))); + ui.checkbox(&mut app.session.prefs.smart_guides, crate::i18n::tr("Show smart guides")); + ui.checkbox(&mut app.session.prefs.snap_to_grid, crate::i18n::tr("Snap objects to grid")); + ui.checkbox(&mut app.session.prefs.smart_quotes, crate::i18n::tr("Replace straight quotes with smart quotes")); + ui.checkbox(&mut app.session.prefs.autocorrect, crate::i18n::tr("AutoCorrect as you type")); ui.horizontal(|ui| { - ui.label("Your name (comments):"); + ui.label(crate::i18n::tr("Your name (comments):")); ui.text_edit_singleline(&mut app.session.prefs.author); }); let (ok, _) = buttons(ui, "Done"); ok } _ => { - ui.label(format!("“{}” is not available yet.", d.id)); + ui.label(crate::i18n::format("“{}” is not available yet.", std::slice::from_ref(&d.id))); let (ok, cancel) = buttons(ui, "OK"); ok || cancel } @@ -791,17 +833,21 @@ pub fn about(_app: &mut SlideApp, ui: &mut Ui) { ui.vertical_centered(|ui| { let (r, _) = ui.allocate_exact_size(vec2(64.0, 64.0), Sense::hover()); paint_logo(ui.painter(), r); - ui.label(egui::RichText::new("DeckCraft").font(theme::bold(22.0))); + ui.label(egui::RichText::new(crate::i18n::tr("DeckCraft")).font(theme::bold(22.0))); ui.label(egui::RichText::new(version_line()).color(t.text_dim)); ui.add_space(6.0); - ui.label("Presentations and slide shows, rebuilt from scratch in pure Rust."); - ui.label("An open-source, clean-room project. Not affiliated with Microsoft."); + ui.label(crate::i18n::tr("Presentations and slide shows, rebuilt from scratch in pure Rust.")); + ui.label(crate::i18n::tr("An open-source, clean-room project. Not affiliated with Microsoft.")); ui.add_space(6.0); - ui.hyperlink_to("Join our community on Discord", deckcraft_engine::links::DISCORD); + ui.hyperlink_to(crate::i18n::tr("Join our community on Discord"), deckcraft_engine::links::DISCORD); ui.hyperlink_to("getartcraft.com/apps/deckcraft", deckcraft_engine::links::APP_PAGE); - ui.hyperlink_to("Source on GitHub", deckcraft_engine::links::GITHUB); + ui.hyperlink_to(crate::i18n::tr("Source on GitHub"), deckcraft_engine::links::GITHUB); ui.add_space(6.0); - ui.label(egui::RichText::new("MIT OR Apache-2.0 · © 2026 ArtCraft Team and the DeckCraft contributors").size(11.0).color(t.text_faint)); + ui.label( + egui::RichText::new(crate::i18n::tr("MIT OR Apache-2.0 · © 2026 ArtCraft Team and the DeckCraft contributors")) + .size(11.0) + .color(t.text_faint), + ); }); } @@ -809,7 +855,7 @@ pub fn about(_app: &mut SlideApp, ui: &mut Ui) { /// from `[workspace.package] version` (the single source of truth, `cargo xtask version`); the /// release workflow sets `DECKCRAFT_BUILD_SHA` / `DECKCRAFT_BUILD_DATE` at build time. pub fn version_line() -> String { - let mut s = format!("Version {}", env!("CARGO_PKG_VERSION")); + let mut s = crate::i18n::format("Version {}", &[(env!("CARGO_PKG_VERSION")).to_string()]); let sha = option_env!("DECKCRAFT_BUILD_SHA").map(|s| s.get(..9).unwrap_or(s)).filter(|s| !s.is_empty()); let date = option_env!("DECKCRAFT_BUILD_DATE").filter(|s| !s.is_empty()); match (sha, date) { @@ -845,7 +891,7 @@ pub fn start_screen(app: &mut SlideApp, ui: &mut Ui) { ui.add_space(40.0); let (r, _) = ui.allocate_exact_size(vec2(40.0, 40.0), Sense::hover()); paint_logo(ui.painter(), r); - ui.label(egui::RichText::new("DeckCraft").font(theme::bold(26.0))); + ui.label(egui::RichText::new(crate::i18n::tr("DeckCraft")).font(theme::bold(26.0))); }); if app.home_open() { let title = app.session.active().map(|d| d.title()).unwrap_or_default(); @@ -860,7 +906,7 @@ pub fn start_screen(app: &mut SlideApp, ui: &mut Ui) { ui.add_space(16.0); ui.horizontal(|ui| { ui.add_space(40.0); - ui.label(egui::RichText::new("New presentation").font(theme::bold(16.0))); + ui.label(egui::RichText::new(crate::i18n::tr("New presentation")).font(theme::bold(16.0))); }); ui.add_space(8.0); inset_wrapped(ui, |ui| { @@ -880,10 +926,10 @@ pub fn start_screen(app: &mut SlideApp, ui: &mut Ui) { ui.add_space(16.0); ui.horizontal(|ui| { ui.add_space(40.0); - if ui.button("Open…").clicked() { + if ui.button(crate::i18n::tr("Open…")).clicked() { let _ = app.run("app.openDialog", json!({})); } - if ui.button("Open the sample deck").clicked() + if ui.button(crate::i18n::tr("Open the sample deck")).clicked() && let Err(e) = deckcraft_engine::sample::open_sample(&mut app.session) { app.set_status(e.to_string()); @@ -893,7 +939,7 @@ pub fn start_screen(app: &mut SlideApp, ui: &mut Ui) { ui.add_space(16.0); ui.horizontal(|ui| { ui.add_space(40.0); - ui.label(egui::RichText::new("Recent").font(theme::bold(16.0))); + ui.label(egui::RichText::new(crate::i18n::tr("Recent")).font(theme::bold(16.0))); }); for p in app.ui.recent.clone() { ui.horizontal(|ui| { @@ -909,7 +955,7 @@ pub fn start_screen(app: &mut SlideApp, ui: &mut Ui) { ui.add_space(30.0); ui.horizontal(|ui| { ui.add_space(40.0); - ui.hyperlink_to("Join the ArtCraft community on Discord", deckcraft_engine::links::DISCORD); + ui.hyperlink_to(crate::i18n::tr("Join the ArtCraft community on Discord"), deckcraft_engine::links::DISCORD); }); }); } diff --git a/crates/ui-egui/src/fillui.rs b/crates/ui-egui/src/fillui.rs index 7f0f7d197..0fe0c1feb 100644 --- a/crates/ui-egui/src/fillui.rs +++ b/crates/ui-egui/src/fillui.rs @@ -109,7 +109,7 @@ pub fn gradient_editor(ui: &mut Ui, id: &str, g: &Gradient, sc: &ColorScheme) -> let mut sel: usize = ui.data_mut(|d| d.get_temp(sel_id).unwrap_or(0)).min(g.stops.len().saturating_sub(1)); // Preset variations of the first stop's colour. - ui.label("Preset gradients"); + ui.label(crate::i18n::tr("Preset gradients")); ui.horizontal_wrapped(|ui| { let base = g.stops.first().map(|s| s.color.clone()).unwrap_or(ColorRef::scheme(SchemeSlot::Accent1)); let mut light = base.clone(); @@ -165,7 +165,7 @@ pub fn gradient_editor(ui: &mut Ui, id: &str, g: &Gradient, sc: &ColorScheme) -> GradientShape::Path { .. } => "Radial", }; ui.horizontal(|ui| { - ui.add_sized(vec2(70.0, 20.0), egui::Label::new("Type")); + ui.add_sized(vec2(70.0, 20.0), egui::Label::new(crate::i18n::tr("Type"))); egui::ComboBox::from_id_salt((id, "kind")).selected_text(kind).width(120.0).show_ui(ui, |ui| { for k in ["Linear", "Radial", "Rectangular", "Path"] { if ui.selectable_label(kind == k, k).clicked() && kind != k { @@ -185,12 +185,12 @@ pub fn gradient_editor(ui: &mut Ui, id: &str, g: &Gradient, sc: &ColorScheme) -> }); }); ui.horizontal_wrapped(|ui| { - ui.add_sized(vec2(70.0, 20.0), egui::Label::new("Direction")); + ui.add_sized(vec2(70.0, 20.0), egui::Label::new(crate::i18n::tr("Direction"))); match &g.shape { GradientShape::Linear { angle, .. } => { for (l, a) in DIRECTIONS_LINEAR { let on = (angle - a).abs() < 0.5; - if ui.selectable_label(on, l).on_hover_text(format!("{a}°")).clicked() { + if ui.selectable_label(on, crate::i18n::tr(l)).on_hover_text(format!("{a}°")).clicked() { out = Some(Gradient { shape: GradientShape::Linear { angle: a, scaled: false }, ..g.clone() }); } } @@ -198,7 +198,7 @@ pub fn gradient_editor(ui: &mut Ui, id: &str, g: &Gradient, sc: &ColorScheme) -> GradientShape::Path { path, focus } => { for (l, f) in DIRECTIONS_PATH { let on = focus.iter().zip(f).all(|(a, b)| (a - b).abs() < 1e-3); - if ui.selectable_label(on, l.trim_start_matches("From ")).on_hover_text(l).clicked() { + if ui.selectable_label(on, l.trim_start_matches("From ")).on_hover_text(crate::i18n::tr(l)).clicked() { out = Some(Gradient { shape: GradientShape::Path { path: path.clone(), focus: f }, ..g.clone() }); } } @@ -207,7 +207,7 @@ pub fn gradient_editor(ui: &mut Ui, id: &str, g: &Gradient, sc: &ColorScheme) -> }); if let GradientShape::Linear { angle, .. } = &g.shape { ui.horizontal(|ui| { - ui.add_sized(vec2(70.0, 20.0), egui::Label::new("Angle")); + ui.add_sized(vec2(70.0, 20.0), egui::Label::new(crate::i18n::tr("Angle"))); if let Some(a) = widgets::spinner(ui, &format!("{id}-angle"), *angle, 15.0, "°", 70.0) { out = Some(Gradient { shape: GradientShape::Linear { angle: a.rem_euclid(360.0), scaled: false }, ..g.clone() }); } @@ -216,7 +216,7 @@ pub fn gradient_editor(ui: &mut Ui, id: &str, g: &Gradient, sc: &ColorScheme) -> // Stop bar: click to add, drag a stop to move it. ui.add_space(4.0); - ui.label("Gradient stops"); + ui.label(crate::i18n::tr("Gradient stops")); let (bar, bar_resp) = ui.allocate_exact_size(vec2(ui.available_width().min(260.0), 18.0), Sense::click()); paint_bar(ui, bar, g, sc); let (marks, _) = ui.allocate_exact_size(vec2(bar.width(), 14.0), Sense::hover()); @@ -272,7 +272,7 @@ pub fn gradient_editor(ui: &mut Ui, id: &str, g: &Gradient, sc: &ColorScheme) -> // The selected stop. if let Some(s) = g.stops.get(sel) { ui.horizontal(|ui| { - ui.add_sized(vec2(70.0, 20.0), egui::Label::new("Color")); + ui.add_sized(vec2(70.0, 20.0), egui::Label::new(crate::i18n::tr("Color"))); let (sw, _) = ui.allocate_exact_size(vec2(18.0, 18.0), Sense::hover()); ui.painter().rect(sw, CornerRadius::same(3), c32(&s.color, sc), Stroke::new(1.0, t.border), egui::StrokeKind::Inside); let r = widgets::drop_button(ui, "", vec2(24.0, 20.0), true); @@ -284,7 +284,7 @@ pub fn gradient_editor(ui: &mut Ui, id: &str, g: &Gradient, sc: &ColorScheme) -> } }); let can_remove = g.stops.len() > 2; - if ui.add_enabled(can_remove, egui::Button::new("Remove stop")).clicked() { + if ui.add_enabled(can_remove, egui::Button::new(crate::i18n::tr("Remove stop"))).clicked() { let mut ng = g.clone(); ng.stops.remove(sel); sel = sel.saturating_sub(1); @@ -292,7 +292,7 @@ pub fn gradient_editor(ui: &mut Ui, id: &str, g: &Gradient, sc: &ColorScheme) -> } }); ui.horizontal(|ui| { - ui.add_sized(vec2(70.0, 20.0), egui::Label::new("Position")); + ui.add_sized(vec2(70.0, 20.0), egui::Label::new(crate::i18n::tr("Position"))); if let Some(v) = widgets::spinner(ui, &format!("{id}-pos-{sel}"), (s.pos * 100.0).round(), 5.0, "%", 70.0) { let mut ng = g.clone(); ng.stops[sel].pos = (v / 100.0).clamp(0.0, 1.0); @@ -301,7 +301,7 @@ pub fn gradient_editor(ui: &mut Ui, id: &str, g: &Gradient, sc: &ColorScheme) -> } }); ui.horizontal(|ui| { - ui.add_sized(vec2(70.0, 20.0), egui::Label::new("Transparency")); + ui.add_sized(vec2(70.0, 20.0), egui::Label::new(crate::i18n::tr("Transparency"))); let key = ui.id().with((id, "tr", sel)); let mut tr: f32 = ui.data_mut(|d| d.get_temp(key).unwrap_or(((1.0 - alpha_of(&s.color)) * 100.0) as f32)); let r = ui.add(egui::Slider::new(&mut tr, 0.0..=100.0).suffix("%")); diff --git a/crates/ui-egui/src/i18n.rs b/crates/ui-egui/src/i18n.rs new file mode 100644 index 000000000..361bf07fb --- /dev/null +++ b/crates/ui-egui/src/i18n.rs @@ -0,0 +1,358 @@ +//! Interface text only: presentation content, file names and command identifiers stay intact. + +use std::cell::Cell; +use std::collections::HashMap; +use std::sync::OnceLock; + +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum Language { + #[default] + System, + En, + Uk, +} + +impl Language { + pub const ALL: [Self; 3] = [Self::System, Self::En, Self::Uk]; + + pub fn label(self) -> &'static str { + match self { + Self::System => tr("System language"), + Self::En => "English", + Self::Uk => "Українська", + } + } + + pub fn from_locale(locale: &str) -> Self { + let language = locale.split(['-', '_', '.', '@']).next().unwrap_or(""); + if language.eq_ignore_ascii_case("uk") { Self::Uk } else { Self::En } + } + + pub fn resolve(self, locale: Option<&str>) -> Self { + if self == Self::System { Self::from_locale(locale.unwrap_or("")) } else { self } + } +} + +thread_local! { + static CURRENT: Cell = const { Cell::new(Language::En) }; +} + +pub fn current() -> Language { + CURRENT.get() +} + +pub fn set_current(language: Language) { + CURRENT.set(language); +} + +const CATALOG: &str = include_str!("i18n/uk.tsv"); +static UK: OnceLock> = OnceLock::new(); + +fn catalog() -> &'static HashMap { + UK.get_or_init(|| { + CATALOG + .lines() + .filter(|line| !line.is_empty() && !line.starts_with('#')) + .filter_map(|line| line.split_once('\t')) + .map(|(source, text)| (source.replace("\\n", "\n"), text.replace("\\n", "\n"))) + .collect() + }) +} + +/// Exact lookup at an explicit interface boundary; unknown text falls back to English. +pub fn tr(text: &str) -> &str { + if CURRENT.get() == Language::Uk { catalog().get(text).map(String::as_str).unwrap_or(text) } else { text } +} + +/// Context-specific interface label with the original English source as fallback. +pub fn tr_context<'a>(source: &'a str, key: &str) -> &'a str { + if CURRENT.get() == Language::Uk { catalog().get(key).map(String::as_str).unwrap_or(source) } else { source } +} + +pub fn placeholder_prompt(kind: deckcraft_model::PhType) -> &'static str { + tr(kind.prompt()) +} + +/// Substitute already-formatted values without interpreting their contents as interface text. +pub fn format(source: &str, values: &[String]) -> String { + let text = tr(source); + let mut out = String::new(); + let mut rest = text; + let mut index = 0; + while let Some(start) = rest.find('{') { + out.push_str(rest.get(..start).unwrap_or("")); + let tail = rest.get(start + 1..).unwrap_or(""); + if let Some(after) = tail.strip_prefix('{') { + out.push('{'); + rest = after; + } else if let Some(end) = tail.find('}') { + out.push_str(values.get(index).map(String::as_str).unwrap_or("")); + index += 1; + rest = tail.get(end + 1..).unwrap_or(""); + } else { + out.push_str(rest); + return out; + } + } + out.push_str(&rest.replace("}}", "}")); + out +} + +/// Ukrainian integer count forms: 1/21; 2–4/22–24; 0/5–19. +pub fn count(n: usize, english: &str, one: &str, few: &str, many: &str) -> String { + let word = if CURRENT.get() == Language::Uk { + match (n % 10, n % 100) { + (1, last) if last != 11 => one, + (2..=4, last) if !(12..=14).contains(&last) => few, + _ => many, + } + } else { + english + }; + format!("{n} {word}") +} + +/// Recovery notice includes accusative Ukrainian forms. +pub fn recovered(n: usize) -> String { + let amount = count( + n, + if n == 1 { "unsaved presentation" } else { "unsaved presentations" }, + "незбережену презентацію", + "незбережені презентації", + "незбережених презентацій", + ); + format("Recovered {} from the last session", &[amount]) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn placeholders(text: &str) -> Vec<&str> { + let mut out = vec![]; + let mut rest = text; + while let Some((_, tail)) = rest.split_once('{') { + if let Some(tail) = tail.strip_prefix('{') { + rest = tail; + continue; + } + if let Some((field, tail)) = tail.split_once('}') { + out.push(field); + rest = tail; + } else { + break; + } + } + out + } + + #[test] + fn all_command_menu_and_gallery_labels_are_translated() { + let mut required = vec![]; + required.extend(crate::UI_COMMANDS.iter().map(|c| c.1)); + required.extend(deckcraft_engine::Session::new().commands().into_iter().map(|c| c.label)); + for (menu, items) in crate::menus::menu_tree() { + required.push(menu); + required.extend(items.into_iter().filter(|(label, _)| *label != "-").map(|(label, _)| label)); + } + required.extend(deckcraft_geom::preset::CATALOG.iter().map(|shape| shape.label)); + required.extend(deckcraft_geom::preset::Category::ALL.iter().map(|category| category.label())); + required.extend(deckcraft_model::anim::ANIMATIONS.iter().map(|effect| effect.1)); + required.extend(deckcraft_model::anim::TRANSITIONS.iter().flat_map(|effect| [effect.1, effect.2])); + required.extend(deckcraft_model::chart::ChartType::MAIN.iter().map(|kind| kind.label())); + required.extend(deckcraft_model::defaults::SLIDE_SIZES.iter().map(|size| size.0)); + required.extend(deckcraft_color::STANDARD_COLORS.iter().map(|color| color.1)); + required.extend(deckcraft_color::SchemeSlot::THEME.iter().map(|slot| slot.label())); + required.extend(["@ribbon:Home", "@ribbon:Insert"]); + for option in deckcraft_model::anim::TRANSITIONS + .iter() + .flat_map(|effect| effect.4) + .chain(deckcraft_model::anim::ANIMATIONS.iter().flat_map(|effect| effect.5)) + { + let label = crate::ribbon::option_label(option); + if !label.is_empty() && !label.chars().all(|c| c.is_ascii_digit()) { + assert!(catalog().contains_key(&label), "missing effect option: {option}: {label}"); + } + } + let missing: Vec<_> = required.into_iter().filter(|key| !catalog().contains_key(*key)).collect(); + assert!(missing.is_empty(), "missing visible metadata: {missing:?}"); + } + + #[test] + fn every_literal_translation_call_has_a_catalog_entry() { + for source in [ + include_str!("canvas.rs"), + include_str!("credits.rs"), + include_str!("dialogs.rs"), + include_str!("fillui.rs"), + include_str!("lib.rs"), + include_str!("media.rs"), + include_str!("menus.rs"), + include_str!("panes.rs"), + include_str!("ribbon.rs"), + include_str!("show.rs"), + include_str!("sorter.rs"), + include_str!("status.rs"), + include_str!("thumbs.rs"), + include_str!("widgets.rs"), + include_str!("../../../apps/deckcraft/src/main.rs"), + include_str!("../../../apps/deckcraft-web/src/web.rs"), + ] { + for prefix in ["i18n::tr(\"", "i18n::format(\""] { + let mut rest = source.split("#[cfg(test)]").next().unwrap_or(source); + while let Some((_, tail)) = rest.split_once(prefix) { + let mut escaped = false; + let end = tail + .char_indices() + .find_map(|(index, c)| { + if escaped { + escaped = false; + None + } else if c == '\\' { + escaped = true; + None + } else if c == '\"' { + Some(index) + } else { + None + } + }) + .unwrap(); + let key: String = serde_json::from_str(&format!("\"{}\"", &tail[..end])).unwrap(); + assert!(catalog().contains_key(&key), "missing literal translation: {key}"); + rest = &tail[end + 1..]; + } + } + } + } + + #[test] + fn language_command_keeps_document_data_and_identifiers_intact() { + let mut app = crate::SlideApp::new(deckcraft_engine::Session::new(), crate::Services::default()); + app.run("file.new", serde_json::json!({})).unwrap(); + app.run("insert.textBox", serde_json::json!({"text": "File {customer} / Звіт", "rect": [1, 2, 100, 40]})).unwrap(); + let before = serde_json::to_value(&app.session.active().unwrap().doc).unwrap(); + app.run("app.language", serde_json::json!({"language": "uk"})).unwrap(); + assert_eq!(app.ui.interface_language, Language::Uk); + assert_eq!(tr("File"), "Файл"); + assert_eq!(before, serde_json::to_value(&app.session.active().unwrap().doc).unwrap()); + assert!(app.run("app.language", serde_json::json!({"language": "invalid"})).is_err()); + assert_eq!(app.ui.interface_language, Language::Uk); + set_current(Language::En); + } + + #[test] + fn bundled_ui_fonts_cover_the_ukrainian_alphabet_without_system_fonts() { + let ctx = egui::Context::default(); + crate::theme::install_fonts(&ctx); + let mut output = ctx.run_ui(egui::RawInput::default(), |ui| { + for family in [egui::FontFamily::Proportional, egui::FontFamily::Name("bold".into()), egui::FontFamily::Monospace] { + // egui 0.36 has_glyph() can report a valid fallback face as a replacement. + // Check the configured fallback and its actual font charmap below instead. + let fallback_installed = ui.ctx().fonts_mut(|fonts| { + fonts.definitions().families.get(&family).is_some_and(|names| names.iter().any(|name| name == "Ubuntu-Light")) + }); + assert!(fallback_installed, "missing bundled Ukrainian fallback in {family:?}"); + } + }); + output.textures_delta.clear(); + let db = deckcraft_fonts::FontDb::with_font_dirs(vec![]); + let face = db.face(deckcraft_fonts::LAST_RESORT_FAMILY, "Regular"); + let letters: std::collections::HashSet<_> = + catalog().values().flat_map(|value| value.chars()).chain("ҐґЄєІіЇї".chars()).filter(|c| ('\u{0400}'..='\u{04ff}').contains(c)).collect(); + for c in letters { + assert_ne!(face.glyph_for(c), 0, "missing document fallback glyph: {c}"); + } + } + + // An explicit local visual check; the generated PNGs stay outside the repository. + #[test] + #[ignore = "writes translation review screenshots; run explicitly with DECKCRAFT_I18N_SHOTS"] + fn ukrainian_interface_preview() { + let dir = std::env::var("DECKCRAFT_I18N_SHOTS").unwrap(); + std::fs::create_dir_all(&dir).unwrap(); + for (code, language) in [("en", Language::En), ("uk", Language::Uk)] { + for (name, dialog) in [("editor", None), ("preferences", Some("preferences")), ("table", Some("table"))] { + let mut app = crate::SlideApp::new(deckcraft_engine::Session::new(), crate::Services::default()); + app.run("file.new", serde_json::json!({})).unwrap(); + app.ui.interface_language = language; + app.dialog = dialog.map(crate::dialogs::Dialog::new); + let mut harness = egui_kittest::Harness::builder().with_size(egui::vec2(1440.0, 900.0)).build_ui(|ui| { + app.logic(ui.ctx()); + app.ui(ui); + }); + harness.run_steps(6); + harness.render().unwrap().save(format!("{dir}/{code}-{name}.png")).unwrap(); + } + } + set_current(Language::En); + } + + #[test] + fn locales_and_preferences_are_backward_compatible() { + for locale in ["uk", "uk-UA", "uk_UA.UTF-8", "UK_ua@euro"] { + assert_eq!(Language::from_locale(locale), Language::Uk); + } + for locale in ["", "en-US", "ru-UA", "ukrainian"] { + assert_eq!(Language::from_locale(locale), Language::En); + } + assert_eq!(Language::Uk.resolve(Some("en")), Language::Uk); + let old: crate::UiState = serde_json::from_str("{}").unwrap(); + assert_eq!(old.interface_language, Language::System); + let mut ui = old; + ui.interface_language = Language::Uk; + let round_trip: crate::UiState = serde_json::from_str(&serde_json::to_string(&ui).unwrap()).unwrap(); + assert_eq!(round_trip.interface_language, Language::Uk); + } + + #[test] + fn catalog_is_unique_complete_and_preserves_placeholders() { + let mut seen = std::collections::HashSet::new(); + for (line_no, line) in CATALOG.lines().enumerate() { + if line.is_empty() || line.starts_with('#') { + continue; + } + let (source, text) = line.split_once('\t').unwrap(); + assert!(seen.insert(source), "duplicate on {}: {source}", line_no + 1); + assert!(!text.trim().is_empty(), "empty translation: {source}"); + assert_eq!(placeholders(source), placeholders(text), "placeholder names/order/format: {source}"); + } + } + + #[test] + fn lookup_and_format_preserve_user_values_and_fallbacks() { + set_current(Language::Uk); + assert_eq!(tr("File"), "Файл"); + assert_eq!(tr_context("Insert", "@ribbon:Insert"), "Вставлення"); + assert_eq!(tr("Insert"), "Вставити"); + assert_eq!(tr("a user-defined slide title"), "a user-defined slide title"); + let user = "File {user} / Звіт.pptx".to_string(); + assert!(format("Saved {path}", std::slice::from_ref(&user)).ends_with(&user)); + set_current(Language::En); + assert_eq!(tr("File"), "File"); + } + + #[test] + fn ukrainian_integer_counts_cover_teens_and_large_values() { + set_current(Language::Uk); + for (n, word) in [ + (0, "слайдів"), + (1, "слайд"), + (2, "слайди"), + (4, "слайди"), + (5, "слайдів"), + (11, "слайдів"), + (12, "слайдів"), + (14, "слайдів"), + (21, "слайд"), + (24, "слайди"), + (111, "слайдів"), + ] { + assert_eq!(count(n, "slides", "слайд", "слайди", "слайдів"), format!("{n} {word}")); + } + set_current(Language::En); + } +} diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv new file mode 100644 index 000000000..1c20bce7b --- /dev/null +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -0,0 +1,1234 @@ +# Ukrainian interface catalog. English sourcetranslation; \n denotes a line break. +# Contributor-original translation, MIT OR Apache-2.0. + Fixed date (empty = update automatically): Фіксована дата (порожнє поле = автоматичне оновлення): + Footer text: Текст нижнього колонтитула: + Present Показати + pt пт + — Edited — Змінено +"Not Allowed" Symbol Знак «Не дозволено» +% of all commits % усіх комітів ++ New Comment + Новий коментар ++ New Slide + Новий слайд ++ Row + Рядок ++ Series + Ряд даних ++Bin +Бін +100% Stacked Bar Нормована лінійчата з накопиченням +100% Stacked Column Нормована стовпчаста з накопиченням +35mm Slides Слайди 35 мм +@ribbon:Home Основне +@ribbon:Insert Вставлення +@{}: {} PRs, {} commits, +{} / −{} lines (Δ {}), +{} / −{} binary assets, {} – {} @{}: запити на злиття: {}, коміти: {}, рядки: +{} / −{} (Δ {}), двійкові ресурси: +{} / −{}, {} – {} +A3 Paper (297x420 mm) Папір A3 (297×420 мм) +A4 Paper (210x297 mm) Папір A4 (210×297 мм) +Aa Aa +Abc Abc +About Про програму +About DeckCraft Про DeckCraft +Accent 1 Акцент 1 +Accent 2 Акцент 2 +Accent 3 Акцент 3 +Accent 4 Акцент 4 +Accent 5 Акцент 5 +Accent 6 Акцент 6 +Accessibility Checker Перевірка доступності +Across Поперек +Action Дія +Action Button: Custom Кнопка дії: Власна +Action Button: Document Кнопка дії: Документ +Action Button: Get Information Кнопка дії: Отримати відомості +Action Button: Go Back Кнопка дії: Повернутися +Action Button: Go Back or Previous Кнопка дії: Назад або до попереднього +Action Button: Go Forward or Next Кнопка дії: Уперед або до наступного +Action Button: Go Home Кнопка дії: На початок +Action Button: Go to Beginning Кнопка дії: До початку +Action Button: Go to End Кнопка дії: До кінця +Action Button: Help Кнопка дії: Довідка +Action Button: Sound Кнопка дії: Звук +Action Button: Video Кнопка дії: Відео +Action Button: {k} Кнопка дії: {k} +Action Buttons Кнопки дій +Add Animation Додати анімацію +Add Section Додати розділ +Add\nAnimation Додати анімацію +Address: Адреса: +Adjust Shape Змінити фігуру +After Previous Після попереднього +After: Після: +Airplane Літак +Align Вирівняти +Align Bottom Вирівняти за нижнім краєм +Align Center Вирівняти по центру +Align Left Вирівняти за лівим краєм +Align Middle Вирівняти посередині +Align Right Вирівняти за правим краєм +Align Text Вирівняти текст +Align Top Вирівняти за верхнім краєм +Alt Text Альтернативний текст +Alt+ Alt+ +Alt+F9 Alt+F9 +Alt\nText Альтернативний текст +An open-source, clean-room project. Not affiliated with Microsoft. Незалежний проєкт із відкритим кодом. Не пов’язаний із Microsoft. +Angle Кут +Animation Gallery Галерея анімації +Animation Pane Панель анімації +Animation Styles Стилі анімації +Animation\nPane Панель анімації +Animations Анімації +Animation Painter Копіювання анімації +Apply Animation Застосувати анімацію +Play/Pause GIF Відтворити/призупинити GIF +Animations on Slide Анімації на слайді +Appear Поява +Apply Застосувати +Apply To All Застосувати до всіх +Apply to All Застосувати до всіх +Apply\nTo All Застосувати до всіх +Arc Дуга +Arcs Дуги +Area З областями +Arrange Упорядкувати +Arrow Стрілка +Arrow at end Стрілка в кінці +Arrow at start Стрілка на початку +Arrow: Bent Стрілка: Зігнута +Arrow: Bent-Up Стрілка: Зігнута вгору +Arrow: Chevron Стрілка: Шеврон +Arrow: Down Стрілка: Униз +Arrow: Left Стрілка: Ліворуч +Arrow: Left-Right Стрілка: Ліворуч і праворуч +Arrow: Left-Right-Up Стрілка: Ліворуч, праворуч і вгору +Arrow: Left-Up Стрілка: Ліворуч і вгору +Arrow: Notched Right Стрілка: Праворуч із вирізом +Arrow: Pentagon Стрілка: П’ятикутник +Arrow: Quad Стрілка: У чотири боки +Arrow: Right Стрілка: Праворуч +Arrow: Striped Right Стрілка: Смугаста праворуч +Arrow: U-Turn Стрілка: Розворот +Arrow: Up Стрілка: Угору +Arrow: Up-Down Стрілка: Угору й униз +Arrows Стрілки +Arrows at both ends Стрілки на обох кінцях +As One Object Як один об’єкт +Audio Аудіо +Audio from File… Аудіо з файлу… +AutoCorrect as you type Автовиправлення під час введення +Autofit Автоматичний добір +Automatic Автоматично +B4 (ISO) Paper (250x353 mm) Папір B4 (ISO) (250×353 мм) +B5 (ISO) Paper (176x250 mm) Папір B5 (ISO) (176×250 мм) +Background Тло +Background Styles Стилі тла +Background\nStyles Стилі тла +Backspace Backspace +Banded Columns Смугасті стовпці +Banded Rows Смугасті рядки +Banner Стрічка +Basic Cycle Простий цикл +Basic List Простий список +Basic Process Простий процес +Basic Shapes Основні фігури +Before text (in): Перед текстом (дюйми): +Bevel Скіс +Binary assets added Додані двійкові ресурси +Binary assets removed Видалені двійкові ресурси +Black Left Через чорний ліворуч +Black Pen Чорне перо +Black Right Через чорний праворуч +Blinds Жалюзі +Block Arc Блокова дуга +Block Arrows Блокові стрілки +Blue Синій +Blue Pen Синє перо +Bold Напівжирний +Bold Flash Спалах напівжирного +Both Обидва +Bottom Низ +Bounce Відскок +Box Коробка +Box In Коробка всередину +Box Out Коробка назовні +Box and Whisker Ящик із вусами +Brightness Яскравість +Brightness {:+}% Яскравість {:+}% +Bring Forward Перемістити вперед +Bring to Front На передній план +Bring\nForward Перемістити вперед +Browsed at a kiosk (full screen) Перегляд у кіоску (повноекранний) +Browsed by an individual (window) Індивідуальний перегляд (у вікні) +Bubble Бульбашкова +Bullets Маркери +Burst Спалах +By Paragraph За абзацами +Callout: Down Arrow Виноска: Стрілка вниз +Callout: Left Arrow Виноска: Стрілка ліворуч +Callout: Line Виноска: Лінія +Callout: Right Arrow Виноска: Стрілка праворуч +Callout: Up Arrow Виноска: Стрілка вгору +Callouts Виноски +Cameo Камео +Camera capture is not available yet Захоплення з камери ще недоступне +Cancel Скасувати +Capitalize Each Word Кожне слово з великої літери +Caps Великі літери +Center По центру +Centered По центру +Change Case Змінити регістр +Change Chart Type Змінити тип діаграми +Change Picture Змінити зображення +Change Shape Змінити фігуру +Change\nChart Type Змінити тип діаграми +Character Spacing Міжсимвольний інтервал +Characters Символи +Chart Діаграма +Chart Data Дані діаграми +Chart Design Оформлення діаграми +Chart Elements Елементи діаграми +Chart… Діаграма… +Check Accessibility Перевірити доступність +Check\nAccessibility Перевірити доступність +Checkerboard Шахова дошка +Chevron Шеврон +Choose a SmartArt Graphic Виберіть графіку SmartArt +Chord Хорда +Circle Коло +Circle In Коло всередину +Circle Out Коло назовні +Clear All Formatting Очистити все форматування +Click a symbol to insert it at the insertion point. Натисніть символ, щоб вставити його в позиції курсора. +Click icon to add SmartArt graphic Натисніть піктограму, щоб додати графіку SmartArt +Click icon to add chart Натисніть піктограму, щоб додати діаграму +Click icon to add media Натисніть піктограму, щоб додати медіа +Click icon to add picture Натисніть піктограму, щоб додати зображення +Click icon to add table Натисніть піктограму, щоб додати таблицю +Click to add first slide Натисніть, щоб додати перший слайд +Click to add notes Натисніть, щоб додати примітки +Click to add subtitle Натисніть, щоб додати підзаголовок +Click to add text Натисніть, щоб додати текст +Click to add title Натисніть, щоб додати заголовок +Click to edit Master text styles Натисніть, щоб змінити стилі тексту зразка +Click to edit Master title style Натисніть, щоб змінити стиль заголовка зразка +Clock Годинник +Clockwise За годинниковою стрілкою +Close Закрити +Close Master View Закрити режим зразка +Close\nMaster View Закрити режим зразка +Cloud Хмара +Clustered Bar Групована лінійчата +Clustered Column Групована стовпчаста +Cmd+ Cmd+ +Cmd+, Cmd+, +Cmd+- Cmd+- +Cmd+= Cmd+= +Cmd+F1 Cmd+F1 +Cmd+Shift+P Cmd+Shift+P +Collapse the Ribbon Згорнути стрічку +Color Колір +Color Pulse Пульсація кольору +Colors Кольори +Columns Стовпці +Comb Гребінка +Combo Комбінована +Command Palette Палітра команд +Comment Коментар +Comments Коментарі +Commits Коміти +Community Links Посилання спільноти +Company Компанія +Complementary Color Доповняльний колір +Connect Shapes З’єднати фігури +Connection Sites Точки з’єднання +Connector: Curved З’єднувач: кривий +Connector: Elbow З’єднувач: кутовий +Connector: Elbow (one bend) З’єднувач: кутовий (один згин) +Content Вміст +Contrast Контрастність +Contrast {:+}% Контраст {:+}% +Contributors Учасники +Conveyor Конвеєр +Copy Копіювати +Corrections Корекція +Couldn't save AutoRecover information: {e} Не вдалося зберегти дані автоматичного відновлення: {e} +Counter Clockwise Проти годинникової стрілки +Cover Накриття +Create Створити +Crop Обітнути +Crop 10% each side Обітнути на 10% з кожного боку +Crop 20% each side Обітнути на 20% з кожного боку +Crop to Shape Обітнути за фігурою +Cross Хрест +Crush Зминання +Ctrl+ Ctrl+ +Cube Куб +Currency Валюта +Curtains Штори +Custom Довільний +Custom Path Власний шлях +Custom Show Власний показ +Custom Show 1 Власний показ 1 +Custom Shows Власні покази +Custom Slide Show Власний показ слайдів +Custom\nShow Власний показ +Cut Вирізати +Cylinder Циліндр +Dark Blue Темно-синій +Dark Mode Темний режим +Dark Red Темно-червоний +Dark appearance Темне оформлення +Dark\nMode Темний режим +Darken Затемнення +Dash Штрих +Dash Dot Штрих і крапка +Dashes Штрихи +Database База даних +Date Дата +Date & Time Дата й час +Date &\nTime Дата й час +Date and Time Дата й час +Date and time Дата й час +Decagon Десятикутник +Decision Рішення +DeckCraft DeckCraft +Decrease Font Size Зменшити розмір шрифту +Decrease List Level Знизити рівень списку +Delay: Затримка: +Delete Видалити +Delete Columns Видалити стовпці +Delete Comment Видалити коментар +Delete Layout Видалити макет +Delete Rows Видалити рядки +Delete Slide Видалити слайд +Delete Table Видалити таблицю +Delete Text Видалити текст +Desaturate Знебарвити +Deselect Скасувати вибір +Design Оформлення +Design Ideas Ідеї оформлення +Design\nIdeas Ідеї оформлення +Deutsch Deutsch +Diagonal Stripe Діагональна смуга +Diamond Ромб +Diamond In Ромб усередину +Diamond Out Ромб назовні +Direction Напрямок +Disappear Зникнення +Discard Recovered Presentations Відкинути відновлені презентації +Display name Відображуване ім'я +Dissolve Розчинення +Dissolve In Поступова поява +Dissolve Out Поступове зникнення +Distribute Розподілити +Distribute Columns Розподілити стовпці +Distribute Horizontally Розподілити горизонтально +Distribute Rows Розподілити рядки +Distribute Vertically Розподілити вертикально +Division Sign Знак ділення +Do not Autofit Без автоматичного добору +Do you want to save the changes you made to {what}? Зберегти зміни, внесені до {what}? +Document Документ +Dodecagon Дванадцятикутник +Don't Save Не зберігати +Don't show on title slide Не показувати на титульному слайді +Done Готово +Donut Кільце +Doors Двері +Double Brace Подвійна фігурна дужка +Double Bracket Подвійна квадратна дужка +Double Wave Подвійна хвиля +Doughnut Кільцева +Down Униз +Drape Завіса +Draw Малювання +Drop Shadow Rectangle Прямокутник із тінню +Duplicate Дублювати +Duplicate Selected Slides Дублювати вибрані слайди +Duplicate Slide Дублювати слайд +Duration: Тривалість: +Dynamic Content Динамічний вміст +Edit Редагування +Edit Alt Text… Змінити альтернативний текст… +Edit Data Змінити дані +Edit Shape Змінити фігуру +Edit Text Редагувати текст +Edit the data: first row = series names, first column = categories. Змініть дані: перший рядок — назви рядів, перший стовпець — категорії. +Edit\nData Змінити дані +Edit\nShape Змінити фігуру +Editing Редагування +Effect Options Параметри ефекту +Effect\nOptions Параметри ефекту +Effects Ефекти +Email Address Адреса електронної пошти +Email address: Адреса електронної пошти: +Emphasis Виділення +End Кінець +End Show Завершити показ +End of slide show Кінець показу слайдів +End of slide show, click to exit. Кінець показу слайдів. Натисніть, щоб вийти. +End trim (s): Обрізання в кінці (с): +English (United Kingdom) Англійська (Велика Британія) +English (United States) Англійська (США) +Enter Enter +Entrance Поява +Equals Дорівнює +Equation Рівняння +Equation Shapes Фігури рівнянь +Eraser Гумка +Escape Escape +Español Español +Exciting Виразні +Exit Зникнення +Expand Розгорнути +Explosion: 14 Points Спалах: 14 променів +Explosion: 8 Points Спалах: 8 променів +Export Експорт +Export the presentation as: Експортувати презентацію як: +Exported {path} Експортовано: {path} +Export… Експортувати… +Fade Послабити +Fade in (s): Поступова поява (с): +Fade out (s): Поступове зникнення (с): +Fall Over Падіння +Ferris Wheel Оглядове колесо +File Файл +Fill Заливка +Fill & Line Заливка й лінія +Fill Color Колір заливки +Filled Radar Заповнена пелюсткова +Find… Знайти… +First Перший +First Column Перший стовпець +First Slide Перший слайд +First commit Перший коміт +Fit Умістити +Fit slide to current window Підігнати слайд до поточного вікна +Fit to Window Умістити у вікні +Fit to\nWindow Умістити у вікні +Flash Спалах +Flip Віддзеркалити +Flip Horizontal Віддзеркалити горизонтально +Flip Vertical Віддзеркалити вертикально +Float In Плавна поява +Float Out Плавне зникнення +Flowchart Блок-схема +Flowchart: Alternate Process Блок-схема: Альтернативний процес +Flowchart: Card Блок-схема: Картка +Flowchart: Collate Блок-схема: Зіставлення +Flowchart: Connector Блок-схема: З’єднувач +Flowchart: Data Блок-схема: Дані +Flowchart: Decision Блок-схема: Рішення +Flowchart: Delay Блок-схема: Затримка +Flowchart: Direct Access Storage Блок-схема: Сховище прямого доступу +Flowchart: Display Блок-схема: Екран +Flowchart: Document Блок-схема: Документ +Flowchart: Extract Блок-схема: Видобування +Flowchart: Internal Storage Блок-схема: Внутрішнє сховище +Flowchart: Magnetic Disk Блок-схема: Магнітний диск +Flowchart: Manual Input Блок-схема: Ручне введення +Flowchart: Manual Operation Блок-схема: Ручна операція +Flowchart: Merge Блок-схема: Об’єднання +Flowchart: Multidocument Блок-схема: Кілька документів +Flowchart: Off-page Connector Блок-схема: З’єднувач між сторінками +Flowchart: Or Блок-схема: Або +Flowchart: Predefined Process Блок-схема: Визначений процес +Flowchart: Preparation Блок-схема: Підготовка +Flowchart: Process Блок-схема: Процес +Flowchart: Punched Tape Блок-схема: Перфострічка +Flowchart: Sequential Access Storage Блок-схема: Сховище послідовного доступу +Flowchart: Sort Блок-схема: Сортування +Flowchart: Stored Data Блок-схема: Збережені дані +Flowchart: Summing Junction Блок-схема: Вузол підсумовування +Flowchart: Terminator Блок-схема: Початок/кінець +Fly In Влітати +Fly Out Вилітання +Fly Through Пролітання +Folded Corner Загнутий кут +Follow the layout Відповідно до макета +Followed Hyperlink Відвідане гіперпосилання +Font Шрифт +Font Color Колір шрифту +Font Size Розмір шрифту +Fonts Шрифти +Footer Нижній колонтитул +Foreground Передній план +Format Формат +Format Background Формат тла +Format Background… Формат тла… +Format Painter Копіювання формату +Format Painter (double-click to keep it on) Копіювання формату (подвійне натискання для постійного застосування) +Format Pane Панель форматування +Format Shape Формат фігури +Format Shape… Формат фігури… +Format\nBackground Формат тла +Format\nPane Панель форматування +Formatting State Стан форматування +Fracture Розлом +Frame Рамка +Français Français +Freeform Довільна фігура +From Від +From Beginning Від початку +From Bottom Знизу +From Bottom Left Знизу ліворуч +From Bottom Right Знизу праворуч +From Current Slide Від поточного слайда +From Left Зліва +From Right Справа +From Top Згори +From Top Left Згори ліворуч +From Top Right Згори праворуч +From bottom left Знизу ліворуч +From bottom right Знизу праворуч +From center Від центра +From top left Згори ліворуч +From top right Згори праворуч +From\nBeginning Від початку +From\nCurrent Slide Від поточного слайда +Full Повна +Full Page Slides Слайди на всю сторінку +Funnel Лійкова +Gallery Галерея +General Загальні +Get Text Отримати текст +Glitter Блиск +Glow Світіння +Go to Перейти +Go to Slide Перейти до слайда +Grab bag Випадкова добірка +Gradient fill Градієнтна заливка +Gradient stops Опорні точки градієнта +Gradient… Градієнт… +Grayscale Градації сірого +Greek Грецька +Green Зелений +Gridlines Лінії сітки +Group Згрупувати +Group Objects Згрупувати об’єкти +Grow & Turn Збільшення й обертання +Grow/Shrink Збільшення/зменшення +Guides Напрямні +Half Половина +Half Frame Половина рамки +Handout Master Зразок видачі +Handout Master (coming soon) Зразок видачі (незабаром) +Handout\nMaster Зразок видачі +Handouts (1 slide per page) Видача (1 слайд на сторінку) +Handouts (2 slides per page) Видача (2 слайди на сторінку) +Handouts (3 slides per page) Видача (3 слайди на сторінку) +Handouts (4 slides per page) Видача (4 слайди на сторінку) +Handouts (6 slides per page) Видача (6 слайдів на сторінку) +Handouts (9 slides per page) Видача (9 слайдів на сторінку) +Header Верхній колонтитул +Header & Footer Колонтитули +Header &\nFooter Колонтитули +Header Row Рядок заголовка +Header and Footer Колонтитули +Header and Footer… Колонтитули… +Heart Серце +Height Висота +Height (in): Висота (дюйми): +Height: Висота: +Help Довідка +Heptagon Семикутник +Hexagon Шестикутник +Hide All Приховати все +Hide Background Graphics Приховати графіку тла +Hide During Show Приховати під час показу +Hide Slide Приховати слайд +Hide background graphics Приховати графіку тла +Hide\nSlide Приховати слайд +Hierarchy Ієрархія +High Висока +Highlighter Маркер виділення +Histogram Гістограма +Home Початковий екран +Honeycomb Стільники +Horizontal Горизонтально +Horz In Горизонтально всередину +Horz Out Горизонтально назовні +How would you describe this object and its context to someone who is blind or has low vision? Як би ви описали цей об’єкт та його контекст людині з порушенням зору? +Hyperlink Гіперпосилання +Icons Піктограми +In Усередину +In Bounce Усередину з відскоком +Include hidden slides Включити приховані слайди +Increase Font Size Збільшити розмір шрифту +Increase List Level Підвищити рівень списку +Ink to Math Рукописне введення в рівняння +Ink to Math (coming soon) Рукописне введення в рівняння (незабаром) +Ink to Shape Рукописне введення у фігуру +Ink to Shape (coming soon) Рукописне введення у фігуру (незабаром) +Ink to Text Рукописне введення в текст +Ink to Text (coming soon) Рукописне введення в текст (незабаром) +Ink to\nMath Рукописне введення в рівняння +Ink to\nShape Рукописне введення у фігуру +Ink to\nText Рукописне введення в текст +Inner Внутрішня +Insert Вставити +Insert Above Вставити вище +Insert Below Вставити нижче +Insert Hyperlink Вставити гіперпосилання +Insert Layout Вставити макет +Insert Left Вставити ліворуч +Insert Placeholder Вставити заповнювач +Insert Right Вставити праворуч +Insert Table Вставити таблицю +Insert Table… Вставити таблицю… +Insert picture from file… Вставити зображення з файлу… +Insert\nAbove Вставити вище +Insert\nBelow Вставити нижче +Insert\nLayout Вставити макет +Insert\nLeft Вставити ліворуч +Insert\nPlaceholder Вставити заповнювач +Insert\nRight Вставити праворуч +Inspect Presentation Переглянути дані презентації +Inspect Shape Переглянути дані фігури +Inspect Slide Переглянути дані слайда +Interface Language Мова інтерфейсу +Interface language: Мова інтерфейсу: +Interface size Розмір інтерфейсу +Isosceles Triangle Рівнобедрений трикутник +Italiano Italiano +Italic Курсивний +JPEG image (current slide) Зображення JPEG (поточний слайд) +Join our community on Discord Приєднуйтеся до нашої спільноти в Discord +Join the ArtCraft community on Discord Приєднуйтеся до спільноти ArtCraft у Discord +Justify По ширині +L-Shape Г-подібна фігура +Language Мова +Last Останній +Last Column Останній стовпець +Last Slide Останній слайд +Last commit Останній коміт +Layout Макет +Learn More Докладніше +Learn\nMore Докладніше +Ledger Paper (11x17 in) Папір Ledger (11×17 дюймів) +Left Ліворуч +Left Brace Ліва фігурна дужка +Left Bracket Ліва квадратна дужка +Letter Paper (8.5x11 in) Папір Letter (8,5×11 дюймів) +Liberation Serif Liberation Serif +Light Blue Світло-синій +Light Green Світло-зелений +Light/Dark appearance Світле/темне оформлення +Lighten Освітлення +Lightning Блискавка +Lightning Bolt Блискавка +Line Лінія +Line Color Колір лінії +Line Spacing Міжрядковий інтервал +Line Spacing Options… Параметри міжрядкового інтервалу… +Line delta Різниця рядків +Line spacing (lines): Міжрядковий інтервал (рядки): +Line with Markers Лінійна з маркерами +Linear Лінійний +Lines Лінії +Lines +/− Рядки +/− +Lines added Додані рядки +Lines deleted Видалені рядки +Link Посилання +Link… Посилання… +List Commands Список команд +Lock Заблокувати +Long Dash Довгий штрих +Long Dash Dot Довгий штрих і крапка +Loop continuously until 'Esc' Повторювати безперервно до натискання «Esc» +Loop until Stopped Повторювати до зупинки +Loops Петлі +Loose Розріджений +Low Низька +MIT OR Apache-2.0 · © 2026 ArtCraft Team and the DeckCraft contributors MIT OR Apache-2.0 · © 2026 Команда ArtCraft та учасники DeckCraft +Margins (pt) Поля (пт) +Mark as decorative Позначити як декоративний +Mark selected text as: Позначити вибраний текст як: +Math Математика +Matrix Матриця +Maximize content (else ensure fit) Збільшити вміст (інакше вмістити) +Media Медіа +Media Info Відомості про медіа +Medium Середній +Merge Cells Об’єднати клітинки +Merge Shapes Об’єднати фігури +Merge\nCells Об’єднати клітинки +Merge\nShapes Об’єднати фігури +Merged PRs Об’єднані запити на злиття +Middle Посередині +Minus Sign Знак мінуса +Model Модель +Models Моделі +Moon Місяць +More Colors… # Інші кольори… # +Morph Трансформація +Motion Рух +Motion Paths Шляхи переміщення +Move Переміщення +Move Down Перемістити вниз +Move Insertion Point Перемістити курсор введення +Move Section Перемістити розділ +Move Section Down Перемістити розділ униз +Move Section Up Перемістити розділ угору +Move Slide Перемістити слайд +Move Up Перемістити вгору +Multiplication Sign Знак множення +Multiply Множення +Mute Вимкнути звук +Name Ім’я +Name (A–Z) Ім'я (А–Я) +Name: Назва: +New Comment Новий коментар +New Presentation Нова презентація +New Slide Новий слайд +New Slide (⇧⌘N) Новий слайд (⇧⌘N) +New presentation Нова презентація +New show name: Назва нового показу: +New\nComment Новий коментар +New\nSlide Новий слайд +Next Далі +Next Slide Наступний слайд +Next Window Наступне вікно +Next ▶ Далі ▶ +No Color Без кольору +No Fill Без заливки +No Glow Без світіння +No Outline Без обведення +No Reflection Без відбиття +No Shadow Без тіні +No Soft Edges Без розмиття країв +No accessibility issues found. People with disabilities should not have difficulty reading this document. Проблем доступності не виявлено. Люди з інвалідністю не повинні мати труднощів із читанням цього документа. +No comments on this slide. На цьому слайді немає коментарів. +No contributor data was built into this copy. У цю копію не вбудовано дані про учасників. +No fill Без заливки +No line Без лінії +No model credits were built into this copy. У цю копію не вбудовано дані про використані моделі. +No notes. Немає приміток. +No sound: {err} Немає звуку: {err} +None Немає +Normal Звичайний +Not Equal Не дорівнює +Not allowed Не дозволено +Notes Примітки +Notes Master Зразок приміток +Notes Master (coming soon) Зразок приміток (незабаром) +Notes Page Сторінка приміток +Notes Pages Сторінки приміток +Notes\nMaster Зразок приміток +Notes\nPage Сторінка приміток +Nudge Зсунути +Number of columns: Кількість стовпців: +Number of rows: Кількість рядків: +Numbering Нумерація +Object Color Колір об’єкта +Objects Об’єкти +Octagon Восьмикутник +On Click Після натискання +On Click of {name} Після натискання на {name} +On Click of… (none) Після натискання на… (немає) +On Mouse Click Після натискання мишею +On-screen Show (16:10) Екранний показ (16:10) +Open Data Відкрити дані +Open Dialog Відкрити діалог +Open Recovered Presentations Відкрити відновлені презентації +Open the sample deck Відкрити зразок презентації +Open… Відкрити… +Orange Помаранчевий +Orbit Орбіта +Order Objects Порядок об’єктів +Origami Оригамі +Out Назовні +Out Bounce Назовні з відскоком +Outer Зовнішня +Outline Структура +Outline (.txt) Структура (.txt) +Outline View Режим структури +Outline\nView Режим структури +Oval Овал +Oval ends Овальні кінці +Overhead Діапроєктор +PDF document (.pdf) Документ PDF (.pdf) +PNG images (all slides) Зображення PNG (усі слайди) +PNG images (current slide) Зображення PNG (поточний слайд) +PRs Запити на злиття +Page Curl Завертання сторінки +Page Setup… Параметри сторінки… +PageDown PageDown +PageUp PageUp +Pan Панорамування +Pane Панель +Paragraph Абзац +Paragraph… Абзац… +Parallelogram Паралелограм +Pareto Парето +Partial Circle Частина кола +Particles Частинки +Paste Вставити +Paste (⌘V) Вставити (⌘V) +Paste Formatting Вставити форматування +Paste and Match Formatting Вставити з відповідним форматуванням +Paste or type an outline. Unindented lines become slide titles; tab-indented lines become bullets. Вставте або введіть структуру. Рядки без відступу стають заголовками слайдів; рядки з відступом табуляції — пунктами списку. +Pasted image.png Вставлене зображення.png +Path Контур +Pattern fill Візерункова заливка +Pause Пауза +Peek In Визирання +Peel Off Відшарування +Pencil Олівець +Pentagon П’ятикутник +Perspective Перспектива +Pick a graphic; type one item per line. Виберіть графіку; введіть по одному елементу в рядку. +Pick a look for your slides. Ideas are generated on your device. Виберіть оформлення слайдів. Ідеї створюються на вашому пристрої. +Picture Зображення +Picture Format Формат зображення +Picture Styles Стилі зображення +Picture from File… Зображення з файлу… +Picture or texture fill Заливка зображенням або текстурою +Picture\nStyles Стилі зображення +Pictures Зображення +Pie Секторна +Pin Прикріпити +Placeholder colour Колір заповнювача +Plan\nBuild\nLaunch Планування\nСтворення\nЗапуск +Plaque Табличка +Play Відтворити +Play Across Slides Відтворювати на кількох слайдах +Play Full Screen Відтворювати на весь екран +Play Narrations Відтворювати звуковий супровід +Play from Current Slide Відтворити з поточного слайда +Play from Start Відтворити від початку +Play from Start (F5) Відтворити від початку (F5) +Play from\nCurrent Slide Відтворити з поточного слайда +Play from\nStart Відтворити від початку +Play/Pause Відтворити/призупинити +Playback Відтворення +Plus Плюс +Plus In Плюс усередину +Plus Out Плюс назовні +Plus Sign Знак плюса +Portrait (9:16) Портретний (9:16) +Português Português +Position Положення +Position Objects Позиція об’єктів +Position and Size Позиція та розмір +Post Надіслати +Poster Frame Кадр афіші +PowerPoint Presentation (.pptx) Презентація PowerPoint (.pptx) +Preferences Налаштування +Preferences… Налаштування… +Presentation Презентація +Presentations Презентації +Presentations and slide shows, rebuilt from scratch in pure Rust. Презентації та покази слайдів, створені з нуля мовою Rust. +Presented by a speaker (full screen) Доповідачем (повноекранний режим) +Presenter View Режим доповідача +Presenter View — DeckCraft Режим доповідача — DeckCraft +Presenter\nView Режим доповідача +Preset gradients Готові градієнти +Prestige Престиж +Preview Попередній перегляд +Previous Slide Попередній слайд +Print layout: Макет друку: +Properties Властивості +Pulse Пульсація +Punctuation Пунктуація +Purple Фіолетовий +Push Зсув +Pyramid Піраміда +Quick Styles Експрес-стилі +Quick\nStyles Експрес-стилі +Radar Пелюсткова +Radial Радіальний +Random Випадковий +Random Bars Випадкові смуги +Reading View Режим читання +Reading\nView Режим читання +Real name Справжнє ім'я +Recent Нещодавні +Record Запис +Recording narration needs audio capture (coming soon) Для запису звукового супроводу потрібне захоплення аудіо (незабаром) +Recovered Presentations Відновлені презентації +Recovered {} from the last session Відновлено {} з попереднього сеансу +Rectangle Прямокутник +Rectangle: Diagonal Corners Rounded Прямокутник: заокруглені діагональні кути +Rectangle: Diagonal Corners Snipped Прямокутник: зрізані діагональні кути +Rectangle: Rounded Corners Прямокутник: заокруглені кути +Rectangle: Single Corner Rounded Прямокутник: один заокруглений кут +Rectangle: Single Corner Snipped Прямокутник: один зрізаний кут +Rectangle: Top Corners One Rounded and One Snipped Прямокутник: один верхній кут заокруглений, інший зрізаний +Rectangle: Top Corners Rounded Прямокутник: заокруглені верхні кути +Rectangle: Top Corners Snipped Прямокутник: зрізані верхні кути +Rectangles Прямокутники +Rectangular Прямокутний +Red Червоний +Red Pen Червоне перо +Redo Повторити +Redo (⌘Y) Повторити (⌘Y) +Reflected Rounded Rectangle Заокруглений прямокутник із відбиттям +Reflection Відбиття +Regroup Перегрупувати +Regular Pentagon Правильний п’ятикутник +Rehearse Timings Репетиція часу показу +Rehearse\nTimings Репетиція часу показу +Remove Вилучити +Remove All Animations Видалити всі анімації +Remove All Sections Видалити всі розділи +Remove Animation Видалити анімацію +Remove Background Видалити тло +Remove Background (coming soon) Видалення тла (незабаром) +Remove Section Видалити розділ +Remove Section & Slides Видалити розділ і слайди +Remove stop Видалити опорну точку +Remove\nBackground Видалити тло +Rename Перейменувати +Rename Layout Перейменувати макет +Rename Section Перейменувати розділ +Rename Section… Перейменувати розділ… +Rename Slide Перейменувати слайд +Rename… Перейменувати… +Render Slide Візуалізувати слайд +Reopen Відкрити знову +Reorder Animation Змінити порядок анімації +Reorder Objects Змінити порядок об’єктів +Replace straight quotes with smart quotes Замінювати прямі лапки типографськими +Reply Відповісти +Reply… Відповісти… +Reset Скинути +Reset Background Скинути тло +Reset Crop Скинути обтинання +Reset Effects Скинути ефекти +Reset Picture Скинути зображення +Reset Slide Скинути слайд +Reset Slide Background Скинути тло слайда +Resize shape to fit text Змінити розмір фігури за текстом +Resolve Позначити як вирішений +Resolve Thread Завершити обговорення +Return Повернутися +Reveal Відкриття +Reverse the order Зворотний порядок +Revert Повернути попередню версію +Review Рецензування +Rewind after Playing Перемотати після відтворення +Ribbon Tab Вкладка стрічки +Ribbon: Tilted Down Стрічка: нахил униз +Ribbon: Tilted Up Стрічка: нахил угору +Right Праворуч +Right Brace Права фігурна дужка +Right Bracket Права квадратна дужка +Right Triangle Прямокутний трикутник +Right-to-Left Text Direction Напрямок тексту справа наліво +Ring Кільце +Ripple Брижі +Rise Up Підняття +Rotate Повернути +Rotate Left 90° Повернути ліворуч на 90° +Rotate Right 90° Повернути праворуч на 90° +Rotate all text 270° Повернути весь текст на 270° +Rotate all text 90° Повернути весь текст на 90° +Rotate with shape Повертати разом із фігурою +Rotation Обертання +Round Dot Кругла крапка +Rounded Diagonal Corner Заокруглений діагональний кут +Ruler Лінійка +Saturation Насиченість +Saturation {}% Насиченість {}% +Save Зберегти +Save (⌘S) Зберегти (⌘S) +Save As… Зберегти як… +Save AutoRecover Information Зберегти дані автоматичного відновлення +Save as Template… Зберегти як шаблон… +Save to Data Зберегти в дані +Saved Збережено +Saved {path} Збережено: {path} +Scatter Точкова +ScreenTip: Підказка: +Screenshot Знімок екрана +Screenshot (not available yet) Знімок екрана (ще недоступно) +Scroll: Horizontal Сувій: горизонтальний +Scroll: Vertical Сувій: вертикальний +Search commands (⇧⌘P) Пошук команд (⇧⌘P) +Search commands… Пошук команд… +Section Розділ +Seek Перейти до позиції +Select Виділення +Select All Виділити все +Select All Text Вибрати весь текст +Select Cells Вибрати клітинки +Select Objects Вибрати об’єкти +Select Paragraph Вибрати абзац +Select Slides Вибрати слайди +Select Text Вибрати текст +Select Word Вибрати слово +Select a chart first. Спочатку виберіть діаграму. +Select a shape to format it. Виберіть фігуру для форматування. +Select an object on the slide and add an animation from the Animations tab. Виберіть об’єкт на слайді та додайте анімацію на вкладці «Анімації». +Selection Виділення +Selection Pane Панель вибору +Selection Pane… Панель вибору… +Selection\nPane Панель вибору +Send Backward Перемістити назад +Send to Back На задній план +Send\nBackward Перемістити назад +Sentence case. Як у реченні. +Set Text Задати текст +Set Up Show… Налаштувати показ… +Set Up Slide Show Налаштувати показ слайдів +Set Up Slide Show… Налаштувати показ слайдів… +Set Up\nSlide Show Налаштувати показ слайдів +Set as Default Shape Установити як стандартну фігуру +Shading Затінення +Shadow Тінь +Shape Фігура +Shape Effects Ефекти фігури +Shape Fill Заливка фігури +Shape Format Формат фігури +Shape Gallery Галерея фігур +Shape Outline Обведення фігури +Shape Styles Стилі фігур +Shape {} Фігура {} +Shape\nStyles Стилі фігур +Shapes Фігури +Shift+ Shift+ +Shift+F9 Shift+F9 +Show Показати +Show All Показати всі +Show Comments Показати коментарі +Show Media Controls Показати елементи керування медіа +Show Slide Показати слайд +Show smart guides Показувати розумні напрямні +Show type Тип показу +Show without animation Показ без анімації +Show without narration Показ без звукового супроводу +Show/Hide Показати/приховати +Show\nComments Показати коментарі +Shred Подрібнення +Shrink & Turn Зменшення й обертання +Shrink text on overflow Зменшувати текст у разі переповнення +Simple Frame, White Проста біла рамка +Size Розмір +Size & Properties Розмір і властивості +Slide Master Зразок слайдів +Slide Number Номер слайда +Slide Show Показ слайдів +Slide Size Розмір слайда +Slide Sorter Сортувальник слайдів +Slide number Номер слайда +Slide timings recorded. Час показу слайдів записано. +Slide {:02}:{:04.1} Total {:02}:{:02} Слайд {:02}:{:04.1} Загалом {:02}:{:02} +Slide {} of {} Слайд {} з {} +Slide {}: {}{} Слайд {}: {}{} +Slide {}: “{w}” Слайд {}: «{w}» +Slide\nMaster Зразок слайдів +Slide\nNumber Номер слайда +Slide\nSize Розмір слайда +Slide\nSorter Сортувальник слайдів +Slides (e.g. 1,3,5): Слайди (наприклад, 1,3,5): +Slides from Outline Слайди зі структури +Slides from Outline… Слайди зі структури… +Slides sized for: Розмір слайдів для: +SmartArt SmartArt +Smile Усмішка +Smiley Face Усміхнене обличчя +Smoothly Плавно +Smoothly Left Плавно ліворуч +Smoothly Right Плавно праворуч +Snap objects to grid Прив’язувати об’єкти до сітки +Soft Edge Rectangle Прямокутник із розмитими краями +Soft Edges Розмиті краї +Solid Суцільний +Solid fill Суцільна заливка +Solid line Суцільна лінія +Sort Сортування +Source on GitHub Вихідний код на GitHub +Space Space +Spacing after (pt): Інтервал після (пт): +Spacing before (pt): Інтервал перед (пт): +Special — hanging (in): Спеціальний — виступ (дюйми): +Speech Мовлення +Speech Bubble: Oval Овальна мовна бульбашка +Speech Bubble: Rectangle Прямокутна мовна бульбашка +Speech Bubble: Rectangle with Corners Rounded Мовна бульбашка: прямокутник із заокругленими кутами +Spelling Орфографія +Spin Обертання +Spinner Кругове обертання +Split Розділення +Split Cells Розділити клітинки +Split\nCells Розділити клітинки +Square Квадрат +Square (1:1) Квадратний (1:1) +Square Dot Квадратна крапка +Stacked Накопичувальна +Stacked Area З областями з накопиченням +Stacked Bar Лінійчата з накопиченням +Stacked Column Стовпчаста з накопиченням +Stacked Line Лінійна з накопиченням +Standard (4:3) Стандартний (4:3) +Standard Colors Стандартні кольори +Star Зірка +Star: 10 Points Зірка: 10 променів +Star: 12 Points Зірка: 12 променів +Star: 16 Points Зірка: 16 променів +Star: 24 Points Зірка: 24 промені +Star: 32 Points Зірка: 32 промені +Star: 4 Points Зірка: 4 промені +Star: 5 Points Зірка: 5 променів +Star: 6 Points Зірка: 6 променів +Star: 7 Points Зірка: 7 променів +Star: 8 Points Зірка: 8 променів +Stars and Banners Зірки та стрічки +Start After Previous Почати після попереднього +Start Automatically Почати автоматично +Start On Click Почати після натискання +Start With Previous Почати разом із попереднім +Start a conversation Почніть розмову +Start time (s): Час початку (с): +Start: Початок: +Stealth end Вістря +Stock Біржова +Stop Зупинити +Stop Editing Text Завершити редагування тексту +Strikethrough Закреслення +Strips Смуги +Style {i} Стиль {i} +Subscript Підрядковий +Subtle Стримані +Sun Сонце +Sunburst Сонячні промені +Superscript Надрядковий +Surface Поверхнева +Switch Перемикання +Swivel Обертання +Symbol Символ +System language Мова системи +Tab Табуляція +Table Таблиця +Table Design Оформлення таблиці +Table Style Options Параметри стилю таблиці +Table Styles Стилі таблиці +Table… Таблиця… +Task Pane Панель завдань +Teardrop Крапля +Teeter Хитання +Text Текст +Text Box Текстове поле +Text Box Margins Поля текстового блока +Text Direction Напрямок тексту +Text Effects Текстові ефекти +Text Fill Заливка тексту +Text Highlight Color Колір виділення тексту +Text Options Параметри тексту +Text Outline Обведення тексту +Text Shadow Тінь тексту +Text/Background - Dark 1 Текст/тло — темний 1 +Text/Background - Dark 2 Текст/тло — темний 2 +Text/Background - Light 1 Текст/тло — світлий 1 +Text/Background - Light 2 Текст/тло — світлий 2 +Text\nBox Текстове поле +The spelling check is complete. No issues found. Перевірку правопису завершено. Помилок не виявлено. +Theme Colors Кольори теми +Theme Gallery Галерея тем +Themes Теми +Thesaurus Тезаурус +Thesaurus (coming soon) Тезаурус (незабаром) +This Document Цей документ +Thought Думка +Thought Bubble: Cloud Бульбашка думки: хмара +Through Black Через чорний +Tight Ущільнений +Timing Час показу +Top Верх +Total Row Рядок підсумку +Transition Перехід +Transition Gallery Галерея переходів +Transitions Переходи +Translate Перекласти +Translation needs a language model (not bundled) Для перекладу потрібна мовна модель (не входить до комплекту) +Transparency Прозорість +Trapezoid Трапеція +Treemap Деревоподібна +Triangle Трикутник +Trigger Умова запуску +Trim Обрізати +Trim Media Обрізати медіа +Turns Повороти +Type Тип +Type Text Ввести текст +Type an equation in linear form (e.g. a^2+b^2=c^2, x=(-b±√(b^2-4ac))/2a). Введіть рівняння в лінійному вигляді (наприклад, a^2+b^2=c^2, x=(-b±√(b^2-4ac))/2a). +Uncover Відкривання +Underline Підкреслення +Undo Скасувати +Undo (⌘Z) Скасувати (⌘Z) +Ungroup Розгрупувати +Up Угору +Use Timings Використовувати час показу +Use timings, if present Використовувати час показу, якщо задано +Username Ім'я користувача +Version Версія +Version {} Версія {} +Vert In Вертикально всередину +Vert Out Вертикально назовні +Vertical Вертикально +Vertical alignment Вертикальне вирівнювання +Very Loose Дуже розріджений +Very Tight Дуже ущільнений +Video Відео +Video from File… Відео з файлу… +View Перегляд +Volume Гучність +Vortex Вихор +Waterfall Водоспадна +Wave Хвиля +Web Page or File Вебсторінка або файл +Wedge Клин +Weight Товщина +Wheel Колесо +Widescreen (13.333x7.5 in) Широкоекранний (13,333×7,5 дюймів) +Widescreen (16:9) Широкоекранний (16:9) +Width Ширина +Width (in): Ширина (дюйми): +Width: Ширина: +Width: Ширина: +Wind Вітер +Window Вікно +Wipe Витирання +With Previous Разом із попереднім +WordArt WordArt +WordArt style {} Стиль WordArt {} +Words Слова +Wrap Text in Shape Переносити текст у фігурі +Wrap text in shape Переносити текст у фігурі +Yellow Жовтий +Your changes will be lost if you don't save them. Якщо не зберегти, зміни будуть втрачені. +Your name (comments): Ваше ім’я (коментарі): +Zoom Масштаб +Zoom In Збільшити +Zoom Out Зменшити +Zoom… Масштаб… +a) b) c) a) b) c) +a. b. c. a. b. c. +dropped file перетягнутий файл +i. ii. iii. i. ii. iii. +no presentation open Немає відкритої презентації +no slides Немає слайдів +no text insertion point Немає позиції введення тексту +nothing selected Нічого не вибрано +nothing to redo Немає дій для повторення +nothing to undo Немає дій для скасування +select text or a shape Виберіть текст або фігуру +select two or more objects Виберіть два або більше об’єктів +size (pt) розмір (пт) +tOGGLE cASE tOGGLE cASE +unknown interface language Невідома мова інтерфейсу +{ch} Bullet {ch} Маркер +{f} (Body) {f} (Основний текст) +{f} (Headings) {f} (Заголовки) +{name}: DeckCraft can't open this kind of file {name}: DeckCraft не може відкрити файл цього типу +{n} Column{} {n} Стовпець{} +{r} pt {r} пт +{r} pt glow світіння {r} пт +{w} pt {w} пт +{} ({} slides) {} ({} слайдів) +{} Slide Master {} Зразок слайдів +{} contributors · {} commits Учасники: {} · Коміти: {} +{} presentations {} презентацій +{}x{} Table Таблиця {}×{} +“{}” is not available yet. «{}» ще недоступно. +−Bin −Бін +▶ Play All ▶ Відтворити все +◀ Previous ◀ Назад +✏ on ✏ увімкнено diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index bef64a4f2..62e18a217 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -11,6 +11,7 @@ pub mod control; pub mod credits; pub mod dialogs; pub mod fillui; +pub mod i18n; pub mod icons; pub mod media; pub mod menus; @@ -43,6 +44,8 @@ pub type Inbox = std::sync::Arc)>>>; /// Platform services injected by the host (desktop or web). #[derive(Default)] pub struct Services { + /// Host locale (OS on desktop, navigator.language on the web). + pub system_locale: Option, /// Open-file dialog for a purpose (`open`, `picture`, `audio`, `video`) → path. pub pick_open: Option, /// Save dialog with a suggested name → path. @@ -75,6 +78,7 @@ pub enum ViewMode { #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(default, rename_all = "camelCase")] pub struct UiState { + pub interface_language: i18n::Language, pub brightness: theme::Brightness, pub tab: String, pub ribbon_collapsed: bool, @@ -103,6 +107,7 @@ pub struct UiState { impl Default for UiState { fn default() -> Self { UiState { + interface_language: i18n::Language::System, brightness: theme::Brightness::Light, tab: "Home".into(), ribbon_collapsed: false, @@ -154,6 +159,7 @@ pub const UI_COMMANDS: &[(&str, &str, Option<&str>, &str)] = &[ ("app.about", "About DeckCraft", None, "{}"), ("app.palette", "Command Palette", Some("Cmd+Shift+P"), "{}"), ("app.preferences", "Preferences…", Some("Cmd+,"), "{}"), + ("app.language", "Interface Language", None, "{language: system|en|uk}"), ("app.openDialog", "Open…", None, "{purpose?}"), ("app.insertPictureDialog", "Picture from File…", None, "{}"), ("app.insertAudioDialog", "Audio from File…", None, "{}"), @@ -253,12 +259,15 @@ impl SlideApp { } pub fn set_status(&mut self, msg: impl Into) { - self.status = Some((msg.into(), now_ms())); + i18n::set_current(self.ui.interface_language.resolve(self.services.system_locale.as_deref())); + let msg = msg.into(); + self.status = Some((i18n::tr(&msg).to_string(), now_ms())); } /// Run a command by id (UI commands here, everything else in the engine). Errors show in the /// status bar and are returned. pub fn run(&mut self, id: &str, params: Value) -> Result { + i18n::set_current(self.ui.interface_language.resolve(self.services.system_locale.as_deref())); let r = if UI_COMMANDS.iter().any(|c| c.0 == id) { self.run_ui(id, ¶ms) } else { @@ -345,6 +354,16 @@ impl SlideApp { } "app.about" => self.dialog = Some(dialogs::Dialog::new("about")), "app.palette" => self.palette = Some((String::new(), 0)), + "app.language" => { + let language = p.get("language").and_then(Value::as_str).unwrap_or("system"); + self.ui.interface_language = match language { + "system" => i18n::Language::System, + "en" => i18n::Language::En, + "uk" => i18n::Language::Uk, + _ => return Err("unknown interface language".into()), + }; + i18n::set_current(self.ui.interface_language.resolve(self.services.system_locale.as_deref())); + } "app.preferences" => self.dialog = Some(dialogs::Dialog::new("preferences")), "app.openDialog" => self.pick_and_open(), "app.insertPictureDialog" => self.pick_and_insert("picture", "insert.picture"), @@ -418,7 +437,7 @@ impl SlideApp { if let Some(path) = pick(&suggested) { match self.session.execute("file.saveAs", &json!({"path": path})) { Ok(_) => { - self.set_status(format!("Saved {path}")); + self.set_status(crate::i18n::format("Saved {path}", std::slice::from_ref(&path))); self.ui.recent.retain(|p| *p != path); self.ui.recent.insert(0, path); } @@ -516,7 +535,7 @@ impl SlideApp { } else if lower.ends_with(".txt") || lower.ends_with(".md") { "slide.fromOutline" } else { - self.set_status(format!("{name}: DeckCraft can't open this kind of file")); + self.set_status(crate::i18n::format("{name}: DeckCraft can't open this kind of file", &[(name).to_string()])); return; }; let p = if cmd == "slide.fromOutline" { json!({"text": String::from_utf8_lossy(bytes)}) } else { json!({"name": name, "data": data}) }; @@ -530,6 +549,7 @@ impl SlideApp { /// Per-frame logic before layout: styles, control requests, screenshots, shortcuts. pub fn logic(&mut self, ctx: &egui::Context) { + i18n::set_current(self.ui.interface_language.resolve(self.services.system_locale.as_deref())); let scale = self.ui.ui_scale.clamp(0.5, 3.0); if (ctx.zoom_factor() - scale).abs() > 1e-3 { ctx.set_zoom_factor(scale); @@ -574,7 +594,7 @@ impl SlideApp { if self.session.documents().iter().any(|d| d.is_dirty()) && let Err(e) = self.session.execute("file.recovery.save", &json!({})) { - self.set_status(format!("Couldn't save AutoRecover information: {e}")); + self.set_status(crate::i18n::format("Couldn't save AutoRecover information: {e}", &[(e).to_string()])); } } // On the web, dropped files can only be read asynchronously: the host reads them and @@ -682,6 +702,7 @@ impl SlideApp { /// Lay out the whole window. pub fn ui(&mut self, ui: &mut egui::Ui) { + i18n::set_current(self.ui.interface_language.resolve(self.services.system_locale.as_deref())); let ctx = ui.ctx().clone(); if !self.fonts_ready { ctx.request_repaint(); diff --git a/crates/ui-egui/src/media.rs b/crates/ui-egui/src/media.rs index f887c2b21..a5a939770 100644 --- a/crates/ui-egui/src/media.rs +++ b/crates/ui-egui/src/media.rs @@ -380,7 +380,7 @@ pub fn control_bar(ui: &mut Ui, rect: Rect, status: Option<&VoiceStatus>, clip: // Time label on the right, timeline between. let label = clock(pos); let label_w = 64.0; - painter.text(pos2(rect.max.x - 6.0, rect.center().y), Align2::RIGHT_CENTER, label, egui::FontId::monospace(11.0), white); + painter.text(pos2(rect.max.x - 6.0, rect.center().y), Align2::RIGHT_CENTER, &label, egui::FontId::monospace(11.0), white); let track = Rect::from_min_max(pos2(x + 6.0, rect.center().y - 3.0), pos2(rect.max.x - label_w - 6.0, rect.center().y + 3.0)); if track.width() > 10.0 { painter.rect_filled(track, CornerRadius::same(3), Color32::from_gray(90)); @@ -414,7 +414,7 @@ pub fn control_bar(ui: &mut Ui, rect: Rect, status: Option<&VoiceStatus>, clip: painter.text( pos2(rect.min.x, rect.max.y + 3.0), Align2::LEFT_TOP, - format!("No sound: {err}"), + crate::i18n::format("No sound: {err}", std::slice::from_ref(err)), egui::FontId::proportional(10.0), Color32::from_rgb(0xC0, 0x40, 0x40), ); diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index 9349dc360..c46eb0e15 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -63,7 +63,12 @@ pub fn palette(app: &mut SlideApp, ctx: &egui::Context) { .anchor(egui::Align2::CENTER_TOP, egui::vec2(0.0, 90.0)) .fixed_size(egui::vec2(520.0, 380.0)) .show(ctx, |ui| { - let r = ui.add(egui::TextEdit::singleline(&mut query).hint_text("Search commands…").desired_width(f32::INFINITY).font(theme::font(15.0))); + let r = ui.add( + egui::TextEdit::singleline(&mut query) + .hint_text(crate::i18n::tr("Search commands…")) + .desired_width(f32::INFINITY) + .font(theme::font(15.0)), + ); r.request_focus(); let q = query.to_lowercase(); let mut items: Vec<(String, String, Option<&'static str>)> = app @@ -71,8 +76,8 @@ pub fn palette(app: &mut SlideApp, ctx: &egui::Context) { .commands() .into_iter() .filter(|c| c.enabled) - .map(|c| (c.id.to_string(), c.label.to_string(), c.shortcut)) - .chain(crate::UI_COMMANDS.iter().map(|c| (c.0.to_string(), c.1.to_string(), c.2))) + .map(|c| (c.id.to_string(), crate::i18n::tr(c.label).to_string(), c.shortcut)) + .chain(crate::UI_COMMANDS.iter().map(|c| (c.0.to_string(), crate::i18n::tr(c.1).to_string(), c.2))) .filter(|(id, label, _)| q.is_empty() || label.to_lowercase().contains(&q) || id.to_lowercase().contains(&q)) .collect(); items.sort_by_key(|(_, l, _)| !l.to_lowercase().starts_with(&q)); @@ -100,7 +105,7 @@ pub fn palette(app: &mut SlideApp, ctx: &egui::Context) { egui::ScrollArea::vertical().max_height(320.0).show(ui, |ui| { for (k, (id, label, sc)) in items.iter().enumerate() { let r = ui.horizontal(|ui| { - let resp = ui.selectable_label(k == sel, label); + let resp = ui.selectable_label(k == sel, crate::i18n::tr(label)); if let Some(s) = sc { ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { ui.label(egui::RichText::new(crate::ribbon::pretty_shortcut(s)).color(t.text_faint)); diff --git a/crates/ui-egui/src/panes.rs b/crates/ui-egui/src/panes.rs index fff1da466..516b7d3f7 100644 --- a/crates/ui-egui/src/panes.rs +++ b/crates/ui-egui/src/panes.rs @@ -30,7 +30,7 @@ pub fn show(app: &mut SlideApp, ui: &mut Ui) { _ => "Pane", }; ui.horizontal(|ui| { - ui.label(egui::RichText::new(title).font(theme::bold(16.0))); + ui.label(egui::RichText::new(crate::i18n::tr(title)).font(theme::bold(16.0))); ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { let (r, resp) = ui.allocate_exact_size(vec2(20.0, 20.0), Sense::click()); if resp.hovered() { @@ -72,7 +72,7 @@ fn color_button( ) { let sc = scheme(app); ui.horizontal(|ui| { - ui.label(label); + ui.label(crate::i18n::tr(label)); ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { let r = widgets::drop_button(ui, "Color", vec2(70.0, 22.0), true); widgets::color_popup(&r).show(|ui| { @@ -89,7 +89,7 @@ fn format_shape(app: &mut SlideApp, ui: &mut Ui) { let t = Tokens::get(ui.ctx()); let Some(st) = app.session.active() else { return }; let Some(sh) = st.selected_shapes().first().map(|s| (*s).clone()) else { - ui.label(egui::RichText::new("Select a shape to format it.").color(t.text_dim)); + ui.label(egui::RichText::new(crate::i18n::tr("Select a shape to format it.")).color(t.text_dim)); return; }; let x = deckcraft_engine::cmd::xfrm_of(&st.doc, &st.selection, &sh); @@ -115,7 +115,7 @@ fn format_shape(app: &mut SlideApp, ui: &mut Ui) { ui.separator(); match app.ui.format_tab.as_str() { "effects" => { - ui.label(egui::RichText::new("Shadow").font(theme::bold(13.0))); + ui.label(egui::RichText::new(crate::i18n::tr("Shadow")).font(theme::bold(13.0))); ui.horizontal_wrapped(|ui| { for (l, v) in [ ("None", "none"), @@ -125,54 +125,54 @@ fn format_shape(app: &mut SlideApp, ui: &mut Ui) { ("Inner", "inner"), ("Perspective", "perspective"), ] { - if ui.button(l).clicked() { + if ui.button(crate::i18n::tr(l)).clicked() { run(app, "shape.effects", json!({"shadow": v})); } } }); ui.add_space(6.0); - ui.label(egui::RichText::new("Reflection").font(theme::bold(13.0))); + ui.label(egui::RichText::new(crate::i18n::tr("Reflection")).font(theme::bold(13.0))); ui.horizontal_wrapped(|ui| { for (l, v) in [("None", "none"), ("Tight", "tight"), ("Half", "half"), ("Full", "full")] { - if ui.button(l).clicked() { + if ui.button(crate::i18n::tr(l)).clicked() { run(app, "shape.effects", json!({"reflection": v})); } } }); ui.add_space(6.0); - ui.label(egui::RichText::new("Glow").font(theme::bold(13.0))); + ui.label(egui::RichText::new(crate::i18n::tr("Glow")).font(theme::bold(13.0))); let mut g = sh.effects.as_ref().and_then(|e| e.glow.as_ref().map(|g| g.radius)).unwrap_or(0.0); - if ui.add(egui::Slider::new(&mut g, 0.0..=40.0).text("size (pt)")).drag_stopped() { + if ui.add(egui::Slider::new(&mut g, 0.0..=40.0).text(crate::i18n::tr("size (pt)"))).drag_stopped() { run(app, "shape.effects", json!({"glow": if g > 0.0 { json!(g) } else { Value::Null }})); } - ui.label(egui::RichText::new("Soft Edges").font(theme::bold(13.0))); + ui.label(egui::RichText::new(crate::i18n::tr("Soft Edges")).font(theme::bold(13.0))); let mut s = sh.effects.as_ref().and_then(|e| e.soft_edge).unwrap_or(0.0); - if ui.add(egui::Slider::new(&mut s, 0.0..=50.0).text("size (pt)")).drag_stopped() { + if ui.add(egui::Slider::new(&mut s, 0.0..=50.0).text(crate::i18n::tr("size (pt)"))).drag_stopped() { run(app, "shape.effects", json!({"softEdges": s})); } - if ui.button("Reset Effects").clicked() { + if ui.button(crate::i18n::tr("Reset Effects")).clicked() { run(app, "shape.effects", json!({"reset": true})); } } "size" => { let mut fields = |ui: &mut Ui, label: &str, id: &str, v: f64, key: &str, cmd: &str, unit: f64| { ui.horizontal(|ui| { - ui.add_sized(vec2(90.0, 20.0), egui::Label::new(label)); + ui.add_sized(vec2(90.0, 20.0), egui::Label::new(crate::i18n::tr(label))); if let Some(nv) = widgets::spinner(ui, id, v / unit, 0.1, if unit == 72.0 { "\"" } else { "°" }, 70.0) { run(app, cmd, json!({key: nv * unit})); } }); }; - ui.label(egui::RichText::new("Size").font(theme::bold(13.0))); + ui.label(egui::RichText::new(crate::i18n::tr("Size")).font(theme::bold(13.0))); fields(ui, "Height", "fh", x.h, "h", "shape.resize", 72.0); fields(ui, "Width", "fw", x.w, "w", "shape.resize", 72.0); fields(ui, "Rotation", "fr", x.rot, "deg", "shape.rotate", 1.0); ui.add_space(6.0); - ui.label(egui::RichText::new("Position").font(theme::bold(13.0))); + ui.label(egui::RichText::new(crate::i18n::tr("Position")).font(theme::bold(13.0))); fields(ui, "Horizontal", "fx", x.x, "x", "shape.move", 72.0); fields(ui, "Vertical", "fy", x.y, "y", "shape.move", 72.0); ui.add_space(6.0); - ui.label(egui::RichText::new("Alt Text").font(theme::bold(13.0))); + ui.label(egui::RichText::new(crate::i18n::tr("Alt Text")).font(theme::bold(13.0))); let id = ui.id().with(("alt", sh.id.0)); let mut alt: String = ui.data_mut(|d| d.get_temp(id).unwrap_or_else(|| sh.descr.clone())); let r = ui.add(egui::TextEdit::multiline(&mut alt).desired_rows(3).desired_width(f32::INFINITY)); @@ -182,28 +182,28 @@ fn format_shape(app: &mut SlideApp, ui: &mut Ui) { ui.data_mut(|d| d.insert_temp(id, alt)); } "text" => { - ui.label(egui::RichText::new("Text Box").font(theme::bold(13.0))); + ui.label(egui::RichText::new(crate::i18n::tr("Text Box")).font(theme::bold(13.0))); ui.horizontal(|ui| { - ui.label("Vertical alignment"); + ui.label(crate::i18n::tr("Vertical alignment")); for (l, a) in [("Top", "top"), ("Middle", "middle"), ("Bottom", "bottom")] { - if ui.small_button(l).clicked() { + if ui.small_button(crate::i18n::tr(l)).clicked() { run(app, "format.anchor", json!({"anchor": a})); } } }); ui.horizontal_wrapped(|ui| { for (l, m) in [("Do not Autofit", "none"), ("Shrink text on overflow", "shrink"), ("Resize shape to fit text", "resize")] { - if ui.small_button(l).clicked() { + if ui.small_button(crate::i18n::tr(l)).clicked() { run(app, "format.autofit", json!({"mode": m})); } } }); let wrap = sh.text.as_ref().and_then(|t| t.body.wrap).unwrap_or(true); let mut w = wrap; - if ui.checkbox(&mut w, "Wrap text in shape").changed() { + if ui.checkbox(&mut w, crate::i18n::tr("Wrap text in shape")).changed() { run(app, "format.wrap", json!({"on": w})); } - ui.label("Margins (pt)"); + ui.label(crate::i18n::tr("Margins (pt)")); let b = sh.text.as_ref().map(|t| t.body.clone()).unwrap_or_default(); for (l, k, v) in [ ("Left", "left", b.inset_l.unwrap_or(7.2)), @@ -212,14 +212,14 @@ fn format_shape(app: &mut SlideApp, ui: &mut Ui) { ("Bottom", "bottom", b.inset_b.unwrap_or(3.6)), ] { ui.horizontal(|ui| { - ui.add_sized(vec2(60.0, 20.0), egui::Label::new(l)); + ui.add_sized(vec2(60.0, 20.0), egui::Label::new(crate::i18n::tr(l))); if let Some(nv) = widgets::spinner(ui, &format!("m{k}"), v, 1.0, " pt", 60.0) { run(app, "format.margins", json!({k: nv.max(0.0)})); } }); } ui.horizontal(|ui| { - ui.label("Columns"); + ui.label(crate::i18n::tr("Columns")); for n in 1..=3 { if ui.small_button(n.to_string()).clicked() { run(app, "format.columns", json!({"count": n, "spacing": 18})); @@ -230,29 +230,29 @@ fn format_shape(app: &mut SlideApp, ui: &mut Ui) { "picture" => { let adj = if let ShapeKind::Picture { fill } = &sh.kind { fill.adjust.clone() } else { Default::default() }; let mut b = adj.brightness as f32; - if ui.add(egui::Slider::new(&mut b, -1.0..=1.0).text("Brightness")).drag_stopped() { + if ui.add(egui::Slider::new(&mut b, -1.0..=1.0).text(crate::i18n::tr("Brightness"))).drag_stopped() { run(app, "picture.adjust", json!({"brightness": b})); } let mut c = adj.contrast as f32; - if ui.add(egui::Slider::new(&mut c, -1.0..=1.0).text("Contrast")).drag_stopped() { + if ui.add(egui::Slider::new(&mut c, -1.0..=1.0).text(crate::i18n::tr("Contrast"))).drag_stopped() { run(app, "picture.adjust", json!({"contrast": c})); } let mut s = adj.saturation.unwrap_or(1.0) as f32; - if ui.add(egui::Slider::new(&mut s, 0.0..=4.0).text("Saturation")).drag_stopped() { + if ui.add(egui::Slider::new(&mut s, 0.0..=4.0).text(crate::i18n::tr("Saturation"))).drag_stopped() { run(app, "picture.adjust", json!({"saturation": s})); } let alpha = if let ShapeKind::Picture { fill } = &sh.kind { fill.alpha.unwrap_or(1.0) } else { 1.0 }; let mut tr = (1.0 - alpha) as f32; - if ui.add(egui::Slider::new(&mut tr, 0.0..=1.0).text("Transparency")).drag_stopped() { + if ui.add(egui::Slider::new(&mut tr, 0.0..=1.0).text(crate::i18n::tr("Transparency"))).drag_stopped() { run(app, "picture.adjust", json!({"transparency": tr})); } - if ui.button("Reset Picture").clicked() { + if ui.button(crate::i18n::tr("Reset Picture")).clicked() { run(app, "picture.reset", json!({})); } } _ => { // Fill & Line. - ui.label(egui::RichText::new("Fill").font(theme::bold(13.0))); + ui.label(egui::RichText::new(crate::i18n::tr("Fill")).font(theme::bold(13.0))); let kind = match &sh.fill { None => "auto", Some(Fill::None) => "none", @@ -265,7 +265,7 @@ fn format_shape(app: &mut SlideApp, ui: &mut Ui) { for (l, k) in [("No fill", "none"), ("Solid fill", "solid"), ("Gradient fill", "gradient"), ("Pattern fill", "pattern"), ("Automatic", "auto")] { - if ui.radio(kind == k, l).clicked() { + if ui.radio(kind == k, crate::i18n::tr(l)).clicked() { match k { "none" => run(app, "shape.fill", json!({"none": true})), "solid" => run(app, "shape.fill", json!({"color": "accent1"})), @@ -284,7 +284,7 @@ fn format_shape(app: &mut SlideApp, ui: &mut Ui) { None => ("shape.fill".into(), json!({"none": true})), }); ui.horizontal(|ui| { - ui.label("Transparency"); + ui.label(crate::i18n::tr("Transparency")); let id = ui.id().with("ftr"); let mut v: f32 = ui.data_mut(|d| d.get_temp(id).unwrap_or(0.0)); let r = ui.add(egui::Slider::new(&mut v, 0.0..=100.0).suffix("%")); @@ -299,21 +299,21 @@ fn format_shape(app: &mut SlideApp, ui: &mut Ui) { run(app, "shape.fill", json!({"gradient": crate::fillui::gradient_json(&ng)})); } let mut rot = g.rotate_with_shape; - if ui.checkbox(&mut rot, "Rotate with shape").changed() { + if ui.checkbox(&mut rot, crate::i18n::tr("Rotate with shape")).changed() { let ng = deckcraft_model::style::Gradient { rotate_with_shape: rot, ..g.clone() }; run(app, "shape.fill", json!({"gradient": crate::fillui::gradient_json(&ng)})); } } ui.add_space(8.0); - ui.label(egui::RichText::new("Line").font(theme::bold(13.0))); + ui.label(egui::RichText::new(crate::i18n::tr("Line")).font(theme::bold(13.0))); let none = matches!(sh.line.as_ref().and_then(|l| l.fill.as_ref()), Some(Fill::None)); - if ui.radio(none, "No line").clicked() { + if ui.radio(none, crate::i18n::tr("No line")).clicked() { run(app, "shape.line", json!({"none": true})); } - if ui.radio(!none && sh.line.is_some(), "Solid line").clicked() { + if ui.radio(!none && sh.line.is_some(), crate::i18n::tr("Solid line")).clicked() { run(app, "shape.line", json!({"color": "tx1", "width": 1})); } - if ui.radio(sh.line.is_none(), "Automatic").clicked() { + if ui.radio(sh.line.is_none(), crate::i18n::tr("Automatic")).clicked() { run(app, "shape.line", json!({"reset": true})); } color_button(app, ui, "Color", Some("No Outline"), |c| match c { @@ -321,16 +321,16 @@ fn format_shape(app: &mut SlideApp, ui: &mut Ui) { None => ("shape.line".into(), json!({"none": true})), }); ui.horizontal(|ui| { - ui.label("Width"); + ui.label(crate::i18n::tr("Width")); let w = sh.line.as_ref().and_then(|l| l.width).unwrap_or(0.75); if let Some(v) = widgets::spinner(ui, "lw", w, 0.25, " pt", 60.0) { run(app, "shape.line", json!({"width": v.max(0.0)})); } }); ui.horizontal_wrapped(|ui| { - ui.label("Dash"); + ui.label(crate::i18n::tr("Dash")); for (l, d) in [("—", "solid"), ("···", "sysDot"), ("- -", "dash"), ("-·-", "dashDot"), ("— —", "lgDash")] { - if ui.small_button(l).clicked() { + if ui.small_button(crate::i18n::tr(l)).clicked() { run(app, "shape.line", json!({"dash": d})); } } @@ -355,7 +355,7 @@ fn background(app: &mut SlideApp, ui: &mut Ui) { }; // What "Apply to All" re-applies: the current slide's background as command params. let mut current: Option = None; - ui.label(egui::RichText::new("Fill").font(theme::bold(13.0))); + ui.label(egui::RichText::new(crate::i18n::tr("Fill")).font(theme::bold(13.0))); for (l, k) in [ ("Solid fill", "solid"), ("Gradient fill", "gradient"), @@ -363,7 +363,7 @@ fn background(app: &mut SlideApp, ui: &mut Ui) { ("Pattern fill", "pattern"), ("Follow the layout", "auto"), ] { - if ui.radio(kind == k, l).clicked() && kind != k { + if ui.radio(kind == k, crate::i18n::tr(l)).clicked() && kind != k { match k { "solid" => run(app, "design.background", json!({"color": "bg1"})), "gradient" => { @@ -389,7 +389,7 @@ fn background(app: &mut SlideApp, ui: &mut Ui) { Some(Fill::Solid { color }) => { current = Some(json!({"color": cref_param(color)})); ui.horizontal(|ui| { - ui.label("Color"); + ui.label(crate::i18n::tr("Color")); let r = widgets::drop_button(ui, "Color", vec2(70.0, 20.0), true); widgets::color_popup(&r).show(|ui| { if let Some(Some(c)) = widgets::color_grid(ui, &sc, None) { @@ -415,7 +415,7 @@ fn background(app: &mut SlideApp, ui: &mut Ui) { }); for (label, fg) in [("Foreground", true), ("Background", false)] { ui.horizontal(|ui| { - ui.label(label); + ui.label(crate::i18n::tr(label)); let r = widgets::drop_button(ui, "Color", vec2(70.0, 20.0), true); widgets::color_popup(&r).show(|ui| { if let Some(Some(c)) = widgets::color_grid(ui, &sc, None) { @@ -427,7 +427,7 @@ fn background(app: &mut SlideApp, ui: &mut Ui) { } } Some(Fill::Picture(_)) => { - if ui.button("Insert picture from file…").clicked() + if ui.button(crate::i18n::tr("Insert picture from file…")).clicked() && let Some(pick) = app.services.pick_open.as_mut() && let Some(path) = pick("picture") && let Some(Ok(bytes)) = app.services.read.as_ref().map(|r| r(&path)) @@ -440,18 +440,18 @@ fn background(app: &mut SlideApp, ui: &mut Ui) { ui.add_space(4.0); let hide = app.session.active().and_then(|d| d.current_slide().map(|s| !s.show_master_shapes)).unwrap_or(false); let mut h = hide; - if ui.checkbox(&mut h, "Hide background graphics").changed() { + if ui.checkbox(&mut h, crate::i18n::tr("Hide background graphics")).changed() { run(app, "design.hideBackgroundGraphics", json!({"hide": h})); } ui.add_space(8.0); ui.horizontal(|ui| { - if ui.add_enabled(current.is_some(), egui::Button::new("Apply to All")).clicked() + if ui.add_enabled(current.is_some(), egui::Button::new(crate::i18n::tr("Apply to All"))).clicked() && let Some(Value::Object(mut o)) = current.clone() { o.insert("all".into(), json!(true)); run(app, "design.background", Value::Object(o)); } - if ui.button("Reset Background").clicked() { + if ui.button(crate::i18n::tr("Reset Background")).clicked() { run(app, "design.background", json!({"reset": true})); } }); @@ -465,7 +465,7 @@ fn animation(app: &mut SlideApp, ui: &mut Ui) { let names: Vec<(u32, String)> = st.shapes().iter().map(|s| (s.id.0, s.name.clone())).collect(); let selected: Vec = st.selection.shapes.iter().map(|i| i.0).collect(); ui.horizontal(|ui| { - if ui.button("▶ Play All").clicked() { + if ui.button(crate::i18n::tr("▶ Play All")).clicked() { let from = app.session.active().map(|d| d.selection.slide).unwrap_or(0); app.start_show(from, true); if let Some(s) = app.show.as_mut() { @@ -476,7 +476,9 @@ fn animation(app: &mut SlideApp, ui: &mut Ui) { }); ui.separator(); if anims.is_empty() { - ui.label(egui::RichText::new("Select an object on the slide and add an animation from the Animations tab.").color(t.text_dim)); + ui.label( + egui::RichText::new(crate::i18n::tr("Select an object on the slide and add an animation from the Animations tab.")).color(t.text_dim), + ); return; } let mut click = 0; @@ -484,7 +486,11 @@ fn animation(app: &mut SlideApp, ui: &mut Ui) { if a.start == AnimStart::OnClick { click += 1; } - let name = names.iter().find(|(id, _)| *id == a.shape.0).map(|(_, n)| n.clone()).unwrap_or_else(|| format!("Shape {}", a.shape)); + let name = names + .iter() + .find(|(id, _)| *id == a.shape.0) + .map(|(_, n)| n.clone()) + .unwrap_or_else(|| crate::i18n::format("Shape {}", &[(a.shape).to_string()])); let (r, resp) = ui.allocate_exact_size(vec2(ui.available_width(), 30.0), Sense::click()); let sel = selected.contains(&a.shape.0); if sel || resp.hovered() { @@ -525,21 +531,21 @@ fn animation(app: &mut SlideApp, ui: &mut Ui) { } resp.context_menu(|ui| { for (l, v) in [("Start On Click", "onClick"), ("Start With Previous", "withPrevious"), ("Start After Previous", "afterPrevious")] { - if ui.button(l).clicked() { + if ui.button(crate::i18n::tr(l)).clicked() { run(app, "animation.timing", json!({"index": i, "start": v})); ui.close(); } } ui.separator(); - if ui.button("Move Up").clicked() { + if ui.button(crate::i18n::tr("Move Up")).clicked() { run(app, "animation.move", json!({"index": i, "to": i.saturating_sub(1)})); ui.close(); } - if ui.button("Move Down").clicked() { + if ui.button(crate::i18n::tr("Move Down")).clicked() { run(app, "animation.move", json!({"index": i, "to": i + 1})); ui.close(); } - if ui.button("Remove").clicked() { + if ui.button(crate::i18n::tr("Remove")).clicked() { run(app, "animation.remove", json!({"index": i})); ui.close(); } @@ -556,11 +562,11 @@ fn selection(app: &mut SlideApp, ui: &mut Ui) { st.shapes().iter().rev().map(|s| (s.id.0, s.name.clone(), s.hidden, s.kind_name())).collect(); let selected: Vec = st.selection.shapes.iter().map(|i| i.0).collect(); ui.horizontal(|ui| { - if ui.button("Show All").clicked() { + if ui.button(crate::i18n::tr("Show All")).clicked() { let ids: Vec = shapes.iter().map(|s| s.0).collect(); run(app, "shape.visible", json!({"ids": ids, "visible": true})); } - if ui.button("Hide All").clicked() { + if ui.button(crate::i18n::tr("Hide All")).clicked() { let ids: Vec = shapes.iter().map(|s| s.0).collect(); run(app, "shape.visible", json!({"ids": ids, "visible": false})); } @@ -580,7 +586,7 @@ fn selection(app: &mut SlideApp, ui: &mut Ui) { theme::font(12.5), if hidden { t.text_faint } else { t.text }, ); - ui.painter().text(pos2(r.max.x - 34.0, r.center().y), Align2::RIGHT_CENTER, kind, theme::font(10.5), t.text_faint); + ui.painter().text(pos2(r.max.x - 34.0, r.center().y), Align2::RIGHT_CENTER, crate::i18n::tr(kind), theme::font(10.5), t.text_faint); let eye = Rect::from_center_size(pos2(r.max.x - 14.0, r.center().y), vec2(16.0, 16.0)); icons::paint(ui.painter(), eye, if hidden { Icon::EyeOff } else { Icon::Eye }, t.text_dim, false); let eye_resp = ui.interact(eye, ui.id().with(("eye", id)), Sense::click()); @@ -596,15 +602,15 @@ fn selection(app: &mut SlideApp, ui: &mut Ui) { app.dialog = Some(d); } resp.context_menu(|ui| { - if ui.button("Bring Forward").clicked() { + if ui.button(crate::i18n::tr("Bring Forward")).clicked() { run(app, "arrange.bringForward", json!({"ids": [id]})); ui.close(); } - if ui.button("Send Backward").clicked() { + if ui.button(crate::i18n::tr("Send Backward")).clicked() { run(app, "arrange.sendBackward", json!({"ids": [id]})); ui.close(); } - if ui.button("Rename…").clicked() { + if ui.button(crate::i18n::tr("Rename…")).clicked() { let mut d = crate::dialogs::Dialog::new("renameShape"); d.params = json!({"id": id, "name": name}); app.dialog = Some(d); @@ -617,14 +623,14 @@ fn selection(app: &mut SlideApp, ui: &mut Ui) { fn comments(app: &mut SlideApp, ui: &mut Ui) { let t = Tokens::get(ui.ctx()); - if ui.button("+ New Comment").clicked() { + if ui.button(crate::i18n::tr("+ New Comment")).clicked() { app.dialog = Some(crate::dialogs::Dialog::new("comment")); } ui.separator(); let Some(st) = app.session.active() else { return }; let list = st.current_slide().map(|s| s.comments.clone()).unwrap_or_default(); if list.is_empty() { - ui.label(egui::RichText::new("No comments on this slide.").color(t.text_dim)); + ui.label(egui::RichText::new(crate::i18n::tr("No comments on this slide.")).color(t.text_dim)); } for (i, c) in list.iter().enumerate() { egui::Frame::NONE.fill(t.chrome).corner_radius(CornerRadius::same(8)).inner_margin(egui::Margin::same(8)).show(ui, |ui| { @@ -645,7 +651,7 @@ fn comments(app: &mut SlideApp, ui: &mut Ui) { ui.horizontal(|ui| { let id = ui.id().with(("reply", i)); let mut txt: String = ui.data_mut(|d| d.get_temp(id).unwrap_or_default()); - let r = ui.add(egui::TextEdit::singleline(&mut txt).hint_text("Reply…").desired_width(ui.available_width() - 70.0)); + let r = ui.add(egui::TextEdit::singleline(&mut txt).hint_text(crate::i18n::tr("Reply…")).desired_width(ui.available_width() - 70.0)); if r.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) && !txt.trim().is_empty() { run(app, "comment.reply", json!({"index": i, "text": txt})); txt.clear(); @@ -666,7 +672,7 @@ fn comments(app: &mut SlideApp, ui: &mut Ui) { /// Design Ideas: offline suggestions — the current slide in each theme and layout variations. fn designer(app: &mut SlideApp, ui: &mut Ui) { let t = Tokens::get(ui.ctx()); - ui.label(egui::RichText::new("Pick a look for your slides. Ideas are generated on your device.").color(t.text_dim)); + ui.label(egui::RichText::new(crate::i18n::tr("Pick a look for your slides. Ideas are generated on your device.")).color(t.text_dim)); ui.add_space(6.0); let Some(st) = app.session.active() else { return }; let doc = st.doc.clone(); diff --git a/crates/ui-egui/src/ribbon.rs b/crates/ui-egui/src/ribbon.rs index c7c251efe..e737f7be2 100644 --- a/crates/ui-egui/src/ribbon.rs +++ b/crates/ui-egui/src/ribbon.rs @@ -62,8 +62,11 @@ pub fn title_bar(app: &mut SlideApp, ui: &mut Ui) { if icon_toggle(&mut child, Icon::PlayFromStart, "Play from Start (F5)", has_doc, false).clicked() { app.start_show(0, false); } - let title = - app.session.active().map(|d| format!("{}{}", d.title(), if dirty { " — Edited" } else { "" })).unwrap_or_else(|| "DeckCraft".into()); + let title = app + .session + .active() + .map(|d| format!("{}{}", d.title(), if dirty { crate::i18n::tr(" — Edited") } else { "" })) + .unwrap_or_else(|| "DeckCraft".into()); ui.painter().text(rect.center(), Align2::CENTER_CENTER, title, theme::font(13.0), t.text); // Right side. let mut right = ui.new_child( @@ -76,7 +79,7 @@ pub fn title_bar(app: &mut SlideApp, ui: &mut Ui) { app.palette = Some((String::new(), 0)); } let share = right.add( - egui::Button::new(egui::RichText::new(" Present ").color(t.accent_text).font(theme::bold(13.0))) + egui::Button::new(egui::RichText::new(crate::i18n::tr(" Present ")).color(t.accent_text).font(theme::bold(13.0))) .fill(t.accent) .corner_radius(CornerRadius::same(13)) .min_size(vec2(84.0, 26.0)), @@ -148,8 +151,13 @@ pub fn show(app: &mut SlideApp, ui: &mut Ui) { let y = rect.min.y + 2.0; let all: Vec<(&str, bool)> = TABS.iter().map(|s| (*s, false)).chain(ctx_tabs.iter().map(|s| (*s, true))).collect(); for (name, contextual) in all { + let label = match name { + "Home" => crate::i18n::tr_context(name, "@ribbon:Home"), + "Insert" => crate::i18n::tr_context(name, "@ribbon:Insert"), + _ => crate::i18n::tr(name), + }; let font = theme::font(13.0); - let w = ui.fonts_mut(|f| f.layout_no_wrap(name.to_string(), font.clone(), t.text).size().x) + 18.0; + let w = ui.fonts_mut(|f| f.layout_no_wrap(label.to_string(), font.clone(), t.text).size().x) + 18.0; let r = Rect::from_min_size(pos2(x, y), vec2(w, 24.0)); let resp = ui.interact(r, ui.id().with(("tab", name)), Sense::click()); let active = app.ui.tab == name; @@ -158,7 +166,7 @@ pub fn show(app: &mut SlideApp, ui: &mut Ui) { } let col = if contextual { t.contextual } else { t.text }; let f = if active { theme::bold(13.0) } else { font }; - ui.painter().text(r.center(), Align2::CENTER_CENTER, name, f, col); + ui.painter().text(r.center(), Align2::CENTER_CENTER, label, f, col); if active { let uw = (w - 20.0).max(16.0); ui.painter().rect_filled(Rect::from_center_size(pos2(r.center().x, r.max.y - 1.0), vec2(uw, 3.0)), CornerRadius::same(1), t.accent); @@ -234,6 +242,7 @@ fn toggle(app: &mut SlideApp, ui: &mut Ui, icon: Icon, label: &str, id: &str, on } fn tip(id: &str, label: &str) -> String { + let label = crate::i18n::tr(label); let sc = deckcraft_engine::find_command(id).and_then(|c| c.shortcut).or_else(|| crate::UI_COMMANDS.iter().find(|c| c.0 == id).and_then(|c| c.2)); match sc { Some(s) => format!("{label} ({})", pretty_shortcut(s)), @@ -266,18 +275,18 @@ pub fn fmt_state(app: &mut SlideApp) -> Value { fn clipboard_group(app: &mut SlideApp, ui: &mut Ui) { group(ui, |ui| { let (main, arrow) = big_button_ex(ui, Icon::Paste, "Paste", enabled(app, "edit.paste"), true, false); - if main.on_hover_text("Paste (⌘V)").clicked() { + if main.on_hover_text(crate::i18n::tr("Paste (⌘V)")).clicked() { run(app, "edit.paste", json!({})); } if let Some(a) = arrow { egui::Popup::menu(&a).show(|ui| { - if ui.button("Paste").clicked() { + if ui.button(crate::i18n::tr("Paste")).clicked() { run(app, "edit.paste", json!({})); } - if ui.button("Paste and Match Formatting").clicked() { + if ui.button(crate::i18n::tr("Paste and Match Formatting")).clicked() { run(app, "edit.pasteText", json!({})); } - if ui.button("Paste Formatting").clicked() { + if ui.button(crate::i18n::tr("Paste Formatting")).clicked() { run(app, "format.painterApply", json!({})); } }); @@ -287,7 +296,8 @@ fn clipboard_group(app: &mut SlideApp, ui: &mut Ui) { small(app, ui, Icon::Copy, "Copy", "edit.copy", json!({})); let on = app.session.painter.is_some(); let en = enabled(app, "format.painter"); - let r = small_button(ui, Icon::FormatPainter, "Format", en, on).on_hover_text("Format Painter (double-click to keep it on)"); + let r = + small_button(ui, Icon::FormatPainter, "Format", en, on).on_hover_text(crate::i18n::tr("Format Painter (double-click to keep it on)")); if r.double_clicked() { run(app, "format.painter", json!({"sticky": true})); } else if r.clicked() { @@ -352,17 +362,17 @@ fn layout_menu(app: &mut SlideApp, ui: &mut Ui, cmd: &str) { fn slides_group(app: &mut SlideApp, ui: &mut Ui) { group(ui, |ui| { let (main, arrow) = big_button_ex(ui, Icon::NewSlide, "New\nSlide", enabled(app, "slide.new"), true, false); - if main.on_hover_text("New Slide (⇧⌘N)").clicked() { + if main.on_hover_text(crate::i18n::tr("New Slide (⇧⌘N)")).clicked() { run(app, "slide.new", json!({})); } if let Some(a) = arrow { egui::Popup::menu(&a).width(420.0).show(|ui| { layout_menu(app, ui, "slide.new"); ui.separator(); - if ui.button("Duplicate Selected Slides").clicked() { + if ui.button(crate::i18n::tr("Duplicate Selected Slides")).clicked() { run(app, "slide.duplicate", json!({})); } - if ui.button("Slides from Outline…").clicked() { + if ui.button(crate::i18n::tr("Slides from Outline…")).clicked() { app.dialog = Some(crate::dialogs::Dialog::new("outline")); } }); @@ -373,10 +383,10 @@ fn slides_group(app: &mut SlideApp, ui: &mut Ui) { small(app, ui, Icon::ResetSlide, "Reset", "slide.reset", json!({})); let (r, _) = small_button_ex(ui, Icon::Section, "Section", enabled(app, "section.add"), false, true); egui::Popup::menu(&r).show(|ui| { - if ui.button("Add Section").clicked() { + if ui.button(crate::i18n::tr("Add Section")).clicked() { run(app, "section.add", json!({})); } - if ui.button("Remove All Sections").clicked() { + if ui.button(crate::i18n::tr("Remove All Sections")).clicked() { run(app, "section.removeAll", json!({})); } }); @@ -408,9 +418,9 @@ fn font_group(app: &mut SlideApp, ui: &mut Ui, st: &Value) { dropdown(ui, "font", &font, 150.0, en, |ui| { for (k, f) in fams.iter().enumerate() { let label = if k == 0 && theme_fonts.len() == 2 { - format!("{f} (Headings)") + crate::i18n::format("{f} (Headings)", std::slice::from_ref(f)) } else if k == 1 && theme_fonts.len() == 2 { - format!("{f} (Body)") + crate::i18n::format("{f} (Body)", std::slice::from_ref(f)) } else { f.clone() }; @@ -449,7 +459,7 @@ fn font_group(app: &mut SlideApp, ui: &mut Ui, st: &Value) { let r = icon_toggle(ui, Icon::CharSpacing, "Character Spacing", en, false); egui::Popup::menu(&r).show(|ui| { for (label, pt) in [("Very Tight", -3.0), ("Tight", -1.5), ("Normal", 0.0), ("Loose", 3.0), ("Very Loose", 6.0)] { - if ui.button(label).clicked() { + if ui.button(crate::i18n::tr(label)).clicked() { run(app, "format.spacing", json!({"pt": pt})); } } @@ -463,7 +473,7 @@ fn font_group(app: &mut SlideApp, ui: &mut Ui, st: &Value) { ("Capitalize Each Word", "title"), ("tOGGLE cASE", "toggle"), ] { - if ui.button(label).clicked() { + if ui.button(crate::i18n::tr(label)).clicked() { run(app, "format.case", json!({"mode": mode})); } } @@ -528,28 +538,28 @@ fn paragraph_group(app: &mut SlideApp, ui: &mut Ui, st: &Value) { ui.spacing_mut().item_spacing.x = 1.0; let b = |k: &str| st.get(k).and_then(Value::as_bool).unwrap_or(false); let (m, a) = small_button_ex(ui, Icon::Bullets, "", en, b("bullets"), true); - if m.on_hover_text("Bullets").clicked() { + if m.on_hover_text(crate::i18n::tr("Bullets")).clicked() { run(app, "format.bullets", json!({})); } if let Some(a) = a { egui::Popup::menu(&a).show(|ui| { - if ui.button("None").clicked() { + if ui.button(crate::i18n::tr("None")).clicked() { run(app, "format.bullets", json!({"on": false})); } for ch in ["•", "○", "■", "□", "◆", "➢", "✓", "–", "★"] { - if ui.button(egui::RichText::new(format!("{ch} Bullet")).size(14.0)).clicked() { + if ui.button(egui::RichText::new(crate::i18n::format("{ch} Bullet", &[(ch).to_string()])).size(14.0)).clicked() { run(app, "format.bullets", json!({"on": true, "char": ch})); } } }); } let (m, a) = small_button_ex(ui, Icon::Numbering, "", en, b("numbering"), true); - if m.on_hover_text("Numbering").clicked() { + if m.on_hover_text(crate::i18n::tr("Numbering")).clicked() { run(app, "format.numbering", json!({})); } if let Some(a) = a { egui::Popup::menu(&a).show(|ui| { - if ui.button("None").clicked() { + if ui.button(crate::i18n::tr("None")).clicked() { run(app, "format.numbering", json!({"on": false})); } for (label, sch) in [ @@ -561,7 +571,7 @@ fn paragraph_group(app: &mut SlideApp, ui: &mut Ui, st: &Value) { ("a) b) c)", "alphaLcParenR"), ("a. b. c.", "alphaLcPeriod"), ] { - if ui.button(label).clicked() { + if ui.button(crate::i18n::tr(label)).clicked() { run(app, "format.numbering", json!({"on": true, "scheme": sch})); } } @@ -576,14 +586,15 @@ fn paragraph_group(app: &mut SlideApp, ui: &mut Ui, st: &Value) { run(app, "format.lineSpacing", json!({"lines": v})); } } - if ui.button("Line Spacing Options…").clicked() { + if ui.button(crate::i18n::tr("Line Spacing Options…")).clicked() { app.dialog = Some(crate::dialogs::Dialog::new("paragraph")); } }); let r = icon_toggle(ui, Icon::Columns, "Columns", en, false); egui::Popup::menu(&r).show(|ui| { for n in 1..=3 { - if ui.button(format!("{n} Column{}", if n > 1 { "s" } else { "" })).clicked() { + if ui.button(crate::i18n::count(n, if n == 1 { "Column" } else { "Columns" }, "стовпець", "стовпці", "стовпців")).clicked() + { run(app, "format.columns", json!({"count": n, "spacing": 18})); } } @@ -605,7 +616,7 @@ fn paragraph_group(app: &mut SlideApp, ui: &mut Ui, st: &Value) { ("Rotate all text 270°", "rotate270"), ("Stacked", "stacked"), ] { - if ui.button(l).clicked() { + if ui.button(crate::i18n::tr(l)).clicked() { run(app, "format.direction", json!({"dir": d})); } } @@ -613,7 +624,7 @@ fn paragraph_group(app: &mut SlideApp, ui: &mut Ui, st: &Value) { let r = icon_toggle(ui, Icon::AlignText, "Align Text", en, false); egui::Popup::menu(&r).show(|ui| { for (l, a) in [("Top", "top"), ("Middle", "middle"), ("Bottom", "bottom")] { - if ui.button(l).clicked() { + if ui.button(crate::i18n::tr(l)).clicked() { run(app, "format.anchor", json!({"anchor": a})); } } @@ -630,7 +641,7 @@ pub fn shapes_gallery(ui: &mut Ui) -> Option<&'static str> { ui.set_max_width(330.0); egui::ScrollArea::vertical().max_height(420.0).show(ui, |ui| { for cat in deckcraft_geom::preset::Category::ALL { - ui.label(egui::RichText::new(cat.label()).font(theme::bold(12.0))); + ui.label(egui::RichText::new(crate::i18n::tr(cat.label())).font(theme::bold(12.0))); ui.horizontal_wrapped(|ui| { ui.spacing_mut().item_spacing = vec2(1.0, 1.0); for p in deckcraft_geom::preset::CATALOG.iter().filter(|p| p.category == cat) { @@ -639,7 +650,7 @@ pub fn shapes_gallery(ui: &mut Ui) -> Option<&'static str> { ui.painter().rect_filled(r, CornerRadius::same(3), t.hover); } paint_preset(ui.painter(), r.shrink(4.0), p.name, Color32::TRANSPARENT, t.text); - if resp.on_hover_text(p.label).clicked() { + if resp.on_hover_text(crate::i18n::tr(p.label)).clicked() { out = Some(p.name); } } @@ -650,7 +661,7 @@ pub fn shapes_gallery(ui: &mut Ui) -> Option<&'static str> { ui.painter().rect_filled(r, CornerRadius::same(3), t.hover); } paint_freeform_icon(ui.painter(), r.shrink(5.0), name, t.text); - if resp.on_hover_text(label).clicked() { + if resp.on_hover_text(crate::i18n::tr(label)).clicked() { out = Some(name); } } @@ -724,7 +735,7 @@ fn kurbo_flatten(path: &deckcraft_geom::BezPath, mut f: impl FnMut(Fl)) { fn drawing_group(app: &mut SlideApp, ui: &mut Ui) { group(ui, |ui| { let en = enabled(app, "shape.insert"); - let r = big_button(ui, Icon::Shapes, "Shapes", en).on_hover_text("Shapes"); + let r = big_button(ui, Icon::Shapes, "Shapes", en).on_hover_text(crate::i18n::tr("Shapes")); egui::Popup::menu(&r).show(|ui| { if let Some(p) = shapes_gallery(ui) { app.session.set_tool(deckcraft_engine::ToolKind::Shape { preset: p.to_string() }); @@ -774,15 +785,15 @@ pub fn outline_menu(app: &mut SlideApp, ui: &mut Ui, sc: &deckcraft_color::Color None => run(app, "shape.line", json!({"none": true})), } } - ui.menu_button("Weight", |ui| { + ui.menu_button(crate::i18n::tr("Weight"), |ui| { for w in [0.25, 0.5, 0.75, 1.0, 1.5, 2.25, 3.0, 4.5, 6.0] { - if ui.button(format!("{w} pt")).clicked() { + if ui.button(crate::i18n::format("{w} pt", &[(w).to_string()])).clicked() { run(app, "shape.line", json!({"width": w})); ui.close(); } } }); - ui.menu_button("Dashes", |ui| { + ui.menu_button(crate::i18n::tr("Dashes"), |ui| { for (l, d) in [ ("Solid", "solid"), ("Round Dot", "sysDot"), @@ -792,13 +803,13 @@ pub fn outline_menu(app: &mut SlideApp, ui: &mut Ui, sc: &deckcraft_color::Color ("Long Dash", "lgDash"), ("Long Dash Dot", "lgDashDot"), ] { - if ui.button(l).clicked() { + if ui.button(crate::i18n::tr(l)).clicked() { run(app, "shape.line", json!({"dash": d})); ui.close(); } } }); - ui.menu_button("Arrows", |ui| { + ui.menu_button(crate::i18n::tr("Arrows"), |ui| { for (l, h, tl) in [ ("None", "none", "none"), ("Arrow at end", "none", "triangle"), @@ -807,7 +818,7 @@ pub fn outline_menu(app: &mut SlideApp, ui: &mut Ui, sc: &deckcraft_color::Color ("Stealth end", "none", "stealth"), ("Oval ends", "oval", "oval"), ] { - if ui.button(l).clicked() { + if ui.button(crate::i18n::tr(l)).clicked() { run(app, "shape.line", json!({"head": h, "tail": tl})); ui.close(); } @@ -816,7 +827,7 @@ pub fn outline_menu(app: &mut SlideApp, ui: &mut Ui, sc: &deckcraft_color::Color } pub fn effects_menu(app: &mut SlideApp, ui: &mut Ui) { - ui.menu_button("Shadow", |ui| { + ui.menu_button(crate::i18n::tr("Shadow"), |ui| { for (l, v) in [ ("No Shadow", "none"), ("Outer", "outer"), @@ -825,36 +836,36 @@ pub fn effects_menu(app: &mut SlideApp, ui: &mut Ui) { ("Inner", "inner"), ("Perspective", "perspective"), ] { - if ui.button(l).clicked() { + if ui.button(crate::i18n::tr(l)).clicked() { run(app, "shape.effects", json!({"shadow": v})); } } }); - ui.menu_button("Reflection", |ui| { + ui.menu_button(crate::i18n::tr("Reflection"), |ui| { for (l, v) in [("No Reflection", "none"), ("Tight", "tight"), ("Half", "half"), ("Full", "full")] { - if ui.button(l).clicked() { + if ui.button(crate::i18n::tr(l)).clicked() { run(app, "shape.effects", json!({"reflection": v})); } } }); - ui.menu_button("Glow", |ui| { - if ui.button("No Glow").clicked() { + ui.menu_button(crate::i18n::tr("Glow"), |ui| { + if ui.button(crate::i18n::tr("No Glow")).clicked() { run(app, "shape.effects", json!({"glow": null})); } for r in [5.0, 8.0, 11.0, 18.0] { - if ui.button(format!("{r} pt glow")).clicked() { + if ui.button(crate::i18n::format("{r} pt glow", &[(r).to_string()])).clicked() { run(app, "shape.effects", json!({"glow": r})); } } }); - ui.menu_button("Soft Edges", |ui| { + ui.menu_button(crate::i18n::tr("Soft Edges"), |ui| { for r in [0.0, 1.0, 2.5, 5.0, 10.0, 25.0] { - if ui.button(if r == 0.0 { "No Soft Edges".to_string() } else { format!("{r} pt") }).clicked() { + if ui.button(if r == 0.0 { "No Soft Edges".to_string() } else { crate::i18n::format("{r} pt", &[(r).to_string()]) }).clicked() { run(app, "shape.effects", json!({"softEdges": r})); } } }); - ui.menu_button("Bevel", |ui| { + ui.menu_button(crate::i18n::tr("Bevel"), |ui| { for b in [ "none", "circle", @@ -875,7 +886,7 @@ pub fn effects_menu(app: &mut SlideApp, ui: &mut Ui) { } } }); - if ui.button("Format Shape…").clicked() { + if ui.button(crate::i18n::tr("Format Shape…")).clicked() { let _ = app.run("view.pane", json!({"pane": "format", "tab": "effects"})); } } @@ -904,7 +915,7 @@ fn quick_styles(app: &mut SlideApp, ui: &mut Ui) { _ => (c, c.lerp_to_gamma(Color32::BLACK, 0.3), Color32::WHITE), }; p.rect(rect.shrink(2.0), CornerRadius::same(3), fill, Stroke::new(1.5, stroke), egui::StrokeKind::Inside); - p.text(rect.center(), Align2::CENTER_CENTER, "Abc", theme::font(11.0), text); + p.text(rect.center(), Align2::CENTER_CENTER, crate::i18n::tr("Abc"), theme::font(11.0), text); if resp.hovered() { p.rect_stroke(rect, CornerRadius::same(3), Stroke::new(1.5, Tokens::get(ui.ctx()).accent), egui::StrokeKind::Inside); } @@ -919,7 +930,7 @@ fn quick_styles(app: &mut SlideApp, ui: &mut Ui) { } pub fn arrange_menu(app: &mut SlideApp, ui: &mut Ui) { - ui.label(egui::RichText::new("Order Objects").font(theme::bold(12.0))); + ui.label(egui::RichText::new(crate::i18n::tr("Order Objects")).font(theme::bold(12.0))); for (l, id, ic) in [ ("Bring to Front", "arrange.bringToFront", Icon::BringToFront), ("Send to Back", "arrange.sendToBack", Icon::SendToBack), @@ -929,20 +940,20 @@ pub fn arrange_menu(app: &mut SlideApp, ui: &mut Ui) { menu_item(app, ui, ic, l, id, json!({})); } ui.separator(); - ui.label(egui::RichText::new("Group Objects").font(theme::bold(12.0))); + ui.label(egui::RichText::new(crate::i18n::tr("Group Objects")).font(theme::bold(12.0))); menu_item(app, ui, Icon::Group, "Group", "arrange.group", json!({})); menu_item(app, ui, Icon::Ungroup, "Ungroup", "arrange.ungroup", json!({})); menu_item(app, ui, Icon::Group, "Regroup", "arrange.regroup", json!({})); ui.separator(); - ui.label(egui::RichText::new("Position Objects").font(theme::bold(12.0))); - ui.menu_button("Align", |ui| align_menu(app, ui)); - ui.menu_button("Rotate", |ui| { + ui.label(egui::RichText::new(crate::i18n::tr("Position Objects")).font(theme::bold(12.0))); + ui.menu_button(crate::i18n::tr("Align"), |ui| align_menu(app, ui)); + ui.menu_button(crate::i18n::tr("Rotate"), |ui| { menu_item(app, ui, Icon::RotateRight, "Rotate Right 90°", "arrange.rotateRight", json!({})); menu_item(app, ui, Icon::RotateLeft, "Rotate Left 90°", "arrange.rotateLeft", json!({})); menu_item(app, ui, Icon::FlipV, "Flip Vertical", "arrange.flipVertical", json!({})); menu_item(app, ui, Icon::FlipH, "Flip Horizontal", "arrange.flipHorizontal", json!({})); }); - if ui.button("Selection Pane…").clicked() { + if ui.button(crate::i18n::tr("Selection Pane…")).clicked() { let _ = app.run("view.pane", json!({"pane": "selection"})); ui.close(); } @@ -1008,7 +1019,7 @@ fn insert(app: &mut SlideApp, ui: &mut Ui) { ui.close(); } ui.separator(); - if ui.button("Insert Table…").clicked() { + if ui.button(crate::i18n::tr("Insert Table…")).clicked() { app.dialog = Some(crate::dialogs::Dialog::new("table")); ui.close(); } @@ -1017,7 +1028,7 @@ fn insert(app: &mut SlideApp, ui: &mut Ui) { group(ui, |ui| { big(app, ui, Icon::Picture, "Pictures", "app.insertPictureDialog", json!({})); let r = big_button(ui, Icon::Screenshot, "Screenshot", false); - let _ = r.on_hover_text("Screenshot (not available yet)"); + let _ = r.on_hover_text(crate::i18n::tr("Screenshot (not available yet)")); }); group(ui, |ui| { let r = big_button(ui, Icon::Shapes, "Shapes", enabled(app, "shape.insert")); @@ -1032,7 +1043,7 @@ fn insert(app: &mut SlideApp, ui: &mut Ui) { let r = big_button(ui, Icon::Chart, "Chart", enabled(app, "insert.chart")); egui::Popup::menu(&r).show(|ui| { for k in deckcraft_model::chart::ChartType::MAIN { - if ui.button(k.label()).clicked() { + if ui.button(crate::i18n::tr(k.label())).clicked() { run(app, "insert.chart", json!({"type": format!("{k:?}")})); } } @@ -1043,7 +1054,7 @@ fn insert(app: &mut SlideApp, ui: &mut Ui) { let r = big_button(ui, Icon::Action, "Action", enabled(app, "insert.actionButton")); egui::Popup::menu(&r).show(|ui| { for k in ["back", "forward", "beginning", "end", "home", "information", "return", "movie", "document", "sound", "help", "custom"] { - if ui.button(format!("Action Button: {k}")).clicked() { + if ui.button(crate::i18n::format("Action Button: {k}", &[(k).to_string()])).clicked() { run(app, "insert.actionButton", json!({"kind": k})); } } @@ -1058,7 +1069,7 @@ fn insert(app: &mut SlideApp, ui: &mut Ui) { let r = big_button(ui, Icon::WordArt, "WordArt", enabled(app, "insert.wordArt")); egui::Popup::menu(&r).show(|ui| { for i in 0..6 { - if ui.button(format!("WordArt style {}", i + 1)).clicked() { + if ui.button(crate::i18n::format("WordArt style {}", &[(i + 1).to_string()])).clicked() { run(app, "insert.wordArt", json!({"style": i})); } } @@ -1082,8 +1093,8 @@ pub fn table_grid_picker(ui: &mut Ui) -> Option<(usize, usize)> { let id = ui.id().with("tgrid"); let mut hover: (usize, usize) = ui.data_mut(|d| d.get_temp(id).unwrap_or((0, 0))); let cell = 16.0; - let label = if hover.0 > 0 { format!("{}x{} Table", hover.1, hover.0) } else { "Insert Table".into() }; - ui.label(label); + let label = if hover.0 > 0 { crate::i18n::format("{}x{} Table", &[(hover.1).to_string(), (hover.0).to_string()]) } else { "Insert Table".into() }; + ui.label(crate::i18n::tr(&label)); let (rect, resp) = ui.allocate_exact_size(vec2(10.0 * (cell + 2.0), 8.0 * (cell + 2.0)), Sense::click()); if let Some(p) = resp.hover_pos() { let c = ((p.x - rect.min.x) / (cell + 2.0)).floor() as usize + 1; @@ -1143,7 +1154,7 @@ fn icon_library(app: &mut SlideApp, ui: &mut Ui) { ui.painter().rect_filled(r, CornerRadius::same(4), t.hover); } paint_preset(ui.painter(), r.shrink(6.0), preset, t.text, t.text); - if resp.on_hover_text(label).clicked() { + if resp.on_hover_text(crate::i18n::tr(label)).clicked() { let size = app.session.active().map(|d| d.doc.slide_size).unwrap_or(deckcraft_model::defaults::WIDE); let _ = app.run("shape.insert", json!({"preset": preset, "rect": [size.width / 2.0 - 48.0, size.height / 2.0 - 48.0, 96, 96]})); let _ = app.run("shape.fill", json!({"color": "tx1"})); @@ -1186,11 +1197,11 @@ fn draw(app: &mut SlideApp, ui: &mut Ui) { }); group(ui, |ui| { let r = big_button(ui, Icon::InkToShape, "Ink to\nShape", false); - let _ = r.on_hover_text("Ink to Shape (coming soon)"); + let _ = r.on_hover_text(crate::i18n::tr("Ink to Shape (coming soon)")); let r = big_button(ui, Icon::InkToText, "Ink to\nText", false); - let _ = r.on_hover_text("Ink to Text (coming soon)"); + let _ = r.on_hover_text(crate::i18n::tr("Ink to Text (coming soon)")); let r = big_button(ui, Icon::InkToMath, "Ink to\nMath", false); - let _ = r.on_hover_text("Ink to Math (coming soon)"); + let _ = r.on_hover_text(crate::i18n::tr("Ink to Math (coming soon)")); }); } @@ -1267,7 +1278,7 @@ fn design(app: &mut SlideApp, ui: &mut Ui) { }; let (rect, resp) = ui.allocate_exact_size(vec2(48.0, 28.0), Sense::click()); ui.painter().rect(rect, CornerRadius::same(2), shade, Stroke::new(1.0, Tokens::get(ui.ctx()).border), egui::StrokeKind::Inside); - if resp.on_hover_text(format!("Style {i}")).clicked() { + if resp.on_hover_text(crate::i18n::format("Style {i}", &[(i).to_string()])).clicked() { run(app, "design.background", json!({"style": i, "all": true})); ui.close(); } @@ -1276,7 +1287,7 @@ fn design(app: &mut SlideApp, ui: &mut Ui) { } } }); - if ui.button("Reset Slide Background").clicked() { + if ui.button(crate::i18n::tr("Reset Slide Background")).clicked() { run(app, "design.background", json!({"reset": true})); } }); @@ -1284,13 +1295,13 @@ fn design(app: &mut SlideApp, ui: &mut Ui) { group(ui, |ui| { let r = big_button(ui, Icon::SlideSize, "Slide\nSize", true); egui::Popup::menu(&r).show(|ui| { - if ui.button("Standard (4:3)").clicked() { + if ui.button(crate::i18n::tr("Standard (4:3)")).clicked() { run(app, "design.slideSize", json!({"preset": "standard"})); } - if ui.button("Widescreen (16:9)").clicked() { + if ui.button(crate::i18n::tr("Widescreen (16:9)")).clicked() { run(app, "design.slideSize", json!({"preset": "widescreen"})); } - if ui.button("Page Setup…").clicked() { + if ui.button(crate::i18n::tr("Page Setup…")).clicked() { app.dialog = Some(crate::dialogs::Dialog::new("slideSize")); } }); @@ -1305,7 +1316,7 @@ pub fn paint_theme_tile(p: &egui::Painter, r: Rect, th: &deckcraft_model::Theme) let bg = theme::to_color32(th.colors.get(deckcraft_color::SchemeSlot::Lt1)); let fg = theme::to_color32(th.colors.get(deckcraft_color::SchemeSlot::Dk1)); p.rect(r, CornerRadius::same(2), bg, Stroke::new(1.0, Color32::from_gray(200)), egui::StrokeKind::Inside); - p.text(r.min + vec2(8.0, r.height() * 0.42), Align2::LEFT_CENTER, "Aa", theme::font(r.height() * 0.38), fg); + p.text(r.min + vec2(8.0, r.height() * 0.42), Align2::LEFT_CENTER, crate::i18n::tr("Aa"), theme::font(r.height() * 0.38), fg); let w = (r.width() - 16.0) / 6.0; for k in 0..6 { let c = theme::to_color32(th.colors.colors.get(4 + k).copied().unwrap_or(deckcraft_color::Rgba::BLACK)); @@ -1345,7 +1356,7 @@ fn transitions(app: &mut SlideApp, ui: &mut Ui) { let more = widgets::drop_button(ui, "", vec2(14.0, 52.0), true); egui::Popup::menu(&more).width(480.0).show(|ui| { for cat in ["Subtle", "Exciting", "Dynamic Content"] { - ui.label(egui::RichText::new(cat).font(theme::bold(12.0))); + ui.label(egui::RichText::new(crate::i18n::tr(cat)).font(theme::bold(12.0))); ui.horizontal_wrapped(|ui| { for x in list.iter().filter(|x| x.2 == cat) { let (r, resp) = ui.allocate_exact_size(vec2(56.0, 58.0), Sense::click()); @@ -1367,7 +1378,7 @@ fn transitions(app: &mut SlideApp, ui: &mut Ui) { let r = big_button(ui, Icon::EffectOptions, "Effect\nOptions", !opts.is_empty()); egui::Popup::menu(&r).show(|ui| { for o in opts { - if ui.button(option_label(o)).clicked() { + if ui.button(crate::i18n::tr(&option_label(o))).clicked() { run(app, "transition.options", json!({"option": o})); } } @@ -1376,7 +1387,7 @@ fn transitions(app: &mut SlideApp, ui: &mut Ui) { group(ui, |ui| { rows(ui, |ui| { ui.horizontal(|ui| { - ui.label("Duration:"); + ui.label(crate::i18n::tr("Duration:")); let d = cur.as_ref().map(|t| t.duration_ms as f64 / 1000.0).unwrap_or(1.0); if let Some(v) = widgets::spinner(ui, "tdur", d, 0.25, "", 46.0) { run(app, "transition.timing", json!({"duration": (v.max(0.0) * 1000.0).round() as u64})); @@ -1384,14 +1395,14 @@ fn transitions(app: &mut SlideApp, ui: &mut Ui) { }); ui.horizontal(|ui| { let mut click = cur.as_ref().is_none_or(|t| t.advance_on_click); - if ui.checkbox(&mut click, "On Mouse Click").changed() { + if ui.checkbox(&mut click, crate::i18n::tr("On Mouse Click")).changed() { run(app, "transition.timing", json!({"onClick": click})); } }); ui.horizontal(|ui| { let after = cur.as_ref().and_then(|t| t.advance_after_ms); let mut on = after.is_some(); - if ui.checkbox(&mut on, "After:").changed() { + if ui.checkbox(&mut on, crate::i18n::tr("After:")).changed() { run(app, "transition.timing", json!({"after": if on { json!(after.unwrap_or(0)) } else { Value::Null }})); } if let Some(v) = widgets::spinner(ui, "tafter", after.unwrap_or(0) as f64 / 1000.0, 1.0, "", 46.0) { @@ -1403,12 +1414,17 @@ fn transitions(app: &mut SlideApp, ui: &mut Ui) { }); } -fn option_label(o: &str) -> String { +pub(crate) fn option_label(o: &str) -> String { match o { "l" => "From Left".into(), "r" => "From Right".into(), "u" => "From Top".into(), - "d" => "From Bottom".into(), + "d" | "b" => "From Bottom".into(), + "t" => "From Top".into(), + "lb" | "ld" => "From Bottom Left".into(), + "rb" | "rd" => "From Bottom Right".into(), + "lt" | "lu" => "From Top Left".into(), + "rt" | "ru" => "From Top Right".into(), "horz" => "Horizontal".into(), "vert" => "Vertical".into(), "in" => "In".into(), @@ -1495,7 +1511,7 @@ pub fn paint_transition_tile(p: &egui::Painter, r: Rect, (id, label): (&str, &st } } p.rect_stroke(pic, CornerRadius::same(2), Stroke::new(1.0, Color32::from_rgb(0x9B, 0xB4, 0xDB)), egui::StrokeKind::Inside); - p.text(pos2(r.center().x, r.max.y - 7.0), Align2::CENTER_CENTER, label, theme::font(10.0), t.text); + p.text(pos2(r.center().x, r.max.y - 7.0), Align2::CENTER_CENTER, crate::i18n::tr(label), theme::font(10.0), t.text); } // ---------- Animations ---------- @@ -1560,7 +1576,7 @@ fn animations(app: &mut SlideApp, ui: &mut Ui) { for (cls, label) in [(AnimClass::Entrance, "Entrance"), (AnimClass::Emphasis, "Emphasis"), (AnimClass::Exit, "Exit"), (AnimClass::Path, "Motion Paths")] { - ui.label(egui::RichText::new(label).font(theme::bold(12.0))); + ui.label(egui::RichText::new(crate::i18n::tr(label)).font(theme::bold(12.0))); ui.horizontal_wrapped(|ui| { for a in deckcraft_model::anim::ANIMATIONS.iter().filter(|a| a.2 == cls) { let (r, resp) = ui.allocate_exact_size(vec2(56.0, 54.0), Sense::click()); @@ -1580,13 +1596,13 @@ fn animations(app: &mut SlideApp, ui: &mut Ui) { let r = big_button(ui, Icon::EffectOptions, "Effect\nOptions", !opts.is_empty()); egui::Popup::menu(&r).show(|ui| { for o in opts { - if ui.button(option_label(o)).clicked() { + if ui.button(crate::i18n::tr(&option_label(o))).clicked() { run(app, "animation.options", json!({"option": o})); } } ui.separator(); for (l, b) in [("As One Object", "asOne"), ("By Paragraph", "byParagraph")] { - if ui.button(l).clicked() { + if ui.button(crate::i18n::tr(l)).clicked() { run(app, "animation.options", json!({"textBuild": b})); } } @@ -1616,13 +1632,13 @@ fn animations(app: &mut SlideApp, ui: &mut Ui) { big(app, ui, Icon::AnimationPane, "Animation\nPane", "view.pane", json!({"pane": "animation"})); let r = big_button(ui, Icon::Trigger, "Trigger", cur.is_some()); egui::Popup::menu(&r).show(|ui| { - if ui.button("On Click of… (none)").clicked() { + if ui.button(crate::i18n::tr("On Click of… (none)")).clicked() { run(app, "animation.timing", json!({"trigger": null})); } let shapes: Vec<(u32, String)> = app.session.active().map(|d| d.shapes().iter().map(|s| (s.id.0, s.name.clone())).collect()).unwrap_or_default(); for (id, name) in shapes { - if ui.button(format!("On Click of {name}")).clicked() { + if ui.button(crate::i18n::format("On Click of {name}", std::slice::from_ref(&name))).clicked() { run(app, "animation.timing", json!({"trigger": id})); } } @@ -1644,15 +1660,15 @@ fn animations(app: &mut SlideApp, ui: &mut Ui) { group(ui, |ui| { rows(ui, |ui| { ui.horizontal(|ui| { - ui.label("Start:"); + ui.label(crate::i18n::tr("Start:")); let s = cur.as_ref().map(|c| match c.start { deckcraft_model::AnimStart::OnClick => "On Click", deckcraft_model::AnimStart::WithPrevious => "With Previous", deckcraft_model::AnimStart::AfterPrevious => "After Previous", }); - dropdown(ui, "astart", s.unwrap_or(""), 112.0, cur.is_some(), |ui| { + dropdown(ui, "astart", crate::i18n::tr(s.unwrap_or("")), 112.0, cur.is_some(), |ui| { for (l, v) in [("On Click", "onClick"), ("With Previous", "withPrevious"), ("After Previous", "afterPrevious")] { - if ui.button(l).clicked() { + if ui.button(crate::i18n::tr(l)).clicked() { run(app, "animation.timing", json!({"start": v})); ui.close(); } @@ -1660,13 +1676,13 @@ fn animations(app: &mut SlideApp, ui: &mut Ui) { }); }); ui.horizontal(|ui| { - ui.label("Duration:"); + ui.label(crate::i18n::tr("Duration:")); if let Some(v) = widgets::spinner(ui, "adur", cur.as_ref().map(|c| c.duration_ms as f64 / 1000.0).unwrap_or(0.5), 0.25, "", 46.0) { run(app, "animation.timing", json!({"duration": (v.max(0.0) * 1000.0).round() as u64})); } }); ui.horizontal(|ui| { - ui.label("Delay:"); + ui.label(crate::i18n::tr("Delay:")); if let Some(v) = widgets::spinner(ui, "adelay", cur.as_ref().map(|c| c.delay_ms as f64 / 1000.0).unwrap_or(0.0), 0.25, "", 46.0) { run(app, "animation.timing", json!({"delay": (v.max(0.0) * 1000.0).round() as u64})); } @@ -1684,7 +1700,13 @@ pub fn paint_anim_tile(p: &egui::Painter, r: Rect, label: &str, class: AnimClass _ => Icon::AnimPath, }; icons::paint(p, Rect::from_center_size(pos2(r.center().x, r.min.y + 18.0), vec2(28.0, 28.0)), icon, t.text, !enabled); - p.text(pos2(r.center().x, r.max.y - 8.0), Align2::CENTER_CENTER, label, theme::font(10.0), if enabled { t.text } else { t.text_faint }); + p.text( + pos2(r.center().x, r.max.y - 8.0), + Align2::CENTER_CENTER, + crate::i18n::tr(label), + theme::font(10.0), + if enabled { t.text } else { t.text_faint }, + ); } // ---------- Slide Show / Record / Review / View ---------- @@ -1719,20 +1741,20 @@ fn slide_show(app: &mut SlideApp, ui: &mut Ui) { } } let r = big_button(ui, Icon::Record, "Record", false); - let _ = r.on_hover_text("Recording narration needs audio capture (coming soon)"); + let _ = r.on_hover_text(crate::i18n::tr("Recording narration needs audio capture (coming soon)")); }); group(ui, |ui| { rows(ui, |ui| { let mut use_t = app.session.active().is_none_or(|d| d.doc.show.use_timings); - if ui.checkbox(&mut use_t, "Use Timings").changed() { + if ui.checkbox(&mut use_t, crate::i18n::tr("Use Timings")).changed() { run(app, "show.setup", json!({"useTimings": use_t})); } let mut narr = app.session.active().is_none_or(|d| !d.doc.show.without_narration); - if ui.checkbox(&mut narr, "Play Narrations").changed() { + if ui.checkbox(&mut narr, crate::i18n::tr("Play Narrations")).changed() { run(app, "show.setup", json!({"noNarration": !narr})); } let mut mc = app.session.active().is_none_or(|d| d.doc.show.show_media_controls); - let _ = ui.checkbox(&mut mc, "Show Media Controls"); + let _ = ui.checkbox(&mut mc, crate::i18n::tr("Show Media Controls")); }); }); } @@ -1740,7 +1762,7 @@ fn slide_show(app: &mut SlideApp, ui: &mut Ui) { fn record(app: &mut SlideApp, ui: &mut Ui) { group(ui, |ui| { let r = big_button(ui, Icon::Cameo, "Cameo", false); - let _ = r.on_hover_text("Camera capture is not available yet"); + let _ = r.on_hover_text(crate::i18n::tr("Camera capture is not available yet")); if big_button(ui, Icon::PlayFromStart, "From\nBeginning", true).clicked() { app.start_show(0, false); if let Some(s) = app.show.as_mut() { @@ -1766,14 +1788,14 @@ fn review(app: &mut SlideApp, ui: &mut Ui) { group(ui, |ui| { big(app, ui, Icon::Spelling, "Spelling", "app.dialog", json!({"id": "spelling"})); let r = big_button(ui, Icon::Thesaurus, "Thesaurus", false); - let _ = r.on_hover_text("Thesaurus (coming soon)"); + let _ = r.on_hover_text(crate::i18n::tr("Thesaurus (coming soon)")); }); group(ui, |ui| { big(app, ui, Icon::Accessibility, "Check\nAccessibility", "app.dialog", json!({"id": "accessibility"})); }); group(ui, |ui| { let r = big_button(ui, Icon::Translate, "Translate", false); - let _ = r.on_hover_text("Translation needs a language model (not bundled)"); + let _ = r.on_hover_text(crate::i18n::tr("Translation needs a language model (not bundled)")); big(app, ui, Icon::Language, "Language", "app.dialog", json!({"id": "language"})); }); group(ui, |ui| { @@ -1808,22 +1830,22 @@ fn view(app: &mut SlideApp, ui: &mut Ui) { let _ = app.run(if in_master { "view.closeMaster" } else { "view.slideMaster" }, json!({})); } let r = big_button(ui, Icon::HandoutMaster, "Handout\nMaster", false); - let _ = r.on_hover_text("Handout Master (coming soon)"); + let _ = r.on_hover_text(crate::i18n::tr("Handout Master (coming soon)")); let r = big_button(ui, Icon::NotesMaster, "Notes\nMaster", false); - let _ = r.on_hover_text("Notes Master (coming soon)"); + let _ = r.on_hover_text(crate::i18n::tr("Notes Master (coming soon)")); }); group(ui, |ui| { rows(ui, |ui| { let mut r = app.ui.ruler; - if ui.checkbox(&mut r, "Ruler").changed() { + if ui.checkbox(&mut r, crate::i18n::tr("Ruler")).changed() { app.ui.ruler = r; } let mut g = app.ui.gridlines; - if ui.checkbox(&mut g, "Gridlines").changed() { + if ui.checkbox(&mut g, crate::i18n::tr("Gridlines")).changed() { app.ui.gridlines = g; } let mut gd = app.ui.guides; - if ui.checkbox(&mut gd, "Guides").changed() { + if ui.checkbox(&mut gd, crate::i18n::tr("Guides")).changed() { app.ui.guides = gd; } }); @@ -1872,16 +1894,16 @@ fn shape_format(app: &mut SlideApp, ui: &mut Ui) { big(app, ui, Icon::TextBox, "Text\nBox", "insert.textBox", json!({})); let r = big_button(ui, Icon::EditShape, "Edit\nShape", sel); egui::Popup::menu(&r).show(|ui| { - ui.label("Change Shape"); + ui.label(crate::i18n::tr("Change Shape")); if let Some(p) = shapes_gallery(ui) { run(app, "shape.change", json!({"preset": p})); } }); let can_merge = enabled(app, "shape.merge"); - let r = big_button(ui, Icon::MergeShapes, "Merge\nShapes", can_merge).on_hover_text("Merge Shapes"); + let r = big_button(ui, Icon::MergeShapes, "Merge\nShapes", can_merge).on_hover_text(crate::i18n::tr("Merge Shapes")); egui::Popup::menu(&r).show(|ui| { for (op, label) in deckcraft_engine::cmd::merge::OPS { - if ui.button(label).clicked() { + if ui.button(crate::i18n::tr(label)).clicked() { run(app, "shape.merge", json!({"op": op})); ui.close(); } @@ -1904,7 +1926,7 @@ fn shape_format(app: &mut SlideApp, ui: &mut Ui) { None => run(app, "shape.fill", json!({"none": true})), } } - if ui.button("Gradient…").clicked() { + if ui.button(crate::i18n::tr("Gradient…")).clicked() { let _ = app.run("view.pane", json!({"pane": "format", "tab": "fill"})); ui.close(); } @@ -1940,7 +1962,7 @@ fn shape_format(app: &mut SlideApp, ui: &mut Ui) { } let r = small_button(ui, Icon::TextEffects, "Text Effects", sel, false); egui::Popup::menu(&r).show(|ui| { - if ui.button("Shadow").clicked() { + if ui.button(crate::i18n::tr("Shadow")).clicked() { run(app, "format.textShadow", json!({})); } }); @@ -2012,18 +2034,18 @@ fn size_group(app: &mut SlideApp, ui: &mut Ui) { group(ui, |ui| { rows(ui, |ui| { let Some(x) = bx else { - ui.label("Height:"); - ui.label("Width:"); + ui.label(crate::i18n::tr("Height:")); + ui.label(crate::i18n::tr("Width:")); return; }; ui.horizontal(|ui| { - ui.label("Height:"); + ui.label(crate::i18n::tr("Height:")); if let Some(v) = widgets::spinner(ui, "sh", x.h / 72.0, 0.1, "\"", 54.0) { run(app, "shape.resize", json!({"h": v.max(0.0) * 72.0})); } }); ui.horizontal(|ui| { - ui.label("Width: "); + ui.label(crate::i18n::tr("Width: ")); if let Some(v) = widgets::spinner(ui, "sw", x.w / 72.0, 0.1, "\"", 54.0) { run(app, "shape.resize", json!({"w": v.max(0.0) * 72.0})); } @@ -2035,17 +2057,17 @@ fn size_group(app: &mut SlideApp, ui: &mut Ui) { fn picture_format(app: &mut SlideApp, ui: &mut Ui) { group(ui, |ui| { let r = big_button(ui, Icon::RemoveBackground, "Remove\nBackground", false); - let _ = r.on_hover_text("Remove Background (coming soon)"); + let _ = r.on_hover_text(crate::i18n::tr("Remove Background (coming soon)")); let r = big_button(ui, Icon::Corrections, "Corrections", true); egui::Popup::menu(&r).show(|ui| { for b in [-0.4, -0.2, 0.0, 0.2, 0.4] { - if ui.button(format!("Brightness {:+}%", (b * 100.0) as i32)).clicked() { + if ui.button(crate::i18n::format("Brightness {:+}%", &[format!("{:+}", (b * 100.0) as i32)])).clicked() { run(app, "picture.adjust", json!({"brightness": b})); } } ui.separator(); for c in [-0.4, -0.2, 0.0, 0.2, 0.4] { - if ui.button(format!("Contrast {:+}%", (c * 100.0) as i32)).clicked() { + if ui.button(crate::i18n::format("Contrast {:+}%", &[format!("{:+}", (c * 100.0) as i32)])).clicked() { run(app, "picture.adjust", json!({"contrast": c})); } } @@ -2053,11 +2075,11 @@ fn picture_format(app: &mut SlideApp, ui: &mut Ui) { let r = big_button(ui, Icon::ColorAdjust, "Color", true); egui::Popup::menu(&r).show(|ui| { for s in [0.0, 0.33, 0.66, 1.0, 1.5, 2.0, 3.0] { - if ui.button(format!("Saturation {}%", (s * 100.0) as i32)).clicked() { + if ui.button(crate::i18n::format("Saturation {}%", &[((s * 100.0) as i32).to_string()])).clicked() { run(app, "picture.adjust", json!({"saturation": s})); } } - if ui.button("Grayscale").clicked() { + if ui.button(crate::i18n::tr("Grayscale")).clicked() { run(app, "picture.adjust", json!({"grayscale": true})); } }); @@ -2077,24 +2099,24 @@ fn picture_format(app: &mut SlideApp, ui: &mut Ui) { group(ui, |ui| { let r = big_button(ui, Icon::QuickStyles, "Picture\nStyles", true); egui::Popup::menu(&r).show(|ui| { - if ui.button("Simple Frame, White").clicked() { + if ui.button(crate::i18n::tr("Simple Frame, White")).clicked() { run(app, "shape.line", json!({"color": "#FFFFFF", "width": 6})); run(app, "shape.effects", json!({"shadow": "outer"})); } - if ui.button("Drop Shadow Rectangle").clicked() { + if ui.button(crate::i18n::tr("Drop Shadow Rectangle")).clicked() { run(app, "shape.effects", json!({"shadow": "bottom"})); } - if ui.button("Rounded Diagonal Corner").clicked() { + if ui.button(crate::i18n::tr("Rounded Diagonal Corner")).clicked() { run(app, "shape.change", json!({"preset": "round2DiagRect"})); } - if ui.button("Soft Edge Rectangle").clicked() { + if ui.button(crate::i18n::tr("Soft Edge Rectangle")).clicked() { run(app, "shape.effects", json!({"softEdges": 10})); } - if ui.button("Reflected Rounded Rectangle").clicked() { + if ui.button(crate::i18n::tr("Reflected Rounded Rectangle")).clicked() { run(app, "shape.change", json!({"preset": "roundRect"})); run(app, "shape.effects", json!({"reflection": "half"})); } - if ui.button("Oval").clicked() { + if ui.button(crate::i18n::tr("Oval")).clicked() { run(app, "shape.change", json!({"preset": "ellipse"})); } }); @@ -2104,13 +2126,13 @@ fn picture_format(app: &mut SlideApp, ui: &mut Ui) { group(ui, |ui| { let r = big_button(ui, Icon::Crop, "Crop", true); egui::Popup::menu(&r).show(|ui| { - ui.label("Crop to Shape"); + ui.label(crate::i18n::tr("Crop to Shape")); if let Some(p) = shapes_gallery(ui) { run(app, "shape.change", json!({"preset": p})); } ui.separator(); for (l, v) in [("Crop 10% each side", 0.1), ("Crop 20% each side", 0.2), ("Reset Crop", 0.0)] { - if ui.button(l).clicked() { + if ui.button(crate::i18n::tr(l)).clicked() { run(app, "picture.crop", json!({"left": v, "top": v, "right": v, "bottom": v})); } } @@ -2129,31 +2151,31 @@ fn table_design(app: &mut SlideApp, ui: &mut Ui) { let flags = tbl.as_ref().map(|t| (t.first_row, t.last_row, t.band_row, t.first_col, t.last_col, t.band_col)).unwrap_or_default(); ui.horizontal(|ui| { let mut a = flags.0; - if ui.checkbox(&mut a, "Header Row").changed() { + if ui.checkbox(&mut a, crate::i18n::tr("Header Row")).changed() { run(app, "table.options", json!({"headerRow": a})); } let mut d = flags.3; - if ui.checkbox(&mut d, "First Column").changed() { + if ui.checkbox(&mut d, crate::i18n::tr("First Column")).changed() { run(app, "table.options", json!({"firstColumn": d})); } }); ui.horizontal(|ui| { let mut b = flags.1; - if ui.checkbox(&mut b, "Total Row").changed() { + if ui.checkbox(&mut b, crate::i18n::tr("Total Row")).changed() { run(app, "table.options", json!({"totalRow": b})); } let mut e = flags.4; - if ui.checkbox(&mut e, "Last Column").changed() { + if ui.checkbox(&mut e, crate::i18n::tr("Last Column")).changed() { run(app, "table.options", json!({"lastColumn": e})); } }); ui.horizontal(|ui| { let mut c = flags.2; - if ui.checkbox(&mut c, "Banded Rows").changed() { + if ui.checkbox(&mut c, crate::i18n::tr("Banded Rows")).changed() { run(app, "table.options", json!({"bandedRows": c})); } let mut f = flags.5; - if ui.checkbox(&mut f, "Banded Columns").changed() { + if ui.checkbox(&mut f, crate::i18n::tr("Banded Columns")).changed() { run(app, "table.options", json!({"bandedColumns": f})); } }); @@ -2186,7 +2208,7 @@ fn table_design(app: &mut SlideApp, ui: &mut Ui) { for (id, label) in &styles { let (r, resp) = ui.allocate_exact_size(vec2(48.0, 36.0), Sense::click()); paint_table_style(ui.painter(), r.shrink(2.0), id, &sc); - if resp.on_hover_text(label).clicked() { + if resp.on_hover_text(crate::i18n::tr(label)).clicked() { run(app, "table.style", json!({"style": id})); ui.close(); } @@ -2275,7 +2297,7 @@ fn table_layout(app: &mut SlideApp, ui: &mut Ui) { }); ui.horizontal(|ui| { for (l, a) in [("Top", "top"), ("Center", "middle"), ("Bottom", "bottom")] { - if ui.small_button(l).clicked() { + if ui.small_button(crate::i18n::tr(l)).clicked() { run(app, "format.anchor", json!({"anchor": a})); } } @@ -2290,7 +2312,7 @@ fn chart_design(app: &mut SlideApp, ui: &mut Ui) { let r = big_button(ui, Icon::ChangeChartType, "Change\nChart Type", true); egui::Popup::menu(&r).show(|ui| { for k in deckcraft_model::chart::ChartType::MAIN { - if ui.button(k.label()).clicked() { + if ui.button(crate::i18n::tr(k.label())).clicked() { // Replace the chart's type keeping its data. if let Some(id) = app.session.active().and_then(|d| d.selection.shapes.first().copied()) { let _ = app.run("chart.type", json!({"id": id.0, "type": format!("{k:?}")})); @@ -2318,29 +2340,29 @@ fn playback(app: &mut SlideApp, ui: &mut Ui) { group(ui, |ui| { rows(ui, |ui| { let mut auto = m.as_ref().is_some_and(|x| x.autoplay); - if ui.checkbox(&mut auto, "Start Automatically").changed() { + if ui.checkbox(&mut auto, crate::i18n::tr("Start Automatically")).changed() { run(app, "media.options", json!({"autoplay": auto})); } let mut across = m.as_ref().is_some_and(|x| x.play_across_slides); - if ui.checkbox(&mut across, "Play Across Slides").changed() { + if ui.checkbox(&mut across, crate::i18n::tr("Play Across Slides")).changed() { run(app, "media.options", json!({"acrossSlides": across})); } let mut lp = m.as_ref().is_some_and(|x| x.loop_play); - if ui.checkbox(&mut lp, "Loop until Stopped").changed() { + if ui.checkbox(&mut lp, crate::i18n::tr("Loop until Stopped")).changed() { run(app, "media.options", json!({"loop": lp})); } }); rows(ui, |ui| { let mut hide = m.as_ref().is_some_and(|x| x.hide_while_not_playing); - if ui.checkbox(&mut hide, "Hide During Show").changed() { + if ui.checkbox(&mut hide, crate::i18n::tr("Hide During Show")).changed() { run(app, "media.options", json!({"hide": hide})); } let mut rw = m.as_ref().is_some_and(|x| x.rewind); - if ui.checkbox(&mut rw, "Rewind after Playing").changed() { + if ui.checkbox(&mut rw, crate::i18n::tr("Rewind after Playing")).changed() { run(app, "media.options", json!({"rewind": rw})); } let mut fs = m.as_ref().is_some_and(|x| x.full_screen); - if ui.checkbox(&mut fs, "Play Full Screen").changed() { + if ui.checkbox(&mut fs, crate::i18n::tr("Play Full Screen")).changed() { run(app, "media.options", json!({"fullScreen": fs})); } }); @@ -2349,7 +2371,7 @@ fn playback(app: &mut SlideApp, ui: &mut Ui) { let r = big_button(ui, Icon::Volume, "Volume", m.is_some()); egui::Popup::menu(&r).show(|ui| { for (l, v) in [("Low", 0.25), ("Medium", 0.5), ("High", 1.0), ("Mute", 0.0)] { - if ui.button(l).clicked() { + if ui.button(crate::i18n::tr(l)).clicked() { run(app, "media.options", json!({"volume": v})); } } @@ -2369,7 +2391,7 @@ fn slide_master(app: &mut SlideApp, ui: &mut Ui) { for (l, k) in [("Content", "content"), ("Text", "text"), ("Picture", "picture"), ("Chart", "chart"), ("Table", "table"), ("Media", "media")] { - if ui.button(l).clicked() { + if ui.button(crate::i18n::tr(l)).clicked() { run(app, "master.insertPlaceholder", json!({"kind": k})); } } diff --git a/crates/ui-egui/src/show.rs b/crates/ui-egui/src/show.rs index 1c8e6e1eb..302b92569 100644 --- a/crates/ui-egui/src/show.rs +++ b/crates/ui-egui/src/show.rs @@ -365,7 +365,7 @@ pub fn ui(app: &mut SlideApp, ui: &mut Ui) { painter.text( pos2(full.center().x, full.min.y + 40.0), Align2::CENTER_CENTER, - "End of slide show, click to exit.", + crate::i18n::tr("End of slide show, click to exit."), theme::font(18.0), Color32::from_gray(200), ); @@ -555,7 +555,7 @@ pub fn ui(app: &mut SlideApp, ui: &mut Ui) { painter.text( pos2(bar.min.x + 14.0, bar.center().y), Align2::LEFT_CENTER, - format!("Slide {} of {}", show.state.slide + 1, doc.slides.len()), + crate::i18n::format("Slide {} of {}", &[(show.state.slide + 1).to_string(), (doc.slides.len()).to_string()]), theme::font(12.0), Color32::from_gray(200), ); @@ -564,7 +564,7 @@ pub fn ui(app: &mut SlideApp, ui: &mut Ui) { .max_rect(Rect::from_min_max(pos2(bar.max.x - 260.0, bar.min.y), bar.max)) .layout(egui::Layout::right_to_left(egui::Align::Center)), ); - if child.button("Close").clicked() { + if child.button(crate::i18n::tr("Close")).clicked() { keep = false; } if child.button("▶").clicked() { @@ -612,7 +612,15 @@ pub fn ui(app: &mut SlideApp, ui: &mut Ui) { painter.text( r.center(), Align2::CENTER_CENTER, - format!("Slide {:02}:{:04.1} Total {:02}:{:02}", (t / 60.0) as u32, t % 60.0, (total / 60.0) as u32, (total % 60.0) as u32), + crate::i18n::format( + "Slide {:02}:{:04.1} Total {:02}:{:02}", + &[ + format!("{:02}", (t / 60.0) as u32), + format!("{:04.1}", t % 60.0), + format!("{:02}", (total / 60.0) as u32), + format!("{:02}", (total % 60.0) as u32), + ], + ), theme::font(13.0), Color32::WHITE, ); @@ -750,7 +758,7 @@ fn presenter_window(ctx: &egui::Context, show: &mut Show, doc: &Presentation, no let mut actions = vec![]; ctx.show_viewport_immediate( egui::ViewportId::from_hash_of("deckcraft_presenter"), - egui::ViewportBuilder::default().with_title("Presenter View — DeckCraft").with_inner_size([1100.0, 700.0]), + egui::ViewportBuilder::default().with_title(crate::i18n::tr("Presenter View — DeckCraft")).with_inner_size([1100.0, 700.0]), |ui, _| { egui::CentralPanel::default().frame(egui::Frame::NONE.fill(Color32::from_gray(24)).inner_margin(egui::Margin::same(16))).show(ui, |ui| { ui.horizontal(|ui| { @@ -765,15 +773,19 @@ fn presenter_window(ctx: &egui::Context, show: &mut Show, doc: &Presentation, no .color(Color32::WHITE), ); ui.add_space(20.0); - ui.label(egui::RichText::new(format!("Slide {} of {}", cur + 1, doc.slides.len())).size(16.0).color(Color32::from_gray(200))); + ui.label( + egui::RichText::new(crate::i18n::format("Slide {} of {}", &[(cur + 1).to_string(), (doc.slides.len()).to_string()])) + .size(16.0) + .color(Color32::from_gray(200)), + ); ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - if ui.button("End Show").clicked() { + if ui.button(crate::i18n::tr("End Show")).clicked() { *keep = false; } - if ui.button("Next ▶").clicked() { + if ui.button(crate::i18n::tr("Next ▶")).clicked() { actions.push(show.state.next(doc)); } - if ui.button("◀ Previous").clicked() { + if ui.button(crate::i18n::tr("◀ Previous")).clicked() { actions.push(show.state.prev(doc)); } }); @@ -791,14 +803,20 @@ fn presenter_window(ctx: &egui::Context, show: &mut Show, doc: &Presentation, no right.min + vec2(0.0, 22.0), vec2(right.width(), right.width() * (doc.slide_size.height / doc.slide_size.width.max(1.0)) as f32), ); - ui.painter().text(right.min, Align2::LEFT_TOP, "Next", theme::font(13.0), Color32::from_gray(170)); + ui.painter().text(right.min, Align2::LEFT_TOP, crate::i18n::tr("Next"), theme::font(13.0), Color32::from_gray(170)); match &next_tex { Some(t) => { ui.painter().image(t.id(), nr, Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)), Color32::WHITE); } None => { ui.painter().rect_filled(nr, CornerRadius::ZERO, Color32::from_gray(40)); - ui.painter().text(nr.center(), Align2::CENTER_CENTER, "End of slide show", theme::font(13.0), Color32::from_gray(170)); + ui.painter().text( + nr.center(), + Align2::CENTER_CENTER, + crate::i18n::tr("End of slide show"), + theme::font(13.0), + Color32::from_gray(170), + ); } } let notes_rect = Rect::from_min_max(pos2(right.min.x, nr.max.y + 16.0), right.max); diff --git a/crates/ui-egui/src/sorter.rs b/crates/ui-egui/src/sorter.rs index dd26dd851..0d5c975ff 100644 --- a/crates/ui-egui/src/sorter.rs +++ b/crates/ui-egui/src/sorter.rs @@ -52,7 +52,7 @@ pub fn show(app: &mut SlideApp, ui: &mut Ui) { Stroke::new(if selected { 3.0 } else { 1.0 }, if selected { t.accent } else { t.border }), egui::StrokeKind::Outside, ); - p.text(pos2(img.min.x, img.max.y + 12.0), Align2::LEFT_CENTER, format!("{}", i + 1), theme::font(12.0), t.text_dim); + p.text(pos2(img.min.x, img.max.y + 12.0), Align2::LEFT_CENTER, (i + 1).to_string(), theme::font(12.0), t.text_dim); if let Some(tr) = doc.slides.get(i).and_then(|s| s.transition.as_ref()).filter(|t| t.kind != "none") { p.text(pos2(img.max.x, img.max.y + 12.0), Align2::RIGHT_CENTER, format!("★ {}", tr.kind), theme::font(10.5), t.text_faint); } @@ -137,7 +137,7 @@ pub fn notes_page(app: &mut SlideApp, ui: &mut Ui) { let mut buf: String = child.data_mut(|d| d.get_temp(id).unwrap_or_else(|| current.clone())); let r = child.add( egui::TextEdit::multiline(&mut buf) - .hint_text("Click to add text") + .hint_text(crate::i18n::tr("Click to add text")) .desired_width(notes_rect.width()) .desired_rows(12) .frame(egui::Frame::NONE) @@ -170,7 +170,7 @@ pub fn outline_pane(app: &mut SlideApp, ui: &mut Ui) { for (i, s) in doc.slides.iter().enumerate() { let title_shape = s.shapes.iter().find(|x| x.ph_type().is_some_and(|k| k.is_title())).map(|x| x.id.0); ui.horizontal(|ui| { - ui.label(egui::RichText::new(format!("{}", i + 1)).color(t.text_dim)); + ui.label(egui::RichText::new((i + 1).to_string()).color(t.text_dim)); let (r, _) = ui.allocate_exact_size(vec2(14.0, 10.0), Sense::hover()); ui.painter().rect_stroke( r, diff --git a/crates/ui-egui/src/status.rs b/crates/ui-egui/src/status.rs index a6f4eb11d..262af45f0 100644 --- a/crates/ui-egui/src/status.rs +++ b/crates/ui-egui/src/status.rs @@ -15,12 +15,20 @@ pub fn status_bar(app: &mut SlideApp, ui: &mut Ui) { let Some(st) = app.session.active() else { return }; let n = st.doc.slides.len(); let left = match st.selection.target { - deckcraft_engine::Target::Slides => format!("Slide {} of {}", (st.selection.slide + 1).min(n.max(1)), n), - _ => "Slide Master".to_string(), + deckcraft_engine::Target::Slides => { + crate::i18n::format("Slide {} of {}", &[((st.selection.slide + 1).min(n.max(1))).to_string(), (n).to_string()]) + } + _ => crate::i18n::tr("Slide Master").to_string(), }; let p = ui.painter(); p.text(pos2(rect.min.x + 14.0, rect.center().y), Align2::LEFT_CENTER, &left, theme::font(12.0), t.text_dim); - p.text(pos2(rect.min.x + 120.0, rect.center().y), Align2::LEFT_CENTER, "English (United States)", theme::font(12.0), t.text_dim); + p.text( + pos2(rect.min.x + 120.0, rect.center().y), + Align2::LEFT_CENTER, + crate::i18n::tr("English (United States)"), + theme::font(12.0), + t.text_dim, + ); // Transient status message. if let Some((msg, at)) = app.status.clone() { if crate::now_ms() - at < 6000.0 { @@ -93,10 +101,11 @@ fn small_icon(ui: &mut Ui, icon: Icon, tip: &str, on: bool) -> egui::Response { ui.painter().rect_filled(r, CornerRadius::same(4), if on { t.selected } else { t.hover }); } icons::paint(ui.painter(), Rect::from_center_size(r.center(), vec2(16.0, 16.0)), icon, t.text_dim, false); - resp.on_hover_text(tip) + resp.on_hover_text(crate::i18n::tr(tip)) } fn labeled(ui: &mut Ui, icon: Icon, label: &str, on: bool) -> egui::Response { + let label = crate::i18n::tr(label); let t = Tokens::get(ui.ctx()); let w = 22.0 + ui.fonts_mut(|f| f.layout_no_wrap(label.into(), theme::font(12.0), t.text).size().x) + 8.0; let (r, resp) = ui.allocate_exact_size(vec2(w, 20.0), Sense::click()); @@ -104,7 +113,7 @@ fn labeled(ui: &mut Ui, icon: Icon, label: &str, on: bool) -> egui::Response { ui.painter().rect_filled(r, CornerRadius::same(4), if on { t.selected } else { t.hover }); } icons::paint(ui.painter(), Rect::from_center_size(pos2(r.min.x + 11.0, r.center().y), vec2(15.0, 15.0)), icon, t.text_dim, false); - ui.painter().text(pos2(r.min.x + 22.0, r.center().y), Align2::LEFT_CENTER, label, theme::font(12.0), t.text_dim); + ui.painter().text(pos2(r.min.x + 22.0, r.center().y), Align2::LEFT_CENTER, crate::i18n::tr(label), theme::font(12.0), t.text_dim); resp } @@ -141,7 +150,7 @@ pub fn notes_pane(app: &mut SlideApp, ui: &mut Ui) { let r = ui.add( egui::TextEdit::multiline(&mut buf) .id(id) - .hint_text("Click to add notes") + .hint_text(crate::i18n::tr("Click to add notes")) .frame(egui::Frame::NONE) .desired_width(f32::INFINITY) .desired_rows(2) diff --git a/crates/ui-egui/src/textures.rs b/crates/ui-egui/src/textures.rs index 84cf27554..f90cd2b5f 100644 --- a/crates/ui-egui/src/textures.rs +++ b/crates/ui-egui/src/textures.rs @@ -14,6 +14,7 @@ struct Key { master: usize, size: (u32, u32), edit: bool, + language: crate::i18n::Language, extra: u64, } @@ -79,6 +80,7 @@ impl Textures { master: master.as_ref().map(|m| Arc::as_ptr(m) as usize).unwrap_or(0), size, edit, + language: if edit { crate::i18n::current() } else { crate::i18n::Language::En }, extra: extra ^ grayscale as u64, }; if let Some(e) = &self.canvas @@ -88,7 +90,15 @@ impl Textures { } let t0 = crate::now_ms(); let scale = size.0 as f64 / p.slide_size.width.max(1.0); - let opts = RenderOpts { scale, edit, gif_times, threads: threads(), size: Some(size), ..Default::default() }; + let opts = RenderOpts { + scale, + edit, + gif_times, + placeholder_prompt: Some(crate::i18n::placeholder_prompt), + threads: threads(), + size: Some(size), + ..Default::default() + }; let img = deckcraft_render::render_slide(p, index, &opts); self.last_render_ms = crate::now_ms() - t0; let ci = to_color_image(&img, grayscale); @@ -120,6 +130,7 @@ impl Textures { master: master.as_ref().map(|m| Arc::as_ptr(m) as usize).unwrap_or(0), size: (width_px, h), edit: false, + language: crate::i18n::Language::En, extra: 0, }; self.clock += 1; @@ -150,3 +161,48 @@ impl Textures { Some(tex) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn switching_interface_language_refreshes_cached_hints_and_preserves_user_content() { + fn updated_canvas(ctx: &egui::Context, textures: &mut Textures, p: &Presentation, language: crate::i18n::Language) -> egui::ImageData { + crate::i18n::set_current(language); + let tex = textures.slide(ctx, p, 0, (480, 270), true, 0, false, &[]).unwrap(); + let mut delta = ctx.tex_manager().write().take_delta(); + let image = delta.set.get(&tex.id()).and_then(|images| images.last()).map(|image| image.image.clone()); + delta.clear(); + image.expect("the changed interface language must update the canvas texture") + } + + let ctx = egui::Context::default(); + let mut textures = Textures::default(); + let mut p = Presentation::default(); + let before = p.clone(); + let english = updated_canvas(&ctx, &mut textures, &p, crate::i18n::Language::En); + let ukrainian = updated_canvas(&ctx, &mut textures, &p, crate::i18n::Language::Uk); + assert!(english != ukrainian, "the default placeholder hints must change visibly"); + assert_eq!(p, before); + + for master in &mut p.masters { + for layout in &mut Arc::make_mut(master).layouts { + for shape in &mut layout.shapes { + if let Some(ph) = &mut shape.ph { + ph.has_custom_prompt = true; + shape.text = Some(deckcraft_model::TextBody::from_text("Custom {project} prompt")); + } + } + } + } + Arc::make_mut(p.slides.first_mut().unwrap()).shapes.first_mut().unwrap().text = + Some(deckcraft_model::TextBody::from_text("User File {project}")); + let before = p.clone(); + let english = updated_canvas(&ctx, &mut textures, &p, crate::i18n::Language::En); + let ukrainian = updated_canvas(&ctx, &mut textures, &p, crate::i18n::Language::Uk); + assert!(english == ukrainian, "stored text and custom prompts must retain their original wording"); + assert_eq!(p, before); + crate::i18n::set_current(crate::i18n::Language::En); + } +} diff --git a/crates/ui-egui/src/thumbs.rs b/crates/ui-egui/src/thumbs.rs index ebdc81d0b..285b04347 100644 --- a/crates/ui-egui/src/thumbs.rs +++ b/crates/ui-egui/src/thumbs.rs @@ -61,25 +61,25 @@ fn list(app: &mut SlideApp, ui: &mut Ui) { t.text_dim, ); resp.context_menu(|ui| { - if ui.button("Rename Section…").clicked() { + if ui.button(crate::i18n::tr("Rename Section…")).clicked() { let mut d = crate::dialogs::Dialog::new("renameSection"); d.params = json!({"index": si, "name": sec.name}); app.dialog = Some(d); ui.close(); } - if ui.button("Remove Section").clicked() { + if ui.button(crate::i18n::tr("Remove Section")).clicked() { let _ = app.run("section.remove", json!({"index": si})); ui.close(); } - if ui.button("Remove Section & Slides").clicked() { + if ui.button(crate::i18n::tr("Remove Section & Slides")).clicked() { let _ = app.run("section.remove", json!({"index": si, "slides": true})); ui.close(); } - if ui.button("Move Section Up").clicked() { + if ui.button(crate::i18n::tr("Move Section Up")).clicked() { let _ = app.run("section.move", json!({"index": si, "to": si.saturating_sub(1)})); ui.close(); } - if ui.button("Move Section Down").clicked() { + if ui.button(crate::i18n::tr("Move Section Down")).clicked() { let _ = app.run("section.move", json!({"index": si, "to": si + 1})); ui.close(); } @@ -95,7 +95,7 @@ fn list(app: &mut SlideApp, ui: &mut Ui) { p.text( pos2(row.min.x + 14.0, img.min.y + 2.0), Align2::CENTER_TOP, - format!("{}", i + 1), + (i + 1).to_string(), theme::font(12.0), if selected { t.text } else { t.text_dim }, ); @@ -186,7 +186,7 @@ fn list(app: &mut SlideApp, ui: &mut Ui) { // Click on empty space below: new slide prompt. let (r, resp) = ui.allocate_exact_size(vec2(width, 40.0), Sense::click()); if resp.hovered() { - ui.painter().text(r.center(), Align2::CENTER_CENTER, "+ New Slide", theme::font(12.0), t.text_faint); + ui.painter().text(r.center(), Align2::CENTER_CENTER, crate::i18n::tr("+ New Slide"), theme::font(12.0), t.text_faint); } if resp.clicked() { let _ = app.run("slide.new", json!({})); @@ -202,30 +202,30 @@ pub fn slide_menu(app: &mut SlideApp, ui: &mut Ui, i: usize) { ("Copy", "edit.copy", json!({"scope": "slides"})), ("Paste", "edit.paste", json!({"scope": "slides"})), ] { - if ui.button(l).clicked() { + if ui.button(crate::i18n::tr(l)).clicked() { let _ = app.run(id, p); ui.close(); } } ui.separator(); - if ui.button("New Slide").clicked() { + if ui.button(crate::i18n::tr("New Slide")).clicked() { let _ = app.run("slide.new", json!({})); ui.close(); } - if ui.button("Duplicate Slide").clicked() { + if ui.button(crate::i18n::tr("Duplicate Slide")).clicked() { let _ = app.run("slide.duplicate", json!({})); ui.close(); } - if ui.button("Delete Slide").clicked() { + if ui.button(crate::i18n::tr("Delete Slide")).clicked() { let _ = app.run("slide.delete", json!({})); ui.close(); } ui.separator(); - if ui.button("Add Section").clicked() { + if ui.button(crate::i18n::tr("Add Section")).clicked() { let _ = app.run("section.add", json!({"at": i})); ui.close(); } - ui.menu_button("Layout", |ui| { + ui.menu_button(crate::i18n::tr("Layout"), |ui| { let layouts: Vec<(String, u32)> = app .session .active() @@ -238,11 +238,11 @@ pub fn slide_menu(app: &mut SlideApp, ui: &mut Ui, i: usize) { } } }); - if ui.button("Reset Slide").clicked() { + if ui.button(crate::i18n::tr("Reset Slide")).clicked() { let _ = app.run("slide.reset", json!({})); ui.close(); } - if ui.button("Format Background…").clicked() { + if ui.button(crate::i18n::tr("Format Background…")).clicked() { let _ = app.run("view.pane", json!({"pane": "background"})); ui.close(); } @@ -298,7 +298,7 @@ fn master_list(app: &mut SlideApp, ui: &mut Ui) { ); let name = match li { Some(l) => m.layouts.get(l).map(|x| x.name.clone()).unwrap_or_default(), - None => format!("{} Slide Master", m.name), + None => crate::i18n::format("{} Slide Master", std::slice::from_ref(&m.name)), }; let resp = resp.on_hover_text(name); if resp.clicked() { diff --git a/crates/ui-egui/src/widgets.rs b/crates/ui-egui/src/widgets.rs index 5827b396e..49e492f6d 100644 --- a/crates/ui-egui/src/widgets.rs +++ b/crates/ui-egui/src/widgets.rs @@ -33,6 +33,7 @@ pub fn big_button(ui: &mut Ui, icon: Icon, label: &str, enabled: bool) -> Respon /// Large button; with `split`, the label row is a separate dropdown target (returns its response). pub fn big_button_ex(ui: &mut Ui, icon: Icon, label: &str, enabled: bool, split: bool, selected: bool) -> (Response, Option) { + let label = crate::i18n::tr(label); let t = Tokens::get(ui.ctx()); let font = theme::font(11.0); let lines: Vec<&str> = wrap_label(label); @@ -65,7 +66,7 @@ pub fn big_button_ex(ui: &mut Ui, icon: Icon, label: &str, enabled: bool, split: let label_w = ui.fonts_mut(|f| f.layout_no_wrap(l.to_string(), font.clone(), col).size().x); let extra = if split && last { 9.0 } else { 0.0 }; let x = rect.center().x - (label_w + extra) / 2.0; - painter.text(pos2(x, y), Align2::LEFT_TOP, l, font.clone(), col); + painter.text(pos2(x, y), Align2::LEFT_TOP, crate::i18n::tr(l), font.clone(), col); if split && last { chevron(&painter, pos2(x + label_w + 5.0, y + 7.0), 3.0, col); } @@ -111,6 +112,7 @@ pub fn small_button(ui: &mut Ui, icon: Icon, label: &str, enabled: bool, selecte } pub fn small_button_ex(ui: &mut Ui, icon: Icon, label: &str, enabled: bool, selected: bool, split: bool) -> (Response, Option) { + let label = crate::i18n::tr(label); let t = Tokens::get(ui.ctx()); let font = theme::font(12.0); let tw = if label.is_empty() { 0.0 } else { ui.fonts_mut(|f| f.layout_no_wrap(label.to_string(), font.clone(), t.text).size().x) + 5.0 }; @@ -138,7 +140,13 @@ pub fn small_button_ex(ui: &mut Ui, icon: Icon, label: &str, enabled: bool, sele } icons::paint(&painter, Rect::from_center_size(pos2(rect.min.x + 12.0, rect.center().y), vec2(17.0, 17.0)), icon, t.text, !enabled); if !label.is_empty() { - painter.text(pos2(rect.min.x + 24.0, rect.center().y), Align2::LEFT_CENTER, label, font, if enabled { t.text } else { t.text_faint }); + painter.text( + pos2(rect.min.x + 24.0, rect.center().y), + Align2::LEFT_CENTER, + crate::i18n::tr(label), + font, + if enabled { t.text } else { t.text_faint }, + ); } if split { chevron(&painter, pos2(rect.max.x - 6.0, rect.center().y), 2.8, t.text_dim); @@ -155,7 +163,7 @@ pub fn icon_toggle(ui: &mut Ui, icon: Icon, tip: &str, enabled: bool, on: bool) painter.rect_filled(rect, CornerRadius::same(4), c); } icons::paint(&painter, rect.shrink(3.0), icon, t.text, !enabled); - resp.on_hover_text(tip) + resp.on_hover_text(crate::i18n::tr(tip)) } /// Icon with a colour bar under it and a dropdown arrow (Font Color, Highlight, Shape Fill…). @@ -182,7 +190,7 @@ pub fn color_split(ui: &mut Ui, icon: Icon, tip: &str, color: Color32, enabled: if enabled { color } else { t.text_faint }, ); chevron(&painter, pos2(rect.max.x - 5.5, rect.center().y), 2.6, t.text_dim); - (main.on_hover_text(tip), arrow) + (main.on_hover_text(crate::i18n::tr(tip)), arrow) } /// A ribbon group: contents, then a thin vertical separator. @@ -252,7 +260,7 @@ pub fn color_grid(ui: &mut Ui, scheme: &ColorScheme, none_label: Option<&str>) - let t = Tokens::get(ui.ctx()); let mut out = None; ui.set_min_width(220.0); - ui.label(egui::RichText::new("Theme Colors").font(theme::bold(12.0))); + ui.label(egui::RichText::new(crate::i18n::tr("Theme Colors")).font(theme::bold(12.0))); let slots = [ SchemeSlot::Lt1, SchemeSlot::Dk1, @@ -275,7 +283,7 @@ pub fn color_grid(ui: &mut Ui, scheme: &ColorScheme, none_label: Option<&str>) - Stroke::new(if resp.hovered() { 2.0 } else { 1.0 }, if resp.hovered() { t.accent } else { t.border }), egui::StrokeKind::Inside, ); - resp.on_hover_text(tip).clicked() + resp.on_hover_text(crate::i18n::tr(tip)).clicked() }; egui::Grid::new("theme_colors").spacing(vec2(3.0, 1.0)).show(ui, |ui| { for slot in slots { @@ -314,7 +322,7 @@ pub fn color_grid(ui: &mut Ui, scheme: &ColorScheme, none_label: Option<&str>) - } }); ui.add_space(4.0); - ui.label(egui::RichText::new("Standard Colors").font(theme::bold(12.0))); + ui.label(egui::RichText::new(crate::i18n::tr("Standard Colors")).font(theme::bold(12.0))); ui.horizontal(|ui| { ui.spacing_mut().item_spacing.x = 3.0; for (c, name) in deckcraft_color::STANDARD_COLORS { @@ -325,7 +333,7 @@ pub fn color_grid(ui: &mut Ui, scheme: &ColorScheme, none_label: Option<&str>) - }); ui.separator(); if let Some(n) = none_label - && ui.button(n).clicked() + && ui.button(crate::i18n::tr(n)).clicked() { out = Some(None); } @@ -333,7 +341,7 @@ pub fn color_grid(ui: &mut Ui, scheme: &ColorScheme, none_label: Option<&str>) - let id = ui.id().with("hex"); let mut hex: String = ui.data_mut(|d| d.get_temp::(id).unwrap_or_default()); ui.horizontal(|ui| { - ui.label("More Colors… #"); + ui.label(crate::i18n::tr("More Colors… #")); let r = ui.add(egui::TextEdit::singleline(&mut hex).desired_width(70.0).hint_text("RRGGBB")); if r.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) @@ -371,7 +379,8 @@ pub fn gallery( if sel { painter.rect_stroke(rect.expand(1.0), CornerRadius::same(4), Stroke::new(1.5, t.accent), egui::StrokeKind::Inside); } - if resp.on_hover_text(tip(i)).clicked() { + let caption = tip(i); + if resp.on_hover_text(crate::i18n::tr(&caption)).clicked() { clicked = Some(i); } } diff --git a/docs/gaps.md b/docs/gaps.md index 6ea4130aa..917e3d785 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -1,6 +1,6 @@ # Where DeckCraft falls short of PowerPoint -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** minor (core-workflow alpha gate added; stage stays alpha; previous: major, first itemized gap list; full re-measure against PowerPoint for Mac 16.113.4 and the open GitHub issues) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** minor (localization gap narrowed after Ukrainian UI) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) Every known shortfall, one entry each, ranked by how much it stops someone who presents with PowerPoint from switching. This is the work list: pick from the top. Numbers are summarized in @@ -21,7 +21,7 @@ revision history. | 2 | Data loss and crashes on common paths (#60, #40, #59) | Stability | B | 6–12 | this file | | 3 | PPTX round trip drops comment threads, media options, rewind (#70, #74, #77) | File format | B | 3–6 | [file-format-parity.md](file-format-parity.md) | | 4 | EMF/WMF and SVG pictures don't render | File format | B | 8–14 | [file-format-parity.md](file-format-parity.md) | -| 5 | No localization: English only | Localization | B (infra + 5 languages) | 55–90 | [localization-parity.md](localization-parity.md) | +| 5 | Target languages beyond English missing; Ukrainian UI available | Localization | B (5 languages) | 55–90 | [localization-parity.md](localization-parity.md) | | 6 | No printing | Feature | B | 5–8 | [file-format-parity.md](file-format-parity.md) | | 7 | Fonts: only Regular, Bold and Italic faces (#69) | Feature | B | 3–5 | [ui-parity.md](ui-parity.md) | | 8 | Image paste fails on Windows and Linux (#53) | Feature | B | 2–4 | [ui-parity.md](ui-parity.md) | @@ -218,9 +218,10 @@ Detail in [hardware-parity.md](hardware-parity.md). Detail in [localization-parity.md](localization-parity.md). -- **English only, no string catalog** (#15, #30; Ukrainian PR #34 open). PowerPoint for Mac ships - 26 languages. Infrastructure 8–14 h, then 3–16 h per language plus native review. Beta needs the - infrastructure and at least Simplified Chinese, Spanish, French, German and Japanese (~30–45 h). +- **Target languages beyond English missing.** English and Ukrainian now have a UI catalog and + system-language selection (#15, #30). PowerPoint for Mac ships 26 languages. Additional + translations need 3–16 h per language plus native review. Beta needs at least Simplified + Chinese, Spanish, French, German and Japanese (~30–45 h). - **No IME composition in text editing** (CJK input). 3–6 h. - **UI isn't mirrored for right-to-left languages.** 6–10 h. - **Browser build draws Arabic and Hebrew as missing-glyph boxes in exports** (#81). 1–2 h. @@ -247,5 +248,6 @@ Detail in [localization-parity.md](localization-parity.md). | Date | Change | Summary | |---|---|---| +| 2026-10-10 | minor | Narrowed localization gap: Ukrainian UI catalog and language selection exist; other target languages, IME and RTL UI remain | | 2026-10-10 | minor | Noted that no gap blocks alpha (core-workflow gate passes) | | 2026-10-10 | major | Created: every gap from the 2026-10-10 re-measure and the 16 open GitHub issues, ranked, with beta blockers marked | diff --git a/docs/localization-parity.md b/docs/localization-parity.md index 24e308a12..8c8caf15c 100644 --- a/docs/localization-parity.md +++ b/docs/localization-parity.md @@ -1,18 +1,21 @@ # Localization parity with PowerPoint -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** major (created; string count measured from source, PowerPoint's localizations read from its bundle) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) - -DeckCraft's interface is English only today: strings are literals in the UI and engine code, with -no catalog and no language switch. Requests: #15 (follow the system language), #30 and PR #34 -(Ukrainian, open). Dimension score: **~8% ready** (estimated: English complete, slide-text script +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** minor (Ukrainian UI catalog and language selection) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) + +DeckCraft's interface supports English and Ukrainian through `crates/ui-egui/src/i18n/uk.tsv`. +Preferences offers the system language or an explicit choice; desktop choices persist across +restarts. Menus, ribbon, command palette, panels, dialogs and presenter controls are translated, +with English fallback. Presentation content, custom placeholder prompts and command IDs keep +their original values. This addresses #15 (follow the system language) and #30 (Ukrainian). +Dimension score: **~8% ready** (estimated: English complete, slide-text script support partly there), **55–90 h** to parity plus native-speaker review. ## How the numbers are measured - **UI strings:** ~1,050 user-visible string literals (measured 2026-10-10: distinct capitalized literals in `crates/ui-egui/src`, `crates/engine/src/cmd` and `crates/model/src`, - including the 226 command labels). A catalog will refine the count; until then percentages are - against this figure. + including the 226 command labels). Ukrainian catalog tests check command, menu, gallery and + literal translation-call coverage, unique keys, placeholders and bundled Cyrillic glyphs. - **Script support** is about slide text (what users type and import), separately from the UI: bidi and shaping live in `crates/text` (UAX #9, HarfRust, UAX #14 line breaking). - **PowerPoint's languages:** `Contents/Resources/*.lproj` in PowerPoint 16.113.4: ar, cs, da, de, @@ -25,6 +28,7 @@ support partly there), **55–90 h** to parity plus native-speaker review. | Language | Code | UI strings translated | Dialogs / tooltips / help | Script support (slide text) | Native review | Status | PowerPoint for Mac UI | Hours to full | |---|---|---|---|---|---|---|---|---| | English | en | ~1,050 / ~1,050 (100%) | yes / yes / no help | full | yes | **full** | yes | 0 | +| Ukrainian | uk | UI catalog; coverage checked by tests | UI captions; diagnostics retain original wording | Cyrillic; bundled UI fonts tested | no | available; English fallback | no | — | | Simplified Chinese (Mandarin) | zh-Hans | 0 (0%) | no | renders with system CJK fonts (asset policy excludes Noto CJK from builds); UAX #14 breaking; **no IME composition**; no true vertical layout | no | none | yes | 6–10 (incl. IME) | | Spanish | es | 0 (0%) | no | full (Latin) | no | none | yes (es, es-MX) | 3–5 | | Hindi | hi | 0 (0%) | no | Devanagari shaping through HarfRust, untested; no bundled Devanagari font | no | none | **no** (Windows only) | 4–7 | @@ -37,14 +41,13 @@ support partly there), **55–90 h** to parity plus native-speaker review. | Korean | ko | 0 (0%) | no | system Hangul fonts; no IME composition | no | none | yes | 4–7 | | Vietnamese | vi | 0 (0%) | no | stacked diacritics through HarfRust | no | none | **no** (Windows only) | 3–5 | -Other languages shipped: **0** (Ukrainian is in review as PR #34). PowerPoint for Mac ships 16 more +Other languages shipped: **1 (Ukrainian)**. PowerPoint for Mac ships 16 more than the twelve above: Czech, Danish, Greek, Finnish, Hebrew, Hungarian, Italian, Dutch, Norwegian, Polish, Russian, Slovak, Swedish, Thai, Turkish and Traditional Chinese. -Per-language hours assume the infrastructure exists: **8–14 h** once for a string catalog (copy a -sibling app's approach, e.g. EffectCraft's menu and panel catalogs), a language setting that follows -the system (#15), plural and number formatting, and an `xtask` check that every key has a -translation. IME composition (3–6 h) is counted once, under Simplified Chinese. +The UI catalog, system-language setting and Ukrainian integer-count formatting now exist. +Additional languages still need catalogs, appropriate plural/number formatting and native review. +IME composition (3–6 h) is counted once, under Simplified Chinese. ## Proofing languages @@ -56,4 +59,5 @@ common misspellings (`crates/engine/src/cmd/review.rs`); see [gaps.md](gaps.md#p | Date | Change | Summary | |---|---|---| +| 2026-10-10 | minor | Added Ukrainian catalog, language selection, English fallback, preserved custom prompts and bundled-font coverage | | 2026-10-10 | major | Created: twelve-language table, ~1,050 UI strings measured, PowerPoint for Mac's 26 languages from its bundle | diff --git a/docs/roadmap.md b/docs/roadmap.md index da2757376..42df29f21 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,6 +1,6 @@ # DeckCraft roadmap: milestones and what's next -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** minor (core-workflow alpha gate added; stage stays alpha; previous: major, created; milestones carried from the local execution plan, the beta plan built from gaps.md) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** minor (localization infrastructure and Ukrainian UI available) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) Forward-looking: where DeckCraft is going and in what order. The one-page summary is [ROADMAP.md](../ROADMAP.md); every shortfall is in [gaps.md](gaps.md); numbers are in @@ -29,7 +29,7 @@ points and lead the beta plan. 2. **PPTX on real decks:** build an owned corpus, round-trip checks in CI, fix what breaks; comment threads, media options and rewind on save (#70, #74, #77); EMF/WMF and SVG. 31–55 h. 3. **Review the open PRs:** equations (#55), browser print (#66), Morph alignment (#67), macOS - open-with (#68), font lookup (#80), Ukrainian (#34, after the catalog exists). 4–8 h. + open-with (#68), font lookup (#80). 4–8 h. Ukrainian UI and its catalog are available (#34). ## Plan to beta (~120–210 h; ~40–70 h wall clock with 3–4 agents) @@ -43,7 +43,7 @@ Beta means ~75% ready for real work and opening and saving PowerPoint's `.pptx` | 3 | PPTX round-trip losses (#70, #74, #77), `.potx`/`.ppsx`/`.pptm` content types, embedded fonts | 8–15 | File formats → 80% | | 4 | EMF/WMF and SVG pictures | 8–14 | File formats, Pictures | | 5 | Vector PDF, printing | 10–16 | File formats, Files | -| 6 | Localization infrastructure + zh-Hans, es, fr, de, ja (with IME) | 27–46 | Localization 8 → ~40% | +| 6 | Additional catalogs for zh-Hans, es, fr, de, ja (with IME) | 27–46 | Localization 8 → ~40% | | 7 | Font weights (#69), image paste (#53), Tab in text (#78) | 6–11 | Features | | 8 | Charts: data grid, elements | 6–10 | Features | | 9 | Animation Pane timeline, preview, effect options | 5–8 | Features | @@ -109,5 +109,6 @@ plan above. | Date | Change | Summary | |---|---|---| +| 2026-10-10 | minor | Recorded Ukrainian UI and catalog infrastructure; beta localization work now focuses on additional languages and IME | | 2026-10-10 | minor | Added the alpha gate (six core workflows): all pass, two with partial depth; stage stays alpha | | 2026-10-10 | major | Created: current focus, ranked beta plan with hours, milestones M0–M14 with status, the old alpha checklist as history | diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 0c27d893d..f0d16be1e 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1,6 +1,6 @@ # DeckCraft vs Microsoft PowerPoint: parity assessment -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** minor (hours to ~95% per audience added; ready recomputed from written weights, 56% → 55%; stage stays alpha; previous: major, full re-measure against PowerPoint for Mac 16.113.4; replaces the parity sections of ROADMAP.md) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** minor (localization evidence updated for Ukrainian UI) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) The authoritative answer to "how close is DeckCraft to PowerPoint, and how much work is left?". [ROADMAP.md](../ROADMAP.md) summarizes it; [gaps.md](gaps.md) itemizes every shortfall; @@ -71,7 +71,7 @@ The weights are what those users actually depend on, written down so the total c | Performance | 5% | ~55% | 10–20 | [hardware-parity.md](hardware-parity.md) | Unmeasured against PowerPoint. Slides rasterize on the CPU (`vello_cpu`); Morph re-aligns outlines every frame (#67) | | Hardware | 5% | ~40% | 25–45 | [hardware-parity.md](hardware-parity.md) | GPU-composited window (wgpu), but CPU slide raster, software-only video decode, presenter view not placed on the second display, no pen pressure, no pinch zoom | | Platforms | 5% | ~80% | 5–10 | [ROADMAP.md](../ROADMAP.md) | Ahead in reach: macOS, Windows x64/x86/arm64, Linux (5 formats), FreeBSD and the web, all from one release workflow. Held back by the Windows GPU crashes and Linux/Windows clipboard gaps | -| Localization | 5% | ~8% | 55–90 + native review | [localization-parity.md](localization-parity.md) | English only; no string catalog. PowerPoint for Mac ships 26 languages. Bidi and Arabic shaping in slide text exist | +| Localization | 5% | ~8% | 55–90 + native review | [localization-parity.md](localization-parity.md) | English and Ukrainian UI catalog with system-language selection. PowerPoint for Mac ships 26 languages; Ukrainian is outside that count. Bidi and Arabic shaping in slide text exist | | Ecosystem (templates, add-ins) | 3% | ~25% | 10–20 | [gaps.md](gaps.md) | 8 original themes vs PowerPoint's large template gallery; no add-ins or VBA (out of scope by policy) | | AI features | 2% | ~10% | 20–40 (owner decision on models) | [gaps.md](gaps.md) | No Designer, Copilot, Speaker Coach, live subtitles or Translate (all Microsoft cloud services). Agents drive every command over MCP instead | | **Total** | 100% | **~55%** (Σ weight × ready = 55.3) | **~365–650** | | | @@ -276,6 +276,7 @@ scored done (16 open issues). | Date | Change | Summary | |---|---|---| +| 2026-10-10 | minor | Updated localization evidence for Ukrainian UI; overall readiness estimates stay unchanged | | 2026-10-10 | minor | Added the readiness-by-audience table: each number gets its own hours to ~95% (full 340–600, mainstream 150–250, essentials 50–90). The full number stays an additive weighted sum; no change to the percentages | | 2026-10-10 | minor | Added the mainstream practitioner (~53%) and essentials user (~62%) numbers with written weights, discounts and user evidence. Features dimension recomputed from written area weights (66% judgement → 62.4% weighted mean), which moves ready for real work from ~56% to ~55%; stage unchanged | | 2026-10-10 | minor | Stage checked against the core-workflow gate (all six pass); stays alpha | From 6ee9c6a99223144db9951b4c1ea97137536fb64a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Marcin=20Brzuchalski?= Date: Mon, 12 Oct 2026 09:37:22 +0200 Subject: [PATCH 2/6] i18n: add Polish (pl) interface A 1,232-entry catalog with the same key set as the Ukrainian one from #34, registered as Language::Pl ("Polski", code pl, locales pl-*). Written from the meaning of the English labels; no other product's localisation was consulted. count() takes the Ukrainian and Polish forms as [one, few, many]; Polish uses the "many" form for 21, unlike Ukrainian. Catalog tests now cover both languages and require identical key sets. Co-Authored-By: Claude Opus 5.5 --- ATTRIBUTION.md | 1 + ROADMAP.md | 2 +- crates/ui-egui/src/dialogs.rs | 15 +- crates/ui-egui/src/i18n.rs | 169 ++++- crates/ui-egui/src/i18n/pl.tsv | 1235 ++++++++++++++++++++++++++++++++ crates/ui-egui/src/lib.rs | 1 + crates/ui-egui/src/ribbon.rs | 9 +- docs/localization-parity.md | 5 +- 8 files changed, 1394 insertions(+), 43 deletions(-) create mode 100644 crates/ui-egui/src/i18n/pl.tsv diff --git a/ATTRIBUTION.md b/ATTRIBUTION.md index 7af26b760..42d3a6363 100644 --- a/ATTRIBUTION.md +++ b/ATTRIBUTION.md @@ -28,6 +28,7 @@ deck and UI icons are original work of the DeckCraft contributors (drawn or gene | `crates/media/tests/fixtures/clip-*` (`clip-h264.mp4`, `clip-h264.mov`, `clip-hevc.mp4`, `clip-vp9.webm`, `clip-av1.mkv`) | DeckCraft contributors | 6 frames of a colour gradient generated by `make.sh` (python3), encoded by ffmpeg (x264 / x265 / libvpx / SVT-AV1 as encoders only) | CC0-1.0 | Video decoding tests | | `crates/media/tests/fixtures/make.sh` | DeckCraft contributors | this repository | MIT OR Apache-2.0 | Regenerates the media fixtures | | `crates/ui-egui/src/i18n/uk.tsv` | @dmatviichuk, assisted by Codex | Contributor-original Ukrainian translation in this repository; terminology checked against VESUM 6.8.6 and ULIF (validation tools/data are not bundled) | MIT OR Apache-2.0 (`LICENSE-MIT`, `LICENSE-APACHE`) | Ukrainian interface text | +| `crates/ui-egui/src/i18n/pl.tsv` | @brzuchal, assisted by Claude | Contributor-original Polish translation written from the English interface labels in this repository; no other product localisation copied | MIT OR Apache-2.0 (`LICENSE-MIT`, `LICENSE-APACHE`) | Polish interface text | ## Bundled through dependencies diff --git a/ROADMAP.md b/ROADMAP.md index d49ff8386..4641d67c8 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -106,7 +106,7 @@ Presentation content and command IDs retain their original values. Detail: | Vietnamese | vi | 0% | none | 3–5 | Other languages shipped: **Ukrainian (uk)**, with English fallback and bundled Cyrillic glyph -coverage. PowerPoint for Mac ships 26 languages; additional catalogs and native review remain. +coverage, and **Polish (pl)**. PowerPoint for Mac ships 26 languages; additional catalogs and native review remain. ## Upcoming diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index affbd6d55..c8081b1ad 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -269,7 +269,12 @@ fn body(app: &mut SlideApp, ui: &mut Ui, d: &mut Dialog) -> bool { let what = if names.len() == 1 { format!("“{}”", names[0]) } else { - crate::i18n::count(names.len(), "presentations", "презентації", "презентацій", "презентацій") + crate::i18n::count( + names.len(), + "presentations", + ["презентації", "презентацій", "презентацій"], + ["prezentacji", "prezentacjach", "prezentacjach"], + ) }; ui.label( egui::RichText::new(crate::i18n::format("Do you want to save the changes you made to {what}?", std::slice::from_ref(&what))).strong(), @@ -609,7 +614,12 @@ fn body(app: &mut SlideApp, ui: &mut Ui, d: &mut Dialog) -> bool { ui.label(format!( "{} ({})", s.name, - crate::i18n::count(s.slides.len(), if s.slides.len() == 1 { "slide" } else { "slides" }, "слайд", "слайди", "слайдів") + crate::i18n::count( + s.slides.len(), + if s.slides.len() == 1 { "slide" } else { "slides" }, + ["слайд", "слайди", "слайдів"], + ["slajd", "slajdy", "slajdów"] + ) )); if ui.small_button(crate::i18n::tr("Delete")).clicked() { let _ = app.run("show.customShow", json!({"name": s.name, "delete": true})); @@ -751,6 +761,7 @@ fn body(app: &mut SlideApp, ui: &mut Ui, d: &mut Dialog) -> bool { crate::i18n::Language::System => "system", crate::i18n::Language::En => "en", crate::i18n::Language::Uk => "uk", + crate::i18n::Language::Pl => "pl", }; let _ = app.run("app.language", json!({"language": code})); } diff --git a/crates/ui-egui/src/i18n.rs b/crates/ui-egui/src/i18n.rs index 361bf07fb..7009d4819 100644 --- a/crates/ui-egui/src/i18n.rs +++ b/crates/ui-egui/src/i18n.rs @@ -13,22 +13,30 @@ pub enum Language { System, En, Uk, + Pl, } impl Language { - pub const ALL: [Self; 3] = [Self::System, Self::En, Self::Uk]; + pub const ALL: [Self; 4] = [Self::System, Self::En, Self::Uk, Self::Pl]; pub fn label(self) -> &'static str { match self { Self::System => tr("System language"), Self::En => "English", Self::Uk => "Українська", + Self::Pl => "Polski", } } pub fn from_locale(locale: &str) -> Self { let language = locale.split(['-', '_', '.', '@']).next().unwrap_or(""); - if language.eq_ignore_ascii_case("uk") { Self::Uk } else { Self::En } + if language.eq_ignore_ascii_case("uk") { + Self::Uk + } else if language.eq_ignore_ascii_case("pl") { + Self::Pl + } else { + Self::En + } } pub fn resolve(self, locale: Option<&str>) -> Self { @@ -49,27 +57,43 @@ pub fn set_current(language: Language) { } const CATALOG: &str = include_str!("i18n/uk.tsv"); +const PL_CATALOG: &str = include_str!("i18n/pl.tsv"); static UK: OnceLock> = OnceLock::new(); +static PL: OnceLock> = OnceLock::new(); + +fn parse(text: &str) -> HashMap { + text.lines() + .filter(|line| !line.is_empty() && !line.starts_with('#')) + .filter_map(|line| line.split_once('\t')) + .map(|(source, text)| (source.replace("\\n", "\n"), text.replace("\\n", "\n"))) + .collect() +} fn catalog() -> &'static HashMap { - UK.get_or_init(|| { - CATALOG - .lines() - .filter(|line| !line.is_empty() && !line.starts_with('#')) - .filter_map(|line| line.split_once('\t')) - .map(|(source, text)| (source.replace("\\n", "\n"), text.replace("\\n", "\n"))) - .collect() - }) + UK.get_or_init(|| parse(CATALOG)) +} + +fn polish() -> &'static HashMap { + PL.get_or_init(|| parse(PL_CATALOG)) +} + +/// The catalog of the language being drawn; `None` for English. +fn active() -> Option<&'static HashMap> { + match CURRENT.get() { + Language::Uk => Some(catalog()), + Language::Pl => Some(polish()), + Language::System | Language::En => None, + } } /// Exact lookup at an explicit interface boundary; unknown text falls back to English. pub fn tr(text: &str) -> &str { - if CURRENT.get() == Language::Uk { catalog().get(text).map(String::as_str).unwrap_or(text) } else { text } + active().and_then(|catalog| catalog.get(text)).map(String::as_str).unwrap_or(text) } /// Context-specific interface label with the original English source as fallback. pub fn tr_context<'a>(source: &'a str, key: &str) -> &'a str { - if CURRENT.get() == Language::Uk { catalog().get(key).map(String::as_str).unwrap_or(source) } else { source } + active().and_then(|catalog| catalog.get(key)).map(String::as_str).unwrap_or(source) } pub fn placeholder_prompt(kind: deckcraft_model::PhType) -> &'static str { @@ -101,28 +125,29 @@ pub fn format(source: &str, values: &[String]) -> String { out } -/// Ukrainian integer count forms: 1/21; 2–4/22–24; 0/5–19. -pub fn count(n: usize, english: &str, one: &str, few: &str, many: &str) -> String { - let word = if CURRENT.get() == Language::Uk { - match (n % 10, n % 100) { - (1, last) if last != 11 => one, - (2..=4, last) if !(12..=14).contains(&last) => few, - _ => many, - } - } else { - english +/// Integer count forms, each as `[one, few, many]`. Ukrainian: 1/21; 2–4/22–24; 0/5–19. +/// Polish: 1 only; 2–4/22–24; 0/5–21 (21 takes the "many" form, unlike Ukrainian). +pub fn count(n: usize, english: &str, ukrainian: [&str; 3], polish: [&str; 3]) -> String { + let few = matches!(n % 10, 2..=4) && !(12..=14).contains(&(n % 100)); + let word = match CURRENT.get() { + Language::Uk if n % 10 == 1 && n % 100 != 11 => ukrainian[0], + Language::Uk if few => ukrainian[1], + Language::Uk => ukrainian[2], + Language::Pl if n == 1 => polish[0], + Language::Pl if few => polish[1], + Language::Pl => polish[2], + Language::System | Language::En => english, }; format!("{n} {word}") } -/// Recovery notice includes accusative Ukrainian forms. +/// Recovery notice includes accusative Ukrainian and Polish forms. pub fn recovered(n: usize) -> String { let amount = count( n, if n == 1 { "unsaved presentation" } else { "unsaved presentations" }, - "незбережену презентацію", - "незбережені презентації", - "незбережених презентацій", + ["незбережену презентацію", "незбережені презентації", "незбережених презентацій"], + ["niezapisaną prezentację", "niezapisane prezentacje", "niezapisanych prezentacji"], ); format("Recovered {} from the last session", &[amount]) } @@ -175,10 +200,13 @@ mod tests { let label = crate::ribbon::option_label(option); if !label.is_empty() && !label.chars().all(|c| c.is_ascii_digit()) { assert!(catalog().contains_key(&label), "missing effect option: {option}: {label}"); + assert!(polish().contains_key(&label), "missing Polish effect option: {option}: {label}"); } } - let missing: Vec<_> = required.into_iter().filter(|key| !catalog().contains_key(*key)).collect(); + let missing: Vec<_> = required.iter().filter(|key| !catalog().contains_key(**key)).collect(); assert!(missing.is_empty(), "missing visible metadata: {missing:?}"); + let missing: Vec<_> = required.iter().filter(|key| !polish().contains_key(**key)).collect(); + assert!(missing.is_empty(), "missing visible metadata in Polish: {missing:?}"); } #[test] @@ -223,6 +251,7 @@ mod tests { .unwrap(); let key: String = serde_json::from_str(&format!("\"{}\"", &tail[..end])).unwrap(); assert!(catalog().contains_key(&key), "missing literal translation: {key}"); + assert!(polish().contains_key(&key), "missing Polish literal translation: {key}"); rest = &tail[end + 1..]; } } @@ -274,7 +303,7 @@ mod tests { fn ukrainian_interface_preview() { let dir = std::env::var("DECKCRAFT_I18N_SHOTS").unwrap(); std::fs::create_dir_all(&dir).unwrap(); - for (code, language) in [("en", Language::En), ("uk", Language::Uk)] { + for (code, language) in [("en", Language::En), ("uk", Language::Uk), ("pl", Language::Pl)] { for (name, dialog) in [("editor", None), ("preferences", Some("preferences")), ("table", Some("table"))] { let mut app = crate::SlideApp::new(deckcraft_engine::Session::new(), crate::Services::default()); app.run("file.new", serde_json::json!({})).unwrap(); @@ -310,15 +339,81 @@ mod tests { #[test] fn catalog_is_unique_complete_and_preserves_placeholders() { - let mut seen = std::collections::HashSet::new(); - for (line_no, line) in CATALOG.lines().enumerate() { - if line.is_empty() || line.starts_with('#') { - continue; + for catalog in [CATALOG, PL_CATALOG] { + let mut seen = std::collections::HashSet::new(); + for (line_no, line) in catalog.lines().enumerate() { + if line.is_empty() || line.starts_with('#') { + continue; + } + let (source, text) = line.split_once('\t').unwrap(); + assert!(seen.insert(source), "duplicate on {}: {source}", line_no + 1); + assert!(!text.trim().is_empty(), "empty translation: {source}"); + assert_eq!(placeholders(source), placeholders(text), "placeholder names/order/format: {source}"); } - let (source, text) = line.split_once('\t').unwrap(); - assert!(seen.insert(source), "duplicate on {}: {source}", line_no + 1); - assert!(!text.trim().is_empty(), "empty translation: {source}"); - assert_eq!(placeholders(source), placeholders(text), "placeholder names/order/format: {source}"); + } + // Polish translates exactly the keys Ukrainian does, so neither falls back to English. + let mut only_one: Vec<_> = catalog().keys().filter(|key| !polish().contains_key(*key)).collect(); + only_one.extend(polish().keys().filter(|key| !catalog().contains_key(*key))); + assert!(only_one.is_empty(), "keys in one catalog only: {only_one:?}"); + } + + #[test] + fn polish_locales_lookup_and_document_data() { + for locale in ["pl", "pl-PL", "pl_PL.UTF-8", "PL_pl@euro"] { + assert_eq!(Language::from_locale(locale), Language::Pl); + } + assert_eq!(Language::from_locale("plt"), Language::En); + let mut app = crate::SlideApp::new(deckcraft_engine::Session::new(), crate::Services::default()); + app.run("file.new", serde_json::json!({})).unwrap(); + app.run("insert.textBox", serde_json::json!({"text": "File {customer} / Raport", "rect": [1, 2, 100, 40]})).unwrap(); + let before = serde_json::to_value(&app.session.active().unwrap().doc).unwrap(); + app.run("app.language", serde_json::json!({"language": "pl"})).unwrap(); + assert_eq!(app.ui.interface_language, Language::Pl); + assert_eq!(tr("File"), "Plik"); + assert_eq!(tr_context("Insert", "@ribbon:Insert"), "Wstawianie"); + assert_eq!(tr("Insert"), "Wstaw"); + assert_eq!(tr("a user-defined slide title"), "a user-defined slide title"); + assert_eq!(before, serde_json::to_value(&app.session.active().unwrap().doc).unwrap()); + let round_trip: crate::UiState = serde_json::from_str(&serde_json::to_string(&app.ui).unwrap()).unwrap(); + assert_eq!(round_trip.interface_language, Language::Pl); + set_current(Language::En); + } + + #[test] + fn polish_integer_counts_differ_from_ukrainian_at_21() { + set_current(Language::Pl); + for (n, word) in [ + (0, "slajdów"), + (1, "slajd"), + (2, "slajdy"), + (4, "slajdy"), + (5, "slajdów"), + (12, "slajdów"), + (14, "slajdów"), + (21, "slajdów"), + (22, "slajdy"), + (25, "slajdów"), + (112, "slajdów"), + (122, "slajdy"), + ] { + assert_eq!(count(n, "slides", ["слайд", "слайди", "слайдів"], ["slajd", "slajdy", "slajdów"]), format!("{n} {word}")); + } + assert_eq!(recovered(3), "Odzyskano 3 niezapisane prezentacje z ostatniej sesji"); + set_current(Language::En); + } + + #[test] + fn the_document_fallback_font_covers_polish_letters_without_system_fonts() { + let db = deckcraft_fonts::FontDb::with_font_dirs(vec![]); + let face = db.face(deckcraft_fonts::LAST_RESORT_FAMILY, "Regular"); + let letters: std::collections::HashSet<_> = polish() + .values() + .flat_map(|value| value.chars()) + .chain("ĄąĆćĘꣳŃńÓóŚśŹźŻż„”".chars()) + .filter(|c| c.is_alphabetic() || "„”".contains(*c)) + .collect(); + for c in letters { + assert_ne!(face.glyph_for(c), 0, "missing document fallback glyph: {c}"); } } @@ -351,7 +446,7 @@ mod tests { (24, "слайди"), (111, "слайдів"), ] { - assert_eq!(count(n, "slides", "слайд", "слайди", "слайдів"), format!("{n} {word}")); + assert_eq!(count(n, "slides", ["слайд", "слайди", "слайдів"], ["slajd", "slajdy", "slajdów"]), format!("{n} {word}")); } set_current(Language::En); } diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv new file mode 100644 index 000000000..17b7dc905 --- /dev/null +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -0,0 +1,1235 @@ +# Polish interface catalog. English sourcetranslation; \n denotes a line break. +# Contributor-original translation written from the meaning of the English labels; no other +# product's localisation was consulted. MIT OR Apache-2.0. + Fixed date (empty = update automatically): Stała data (puste = aktualizuj automatycznie): + Footer text: Tekst stopki: + Present Pokaż + pt pkt + — Edited — edytowano +"Not Allowed" Symbol Symbol „Zakaz” +% of all commits % wszystkich commitów ++ New Comment + Nowy komentarz ++ New Slide + Nowy slajd ++ Row + Wiersz ++ Series + Seria ++Bin +Przedział +100% Stacked Bar Słupkowy 100% skumulowany +100% Stacked Column Kolumnowy 100% skumulowany +35mm Slides Slajdy 35 mm +@ribbon:Home Narzędzia główne +@ribbon:Insert Wstawianie +@{}: {} PRs, {} commits, +{} / −{} lines (Δ {}), +{} / −{} binary assets, {} – {} @{}: {} PR-ów, {} commitów, +{} / −{} wierszy (Δ {}), +{} / −{} zasobów binarnych, {} – {} +A3 Paper (297x420 mm) Papier A3 (297x420 mm) +A4 Paper (210x297 mm) Papier A4 (210x297 mm) +Aa Aa +Abc Abc +About Informacje +About DeckCraft DeckCraft — informacje +Accent 1 Akcent 1 +Accent 2 Akcent 2 +Accent 3 Akcent 3 +Accent 4 Akcent 4 +Accent 5 Akcent 5 +Accent 6 Akcent 6 +Accessibility Checker Sprawdzanie ułatwień dostępu +Across W poprzek +Action Akcja +Action Button: Custom Przycisk akcji: niestandardowy +Action Button: Document Przycisk akcji: dokument +Action Button: Get Information Przycisk akcji: informacje +Action Button: Go Back Przycisk akcji: wstecz +Action Button: Go Back or Previous Przycisk akcji: wstecz lub poprzedni +Action Button: Go Forward or Next Przycisk akcji: dalej lub następny +Action Button: Go Home Przycisk akcji: początek +Action Button: Go to Beginning Przycisk akcji: przejdź na początek +Action Button: Go to End Przycisk akcji: przejdź na koniec +Action Button: Help Przycisk akcji: pomoc +Action Button: Sound Przycisk akcji: dźwięk +Action Button: Video Przycisk akcji: wideo +Action Button: {k} Przycisk akcji: {k} +Action Buttons Przyciski akcji +Add Animation Dodaj animację +Add Section Dodaj sekcję +Add\nAnimation Dodaj\nanimację +Address: Adres: +Adjust Shape Dostosuj kształt +After Previous Po poprzedniej +After: Po: +Airplane Samolot +Align Wyrównaj +Align Bottom Wyrównaj do dołu +Align Center Wyrównaj do środka w poziomie +Align Left Wyrównaj do lewej +Align Middle Wyrównaj do środka w pionie +Align Right Wyrównaj do prawej +Align Text Wyrównaj tekst +Align Top Wyrównaj do góry +Alt Text Tekst alternatywny +Alt+ Alt+ +Alt+F9 Alt+F9 +Alt\nText Tekst\nalternatywny +An open-source, clean-room project. Not affiliated with Microsoft. Projekt open source tworzony metodą clean room. Niezwiązany z firmą Microsoft. +Angle Kąt +Animation Gallery Galeria animacji +Animation Pane Okienko animacji +Animation Styles Style animacji +Animation\nPane Okienko\nanimacji +Animations Animacje +Animation Painter Malarz animacji +Apply Animation Zastosuj animację +Play/Pause GIF Odtwórz/wstrzymaj GIF +Animations on Slide Animacje na slajdzie +Appear Pojawienie się +Apply Zastosuj +Apply To All Zastosuj do wszystkich +Apply to All Zastosuj do wszystkich +Apply\nTo All Zastosuj\ndo wszystkich +Arc Łuk +Arcs Łuki +Area Warstwowy +Arrange Rozmieść +Arrow Strzałka +Arrow at end Strzałka na końcu +Arrow at start Strzałka na początku +Arrow: Bent Strzałka: zagięta +Arrow: Bent-Up Strzałka: zagięta w górę +Arrow: Chevron Strzałka: szewron +Arrow: Down Strzałka: w dół +Arrow: Left Strzałka: w lewo +Arrow: Left-Right Strzałka: w lewo i w prawo +Arrow: Left-Right-Up Strzałka: w lewo, w prawo i w górę +Arrow: Left-Up Strzałka: w lewo i w górę +Arrow: Notched Right Strzałka: w prawo z wcięciem +Arrow: Pentagon Strzałka: pięciokąt +Arrow: Quad Strzałka: w cztery strony +Arrow: Right Strzałka: w prawo +Arrow: Striped Right Strzałka: w prawo w paski +Arrow: U-Turn Strzałka: zawracanie +Arrow: Up Strzałka: w górę +Arrow: Up-Down Strzałka: w górę i w dół +Arrows Strzałki +Arrows at both ends Strzałki na obu końcach +As One Object Jako jeden obiekt +Audio Dźwięk +Audio from File… Dźwięk z pliku… +AutoCorrect as you type Autokorekta podczas pisania +Autofit Autodopasowanie +Automatic Automatycznie +B4 (ISO) Paper (250x353 mm) Papier B4 (ISO) (250x353 mm) +B5 (ISO) Paper (176x250 mm) Papier B5 (ISO) (176x250 mm) +Background Tło +Background Styles Style tła +Background\nStyles Style\ntła +Backspace Backspace +Banded Columns Kolumny naprzemienne +Banded Rows Wiersze naprzemienne +Banner Transparent +Basic Cycle Cykl podstawowy +Basic List Lista podstawowa +Basic Process Proces podstawowy +Basic Shapes Kształty podstawowe +Before text (in): Przed tekstem (cale): +Bevel Skos +Binary assets added Dodane zasoby binarne +Binary assets removed Usunięte zasoby binarne +Black Left Czarny z lewej +Black Pen Czarne pióro +Black Right Czarny z prawej +Blinds Żaluzje +Block Arc Łuk blokowy +Block Arrows Strzałki blokowe +Blue Niebieski +Blue Pen Niebieskie pióro +Bold Pogrubienie +Bold Flash Błysk pogrubienia +Both Oba +Bottom Dół +Bounce Odbicie +Box Pole +Box In Pole do środka +Box Out Pole na zewnątrz +Box and Whisker Skrzynka i wąsy +Brightness Jasność +Brightness {:+}% Jasność {:+}% +Bring Forward Przesuń do przodu +Bring to Front Przesuń na wierzch +Bring\nForward Przesuń\ndo przodu +Browsed at a kiosk (full screen) Przeglądana w kiosku (pełny ekran) +Browsed by an individual (window) Przeglądana przez użytkownika (okno) +Bubble Bąbelkowy +Bullets Punktory +Burst Wybuch +By Paragraph Akapitami +Callout: Down Arrow Objaśnienie: strzałka w dół +Callout: Left Arrow Objaśnienie: strzałka w lewo +Callout: Line Objaśnienie: linia +Callout: Right Arrow Objaśnienie: strzałka w prawo +Callout: Up Arrow Objaśnienie: strzałka w górę +Callouts Objaśnienia +Cameo Kamea +Camera capture is not available yet Przechwytywanie z kamery nie jest jeszcze dostępne +Cancel Anuluj +Capitalize Each Word Jak Nazwy Własne +Caps Wersaliki +Center Do środka +Centered Wyśrodkowany +Change Case Zmień wielkość liter +Change Chart Type Zmień typ wykresu +Change Picture Zmień obraz +Change Shape Zmień kształt +Change\nChart Type Zmień typ\nwykresu +Character Spacing Odstępy między znakami +Characters Znaki +Chart Wykres +Chart Data Dane wykresu +Chart Design Projekt wykresu +Chart Elements Elementy wykresu +Chart… Wykres… +Check Accessibility Sprawdź ułatwienia dostępu +Check\nAccessibility Sprawdź\nułatwienia dostępu +Checkerboard Szachownica +Chevron Szewron +Choose a SmartArt Graphic Wybierz grafikę SmartArt +Chord Odcinek koła +Circle Okrąg +Circle In Okrąg do środka +Circle Out Okrąg na zewnątrz +Clear All Formatting Wyczyść całe formatowanie +Click a symbol to insert it at the insertion point. Kliknij symbol, aby wstawić go w punkcie wstawiania. +Click icon to add SmartArt graphic Kliknij ikonę, aby dodać grafikę SmartArt +Click icon to add chart Kliknij ikonę, aby dodać wykres +Click icon to add media Kliknij ikonę, aby dodać multimedia +Click icon to add picture Kliknij ikonę, aby dodać obraz +Click icon to add table Kliknij ikonę, aby dodać tabelę +Click to add first slide Kliknij, aby dodać pierwszy slajd +Click to add notes Kliknij, aby dodać notatki +Click to add subtitle Kliknij, aby dodać podtytuł +Click to add text Kliknij, aby dodać tekst +Click to add title Kliknij, aby dodać tytuł +Click to edit Master text styles Kliknij, aby edytować style tekstu wzorca +Click to edit Master title style Kliknij, aby edytować styl tytułu wzorca +Clock Zegar +Clockwise Zgodnie z ruchem wskazówek zegara +Close Zamknij +Close Master View Zamknij widok wzorca +Close\nMaster View Zamknij\nwidok wzorca +Cloud Chmura +Clustered Bar Słupkowy grupowany +Clustered Column Kolumnowy grupowany +Cmd+ Cmd+ +Cmd+, Cmd+, +Cmd+- Cmd+- +Cmd+= Cmd+= +Cmd+F1 Cmd+F1 +Cmd+Shift+P Cmd+Shift+P +Collapse the Ribbon Zwiń wstążkę +Color Kolor +Color Pulse Pulsowanie koloru +Colors Kolory +Columns Kolumny +Comb Grzebień +Combo Złożony +Command Palette Paleta poleceń +Comment Komentarz +Comments Komentarze +Commits Commity +Community Links Linki do społeczności +Company Firma +Complementary Color Kolor dopełniający +Connect Shapes Połącz kształty +Connection Sites Punkty połączeń +Connector: Curved Łącznik: zakrzywiony +Connector: Elbow Łącznik: łamany +Connector: Elbow (one bend) Łącznik: łamany (jedno zagięcie) +Content Zawartość +Contrast Kontrast +Contrast {:+}% Kontrast {:+}% +Contributors Współtwórcy +Conveyor Taśmociąg +Copy Kopiuj +Corrections Korekty +Couldn't save AutoRecover information: {e} Nie udało się zapisać informacji Autoodzyskiwania: {e} +Counter Clockwise Przeciwnie do ruchu wskazówek zegara +Cover Przykrycie +Create Utwórz +Crop Przytnij +Crop 10% each side Przytnij 10% z każdej strony +Crop 20% each side Przytnij 20% z każdej strony +Crop to Shape Przytnij do kształtu +Cross Krzyż +Crush Zgniecenie +Ctrl+ Ctrl+ +Cube Sześcian +Currency Walutowy +Curtains Kurtyny +Custom Niestandardowy +Custom Path Ścieżka niestandardowa +Custom Show Pokaz niestandardowy +Custom Show 1 Pokaz niestandardowy 1 +Custom Shows Pokazy niestandardowe +Custom Slide Show Niestandardowy pokaz slajdów +Custom\nShow Pokaz\nniestandardowy +Cut Wytnij +Cylinder Walec +Dark Blue Ciemnoniebieski +Dark Mode Tryb ciemny +Dark Red Ciemnoczerwony +Dark appearance Ciemny wygląd +Dark\nMode Tryb\nciemny +Darken Przyciemnienie +Dash Kreska +Dash Dot Kreska-kropka +Dashes Kreski +Database Baza danych +Date Data +Date & Time Data i godzina +Date &\nTime Data\ni godzina +Date and Time Data i godzina +Date and time Data i godzina +Decagon Dziesięciokąt +Decision Decyzja +DeckCraft DeckCraft +Decrease Font Size Zmniejsz rozmiar czcionki +Decrease List Level Zmniejsz poziom listy +Delay: Opóźnienie: +Delete Usuń +Delete Columns Usuń kolumny +Delete Comment Usuń komentarz +Delete Layout Usuń układ +Delete Rows Usuń wiersze +Delete Slide Usuń slajd +Delete Table Usuń tabelę +Delete Text Usuń tekst +Desaturate Zmniejszenie nasycenia +Deselect Odznacz +Design Projektowanie +Design Ideas Pomysły projektowe +Design\nIdeas Pomysły\nprojektowe +Deutsch Deutsch +Diagonal Stripe Pasek ukośny +Diamond Romb +Diamond In Romb do środka +Diamond Out Romb na zewnątrz +Direction Kierunek +Disappear Zniknięcie +Discard Recovered Presentations Odrzuć odzyskane prezentacje +Display name Nazwa wyświetlana +Dissolve Rozpuszczanie +Dissolve In Rozpuszczanie do środka +Dissolve Out Rozpuszczanie na zewnątrz +Distribute Rozłóż +Distribute Columns Rozłóż kolumny +Distribute Horizontally Rozłóż w poziomie +Distribute Rows Rozłóż wiersze +Distribute Vertically Rozłóż w pionie +Division Sign Znak dzielenia +Do not Autofit Nie dopasowuj automatycznie +Do you want to save the changes you made to {what}? Czy chcesz zapisać zmiany wprowadzone w {what}? +Document Dokument +Dodecagon Dwunastokąt +Don't Save Nie zapisuj +Don't show on title slide Nie pokazuj na slajdzie tytułowym +Done Gotowe +Donut Pierścień +Doors Drzwi +Double Brace Podwójny nawias klamrowy +Double Bracket Podwójny nawias kwadratowy +Double Wave Podwójna fala +Doughnut Pierścieniowy +Down W dół +Drape Draperia +Draw Rysowanie +Drop Shadow Rectangle Prostokąt z cieniem +Duplicate Duplikuj +Duplicate Selected Slides Duplikuj zaznaczone slajdy +Duplicate Slide Duplikuj slajd +Duration: Czas trwania: +Dynamic Content Zawartość dynamiczna +Edit Edycja +Edit Alt Text… Edytuj tekst alternatywny… +Edit Data Edytuj dane +Edit Shape Edytuj kształt +Edit Text Edytuj tekst +Edit the data: first row = series names, first column = categories. Edytuj dane: pierwszy wiersz = nazwy serii, pierwsza kolumna = kategorie. +Edit\nData Edytuj\ndane +Edit\nShape Edytuj\nkształt +Editing Edytowanie +Effect Options Opcje efektu +Effect\nOptions Opcje\nefektu +Effects Efekty +Email Address Adres e-mail +Email address: Adres e-mail: +Emphasis Wyróżnienie +End Koniec +End Show Zakończ pokaz +End of slide show Koniec pokazu slajdów +End of slide show, click to exit. Koniec pokazu slajdów, kliknij, aby zakończyć. +End trim (s): Przycięcie końca (s): +English (United Kingdom) English (United Kingdom) +English (United States) English (United States) +Enter Enter +Entrance Wejście +Equals Znak równości +Equation Równanie +Equation Shapes Kształty równań +Eraser Gumka +Escape Escape +Español Español +Exciting Efektowne +Exit Wyjście +Expand Rozwiń +Explosion: 14 Points Eksplozja: 14 ramion +Explosion: 8 Points Eksplozja: 8 ramion +Export Eksport +Export the presentation as: Eksportuj prezentację jako: +Exported {path} Wyeksportowano {path} +Export… Eksportuj… +Fade Zanikanie +Fade in (s): Narastanie (s): +Fade out (s): Wyciszanie (s): +Fall Over Przewrócenie +Ferris Wheel Diabelski młyn +File Plik +Fill Wypełnienie +Fill & Line Wypełnienie i linia +Fill Color Kolor wypełnienia +Filled Radar Radarowy wypełniony +Find… Znajdź… +First Pierwszy +First Column Pierwsza kolumna +First Slide Pierwszy slajd +First commit Pierwszy commit +Fit Dopasuj +Fit slide to current window Dopasuj slajd do bieżącego okna +Fit to Window Dopasuj do okna +Fit to\nWindow Dopasuj\ndo okna +Flash Błysk +Flip Przerzucenie +Flip Horizontal Przerzuć w poziomie +Flip Vertical Przerzuć w pionie +Float In Wpłynięcie +Float Out Wypłynięcie +Flowchart Schemat blokowy +Flowchart: Alternate Process Schemat blokowy: proces alternatywny +Flowchart: Card Schemat blokowy: karta +Flowchart: Collate Schemat blokowy: sortowanie zbiorcze +Flowchart: Connector Schemat blokowy: łącznik +Flowchart: Data Schemat blokowy: dane +Flowchart: Decision Schemat blokowy: decyzja +Flowchart: Delay Schemat blokowy: opóźnienie +Flowchart: Direct Access Storage Schemat blokowy: pamięć o dostępie bezpośrednim +Flowchart: Display Schemat blokowy: wyświetlacz +Flowchart: Document Schemat blokowy: dokument +Flowchart: Extract Schemat blokowy: wyodrębnianie +Flowchart: Internal Storage Schemat blokowy: pamięć wewnętrzna +Flowchart: Magnetic Disk Schemat blokowy: dysk magnetyczny +Flowchart: Manual Input Schemat blokowy: ręczne wprowadzanie danych +Flowchart: Manual Operation Schemat blokowy: operacja ręczna +Flowchart: Merge Schemat blokowy: scalanie +Flowchart: Multidocument Schemat blokowy: wiele dokumentów +Flowchart: Off-page Connector Schemat blokowy: łącznik międzystronicowy +Flowchart: Or Schemat blokowy: lub +Flowchart: Predefined Process Schemat blokowy: proces zdefiniowany +Flowchart: Preparation Schemat blokowy: przygotowanie +Flowchart: Process Schemat blokowy: proces +Flowchart: Punched Tape Schemat blokowy: taśma perforowana +Flowchart: Sequential Access Storage Schemat blokowy: pamięć o dostępie sekwencyjnym +Flowchart: Sort Schemat blokowy: sortowanie +Flowchart: Stored Data Schemat blokowy: dane przechowywane +Flowchart: Summing Junction Schemat blokowy: węzeł sumujący +Flowchart: Terminator Schemat blokowy: zakończenie +Fly In Wlot +Fly Out Wylot +Fly Through Przelot +Folded Corner Zagięty narożnik +Follow the layout Zgodnie z układem +Followed Hyperlink Odwiedzone hiperłącze +Font Czcionka +Font Color Kolor czcionki +Font Size Rozmiar czcionki +Fonts Czcionki +Footer Stopka +Foreground Pierwszy plan +Format Formatowanie +Format Background Formatuj tło +Format Background… Formatuj tło… +Format Painter Malarz formatów +Format Painter (double-click to keep it on) Malarz formatów (kliknij dwukrotnie, aby pozostał włączony) +Format Pane Okienko formatowania +Format Shape Formatuj kształt +Format Shape… Formatuj kształt… +Format\nBackground Formatuj\ntło +Format\nPane Okienko\nformatowania +Formatting State Stan formatowania +Fracture Pęknięcie +Frame Ramka +Français Français +Freeform Dowolny kształt +From Od +From Beginning Od początku +From Bottom Od dołu +From Bottom Left Od dołu z lewej +From Bottom Right Od dołu z prawej +From Current Slide Od bieżącego slajdu +From Left Od lewej +From Right Od prawej +From Top Od góry +From Top Left Od góry z lewej +From Top Right Od góry z prawej +From bottom left Od lewego dolnego rogu +From bottom right Od prawego dolnego rogu +From center Od środka +From top left Od lewego górnego rogu +From top right Od prawego górnego rogu +From\nBeginning Od\npoczątku +From\nCurrent Slide Od bieżącego\nslajdu +Full Pełne +Full Page Slides Slajdy na całą stronę +Funnel Lejkowy +Gallery Galeria +General Ogólne +Get Text Pobierz tekst +Glitter Brokat +Glow Poświata +Go to Przejdź do +Go to Slide Przejdź do slajdu +Grab bag Mieszanka +Gradient fill Wypełnienie gradientowe +Gradient stops Punkty gradientu +Gradient… Gradient… +Grayscale Skala szarości +Greek Grecki +Green Zielony +Gridlines Linie siatki +Group Grupuj +Group Objects Grupuj obiekty +Grow & Turn Powiększenie i obrót +Grow/Shrink Powiększenie/zmniejszenie +Guides Prowadnice +Half Połowiczne +Half Frame Pół ramki +Handout Master Wzorzec materiałów informacyjnych +Handout Master (coming soon) Wzorzec materiałów informacyjnych (wkrótce) +Handout\nMaster Wzorzec\nmateriałów +Handouts (1 slide per page) Materiały informacyjne (1 slajd na stronie) +Handouts (2 slides per page) Materiały informacyjne (2 slajdy na stronie) +Handouts (3 slides per page) Materiały informacyjne (3 slajdy na stronie) +Handouts (4 slides per page) Materiały informacyjne (4 slajdy na stronie) +Handouts (6 slides per page) Materiały informacyjne (6 slajdów na stronie) +Handouts (9 slides per page) Materiały informacyjne (9 slajdów na stronie) +Header Nagłówek +Header & Footer Nagłówek i stopka +Header &\nFooter Nagłówek\ni stopka +Header Row Wiersz nagłówka +Header and Footer Nagłówek i stopka +Header and Footer… Nagłówek i stopka… +Heart Serce +Height Wysokość +Height (in): Wysokość (cale): +Height: Wysokość: +Help Pomoc +Heptagon Siedmiokąt +Hexagon Sześciokąt +Hide All Ukryj wszystko +Hide Background Graphics Ukryj grafikę tła +Hide During Show Ukryj podczas pokazu +Hide Slide Ukryj slajd +Hide background graphics Ukryj grafikę tła +Hide\nSlide Ukryj\nslajd +Hierarchy Hierarchia +High Wysoka +Highlighter Zakreślacz +Histogram Histogram +Home Narzędzia główne +Honeycomb Plaster miodu +Horizontal Poziomo +Horz In Poziomo do środka +Horz Out Poziomo na zewnątrz +How would you describe this object and its context to someone who is blind or has low vision? Jak opiszesz ten obiekt i jego kontekst osobie niewidomej lub słabowidzącej? +Hyperlink Hiperłącze +Icons Ikony +In Do środka +In Bounce Odbicie do środka +Include hidden slides Uwzględnij ukryte slajdy +Increase Font Size Zwiększ rozmiar czcionki +Increase List Level Zwiększ poziom listy +Ink to Math Pismo odręczne na równanie +Ink to Math (coming soon) Pismo odręczne na równanie (wkrótce) +Ink to Shape Pismo odręczne na kształt +Ink to Shape (coming soon) Pismo odręczne na kształt (wkrótce) +Ink to Text Pismo odręczne na tekst +Ink to Text (coming soon) Pismo odręczne na tekst (wkrótce) +Ink to\nMath Odręczne\nna równanie +Ink to\nShape Odręczne\nna kształt +Ink to\nText Odręczne\nna tekst +Inner Wewnętrzny +Insert Wstaw +Insert Above Wstaw powyżej +Insert Below Wstaw poniżej +Insert Hyperlink Wstaw hiperłącze +Insert Layout Wstaw układ +Insert Left Wstaw z lewej +Insert Placeholder Wstaw symbol zastępczy +Insert Right Wstaw z prawej +Insert Table Wstaw tabelę +Insert Table… Wstaw tabelę… +Insert picture from file… Wstaw obraz z pliku… +Insert\nAbove Wstaw\npowyżej +Insert\nBelow Wstaw\nponiżej +Insert\nLayout Wstaw\nukład +Insert\nLeft Wstaw\nz lewej +Insert\nPlaceholder Wstaw symbol\nzastępczy +Insert\nRight Wstaw\nz prawej +Inspect Presentation Sprawdź prezentację +Inspect Shape Sprawdź kształt +Inspect Slide Sprawdź slajd +Interface Language Język interfejsu +Interface language: Język interfejsu: +Interface size Rozmiar interfejsu +Isosceles Triangle Trójkąt równoramienny +Italiano Italiano +Italic Kursywa +JPEG image (current slide) Obraz JPEG (bieżący slajd) +Join our community on Discord Dołącz do naszej społeczności na Discordzie +Join the ArtCraft community on Discord Dołącz do społeczności ArtCraft na Discordzie +Justify Wyjustuj +L-Shape Kształt L +Language Język +Last Ostatni +Last Column Ostatnia kolumna +Last Slide Ostatni slajd +Last commit Ostatni commit +Layout Układ +Learn More Dowiedz się więcej +Learn\nMore Dowiedz się\nwięcej +Ledger Paper (11x17 in) Papier Ledger (11x17 cali) +Left Do lewej +Left Brace Lewy nawias klamrowy +Left Bracket Lewy nawias kwadratowy +Letter Paper (8.5x11 in) Papier Letter (8,5x11 cali) +Liberation Serif Liberation Serif +Light Blue Jasnoniebieski +Light Green Jasnozielony +Light/Dark appearance Wygląd jasny/ciemny +Lighten Rozjaśnienie +Lightning Błyskawica +Lightning Bolt Błyskawica +Line Liniowy +Line Color Kolor linii +Line Spacing Interlinia +Line Spacing Options… Opcje interlinii… +Line delta Bilans wierszy +Line spacing (lines): Interlinia (wiersze): +Line with Markers Liniowy ze znacznikami +Linear Liniowy +Lines Linie +Lines +/− Wiersze +/− +Lines added Dodane wiersze +Lines deleted Usunięte wiersze +Link Łącze +Link… Łącze… +List Commands Lista poleceń +Lock Zablokuj +Long Dash Długa kreska +Long Dash Dot Długa kreska-kropka +Loop continuously until 'Esc' W pętli ciągłej do naciśnięcia klawisza „Esc” +Loop until Stopped W pętli do zatrzymania +Loops Pętle +Loose Luźne +Low Niska +MIT OR Apache-2.0 · © 2026 ArtCraft Team and the DeckCraft contributors MIT OR Apache-2.0 · © 2026 ArtCraft Team i współtwórcy DeckCraft +Margins (pt) Marginesy (pkt) +Mark as decorative Oznacz jako dekoracyjny +Mark selected text as: Oznacz zaznaczony tekst jako: +Math Matematyka +Matrix Macierz +Maximize content (else ensure fit) Maksymalizuj zawartość (w przeciwnym razie tylko dopasuj) +Media Multimedia +Media Info Informacje o multimediach +Medium Średnia +Merge Cells Scal komórki +Merge Shapes Scal kształty +Merge\nCells Scal\nkomórki +Merge\nShapes Scal\nkształty +Merged PRs Scalone PR-y +Middle Środek +Minus Sign Znak minus +Model Model +Models Modele +Moon Księżyc +More Colors… # Więcej kolorów… # +Morph Przekształcenie +Motion Ruch +Motion Paths Ścieżki ruchu +Move Przenieś +Move Down Przenieś w dół +Move Insertion Point Przenieś punkt wstawiania +Move Section Przenieś sekcję +Move Section Down Przenieś sekcję w dół +Move Section Up Przenieś sekcję w górę +Move Slide Przenieś slajd +Move Up Przenieś w górę +Multiplication Sign Znak mnożenia +Multiply Mnożenie +Mute Wycisz +Name Nazwa +Name (A–Z) Nazwa (A–Z) +Name: Nazwa: +New Comment Nowy komentarz +New Presentation Nowa prezentacja +New Slide Nowy slajd +New Slide (⇧⌘N) Nowy slajd (⇧⌘N) +New presentation Nowa prezentacja +New show name: Nazwa nowego pokazu: +New\nComment Nowy\nkomentarz +New\nSlide Nowy\nslajd +Next Następny +Next Slide Następny slajd +Next Window Następne okno +Next ▶ Następny ▶ +No Color Brak koloru +No Fill Brak wypełnienia +No Glow Brak poświaty +No Outline Brak konturu +No Reflection Brak odbicia +No Shadow Brak cienia +No Soft Edges Brak miękkich krawędzi +No accessibility issues found. People with disabilities should not have difficulty reading this document. Nie znaleziono problemów z ułatwieniami dostępu. Osoby z niepełnosprawnościami nie powinny mieć trudności z czytaniem tego dokumentu. +No comments on this slide. Brak komentarzy na tym slajdzie. +No contributor data was built into this copy. Ta kopia nie zawiera danych o współtwórcach. +No fill Brak wypełnienia +No line Brak linii +No model credits were built into this copy. Ta kopia nie zawiera informacji o modelach. +No notes. Brak notatek. +No sound: {err} Brak dźwięku: {err} +None Brak +Normal Normalny +Not Equal Znak nierówności +Not allowed Zakaz +Notes Notatki +Notes Master Wzorzec notatek +Notes Master (coming soon) Wzorzec notatek (wkrótce) +Notes Page Strona notatek +Notes Pages Strony notatek +Notes\nMaster Wzorzec\nnotatek +Notes\nPage Strona\nnotatek +Nudge Przesuń lekko +Number of columns: Liczba kolumn: +Number of rows: Liczba wierszy: +Numbering Numerowanie +Object Color Kolor obiektu +Objects Obiekty +Octagon Ośmiokąt +On Click Po kliknięciu +On Click of {name} Po kliknięciu: {name} +On Click of… (none) Po kliknięciu… (brak) +On Mouse Click Po kliknięciu myszą +On-screen Show (16:10) Pokaz na ekranie (16:10) +Open Data Otwórz dane +Open Dialog Otwórz okno dialogowe +Open Recovered Presentations Otwórz odzyskane prezentacje +Open the sample deck Otwórz przykładową prezentację +Open… Otwórz… +Orange Pomarańczowy +Orbit Orbita +Order Objects Kolejność obiektów +Origami Origami +Out Na zewnątrz +Out Bounce Odbicie na zewnątrz +Outer Zewnętrzny +Outline Kontur +Outline (.txt) Konspekt (.txt) +Outline View Widok konspektu +Outline\nView Widok\nkonspektu +Oval Owal +Oval ends Owalne zakończenia +Overhead Folia +PDF document (.pdf) Dokument PDF (.pdf) +PNG images (all slides) Obrazy PNG (wszystkie slajdy) +PNG images (current slide) Obrazy PNG (bieżący slajd) +PRs PR-y +Page Curl Zawinięcie strony +Page Setup… Ustawienia strony… +PageDown PageDown +PageUp PageUp +Pan Panorama +Pane Okienko +Paragraph Akapit +Paragraph… Akapit… +Parallelogram Równoległobok +Pareto Pareto +Partial Circle Wycinek koła +Particles Cząsteczki +Paste Wklej +Paste (⌘V) Wklej (⌘V) +Paste Formatting Wklej formatowanie +Paste and Match Formatting Wklej i dopasuj formatowanie +Paste or type an outline. Unindented lines become slide titles; tab-indented lines become bullets. Wklej lub wpisz konspekt. Wiersze bez wcięcia stają się tytułami slajdów, a wiersze wcięte tabulatorem — punktami listy. +Pasted image.png Wklejony obraz.png +Path Ścieżka +Pattern fill Wypełnienie deseniem +Pause Wstrzymaj +Peek In Wsunięcie +Peel Off Odklejenie +Pencil Ołówek +Pentagon Pięciokąt +Perspective Perspektywa +Pick a graphic; type one item per line. Wybierz grafikę i wpisz po jednym elemencie w wierszu. +Pick a look for your slides. Ideas are generated on your device. Wybierz wygląd slajdów. Pomysły są generowane na Twoim urządzeniu. +Picture Obraz +Picture Format Formatowanie obrazu +Picture Styles Style obrazu +Picture from File… Obraz z pliku… +Picture or texture fill Wypełnienie obrazem lub teksturą +Picture\nStyles Style\nobrazu +Pictures Obrazy +Pie Kołowy +Pin Przypnij +Placeholder colour Kolor symbolu zastępczego +Plan\nBuild\nLaunch Planuj\nBuduj\nWdrażaj +Plaque Plakietka +Play Odtwórz +Play Across Slides Odtwarzaj na kolejnych slajdach +Play Full Screen Odtwarzaj na pełnym ekranie +Play Narrations Odtwarzaj narrację +Play from Current Slide Odtwórz od bieżącego slajdu +Play from Start Odtwórz od początku +Play from Start (F5) Odtwórz od początku (F5) +Play from\nCurrent Slide Od bieżącego\nslajdu +Play from\nStart Od\npoczątku +Play/Pause Odtwórz/wstrzymaj +Playback Odtwarzanie +Plus Plus +Plus In Plus do środka +Plus Out Plus na zewnątrz +Plus Sign Znak plus +Portrait (9:16) Pionowo (9:16) +Português Português +Position Położenie +Position Objects Położenie obiektów +Position and Size Położenie i rozmiar +Post Opublikuj +Poster Frame Klatka tytułowa +PowerPoint Presentation (.pptx) Prezentacja programu PowerPoint (.pptx) +Preferences Preferencje +Preferences… Preferencje… +Presentation Prezentacja +Presentations Prezentacje +Presentations and slide shows, rebuilt from scratch in pure Rust. Prezentacje i pokazy slajdów, napisane od zera w czystym języku Rust. +Presented by a speaker (full screen) Prezentowana przez prelegenta (pełny ekran) +Presenter View Widok prezentera +Presenter View — DeckCraft Widok prezentera — DeckCraft +Presenter\nView Widok\nprezentera +Preset gradients Gotowe gradienty +Prestige Prestiż +Preview Podgląd +Previous Slide Poprzedni slajd +Print layout: Układ wydruku: +Properties Właściwości +Pulse Pulsowanie +Punctuation Interpunkcja +Purple Fioletowy +Push Wypchnięcie +Pyramid Piramida +Quick Styles Szybkie style +Quick\nStyles Szybkie\nstyle +Radar Radarowy +Radial Promienisty +Random Losowo +Random Bars Losowe paski +Reading View Widok do czytania +Reading\nView Widok\ndo czytania +Real name Imię i nazwisko +Recent Ostatnie +Record Nagraj +Recording narration needs audio capture (coming soon) Nagrywanie narracji wymaga przechwytywania dźwięku (wkrótce) +Recovered Presentations Odzyskane prezentacje +Recovered {} from the last session Odzyskano {} z ostatniej sesji +Rectangle Prostokąt +Rectangle: Diagonal Corners Rounded Prostokąt: zaokrąglone rogi po przekątnej +Rectangle: Diagonal Corners Snipped Prostokąt: ścięte rogi po przekątnej +Rectangle: Rounded Corners Prostokąt: zaokrąglone rogi +Rectangle: Single Corner Rounded Prostokąt: jeden róg zaokrąglony +Rectangle: Single Corner Snipped Prostokąt: jeden róg ścięty +Rectangle: Top Corners One Rounded and One Snipped Prostokąt: górne rogi, jeden zaokrąglony i jeden ścięty +Rectangle: Top Corners Rounded Prostokąt: zaokrąglone górne rogi +Rectangle: Top Corners Snipped Prostokąt: ścięte górne rogi +Rectangles Prostokąty +Rectangular Prostokątny +Red Czerwony +Red Pen Czerwone pióro +Redo Ponów +Redo (⌘Y) Ponów (⌘Y) +Reflected Rounded Rectangle Odbity prostokąt zaokrąglony +Reflection Odbicie +Regroup Grupuj ponownie +Regular Pentagon Pięciokąt foremny +Rehearse Timings Próba tempa +Rehearse\nTimings Próba\ntempa +Remove Usuń +Remove All Animations Usuń wszystkie animacje +Remove All Sections Usuń wszystkie sekcje +Remove Animation Usuń animację +Remove Background Usuń tło +Remove Background (coming soon) Usuń tło (wkrótce) +Remove Section Usuń sekcję +Remove Section & Slides Usuń sekcję i slajdy +Remove stop Usuń punkt gradientu +Remove\nBackground Usuń\ntło +Rename Zmień nazwę +Rename Layout Zmień nazwę układu +Rename Section Zmień nazwę sekcji +Rename Section… Zmień nazwę sekcji… +Rename Slide Zmień nazwę slajdu +Rename… Zmień nazwę… +Render Slide Renderuj slajd +Reopen Otwórz ponownie +Reorder Animation Zmień kolejność animacji +Reorder Objects Zmień kolejność obiektów +Replace straight quotes with smart quotes Zamieniaj cudzysłowy proste na drukarskie +Reply Odpowiedz +Reply… Odpowiedz… +Reset Resetuj +Reset Background Resetuj tło +Reset Crop Resetuj przycięcie +Reset Effects Resetuj efekty +Reset Picture Resetuj obraz +Reset Slide Resetuj slajd +Reset Slide Background Resetuj tło slajdu +Resize shape to fit text Dopasuj rozmiar kształtu do tekstu +Resolve Rozwiąż +Resolve Thread Rozwiąż wątek +Return Return +Reveal Odsłonięcie +Reverse the order Odwróć kolejność +Revert Przywróć +Review Recenzja +Rewind after Playing Przewiń po odtworzeniu +Ribbon Tab Karta wstążki +Ribbon: Tilted Down Wstęga: pochylona w dół +Ribbon: Tilted Up Wstęga: pochylona w górę +Right Do prawej +Right Brace Prawy nawias klamrowy +Right Bracket Prawy nawias kwadratowy +Right Triangle Trójkąt prostokątny +Right-to-Left Text Direction Kierunek tekstu od prawej do lewej +Ring Pierścień +Ripple Zmarszczki +Rise Up Uniesienie +Rotate Obróć +Rotate Left 90° Obróć w lewo o 90° +Rotate Right 90° Obróć w prawo o 90° +Rotate all text 270° Obróć cały tekst o 270° +Rotate all text 90° Obróć cały tekst o 90° +Rotate with shape Obracaj razem z kształtem +Rotation Obrót +Round Dot Okrągła kropka +Rounded Diagonal Corner Zaokrąglony róg po przekątnej +Ruler Linijka +Saturation Nasycenie +Saturation {}% Nasycenie {}% +Save Zapisz +Save (⌘S) Zapisz (⌘S) +Save As… Zapisz jako… +Save AutoRecover Information Zapisz informacje Autoodzyskiwania +Save as Template… Zapisz jako szablon… +Save to Data Zapisz do danych +Saved Zapisano +Saved {path} Zapisano {path} +Scatter Punktowy +ScreenTip: Etykietka ekranowa: +Screenshot Zrzut ekranu +Screenshot (not available yet) Zrzut ekranu (jeszcze niedostępny) +Scroll: Horizontal Zwój: poziomy +Scroll: Vertical Zwój: pionowy +Search commands (⇧⌘P) Szukaj poleceń (⇧⌘P) +Search commands… Szukaj poleceń… +Section Sekcja +Seek Przejdź do pozycji +Select Zaznacz +Select All Zaznacz wszystko +Select All Text Zaznacz cały tekst +Select Cells Zaznacz komórki +Select Objects Zaznacz obiekty +Select Paragraph Zaznacz akapit +Select Slides Zaznacz slajdy +Select Text Zaznacz tekst +Select Word Zaznacz wyraz +Select a chart first. Najpierw zaznacz wykres. +Select a shape to format it. Zaznacz kształt, aby go sformatować. +Select an object on the slide and add an animation from the Animations tab. Zaznacz obiekt na slajdzie i dodaj animację z karty Animacje. +Selection Zaznaczenie +Selection Pane Okienko zaznaczenia +Selection Pane… Okienko zaznaczenia… +Selection\nPane Okienko\nzaznaczenia +Send Backward Przesuń do tyłu +Send to Back Przesuń na spód +Send\nBackward Przesuń\ndo tyłu +Sentence case. Jak w zdaniu. +Set Text Ustaw tekst +Set Up Show… Przygotuj pokaz… +Set Up Slide Show Przygotuj pokaz slajdów +Set Up Slide Show… Przygotuj pokaz slajdów… +Set Up\nSlide Show Przygotuj\npokaz slajdów +Set as Default Shape Ustaw jako kształt domyślny +Shading Cieniowanie +Shadow Cień +Shape Kształt +Shape Effects Efekty kształtów +Shape Fill Wypełnienie kształtu +Shape Format Formatowanie kształtu +Shape Gallery Galeria kształtów +Shape Outline Kontur kształtu +Shape Styles Style kształtów +Shape {} Kształt {} +Shape\nStyles Style\nkształtów +Shapes Kształty +Shift+ Shift+ +Shift+F9 Shift+F9 +Show Pokaż +Show All Pokaż wszystko +Show Comments Pokaż komentarze +Show Media Controls Pokaż kontrolki multimediów +Show Slide Pokaż slajd +Show smart guides Pokaż inteligentne prowadnice +Show type Typ pokazu +Show without animation Pokaż bez animacji +Show without narration Pokaż bez narracji +Show/Hide Pokaż/ukryj +Show\nComments Pokaż\nkomentarze +Shred Niszczarka +Shrink & Turn Zmniejszenie i obrót +Shrink text on overflow Zmniejszaj tekst przy przepełnieniu +Simple Frame, White Prosta ramka, biała +Size Rozmiar +Size & Properties Rozmiar i właściwości +Slide Master Wzorzec slajdów +Slide Number Numer slajdu +Slide Show Pokaz slajdów +Slide Size Rozmiar slajdu +Slide Sorter Sortowanie slajdów +Slide number Numer slajdu +Slide timings recorded. Zapisano chronometraż slajdów. +Slide {:02}:{:04.1} Total {:02}:{:02} Slajd {:02}:{:04.1} Razem {:02}:{:02} +Slide {} of {} Slajd {} z {} +Slide {}: {}{} Slajd {}: {}{} +Slide {}: “{w}” Slajd {}: „{w}” +Slide\nMaster Wzorzec\nslajdów +Slide\nNumber Numer\nslajdu +Slide\nSize Rozmiar\nslajdu +Slide\nSorter Sortowanie\nslajdów +Slides (e.g. 1,3,5): Slajdy (np. 1,3,5): +Slides from Outline Slajdy z konspektu +Slides from Outline… Slajdy z konspektu… +Slides sized for: Rozmiar slajdów dla: +SmartArt SmartArt +Smile Uśmiech +Smiley Face Uśmiechnięta buźka +Smoothly Płynnie +Smoothly Left Płynnie w lewo +Smoothly Right Płynnie w prawo +Snap objects to grid Przyciągaj obiekty do siatki +Soft Edge Rectangle Prostokąt o miękkich krawędziach +Soft Edges Miękkie krawędzie +Solid Ciągła +Solid fill Wypełnienie pełne +Solid line Linia ciągła +Sort Sortuj +Source on GitHub Kod źródłowy na GitHubie +Space Spacja +Spacing after (pt): Odstęp po (pkt): +Spacing before (pt): Odstęp przed (pkt): +Special — hanging (in): Specjalne — wysunięcie (cale): +Speech Mowa +Speech Bubble: Oval Dymek mowy: owalny +Speech Bubble: Rectangle Dymek mowy: prostokątny +Speech Bubble: Rectangle with Corners Rounded Dymek mowy: prostokątny z zaokrąglonymi rogami +Spelling Pisownia +Spin Obrót +Spinner Wirowanie +Split Podział +Split Cells Podziel komórki +Split\nCells Podziel\nkomórki +Square Kwadrat +Square (1:1) Kwadrat (1:1) +Square Dot Kwadratowa kropka +Stacked Skumulowany +Stacked Area Warstwowy skumulowany +Stacked Bar Słupkowy skumulowany +Stacked Column Kolumnowy skumulowany +Stacked Line Liniowy skumulowany +Standard (4:3) Standardowy (4:3) +Standard Colors Kolory standardowe +Star Gwiazda +Star: 10 Points Gwiazda: 10 ramion +Star: 12 Points Gwiazda: 12 ramion +Star: 16 Points Gwiazda: 16 ramion +Star: 24 Points Gwiazda: 24 ramiona +Star: 32 Points Gwiazda: 32 ramiona +Star: 4 Points Gwiazda: 4 ramiona +Star: 5 Points Gwiazda: 5 ramion +Star: 6 Points Gwiazda: 6 ramion +Star: 7 Points Gwiazda: 7 ramion +Star: 8 Points Gwiazda: 8 ramion +Stars and Banners Gwiazdy i transparenty +Start After Previous Rozpocznij po poprzedniej +Start Automatically Rozpocznij automatycznie +Start On Click Rozpocznij po kliknięciu +Start With Previous Rozpocznij z poprzednią +Start a conversation Rozpocznij rozmowę +Start time (s): Czas rozpoczęcia (s): +Start: Początek: +Stealth end Zakończenie ostre +Stock Giełdowy +Stop Zatrzymaj +Stop Editing Text Zakończ edycję tekstu +Strikethrough Przekreślenie +Strips Paski +Style {i} Styl {i} +Subscript Indeks dolny +Subtle Subtelne +Sun Słońce +Sunburst Pierścieniowy wielopoziomowy +Superscript Indeks górny +Surface Powierzchniowy +Switch Przełączenie +Swivel Obrót wokół osi +Symbol Symbol +System language Język systemu +Tab Tab +Table Tabela +Table Design Projekt tabeli +Table Style Options Opcje stylu tabeli +Table Styles Style tabeli +Table… Tabela… +Task Pane Okienko zadań +Teardrop Łza +Teeter Huśtawka +Text Tekst +Text Box Pole tekstowe +Text Box Margins Marginesy pola tekstowego +Text Direction Kierunek tekstu +Text Effects Efekty tekstowe +Text Fill Wypełnienie tekstu +Text Highlight Color Kolor wyróżnienia tekstu +Text Options Opcje tekstu +Text Outline Kontur tekstu +Text Shadow Cień tekstu +Text/Background - Dark 1 Tekst/tło — ciemny 1 +Text/Background - Dark 2 Tekst/tło — ciemny 2 +Text/Background - Light 1 Tekst/tło — jasny 1 +Text/Background - Light 2 Tekst/tło — jasny 2 +Text\nBox Pole\ntekstowe +The spelling check is complete. No issues found. Sprawdzanie pisowni zostało zakończone. Nie znaleziono problemów. +Theme Colors Kolory motywu +Theme Gallery Galeria motywów +Themes Motywy +Thesaurus Tezaurus +Thesaurus (coming soon) Tezaurus (wkrótce) +This Document Ten dokument +Thought Myśl +Thought Bubble: Cloud Dymek myśli: chmurka +Through Black Przez czerń +Tight Ciasne +Timing Chronometraż +Top Góra +Total Row Wiersz sumy +Transition Przejście +Transition Gallery Galeria przejść +Transitions Przejścia +Translate Przetłumacz +Translation needs a language model (not bundled) Tłumaczenie wymaga modelu językowego (niedołączonego) +Transparency Przezroczystość +Trapezoid Trapez +Treemap Mapa drzewa +Triangle Trójkąt +Trigger Wyzwalacz +Trim Przytnij +Trim Media Przytnij multimedia +Turns Zakręty +Type Typ +Type Text Wpisz tekst +Type an equation in linear form (e.g. a^2+b^2=c^2, x=(-b±√(b^2-4ac))/2a). Wpisz równanie w formie liniowej (np. a^2+b^2=c^2, x=(-b±√(b^2-4ac))/2a). +Uncover Odkrycie +Underline Podkreślenie +Undo Cofnij +Undo (⌘Z) Cofnij (⌘Z) +Ungroup Rozgrupuj +Up W górę +Use Timings Użyj chronometrażu +Use timings, if present Użyj chronometrażu, jeśli istnieje +Username Nazwa użytkownika +Version Wersja +Version {} Wersja {} +Vert In Pionowo do środka +Vert Out Pionowo na zewnątrz +Vertical Pionowo +Vertical alignment Wyrównanie w pionie +Very Loose Bardzo luźne +Very Tight Bardzo ciasne +Video Wideo +Video from File… Wideo z pliku… +View Widok +Volume Głośność +Vortex Wir +Waterfall Kaskadowy +Wave Fala +Web Page or File Strona internetowa lub plik +Wedge Klin +Weight Grubość +Wheel Koło +Widescreen (13.333x7.5 in) Panoramiczny (13,333x7,5 cala) +Widescreen (16:9) Panoramiczny (16:9) +Width Szerokość +Width (in): Szerokość (cale): +Width: Szerokość: +Width: Szerokość: +Wind Wiatr +Window Okno +Wipe Wytarcie +With Previous Z poprzednią +WordArt WordArt +WordArt style {} Styl WordArt {} +Words Wyrazy +Wrap Text in Shape Zawijaj tekst w kształcie +Wrap text in shape Zawijaj tekst w kształcie +Yellow Żółty +Your changes will be lost if you don't save them. Jeśli nie zapiszesz zmian, zostaną one utracone. +Your name (comments): Twoje imię i nazwisko (komentarze): +Zoom Powiększenie +Zoom In Powiększ +Zoom Out Pomniejsz +Zoom… Powiększenie… +a) b) c) a) b) c) +a. b. c. a. b. c. +dropped file upuszczony plik +i. ii. iii. i. ii. iii. +no presentation open żadna prezentacja nie jest otwarta +no slides brak slajdów +no text insertion point brak punktu wstawiania tekstu +nothing selected nic nie zaznaczono +nothing to redo nie ma czego ponowić +nothing to undo nie ma czego cofnąć +select text or a shape zaznacz tekst lub kształt +select two or more objects zaznacz co najmniej dwa obiekty +size (pt) rozmiar (pkt) +tOGGLE cASE oDWRÓĆ wIELKOŚĆ lITER +unknown interface language nieznany język interfejsu +{ch} Bullet {ch} Punktor +{f} (Body) {f} (tekst podstawowy) +{f} (Headings) {f} (nagłówki) +{name}: DeckCraft can't open this kind of file {name}: DeckCraft nie może otworzyć pliku tego typu +{n} Column{} Liczba kolumn: {n}{} +{r} pt {r} pkt +{r} pt glow Poświata {r} pkt +{w} pt {w} pkt +{} ({} slides) {} (slajdy: {}) +{} Slide Master Wzorzec slajdów {} +{} contributors · {} commits Współtwórcy: {} · commity: {} +{} presentations Prezentacje: {} +{}x{} Table Tabela {}x{} +“{}” is not available yet. Funkcja „{}” nie jest jeszcze dostępna. +−Bin −Przedział +▶ Play All ▶ Odtwórz wszystko +◀ Previous ◀ Poprzedni +✏ on ✏ wł. diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 62e18a217..4634b845a 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -360,6 +360,7 @@ impl SlideApp { "system" => i18n::Language::System, "en" => i18n::Language::En, "uk" => i18n::Language::Uk, + "pl" => i18n::Language::Pl, _ => return Err("unknown interface language".into()), }; i18n::set_current(self.ui.interface_language.resolve(self.services.system_locale.as_deref())); diff --git a/crates/ui-egui/src/ribbon.rs b/crates/ui-egui/src/ribbon.rs index e737f7be2..0e8ef39dd 100644 --- a/crates/ui-egui/src/ribbon.rs +++ b/crates/ui-egui/src/ribbon.rs @@ -593,7 +593,14 @@ fn paragraph_group(app: &mut SlideApp, ui: &mut Ui, st: &Value) { let r = icon_toggle(ui, Icon::Columns, "Columns", en, false); egui::Popup::menu(&r).show(|ui| { for n in 1..=3 { - if ui.button(crate::i18n::count(n, if n == 1 { "Column" } else { "Columns" }, "стовпець", "стовпці", "стовпців")).clicked() + if ui + .button(crate::i18n::count( + n, + if n == 1 { "Column" } else { "Columns" }, + ["стовпець", "стовпці", "стовпців"], + ["kolumna", "kolumny", "kolumn"], + )) + .clicked() { run(app, "format.columns", json!({"count": n, "spacing": 18})); } diff --git a/docs/localization-parity.md b/docs/localization-parity.md index 8c8caf15c..24cff7fa0 100644 --- a/docs/localization-parity.md +++ b/docs/localization-parity.md @@ -1,6 +1,6 @@ # Localization parity with PowerPoint -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** minor (Ukrainian UI catalog and language selection) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (Polish UI catalog) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) DeckCraft's interface supports English and Ukrainian through `crates/ui-egui/src/i18n/uk.tsv`. Preferences offers the system language or an explicit choice; desktop choices persist across @@ -41,7 +41,7 @@ support partly there), **55–90 h** to parity plus native-speaker review. | Korean | ko | 0 (0%) | no | system Hangul fonts; no IME composition | no | none | yes | 4–7 | | Vietnamese | vi | 0 (0%) | no | stacked diacritics through HarfRust | no | none | **no** (Windows only) | 3–5 | -Other languages shipped: **1 (Ukrainian)**. PowerPoint for Mac ships 16 more +Other languages shipped: **2 (Ukrainian, Polish)**. The Polish catalog (`i18n/pl.tsv`) has the same 1,232 keys as the Ukrainian one, its own integer-count forms, and no second native-speaker review yet. PowerPoint for Mac ships 16 more than the twelve above: Czech, Danish, Greek, Finnish, Hebrew, Hungarian, Italian, Dutch, Norwegian, Polish, Russian, Slovak, Swedish, Thai, Turkish and Traditional Chinese. @@ -59,5 +59,6 @@ common misspellings (`crates/engine/src/cmd/review.rs`); see [gaps.md](gaps.md#p | Date | Change | Summary | |---|---|---| +| 2026-10-12 | minor | Added Polish catalog (1,232 keys, same set as Ukrainian), `pl` locales and Polish count forms | | 2026-10-10 | minor | Added Ukrainian catalog, language selection, English fallback, preserved custom prompts and bundled-font coverage | | 2026-10-10 | major | Created: twelve-language table, ~1,050 UI strings measured, PowerPoint for Mac's 26 languages from its bundle | From fd601c3416002837978f7209ed41bbccceffb437 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Marcin=20Brzuchalski?= Date: Mon, 19 Oct 2026 20:34:52 +0200 Subject: [PATCH 3/6] i18n: Cancel restores the interface language; app.language lists pl; labels translated once Preferences remembers the interface language with the other settings it opened with, so Cancel, Esc and the close button put it back (a test covers Esc). Language::code() gives the app.language value for the list and the snapshot. The app.language description names pl. Ribbon buttons translated their label and then each wrapped line or the label again; they now draw the translated text as it is, so a fragment can't match an unrelated catalog key. Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/dialogs.rs | 14 +++++++------- crates/ui-egui/src/i18n.rs | 10 ++++++++++ crates/ui-egui/src/lib.rs | 27 ++++++++++++++++++++++++++- crates/ui-egui/src/widgets.rs | 10 ++-------- 4 files changed, 45 insertions(+), 16 deletions(-) diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index 0cf4dd3ab..2b5fff780 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -147,6 +147,7 @@ fn preferences_snapshot(app: &SlideApp) -> Value { json!({ "dark": app.ui.brightness == theme::Brightness::Dark, "uiScale": app.ui.ui_scale, + "language": app.ui.interface_language.code(), "prefs": serde_json::to_value(&app.session.prefs).unwrap_or(Value::Null), }) } @@ -162,6 +163,11 @@ fn restore_preferences(app: &mut SlideApp, d: &Dialog) { if let Some(scale) = before.get("uiScale").and_then(Value::as_f64) { app.ui.ui_scale = scale as f32; } + if let Some(code) = before.get("language").and_then(Value::as_str) + && code != app.ui.interface_language.code() + { + let _ = app.run("app.language", json!({ "language": code })); + } if let Some(prefs) = before.get("prefs").and_then(|p| serde_json::from_value(p.clone()).ok()) { app.session.prefs = prefs; } @@ -2190,13 +2196,7 @@ fn body(app: &mut SlideApp, ui: &mut Ui, d: &mut Dialog) -> bool { egui::ComboBox::from_id_salt("interface_language").selected_text(app.ui.interface_language.label()).show_ui(ui, |ui| { for language in crate::i18n::Language::ALL { if ui.selectable_label(app.ui.interface_language == language, crate::i18n::tr(language.label())).clicked() { - let code = match language { - crate::i18n::Language::System => "system", - crate::i18n::Language::En => "en", - crate::i18n::Language::Uk => "uk", - crate::i18n::Language::Pl => "pl", - }; - let _ = app.run("app.language", json!({"language": code})); + let _ = app.run("app.language", json!({"language": language.code()})); } } }); diff --git a/crates/ui-egui/src/i18n.rs b/crates/ui-egui/src/i18n.rs index 003a5fa9d..03a5e951b 100644 --- a/crates/ui-egui/src/i18n.rs +++ b/crates/ui-egui/src/i18n.rs @@ -19,6 +19,16 @@ pub enum Language { impl Language { pub const ALL: [Self; 4] = [Self::System, Self::En, Self::Uk, Self::Pl]; + /// The `app.language` value for this choice. + pub fn code(self) -> &'static str { + match self { + Self::System => "system", + Self::En => "en", + Self::Uk => "uk", + Self::Pl => "pl", + } + } + pub fn label(self) -> &'static str { match self { Self::System => tr("System language"), diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index f39972d56..f4b7091b9 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -400,7 +400,7 @@ pub const UI_COMMANDS: &[(&str, &str, Option<&str>, &str)] = &[ ("app.about", "About DeckCraft", None, "{}"), ("app.palette", "Command Palette", Some("Cmd+Shift+P"), "{}"), ("app.preferences", "Preferences…", Some("Cmd+,"), "{}"), - ("app.language", "Interface Language", None, "{language: system|en|uk}"), + ("app.language", "Interface Language", None, "{language: system|en|uk|pl}"), ("app.openDialog", "Open…", None, "{purpose?}"), ("app.insertPictureDialog", "Picture from File…", None, "{}"), ("app.smartArtPictureDialog", "Insert Picture in SmartArt…", None, "{id?, index}"), @@ -2658,6 +2658,31 @@ mod tests { /// Preferences (#752): Cancel, Esc and the window's close button put back what the dialog /// changed while it was open; Done keeps it. + #[test] + fn preferences_cancel_restores_the_interface_language() { + let mut harness = egui_kittest::Harness::builder().with_size(egui::vec2(1200.0, 800.0)).build_ui_state( + |ui, app: &mut SlideApp| { + app.logic(ui.ctx()); + app.ui(ui); + }, + SlideApp::new(Session::with_new(), Services::default()), + ); + harness.run_steps(2); + let before = harness.state().ui.interface_language; + harness.state_mut().run("app.preferences", json!({})).unwrap(); + harness.run_steps(2); + // The language list applies a choice at once, as the other Preferences controls do. + harness.state_mut().run("app.language", json!({"language": "pl"})).unwrap(); + harness.run_steps(2); + assert_eq!(harness.state().ui.interface_language, i18n::Language::Pl); + harness.key_press(egui::Key::Escape); + harness.run_steps(2); + let s = harness.state_mut(); + assert!(s.dialog.is_none(), "Esc closes Preferences"); + assert_eq!(s.ui.interface_language, before, "Esc restores the interface language"); + let _ = s.run("app.language", json!({"language": "en"})); + } + #[test] fn preferences_cancel_restores_the_settings_done_keeps_them() { use egui_kittest::kittest::Queryable; diff --git a/crates/ui-egui/src/widgets.rs b/crates/ui-egui/src/widgets.rs index 9c61b9da9..9db95bf3c 100644 --- a/crates/ui-egui/src/widgets.rs +++ b/crates/ui-egui/src/widgets.rs @@ -69,7 +69,7 @@ pub fn big_button_ex(ui: &mut Ui, icon: Icon, label: &str, enabled: bool, split: let label_w = ui.fonts_mut(|f| f.layout_no_wrap(l.to_string(), font.clone(), col).size().x); let extra = if split && last { 9.0 } else { 0.0 }; let x = rect.center().x - (label_w + extra) / 2.0; - painter.text(pos2(x, y), Align2::LEFT_TOP, crate::i18n::tr(l), font.clone(), col); + painter.text(pos2(x, y), Align2::LEFT_TOP, *l, font.clone(), col); if split && last { chevron(&painter, pos2(x + label_w + 5.0, y + 7.0), 3.0, col); } @@ -146,13 +146,7 @@ pub fn small_button_ex(ui: &mut Ui, icon: Icon, label: &str, enabled: bool, sele } icons::paint(&painter, Rect::from_center_size(pos2(rect.min.x + 12.0, rect.center().y), vec2(17.0, 17.0)), icon, t.text, !enabled); if !label.is_empty() { - painter.text( - pos2(rect.min.x + 24.0, rect.center().y), - Align2::LEFT_CENTER, - crate::i18n::tr(label), - font, - if enabled { t.text } else { t.text_faint }, - ); + painter.text(pos2(rect.min.x + 24.0, rect.center().y), Align2::LEFT_CENTER, label, font, if enabled { t.text } else { t.text_faint }); } if split { chevron(&painter, pos2(rect.max.x - 6.0, rect.center().y), 2.8, t.text_dim); From 862e91e23ddd2fabc04619dd4b507c7bd845d987 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Marcin=20Brzuchalski?= Date: Mon, 19 Oct 2026 20:34:53 +0200 Subject: [PATCH 4/6] Docs: name Polish next to Ukrainian in the localization summaries ROADMAP's Languages section, the gaps ranking row and Localization entry, localization-parity's overview (with pl.tsv) and target-app-parity's Localization row now list Polish, as the shipped catalogs and the rest of each document do. gaps.md logs the change. Co-Authored-By: Claude Opus 5.5 --- ROADMAP.md | 5 +++-- docs/gaps.md | 9 +++++---- docs/localization-parity.md | 3 ++- docs/target-app-parity.md | 2 +- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 0aabef67f..770717ae2 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -84,8 +84,9 @@ EMF/SVG pictures, the data-loss and startup-crash bugs (#60, #40, #59) and the b ## Languages -English and Ukrainian: Preferences offers System language, English and Українська. Ukrainian -OS/browser locales select Ukrainian automatically; explicit desktop choices persist in `ui.json`. +English, Ukrainian and Polish: Preferences offers System language, English, Українська and Polski. +Ukrainian and Polish OS/browser locales select those languages automatically; explicit desktop +choices persist in `ui.json`. Menus, ribbon, command palette, panels, dialogs and presenter controls use the interface catalog. Presentation content and command IDs retain their original values. Detail: [docs/localization-parity.md](docs/localization-parity.md). diff --git a/docs/gaps.md b/docs/gaps.md index b29b56660..07508ff5f 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -1,6 +1,6 @@ # Where DeckCraft falls short of PowerPoint -> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-18 · **Change:** minor (Ukrainian and Polish UI catalogs and language selection; previous: minor, native chart payloads stored in bounded ZIP members; before that: major, community PR wave 2026-10-14: soak tests, PPTX corpus, comment threads (#70), embedded fonts, SmartArt Text Pane, Animation Pane timeline, Hunspell spelling, thesaurus, AutoCorrect, 14 new dialogs, pen pressure and trackpad gestures; duplicated entries and headers folded; previous: minor, 3-D bevel, camera and face lighting rendered) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) +> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-19 · **Change:** minor (Ukrainian and Polish UI catalogs and language selection; previous: minor, native chart payloads stored in bounded ZIP members; before that: major, community PR wave 2026-10-14: soak tests, PPTX corpus, comment threads (#70), embedded fonts, SmartArt Text Pane, Animation Pane timeline, Hunspell spelling, thesaurus, AutoCorrect, 14 new dialogs, pen pressure and trackpad gestures; duplicated entries and headers folded; previous: minor, 3-D bevel, camera and face lighting rendered) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) Every known shortfall, one entry each, ranked by how much it stops someone who presents with PowerPoint from switching. This is the work list: pick from the top. Numbers are summarized in @@ -21,7 +21,7 @@ revision history. | 2 | Data loss and crashes on common paths (#60, #40, #59) | Stability | B | 6–12 | this file | | 3 | PPTX round trip drops media options and rewind (#74, #77) | File format | B | 2–4 | [file-format-parity.md](file-format-parity.md) | | 4 | SVG pictures don't render (EMF/WMF do) | File format | B | 4–8 | [file-format-parity.md](file-format-parity.md) | -| 5 | Target languages beyond English missing; Ukrainian UI available | Localization | B (5 languages) | 55–90 | [localization-parity.md](localization-parity.md) | +| 5 | Target languages beyond English missing; Ukrainian and Polish UI available | Localization | B (5 languages) | 55–90 | [localization-parity.md](localization-parity.md) | | 6 | No printing | Feature | B | 5–8 | [file-format-parity.md](file-format-parity.md) | | 7 | Fonts: only Regular, Bold and Italic faces (#69) | Feature | B | 3–5 | [ui-parity.md](ui-parity.md) | | 8 | Image paste fails on Windows and Linux (#53) | Feature | B | 2–4 | [ui-parity.md](ui-parity.md) | @@ -323,8 +323,8 @@ Detail in [localization-parity.md](localization-parity.md). (Noto CJK is excluded by the asset policy), so the web build draws Korean file names, notes and outline text as boxes; desktop borrows Malgun Gothic, Apple SD Gothic Neo or Noto Sans CJK. Adding an openly licensed Korean face (e.g. Nanum Gothic, OFL) to craft-fonts: 1–2 h. -- **Target languages beyond English missing.** English and Ukrainian now have a UI catalog and - system-language selection (#15, #30). PowerPoint for Mac ships 26 languages. Additional +- **Target languages beyond English missing.** English, Ukrainian and Polish now have a UI catalog + and system-language selection (#15, #30). PowerPoint for Mac ships 26 languages. Additional translations need 3–16 h per language plus native review. Beta needs at least Simplified Chinese, Spanish, French, German and Japanese (~30–45 h). - **UI isn't mirrored for right-to-left languages.** 6–10 h. @@ -357,6 +357,7 @@ Detail in [localization-parity.md](localization-parity.md). | Date | Change | Summary | |---|---|---| +| 2026-10-19 | minor | Localization gap names the Polish UI catalog next to the Ukrainian one | | 2026-10-18 | minor | Native chart payloads use referenced ZIP members and share media byte/item limits; old inline sidecars remain readable. Synthetic tests cover multi-megabyte layout, all shape owners, shared budgets, missing/corrupt members and invalid references. Scores and milestone scope unchanged | | 2026-10-15 | trivial | Native saves retain chart package parts and relationships, including charts in groups; edited chart data still does not export a stale source workbook. Chart editing gaps and scores remain unchanged | | 2026-10-15 | trivial | PPTX trigger sequences keep later On Click steps instead of converting them to After Previous; mixed starts, independent buttons and paragraph builds have synthetic round-trip regressions | diff --git a/docs/localization-parity.md b/docs/localization-parity.md index c9c7eee8b..d730a8d49 100644 --- a/docs/localization-parity.md +++ b/docs/localization-parity.md @@ -2,7 +2,8 @@ > **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** minor (Polish UI catalog; previous: minor, IME composition underlined in table cells; previous: minor, IME composition and CJK interface text, #101; previous: major, created) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) -DeckCraft's interface supports English and Ukrainian through `crates/ui-egui/src/i18n/uk.tsv`. +DeckCraft's interface supports English, Ukrainian and Polish through `crates/ui-egui/src/i18n/uk.tsv` +and `crates/ui-egui/src/i18n/pl.tsv`. Preferences offers the system language or an explicit choice; desktop choices persist across restarts. Menus, ribbon, command palette, panels, dialogs and presenter controls are translated, with English fallback. Presentation content, custom placeholder prompts and command IDs keep diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 7420adf9f..db10264aa 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -71,7 +71,7 @@ The weights are what those users actually depend on, written down so the total c | Performance | 5% | ~55% | 10–20 | [hardware-parity.md](hardware-parity.md) | Unmeasured against PowerPoint. Slides rasterize on the CPU (`vello_cpu`); Morph re-aligns outlines every frame (#67) | | Hardware | 5% | ~40% | 25–45 | [hardware-parity.md](hardware-parity.md) | GPU-composited window (wgpu), but CPU slide raster, software-only video decode, presenter view not placed on the second display, no pen tilt, no pinch zoom | | Platforms | 5% | ~80% | 5–10 | [ROADMAP.md](../ROADMAP.md) | Ahead in reach: macOS, Windows x64/x86/arm64, Linux (5 formats), FreeBSD and the web, all from one release workflow. Held back by the Windows GPU crashes and Linux/Windows clipboard gaps | -| Localization | 5% | ~8% | 55–90 + native review | [localization-parity.md](localization-parity.md) | English and Ukrainian UI catalog with system-language selection. PowerPoint for Mac ships 26 languages; Ukrainian is outside that count. Bidi and Arabic shaping in slide text exist | +| Localization | 5% | ~8% | 55–90 + native review | [localization-parity.md](localization-parity.md) | English, Ukrainian and Polish UI catalogs with system-language selection. PowerPoint for Mac ships 26 languages; Ukrainian is outside that count. Bidi and Arabic shaping in slide text exist | | Ecosystem (templates, add-ins) | 3% | ~25% | 10–20 | [gaps.md](gaps.md) | 8 original themes and 6 original starter decks; PowerPoint's large template gallery remains; no add-ins or VBA (out of scope by policy) | | AI features | 2% | ~10% | 20–40 (owner decision on models) | [gaps.md](gaps.md) | No Designer, Copilot, Speaker Coach, live subtitles or Translate (all Microsoft cloud services). Agents drive every command over MCP instead | | **Total** | 100% | **~55%** (Σ weight × ready = 55.3) | **~365–650** | | | From f4a77cd880200fbd1ddb08ad57dfaf8c504e8ae9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Marcin=20Brzuchalski?= Date: Sun, 25 Oct 2026 16:17:21 +0100 Subject: [PATCH 5/6] i18n: tabs, colour controls and checker findings read in the interface language MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ribbon tabs registered their English name for screen readers while painting the translated label; they register the label now. Colour split buttons (Font Color, Shape Fill…) named their main half and arrow with the English tooltip; both use the translated one, and the arrow's "{} options" suffix is a catalog entry, as is the split buttons' one. The accessibility checker's findings came straight from the engine in English; issue_text translates the three it reports, keeping the contrast ratio, and leaves any other as it came. Ukrainian and Polish entries added; a test covers the findings. Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/dialogs.rs | 34 +++++++++++++++++++++++++++++++++- crates/ui-egui/src/i18n/pl.tsv | 4 ++++ crates/ui-egui/src/i18n/uk.tsv | 4 ++++ crates/ui-egui/src/ribbon.rs | 2 +- crates/ui-egui/src/widgets.rs | 8 +++++--- 5 files changed, 47 insertions(+), 5 deletions(-) diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index e212135c5..8df84df4e 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -2064,7 +2064,7 @@ fn body(app: &mut SlideApp, ui: &mut Ui, d: &mut Dialog) -> bool { "Slide {}: {}{}", &[ (item.get("slide").and_then(Value::as_u64).unwrap_or(0) + 1).to_string(), - (item.get("issue").and_then(Value::as_str).unwrap_or("")).to_string(), + issue_text(item.get("issue").and_then(Value::as_str).unwrap_or("")), (item.get("name").and_then(Value::as_str).map(|n| format!(" — {n}")).unwrap_or_default()).to_string(), ], )); @@ -3530,6 +3530,19 @@ fn find_dialog(app: &mut SlideApp, ui: &mut Ui, d: &mut Dialog) -> bool { close } +/// An accessibility checker finding in the interface language. The engine reports them in +/// English (`review.accessibility` is a command, so its output stays stable); the known ones are +/// translated here, the contrast one with its ratio kept, and any other is shown as it came. +fn issue_text(issue: &str) -> String { + if let Some(ratio) = issue.strip_prefix("Hard-to-read text contrast (").and_then(|r| r.strip_suffix(":1)")) { + return crate::i18n::format("Hard-to-read text contrast ({}:1)", &[ratio.to_string()]); + } + match issue { + "Missing slide title" | "Missing alternative text" => crate::i18n::tr(issue).to_string(), + other => other.to_string(), + } +} + #[cfg(test)] #[path = "font_dialog_tests.rs"] mod font_dialog_tests; @@ -4056,3 +4069,22 @@ mod about_credits_tests { #[cfg(test)] #[path = "preferences_tests.rs"] mod preferences_tests; + +#[cfg(test)] +mod issue_text_tests { + use crate::i18n::{Language, set_current}; + + /// The checker's findings read in the interface language, the contrast ratio kept; an unknown + /// finding is shown as the engine wrote it. + #[test] + fn accessibility_findings_follow_the_interface_language() { + set_current(Language::Pl); + assert_eq!(super::issue_text("Missing slide title"), "Brak tytułu slajdu"); + assert_eq!(super::issue_text("Hard-to-read text contrast (2.3:1)"), "Słabo czytelny kontrast tekstu (2.3:1)"); + assert_eq!(super::issue_text("Something new"), "Something new"); + set_current(Language::Uk); + assert_eq!(super::issue_text("Missing alternative text"), "Немає замінного тексту"); + set_current(Language::En); + assert_eq!(super::issue_text("Hard-to-read text contrast (2.3:1)"), "Hard-to-read text contrast (2.3:1)"); + } +} diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index dda6b290a..3343a53e3 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -1473,3 +1473,7 @@ Reject Revision Odrzuć poprawkę Duration Czas trwania Delay Opóźnienie Delete Surrounding Text Usuń otaczający tekst +{} options {} — opcje +Missing slide title Brak tytułu slajdu +Missing alternative text Brak tekstu alternatywnego +Hard-to-read text contrast ({}:1) Słabo czytelny kontrast tekstu ({}:1) diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 554391eb1..757bb2c10 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -1472,3 +1472,7 @@ Reject Revision Відхилити виправлення Duration Тривалість Delay Затримка Delete Surrounding Text Видалити навколишній текст +{} options {} — параметри +Missing slide title Немає заголовка слайда +Missing alternative text Немає замінного тексту +Hard-to-read text contrast ({}:1) Важкочитний контраст тексту ({}:1) diff --git a/crates/ui-egui/src/ribbon.rs b/crates/ui-egui/src/ribbon.rs index 6a86cdea2..c89710939 100644 --- a/crates/ui-egui/src/ribbon.rs +++ b/crates/ui-egui/src/ribbon.rs @@ -199,7 +199,7 @@ pub fn show(app: &mut SlideApp, ui: &mut Ui) { let w = ui.fonts_mut(|f| f.layout_no_wrap(label.to_string(), font.clone(), t.text).size().x) + 18.0; let r = Rect::from_min_size(pos2(x, y), vec2(w, 24.0)); let resp = ui.interact(r, ui.scope_id().with(("tab", name)), Sense::click()); - resp.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, true, name)); + resp.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, true, label)); let active = app.ui.tab == name; if resp.hovered() && !active { ui.painter().rect_filled(r.shrink2(vec2(2.0, 2.0)), CornerRadius::same(4), t.hover); diff --git a/crates/ui-egui/src/widgets.rs b/crates/ui-egui/src/widgets.rs index 19728b5ee..0afd75150 100644 --- a/crates/ui-egui/src/widgets.rs +++ b/crates/ui-egui/src/widgets.rs @@ -175,7 +175,7 @@ pub fn small_button_named( if split { main_resp.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, enabled, name)); if let Some(a) = &arrow_resp { - a.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, enabled, format!("{name} options"))); + a.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, enabled, crate::i18n::format("{} options", &[name.to_string()]))); } } if let Some(c) = bg(ui, &main_resp, selected, &t) { @@ -280,6 +280,8 @@ pub fn icon_toggle(ui: &mut Ui, icon: Icon, tip: &str, enabled: bool, on: bool) /// Icon with a colour bar under it and a dropdown arrow (Font Color, Highlight, Shape Fill…). pub fn color_split(ui: &mut Ui, icon: Icon, tip: &str, color: Color32, enabled: bool) -> (Response, Response) { + // The accessible names and the tooltip, in the interface language. + let tip = crate::i18n::tr(tip); let t = Tokens::get(ui.ctx()); let (rect, resp) = ui.allocate_exact_size(vec2(34.0, 22.0), Sense::hover()); let main = ui.interact(Rect::from_min_size(rect.min, vec2(23.0, 22.0)), resp.id.with("m"), if enabled { Sense::click() } else { Sense::hover() }); @@ -289,7 +291,7 @@ pub fn color_split(ui: &mut Ui, icon: Icon, tip: &str, color: Color32, enabled: resp.id.with("a"), if enabled { Sense::click() } else { Sense::hover() }, ); - arrow.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, enabled, format!("{tip} options"))); + arrow.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, enabled, crate::i18n::format("{} options", &[tip.to_string()]))); let painter = ui.painter_at(rect); if let Some(c) = bg(ui, &main, false, &t) { painter.rect_filled(main.rect, CornerRadius::same(4), c); @@ -304,7 +306,7 @@ pub fn color_split(ui: &mut Ui, icon: Icon, tip: &str, color: Color32, enabled: if enabled { color } else { t.text_faint }, ); chevron(&painter, pos2(rect.max.x - 5.5, rect.center().y), 2.6, t.text_dim); - (main.on_hover_text(crate::i18n::tr(tip)), arrow) + (main.on_hover_text(tip), arrow) } /// A ribbon group: contents, then a thin vertical separator. From 66c4e08bdd899e78d8203c74fbb0771821ef2d51 Mon Sep 17 00:00:00 2001 From: Jaebeen Ahn <348597085+JaebeenAhn@users.noreply.github.com> Date: Tue, 27 Oct 2026 13:06:52 +0900 Subject: [PATCH 6/6] i18n: add Korean (ko) interface catalog (1,486 entries) and language selection --- ATTRIBUTION.md | 1 + crates/ui-egui/src/i18n.rs | 32 +- crates/ui-egui/src/i18n/ko.tsv | 1488 ++++++++++++++++++++++++++++++++ crates/ui-egui/src/lib.rs | 1 + docs/localization-parity.md | 6 +- 5 files changed, 1521 insertions(+), 7 deletions(-) create mode 100644 crates/ui-egui/src/i18n/ko.tsv diff --git a/ATTRIBUTION.md b/ATTRIBUTION.md index c07c88cd3..2b565b6fc 100644 --- a/ATTRIBUTION.md +++ b/ATTRIBUTION.md @@ -33,6 +33,7 @@ artwork. The ribbon and menu icons stay original drawings. | `crates/media/tests/fixtures/make.sh` | DeckCraft contributors | this repository | MIT OR Apache-2.0 | Regenerates the media fixtures | | `crates/ui-egui/src/i18n/uk.tsv` | @dmatviichuk, assisted by Codex | Contributor-original Ukrainian translation in this repository; terminology checked against VESUM 6.8.6 and ULIF (validation tools/data are not bundled) | MIT OR Apache-2.0 (`LICENSE-MIT`, `LICENSE-APACHE`) | Ukrainian interface text | | `crates/ui-egui/src/i18n/pl.tsv` | @brzuchal, assisted by Claude | Contributor-original Polish translation written from the English interface labels in this repository; no other product localisation copied | MIT OR Apache-2.0 (`LICENSE-MIT`, `LICENSE-APACHE`) | Polish interface text | +| `crates/ui-egui/src/i18n/ko.tsv` | @JaebeenAhn | Contributor-original Korean translation written from the English interface labels in this repository (1,486 entries); no other product localisation copied | MIT OR Apache-2.0 (`LICENSE-MIT`, `LICENSE-APACHE`) | Korean interface text | | `crates/render/tests/fixtures/colr-squares.ttf` | DeckCraft contributors | two squares as a COLRv0/CPAL test font, built by `make_colr_font.py` (fontTools as the builder only) | CC0-1.0 | Colour glyph rendering tests | | `crates/fonts/tests/fixtures/legacy-semibold.ttf` | DeckCraft contributors | a one-glyph Semibold face named like Segoe UI Semibold (its Windows family differs from the typographic one), built by `make_legacy_font.py` (fontTools as the builder only) | CC0-1.0 | Font lookup by Windows family name tests | | `crates/fonts/tests/fixtures/legacy-regular.ttf` | DeckCraft contributors | the same family's one-glyph Regular face, built by `make_legacy_font.py` (fontTools as the builder only) | CC0-1.0 | Font lookup by Windows family name tests | diff --git a/crates/ui-egui/src/i18n.rs b/crates/ui-egui/src/i18n.rs index 03a5e951b..9169a8b21 100644 --- a/crates/ui-egui/src/i18n.rs +++ b/crates/ui-egui/src/i18n.rs @@ -14,10 +14,11 @@ pub enum Language { En, Uk, Pl, + Ko, } impl Language { - pub const ALL: [Self; 4] = [Self::System, Self::En, Self::Uk, Self::Pl]; + pub const ALL: [Self; 5] = [Self::System, Self::En, Self::Uk, Self::Pl, Self::Ko]; /// The `app.language` value for this choice. pub fn code(self) -> &'static str { @@ -26,6 +27,7 @@ impl Language { Self::En => "en", Self::Uk => "uk", Self::Pl => "pl", + Self::Ko => "ko", } } @@ -35,6 +37,7 @@ impl Language { Self::En => "English", Self::Uk => "Українська", Self::Pl => "Polski", + Self::Ko => "한국어", } } @@ -44,6 +47,8 @@ impl Language { Self::Uk } else if language.eq_ignore_ascii_case("pl") { Self::Pl + } else if language.eq_ignore_ascii_case("ko") { + Self::Ko } else { Self::En } @@ -68,8 +73,10 @@ pub fn set_current(language: Language) { const CATALOG: &str = include_str!("i18n/uk.tsv"); const PL_CATALOG: &str = include_str!("i18n/pl.tsv"); +const KO_CATALOG: &str = include_str!("i18n/ko.tsv"); static UK: OnceLock> = OnceLock::new(); static PL: OnceLock> = OnceLock::new(); +static KO: OnceLock> = OnceLock::new(); fn parse(text: &str) -> HashMap { text.lines() @@ -87,11 +94,16 @@ fn polish() -> &'static HashMap { PL.get_or_init(|| parse(PL_CATALOG)) } +fn korean() -> &'static HashMap { + KO.get_or_init(|| parse(KO_CATALOG)) +} + /// The catalog of the language being drawn; `None` for English. fn active() -> Option<&'static HashMap> { match CURRENT.get() { Language::Uk => Some(catalog()), Language::Pl => Some(polish()), + Language::Ko => Some(korean()), Language::System | Language::En => None, } } @@ -146,6 +158,11 @@ pub fn count(n: usize, english: &str, ukrainian: [&str; 3], polish: [&str; 3]) - Language::Pl if n == 1 => polish[0], Language::Pl if few => polish[1], Language::Pl => polish[2], + Language::Ko => match english { + "slide" | "slides" => return format!("슬라이드 {n}개"), + "unsaved presentation" | "unsaved presentations" => return format!("저장되지 않은 프레젠테이션 {n}개"), + _ => return format!("{n}개"), + }, Language::System | Language::En => english, }; format!("{n} {word}") @@ -216,10 +233,14 @@ mod tests { assert!(missing.is_empty(), "missing effect options: {missing:?}"); let missing: Vec<_> = option_labels.iter().filter(|label| !polish().contains_key(*label)).collect(); assert!(missing.is_empty(), "missing Polish effect options: {missing:?}"); + let missing: Vec<_> = option_labels.iter().filter(|label| !korean().contains_key(*label)).collect(); + assert!(missing.is_empty(), "missing Korean effect options: {missing:?}"); let missing: Vec<_> = required.iter().filter(|key| !catalog().contains_key(**key)).collect(); assert!(missing.is_empty(), "missing visible metadata: {missing:?}"); let missing: Vec<_> = required.iter().filter(|key| !polish().contains_key(**key)).collect(); assert!(missing.is_empty(), "missing visible metadata in Polish: {missing:?}"); + let missing: Vec<_> = required.iter().filter(|key| !korean().contains_key(**key)).collect(); + assert!(missing.is_empty(), "missing visible metadata in Korean: {missing:?}"); } #[test] @@ -266,6 +287,7 @@ mod tests { let key: String = serde_json::from_str(&format!("\"{}\"", &tail[..end])).unwrap(); assert!(catalog().contains_key(&key), "missing literal translation: {key}"); assert!(polish().contains_key(&key), "missing Polish literal translation: {key}"); + assert!(korean().contains_key(&key), "missing Korean literal translation: {key}"); rest = &tail[end + 1..]; } } @@ -317,7 +339,7 @@ mod tests { fn ukrainian_interface_preview() { let dir = std::env::var("DECKCRAFT_I18N_SHOTS").unwrap(); std::fs::create_dir_all(&dir).unwrap(); - for (code, language) in [("en", Language::En), ("uk", Language::Uk), ("pl", Language::Pl)] { + for (code, language) in [("en", Language::En), ("uk", Language::Uk), ("pl", Language::Pl), ("ko", Language::Ko)] { for (name, dialog) in [("editor", None), ("preferences", Some("preferences")), ("table", Some("table"))] { let mut app = crate::SlideApp::new(deckcraft_engine::Session::new(), crate::Services::default()); app.run("file.new", serde_json::json!({})).unwrap(); @@ -353,7 +375,7 @@ mod tests { #[test] fn catalog_is_unique_complete_and_preserves_placeholders() { - for catalog in [CATALOG, PL_CATALOG] { + for catalog in [CATALOG, PL_CATALOG, KO_CATALOG] { let mut seen = std::collections::HashSet::new(); for (line_no, line) in catalog.lines().enumerate() { if line.is_empty() || line.starts_with('#') { @@ -365,9 +387,11 @@ mod tests { assert_eq!(placeholders(source), placeholders(text), "placeholder names/order/format: {source}"); } } - // Polish translates exactly the keys Ukrainian does, so neither falls back to English. + // Polish and Korean translate exactly the keys Ukrainian does, so none falls back to English. let mut only_one: Vec<_> = catalog().keys().filter(|key| !polish().contains_key(*key)).collect(); only_one.extend(polish().keys().filter(|key| !catalog().contains_key(*key))); + only_one.extend(catalog().keys().filter(|key| !korean().contains_key(*key))); + only_one.extend(korean().keys().filter(|key| !catalog().contains_key(*key))); assert!(only_one.is_empty(), "keys in one catalog only: {only_one:?}"); } diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv new file mode 100644 index 000000000..500d2ec07 --- /dev/null +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -0,0 +1,1488 @@ +# Korean interface catalog. English sourcetranslation; \n denotes a line break. +# Contributor-original translation, MIT OR Apache-2.0. + Fixed date (empty = update automatically): 고정 날짜 (비워 두면 자동 업데이트): + Footer text: 바닥글 텍스트: + Present 발표 시작 + pt pt + — Edited — 편집됨 +"Not Allowed" Symbol 금지 기호 +% of all commits 전체 커밋 비율(%) ++ New Comment + 새 메모 ++ New Slide + 새 슬라이드 ++ Row + 행 추가 ++ Series + 계열 추가 ++Bin +구간 + (not checked) (검사 안 함) +100% Stacked Bar 100% 기준 누적 가로 막대형 +100% Stacked Column 100% 기준 누적 세로 막대형 +35mm Slides 35mm 슬라이드 +4 Point Star 십자형 별 (4각 별) +5 Point Star 오각 별 +6 Point Star 육각 별 +8 Point Star 팔각 별 +@ribbon:Home 홈 +@ribbon:Insert 삽입 +@{}: {} PRs, {} commits, +{} / −{} lines (Δ {}), +{} / −{} binary assets, {} – {} @{}: PR {}건, 커밋 {}건, +{} / −{}줄 (Δ {}), 바이너리 에셋 +{} / −{}, {} – {} +A3 Paper (297x420 mm) A3 용지 (297×420 mm) +A4 Paper (210x297 mm) A4 용지 (210×297 mm) +Aa Aa +Abc Abc +About 정보 +About DeckCraft DeckCraft 정보 +Accent 1 강조 1 +Accent 2 강조 2 +Accent 3 강조 3 +Accent 4 강조 4 +Accent 5 강조 5 +Accent 6 강조 6 +Accept Revision 수정 내용 적용 +Accessibility Checker 접근성 검사 +Across 가로 방향 +Action 실행 +Action Button: Custom 실행 단추: 사용자 지정 +Action Button: Document 실행 단추: 문서 +Action Button: Get Information 실행 단추: 정보 보기 +Action Button: Go Back 실행 단추: 뒤로 가기 +Action Button: Go Back or Previous 실행 단추: 뒤로 또는 이전 +Action Button: Go Forward or Next 실행 단추: 앞으로 또는 다음 +Action Button: Go Home 실행 단추: 홈으로 +Action Button: Go to Beginning 실행 단추: 처음으로 +Action Button: Go to End 실행 단추: 끝으로 +Action Button: Help 실행 단추: 도움말 +Action Button: Sound 실행 단추: 소리 +Action Button: Video 실행 단추: 비디오 +Action Button: {k} 실행 단추: {k} +Action Buttons 실행 단추 +Action Settings 실행 설정 +Add Animation 애니메이션 추가 +Add Font 글꼴 추가 +Add Guide 안내선 추가 +Add Horizontal Guide 가로 안내선 추가 +Add Point 점 추가 +Add Section 구역 추가 +Add Shape 도형 추가 +Add to Dictionary 사전에 추가 +Add to Quick Access Toolbar 빠른 실행 도구 모음에 추가 +Add Vertical Guide 세로 안내선 추가 +Add\nAnimation 애니메이션\n추가 +Address: 주소: +Adjust Shape 도형 조정 +After Previous 이전 효과 다음에 +After: 다음: +Airplane 비행기 +Align 맞춤 +Align Bottom 아래쪽 맞춤 +Align Cell Text 셀 텍스트 맞춤 +Align Center 가운데 맞춤 +Align Left 왼쪽 맞춤 +Align Middle 중간 맞춤 +Align Right 오른쪽 맞춤 +Align Text 텍스트 맞춤 +Align Top 위쪽 맞춤 +Alt Text 대체 텍스트 +Alt+ Alt+ +Alt+F9 Alt+F9 +Alt\nText 대체\n텍스트 +An open-source, clean-room project. Not affiliated with Microsoft. 오픈소스 클린룸 프로젝트입니다. Microsoft와 무관합니다. +Angle 각도 +Animation Gallery 애니메이션 갤러리 +Animation Pane 애니메이션 창 +Animation Styles 애니메이션 스타일 +Animation\nPane 애니메이션\n창 +Animations 애니메이션 +Animation Painter 애니메이션 복사 +Apply Animation 애니메이션 적용 +Play/Pause GIF GIF 재생/일시 중지 +Animations on Slide 슬라이드의 애니메이션 +Appear 나타내기 +Apply 적용 +Apply Design Idea 디자인 아이디어 적용 +Apply To All 모두 적용 +Apply to All 모두 적용 +Apply\nTo All 모두\n적용 +Arc 원호 +Arcs 원호 +Area 영역형 +Arrange 정렬 +Arrow 화살표 +Arrow at end 끝 화살표 +Arrow at start 시작 화살표 +Arrow: Bent 화살표: 굽은 화살표 +Arrow: Bent-Up 화살표: 위로 굽은 화살표 +Arrow: Chevron 화살표: 갈매기형 수장 +Arrow: Down 화살표: 아래쪽 +Arrow: Left 화살표: 왼쪽 +Arrow: Left-Right 화살표: 양방향(좌우) +Arrow: Left-Right-Up 화살표: 세 방향(좌·우·상) +Arrow: Left-Up 화살표: 두 방향(좌·상) +Arrow: Notched Right 화살표: 홈이 파인 오른쪽 +Arrow: Pentagon 화살표: 오각형 +Arrow: Quad 화살표: 네 방향 +Arrow: Right 화살표: 오른쪽 +Arrow: Striped Right 화살표: 줄무늬 오른쪽 +Arrow: U-Turn 화살표: 유턴 +Arrow: Up 화살표: 위쪽 +Arrow: Up-Down 화살표: 양방향(상하) +Arrows 화살표 +Arrows at both ends 양끝 화살표 +As One Object 하나의 개체로 +Audio 오디오 +Audio from File… 파일에서 오디오… +AutoCorrect as you type 입력할 때 자동 고침 +Autofit 자동 맞춤 +Automatic 자동 +B4 (ISO) Paper (250x353 mm) B4 (ISO) 용지 (250×353 mm) +B5 (ISO) Paper (176x250 mm) B5 (ISO) 용지 (176×250 mm) +Background 배경 +Background Styles 배경 스타일 +Background\nStyles 배경\n스타일 +Backspace Backspace +Banded Columns 줄무늬 열 +Banded Rows 줄무늬 행 +Banner 배너 +Basic Cycle 기본 주기형 +Basic List 기본 목록형 +Basic Process 기본 프로세스형 +Basic Shapes 기본 도형 +Before text (in): 텍스트 앞 여백 (인치): +Bevel 입체 효과 +Binary assets added 추가된 바이너리 에셋 +Binary assets removed 삭제된 바이너리 에셋 +Black Left 검은색을 거쳐 왼쪽으로 +Black Pen 검은색 펜 +Black Right 검은색을 거쳐 오른쪽으로 +Blinds 블라인드 +Block Arc 막대형 원호 +Block Arrows 블록 화살표 +Blue 파랑 +Blue Pen 파란색 펜 +Bold 굵게 +Bold Flash 굵게 깜박이기 +Both 모두 +Bottom 아래쪽 +Bounce 통통 튀기 +Box 상자 +Box In 상자 안으로 +Box Out 상자 밖으로 +Box and Whisker 상자 수염 그림 +Brightness 밝기 +Brightness {:+}% 밝기 {:+}% +Bring Forward 앞으로 가져오기 +Bring to Front 맨 앞으로 가져오기 +Bring\nForward 앞으로\n가져오기 +Browsed at a kiosk (full screen) 대화형 자동 진행 (전체 화면) +Browsed by an individual (window) 개별 창에서 진행 (창 모드) +Bubble 거품형 +Bullets 글머리 기호 +Burst 폭발 +By Paragraph 단락별 +Callout: Down Arrow 설명선: 아래쪽 화살표 +Callout: Left Arrow 설명선: 왼쪽 화살표 +Callout: Line 설명선: 선 +Callout: Right Arrow 설명선: 오른쪽 화살표 +Callout: Up Arrow 설명선: 위쪽 화살표 +Callouts 설명선 +Cameo 카메오 +Camera capture is not available yet 카메라 캡처 기능은 아직 지원되지 않습니다 +Cancel 취소 +Capitalize Each Word 단어 첫 글자만 대문자 +Caps 대문자 +Center 가운데 +Centered 가운데 맞춤 +Change Case 대/소문자 바꾸기 +Change Chart Type 차트 종류 변경 +Change Picture 그림 바꾸기 +Change Shape 도형 모양 변경 +Change\nChart Type 차트 종류\n변경 +Character Spacing 문자 간격 +Characters 문자 단위 +Chart 차트 +Chart Data 차트 데이터 +Chart Design 차트 디자인 +Chart Elements 차트 요소 +Chart… 차트… +Check Accessibility 접근성 검사 +Check\nAccessibility 접근성\n검사 +Checkerboard 바둑판식 +Chevron 갈매기형 수장 +Choose a SmartArt Graphic SmartArt 그래픽 선택 +Chord 현 +Circle 원 +Circle In 원 안으로 +Circle Out 원 밖으로 +Clear All Formatting 서식 모두 지우기 +Click a symbol to insert it at the insertion point. 기호를 클릭하면 커서 위치에 삽입됩니다. +Click icon to add SmartArt graphic 아이콘을 클릭하여 SmartArt 그래픽 추가 +Click icon to add chart 아이콘을 클릭하여 차트 추가 +Click icon to add media 아이콘을 클릭하여 미디어 추가 +Click icon to add picture 아이콘을 클릭하여 그림 추가 +Click icon to add table 아이콘을 클릭하여 표 추가 +Click to add first slide 첫 슬라이드를 추가하려면 클릭하세요 +Click to add notes 슬라이드 노트를 입력하려면 클릭하세요 +Click to add subtitle 부제목을 입력하려면 클릭하세요 +Click to add text 텍스트를 입력하려면 클릭하세요 +Click to add title 제목을 입력하려면 클릭하세요 +Click to edit Master text styles 마스터 텍스트 스타일을 편집하려면 클릭하세요 +Click to edit Master title style 마스터 제목 스타일을 편집하려면 클릭하세요 +Clock 시계 방향 +Clockwise 시계 방향 +Close 닫기 +Close Master View 마스터 보기 닫기 +Close\nMaster View 마스터 보기\n닫기 +Cloud 구름 +Clustered Bar 묶은 가로 막대형 +Clustered Column 묶은 세로 막대형 +Cmd+ Cmd+ +Cmd+, Cmd+, +Cmd+- Cmd+- +Cmd+= Cmd+= +Cmd+F1 Cmd+F1 +Cmd+Shift+P Cmd+Shift+P +Collapse the Ribbon 리본 메뉴 축소 +Color 색 +Color At 색상 추출 +Color Pulse 색 진동 +Colors 색 +Columns 열 +Comb 빗질 +Combo 콤보 +Command Palette 명령 팔레트 +Comment 메모 +Comments 메모 +Commits 커밋 +Community Links 커뮤니티 링크 +Company 회사 +Complementary Color 보색 +Connect Shapes 도형 연결 +Connection Sites 연결점 +Connector: Curved 연결선: 곡선 +Connector: Elbow 연결선: 꺾인 선 +Connector: Elbow (one bend) 연결선: 꺾인 선(한 번 꺾임) +Content 콘텐츠 +Contract 축소 +Contrast 대비 +Contrast {:+}% 대비 {:+}% +Contributors 기여자 +Conveyor 컨베이어 +Copy 복사 +Corrections 보정 +Couldn't save AutoRecover information: {e} 자동 복구 정보를 저장할 수 없습니다: {e} +Counter Clockwise 시계 반대 방향 +Cover 덮기 +Create 만들기 +Crop 자르기 +Crop 10% each side 각 변 10% 자르기 +Crop 20% each side 각 변 20% 자르기 +Crop to Shape 도형에 맞춰 자르기 +Cross 십자형 +Crush 구기기 +Ctrl+ Ctrl+ +Cube 정육면체 +Currency 통화 +Curtains 커튼 +Custom 사용자 지정 +Custom Path 사용자 지정 경로 +Custom Show 사용자 지정 쇼 +Custom Show 1 사용자 지정 쇼 1 +Custom Shows 사용자 지정 쇼 +Custom Slide Show 사용자 지정 슬라이드 쇼 +Custom\nShow 사용자 지정\n쇼 +Cut 잘라내기 +Cylinder 원기둥 +Dark Blue 진한 파랑 +Dark Mode 다크 모드 +Dark Red 진한 빨강 +Dark appearance 어두운 테마(다크 모드) +Dark\nMode 다크\n모드 +Darken 어둡게 +Dash 파선 +Dash Dot 일점쇄선 +Dashes 대시 스타일 +Database 데이터베이스 +Date 날짜 +Date & Time 날짜 및 시간 +Date &\nTime 날짜 및\n시간 +Date and Time 날짜 및 시간 +Date and time 날짜 및 시간 +Decagon 십각형 +Decision 판단 +DeckCraft DeckCraft +Decrease Font Size 글꼴 크기 작게 +Decrease List Level 목록 수준 감소 +Delay: 지연 시간: +Delete 삭제 +Delete Columns 열 삭제 +Delete Comment 메모 삭제 +Delete Layout 레이아웃 삭제 +Delete Rows 행 삭제 +Delete Slide 슬라이드 삭제 +Delete Table 표 삭제 +Delete Text 텍스트 삭제 +Desaturate 채도 낮추기 +Deselect 선택 해제 +Design 디자인 +Design Ideas 디자인 아이디어 +Design\nIdeas 디자인\n아이디어 +Deutsch Deutsch +Diagonal Stripe 대각선 줄무늬 +Diamond 다이아몬드 +Diamond In 다이아몬드 안으로 +Diamond Out 다이아몬드 밖으로 +Direction 방향 +Disappear 사라지기 +Discard Recovered Presentations 복구된 프레젠테이션 삭제 +Display name 표시 이름 +Dissolve 디졸브 +Dissolve In 디졸브(나타내기) +Dissolve Out 디졸브(끝내기) +Distribute 배분 +Distribute Columns 열 너비 같게 +Distribute Horizontally 가로 간격 동일하게 +Distribute Rows 행 높이 같게 +Distribute Vertically 세로 간격 동일하게 +Division Sign 나눗셈 기호 +Do not Autofit 자동 맞춤 안 함 +Do you want to save the changes you made to {what}? {what}의 변경 내용을 저장하시겠습니까? +Document 문서 +Dodecagon 십이각형 +Don't Save 저장 안 함 +Don't show on title slide 제목 슬라이드에는 표시 안 함 +Done 완료 +Donut 도넛 +Doors 문 열기 +Double Brace 이중 중괄호 +Double Bracket 이중 대괄호 +Double Wave 이중 물결 +Doughnut 도넛형 +Down 아래로 +Drape 드레이프 +Draw 그리기 +Drop Shadow Rectangle 그림자 있는 직사각형 +Duplicate 복제 +Duplicate Selected Slides 선택한 슬라이드 복제 +Duplicate Slide 슬라이드 복제 +Duration: 재생 시간: +Dynamic Content 동적 콘텐츠 +Edit 편집 +Edit Alt Text… 대체 텍스트 편집… +Edit Data 데이터 편집 +Edit Shape 도형 편집 +Edit Text 텍스트 편집 +Edit the data: first row = series names, first column = categories. 데이터 편집: 첫 번째 행은 계열 이름, 첫 번째 열은 항목입니다. +Edit\nData 데이터\n편집 +Edit\nShape 도형\n편집 +Editing 편집 옵션 +Effect Options 효과 옵션 +Effect\nOptions 효과\n옵션 +Effects 효과 +Email Address 이메일 주소 +Email address: 이메일 주소: +Emphasis 강조 +End 끝 +End Show 쇼 마침 +End of slide show 슬라이드 쇼 끝 +End of slide show, click to exit. 슬라이드 쇼가 끝났습니다. 종료하려면 클릭하세요. +End trim (s): 끝 지점 자르기 (초): +English (United Kingdom) 영어 (영국) +English (United States) 한국어 (대한민국) +Enter Enter +Entrance 나타내기 +Equals 등호 +Equation 수식 +Equation Shapes 수식 도형 +Eraser 지우개 +Escape Escape +Español Español +Exciting 화려한 효과 +Exit 끝내기 +Expand 확장 +Explosion: 14 Points 폭발: 14각 +Explosion: 8 Points 폭발: 8각 +Export 내보내기 +Export the presentation as: 프레젠테이션 내보내기 형식: +Exported {path} 내보내기 완료: {path} +Export… 내보내기… +Fade 밝기 변화 +Fade in (s): 페이드 인 (초): +Fade out (s): 페이드 아웃 (초): +Fall Over 넘어지기 +Ferris Wheel 대관람차 +File 파일 +Fill 채우기 +Fill & Line 채우기 및 선 +Fill Color 채우기 색 +Filled Radar 채워진 방사형 +Find… 찾기… +First 처음 +First Column 첫째 열 강조 +First Slide 첫 슬라이드 +First commit 첫 커밋 +Fit 창에 맞춤 +Fit slide to current window 현재 창 크기에 슬라이드 맞춤 +Fit to Window 창에 맞춤 +Fit to\nWindow 창에\n맞춤 +Flash 섬광 +Flip 대칭 이동 +Flip Horizontal 좌우 대칭 +Flip Vertical 상하 대칭 +Float In 떠오르기 +Float Out 가라앉기 +Flowchart 순서도 +Flowchart: Alternate Process 순서도: 대체 처리 +Flowchart: Card 순서도: 카드 +Flowchart: Collate 순서도: 조합 +Flowchart: Connector 순서도: 연결선 +Flowchart: Data 순서도: 데이터 +Flowchart: Decision 순서도: 판단 +Flowchart: Delay 순서도: 지연 +Flowchart: Direct Access Storage 순서도: 직접 액세스 저장소 +Flowchart: Display 순서도: 화면 표시 +Flowchart: Document 순서도: 문서 +Flowchart: Extract 순서도: 추출 +Flowchart: Internal Storage 순서도: 내부 저장소 +Flowchart: Magnetic Disk 순서도: 자기 디스크 +Flowchart: Manual Input 순서도: 수동 입력 +Flowchart: Manual Operation 순서도: 수동 작업 +Flowchart: Merge 순서도: 병합 +Flowchart: Multidocument 순서도: 다중 문서 +Flowchart: Off-page Connector 순서도: 다른 페이지 연결선 +Flowchart: Or 순서도: 논리합(OR) +Flowchart: Predefined Process 순서도: 정의된 처리 +Flowchart: Preparation 순서도: 준비 +Flowchart: Process 순서도: 처리 +Flowchart: Punched Tape 순서도: 천공 테이프 +Flowchart: Sequential Access Storage 순서도: 순차 액세스 저장소 +Flowchart: Sort 순서도: 정렬 +Flowchart: Stored Data 순서도: 저장 데이터 +Flowchart: Summing Junction 순서도: 합산 접합 +Flowchart: Terminator 순서도: 단말(시작/종료) +Fly In 날아오기 +Fly Out 날아가기 +Fly Through 통과하기 +Folded Corner 모서리가 접힌 도형 +Follow the layout 레이아웃 따르기 +Followed Hyperlink 열어본 하이퍼링크 +Font 글꼴 +Font Color 글꼴 색 +Font Size 글꼴 크기 +Fonts 글꼴 +Footer 바닥글 +Foreground 전경 +Format 서식 +Format Background 배경 서식 +Format Background… 배경 서식… +Format Painter 서식 복사 +Format Painter (double-click to keep it on) 서식 복사 (유지하려면 두 번 클릭) +Format Pane 서식 창 +Format Shape 도형 서식 +Format Shape… 도형 서식… +Format\nBackground 배경\n서식 +Format\nPane 서식\n창 +Formatting State 서식 상태 +Fracture 산산조각 +Frame 액자 +Français Français +Freeform 자유형 도형 +From 시작: +From Beginning 처음부터 +From Bottom 아래에서 +From Bottom Left 왼쪽 아래에서 +From Bottom Right 오른쪽 아래에서 +From Current Slide 현재 슬라이드부터 +From Left 왼쪽에서 +From Right 오른쪽에서 +From Top 위에서 +From Top Left 왼쪽 위에서 +From Top Right 오른쪽 위에서 +From bottom left 왼쪽 아래에서 +From bottom right 오른쪽 아래에서 +From center 가운데에서 +From top left 왼쪽 위에서 +From top right 오른쪽 위에서 +From\nBeginning 처음부터\n시작 +From\nCurrent Slide 현재 슬라이드\n부터 +Full 전체 +Full Page Slides 전체 페이지 슬라이드 +Funnel 깔때기형 +Gallery 갤러리 +General 일반 +Get Text 텍스트 가져오기 +Glitter 반짝임 +Glow 네온 +Go to 이동 +Go to Slide 슬라이드로 이동 +Grab bag 모음 +Gradient fill 그라데이션 채우기 +Gradient stops 그라데이션 중지점 +Gradient… 그라데이션… +Grayscale 회색조 +Greek 그리스 문자 +Green 초록 +Gridlines 눈금선 +Group 그룹 +Group Objects 개체 그룹화 +Grow & Turn 크기 변경 및 회전 +Grow/Shrink 크기 확대/축소 +Guides 안내선 +Half 절반 +Half Frame 반쪽 액자 +Handout Master 유인물 마스터 +Handout Master (coming soon) 유인물 마스터 (지원 예정) +Handout\nMaster 유인물\n마스터 +Handouts (1 slide per page) 유인물 (페이지당 1슬라이드) +Handouts (2 slides per page) 유인물 (페이지당 2슬라이드) +Handouts (3 slides per page) 유인물 (페이지당 3슬라이드) +Handouts (4 slides per page) 유인물 (페이지당 4슬라이드) +Handouts (6 slides per page) 유인물 (페이지당 6슬라이드) +Handouts (9 slides per page) 유인물 (페이지당 9슬라이드) +Header 머리글 +Header & Footer 머리글 및 바닥글 +Header &\nFooter 머리글 및\n바닥글 +Header Row 머리글 행 +Header and Footer 머리글 및 바닥글 +Header and Footer… 머리글 및 바닥글… +Heart 하트 +Height 높이 +Height (in): 높이 (인치): +Height: 높이: +Help 도움말 +Heptagon 칠각형 +Hexagon 육각형 +Hide All 모두 숨기기 +Hide Background Graphics 배경 그래픽 숨기기 +Hide During Show 쇼 동안 숨기기 +Hide Slide 슬라이드 숨기기 +Hide background graphics 배경 그래픽 숨기기 +Hide\nSlide 슬라이드\n숨기기 +Hierarchy 계층 구조형 +High 높음 +Highlighter 형광펜 +Histogram 히스토그램 +Home 시작 화면 +Honeycomb 벌집 +Horizontal 가로 +Horz In 가로 안으로 +Horz Out 가로 밖으로 +How would you describe this object and its context to someone who is blind or has low vision? 시각 장애가 있는 사용자에게 이 개체와 문맥을 어떻게 설명하시겠습니까? +Hyperlink 하이퍼링크 +Icons 아이콘 +In 안으로 +In Bounce 안으로 통통 튀기 +Include hidden slides 숨겨진 슬라이드 포함 +Increase Font Size 글꼴 크기 크게 +Increase List Level 목록 수준 증가 +Ink to Math 잉크를 수식으로 +Ink to Math (coming soon) 잉크를 수식으로 (지원 예정) +Ink to Shape 잉크를 도형으로 +Ink to Shape (coming soon) 잉크를 도형으로 (지원 예정) +Ink to Text 잉크를 텍스트로 +Ink to Text (coming soon) 잉크를 텍스트로 (지원 예정) +Ink to\nMath 잉크를\n수식으로 +Ink to\nShape 잉크를\n도형으로 +Ink to\nText 잉크를\n텍스트로 +Inner 안쪽 +Insert 삽입 +Insert Above 위에 삽입 +Insert Below 아래에 삽입 +Insert Hyperlink 하이퍼링크 삽입 +Insert Layout 레이아웃 삽입 +Insert Left 왼쪽에 삽입 +Insert Placeholder 개체 틀 삽입 +Insert Right 오른쪽에 삽입 +Insert Table 표 삽입 +Insert Table… 표 삽입… +Insert picture from file… 파일에서 그림 삽입… +Insert\nAbove 위에\n삽입 +Insert\nBelow 아래에\n삽입 +Insert\nLayout 레이아웃\n삽입 +Insert\nLeft 왼쪽에\n삽입 +Insert\nPlaceholder 개체 틀\n삽입 +Insert\nRight 오른쪽에\n삽입 +Inspect Presentation 프레젠테이션 정보 검사 +Inspect Shape 도형 정보 검사 +Inspect Slide 슬라이드 정보 검사 +Interface Language 인터페이스 언어 +Interface language: 인터페이스 언어: +Interface size 인터페이스 배율 +Isosceles Triangle 이등변 삼각형 +Italiano Italiano +Italic 기울임꼴 +JPEG image (current slide) JPEG 이미지 (현재 슬라이드) +Join our community on Discord Discord 커뮤니티 참여하기 +Join the ArtCraft community on Discord Discord에서 ArtCraft 커뮤니티 참여하기 +Justify 양쪽 맞춤 +L-Shape L자 도형 +Language 언어 +Last 마지막 +Last Column 줄무늬 마지막 열 +Last Slide 마지막 슬라이드 +Last commit 최근 커밋 +Layout 레이아웃 +Learn More 자세히 알아보기 +Learn\nMore 자세히\n알아보기 +Ledger Paper (11x17 in) Ledger 용지 (11×17 인치) +Left 왼쪽 +Left Brace 왼쪽 중괄호 +Left Bracket 왼쪽 대괄호 +Letter Paper (8.5x11 in) Letter 용지 (8.5×11 인치) +Liberation Serif Liberation Serif +Light Blue 연한 파랑 +Light Green 연한 초록 +Light/Dark appearance 밝은/어두운 테마 전환 +Lighten 밝게 +Lightning 번개 +Lightning Bolt 번개 +Line 선 +Line Color 선 색 +Line Spacing 줄 간격 +Line Spacing Options… 줄 간격 옵션… +Line delta 줄 변경량 +Line spacing (lines): 줄 간격 (줄): +Line with Markers 표식이 있는 꺾은선형 +Linear 선형 +Lines 선 +Lines +/− 코드 줄 +/− +Lines added 추가된 줄 +Lines deleted 삭제된 줄 +Link 링크 +Link… 링크… +List Commands 명령 목록 +Lock 잠금 +Long Dash 긴 파선 +Long Dash Dot 긴 일점쇄선 +Loop continuously until 'Esc' 'Esc' 키를 누를 때까지 계속 실행 +Loop until Stopped 멈출 때까지 반복 재생 +Loops 반복 +Loose 넓게 +Low 낮음 +MIT OR Apache-2.0 · © 2026 ArtCraft Team and the DeckCraft contributors MIT OR Apache-2.0 · © 2026 ArtCraft Team 및 DeckCraft 기여자 +Margins (pt) 여백 (pt) +Mark as decorative 장식용으로 표시 +Mark selected text as: 선택한 텍스트의 교정 언어 설정: +Math 수학 기호 +Matrix 행렬형 +Maximize content (else ensure fit) 콘텐츠 최대화 (또는 맞춤 유지) +Media 미디어 +Media Info 미디어 정보 +Medium 보통 +Merge Cells 셀 병합 +Merge Shapes 도형 병합 +Merge\nCells 셀\n병합 +Merge\nShapes 도형\n병합 +Merged PRs 병합된 PR +Middle 중간 +Minus Sign 뺄셈 기호 +Model 모델 +Models 모델 +Moon 달 +More Colors… # 다른 색… # +Morph 모핑 +Motion 이동 경로 +Motion Paths 이동 경로 +Move 이동 +Move Down 아래로 이동 +Move Insertion Point 삽입 지점 이동 +Move Section 구역 이동 +Move Section Down 구역 아래로 이동 +Move Section Up 구역 위로 이동 +Move Slide 슬라이드 이동 +Move Up 위로 이동 +Multiplication Sign 곱셈 기호 +Multiply 곱하기 +Mute 음소거 +Name 이름 +Name (A–Z) 이름순 (가–하) +Name: 이름: +New Comment 새 메모 +New Presentation 새 프레젠테이션 +New Slide 새 슬라이드 +New Slide (⇧⌘N) 새 슬라이드 (⇧⌘N) +New presentation 새 프레젠테이션 +New show name: 새 쇼 이름: +New\nComment 새\n메모 +New\nSlide 새\n슬라이드 +Next 다음 +Next Slide 다음 슬라이드 +Next Window 다음 창 +Next ▶ 다음 ▶ +No Color 색 없음 +No Fill 채우기 없음 +No Glow 네온 없음 +No Outline 윤곽선 없음 +No Reflection 반사 없음 +No Shadow 그림자 없음 +No Soft Edges 부드러운 가장자리 없음 +No accessibility issues found. People with disabilities should not have difficulty reading this document. 접근성 문제가 발견되지 않았습니다. 장애가 있는 사용자도 이 문서를 읽는 데 어려움이 없습니다. +No comments on this slide. 이 슬라이드에는 메모가 없습니다. +No contributor data was built into this copy. 이 빌드에는 기여자 데이터가 포함되지 않았습니다. +No fill 채우기 없음 +No line 선 없음 +No model credits were built into this copy. 이 빌드에는 모델 크레딧 정보가 포함되지 않았습니다. +No notes. 노트가 없습니다. +No sound: {err} 소리 없음: {err} +None 없음 +Normal 기본 보기 +Not Equal 같지 않음 +Not allowed 금지 +Notes 슬라이드 노트 +Notes Master 슬라이드 노트 마스터 +Notes Master (coming soon) 슬라이드 노트 마스터 (지원 예정) +Notes Page 슬라이드 노트 +Notes Pages 슬라이드 노트 페이지 +Notes\nMaster 슬라이드 노트\n마스터 +Notes\nPage 슬라이드\n노트 +Nudge 미세 이동 +Number of columns: 열 개수: +Number of rows: 행 개수: +Numbering 번호 매기기 +Object Color 개체 색 +Objects 개체 +Octagon 팔각형 +On Click 클릭할 때 +On Click of {name} {name} 클릭 시 +On Click of… (none) 클릭할 개체… (없음) +On Mouse Click 마우스 클릭 시 +On-screen Show (16:10) 화면 슬라이드 쇼 (16:10) +Open Data 데이터 열기 +Open Dialog 대화상자 열기 +Open Recovered Presentations 복구된 프레젠테이션 열기 +Open the sample deck 샘플 프레젠테이션 열기 +Open… 열기… +Orange 주황 +Orbit 궤도 +Order Objects 개체 순서 +Origami 종이접기 +Out 밖으로 +Out Bounce 밖으로 통통 튀기 +Outer 바깥쪽 +Outline 개요 +Outline (.txt) 개요 텍스트 (.txt) +Outline View 개요 보기 +Outline\nView 개요\n보기 +Oval 타원 +Oval ends 원형 끝 +Overhead 오버헤드 +PDF document (.pdf) PDF 문서 (.pdf) +PNG images (all slides) PNG 이미지 (모든 슬라이드) +PNG images (current slide) PNG 이미지 (현재 슬라이드) +PRs PR +Page Curl 페이지 말기 +Page Setup… 페이지 설정… +PageDown PageDown +PageUp PageUp +Pan 화면 이동 +Pane 창 +Paragraph 단락 +Paragraph… 단락… +Parallelogram 평행사변형 +Pareto 파레토 +Partial Circle 부분 원 +Particles 입자 +Paste 붙여넣기 +Paste (⌘V) 붙여넣기 (⌘V) +Paste Formatting 서식 붙여넣기 +Paste and Match Formatting 붙여넣고 서식 맞추기 +Paste or type an outline. Unindented lines become slide titles; tab-indented lines become bullets. 개요를 붙여넣거나 입력하세요. 들여쓰기 없는 줄은 슬라이드 제목이 되고, 탭으로 들여쓴 줄은 글머리 기호가 됩니다. +Pasted image.png 붙여넣은 이미지.png +Path 경로 +Pattern fill 패턴 채우기 +Pause 일시 중지 +Peek In 살짝 들여다보기 +Peek Out 숨기기 +Peel Off 벗겨내기 +Pencil 연필 +Pentagon 오각형 +Perspective 원근감 +Pick a graphic; type one item per line. 그래픽을 선택하고 한 줄에 항목을 하나씩 입력하세요. +Pick a look for your slides. Ideas are generated on your device. 슬라이드 디자인을 선택하세요. 아이디어는 기기에서 직접 생성됩니다. +Picture 그림 +Picture Format 그림 서식 +Picture Styles 그림 스타일 +Picture from File… 파일에서 그림… +Picture or texture fill 그림 또는 질감 채우기 +Picture\nStyles 그림\n스타일 +Pictures 그림 +Pie 원형 +Pin 고정 +Placeholder colour 개체 틀 색 +Plan\nBuild\nLaunch 계획\n구축\n출시 +Plaque 배지 +Play 재생 +Play Across Slides 슬라이드 간 계속 재생 +Play Full Screen 전체 화면 재생 +Play Narrations 설명 재생 +Play from Current Slide 현재 슬라이드부터 재생 +Play from Start 처음부터 재생 +Play from Start (F5) 처음부터 재생 (F5) +Play from\nCurrent Slide 현재 슬라이드\n부터 재생 +Play from\nStart 처음부터\n재생 +Play/Pause 재생/일시 중지 +Playback 재생 +Plus 더하기 +Plus In 십자 안으로 +Plus Out 십자 밖으로 +Plus Sign 덧셈 기호 +Portrait (9:16) 세로 방향 (9:16) +Português Português +Position 위치 +Position Objects 개체 위치 +Position and Size 위치 및 크기 +Post 게시 +Poster Frame 포스터 프레임 +PowerPoint Presentation (.pptx) PowerPoint 프레젠테이션 (.pptx) +Preferences 환경 설정 +Preferences… 환경 설정… +Presentation 프레젠테이션 +Presentations 프레젠테이션 +Presentations and slide shows, rebuilt from scratch in pure Rust. 순수 Rust로 처음부터 새로 구축한 프레젠테이션 및 슬라이드 쇼 도구입니다. +Presented by a speaker (full screen) 발표자 진행 (전체 화면) +Presenter View 발표자 도구 +Presenter View — DeckCraft 발표자 도구 — DeckCraft +Presenter\nView 발표자\n도구 +Preset gradients 미리 설정된 그라데이션 +Prestige 프레스티지 +Preview 미리 보기 +Previous Slide 이전 슬라이드 +Print layout: 인쇄 모양: +Properties 속성 +Pulse 진동 +Punctuation 문장 부호 +Purple 보라 +Push 밀어내기 +Pyramid 피라미드형 +Quick Styles 빠른 스타일 +Quick\nStyles 빠른\n스타일 +Radar 방사형 +Radial 방사형 +Random 임의 효과 +Random Bars 임의의 줄무늬 +Reading View 읽기용 보기 +Reading\nView 읽기용\n보기 +Real name 성명 +Recent 최근 항목 +Record 녹화 +Recording narration needs audio capture (coming soon) 설명 녹음에는 오디오 캡처가 필요합니다 (지원 예정) +Recovered Presentations 복구된 프레젠테이션 +Recovered {} from the last session 이전 세션에서 {} 복구됨 +Rectangle 직사각형 +Rectangle: Diagonal Corners Rounded 직사각형: 대각선 방향의 두 모서리가 둥근형 +Rectangle: Diagonal Corners Snipped 직사각형: 대각선 방향의 두 모서리가 잘린형 +Rectangle: Rounded Corners 직사각형: 모서리가 둥근형 +Rectangle: Single Corner Rounded 직사각형: 한쪽 모서리가 둥근형 +Rectangle: Single Corner Snipped 직사각형: 한쪽 모서리가 잘린형 +Rectangle: Top Corners One Rounded and One Snipped 직사각형: 위쪽 한 모서리는 둥글고 다른 모서리는 잘린형 +Rectangle: Top Corners Rounded 직사각형: 위쪽 두 모서리가 둥근형 +Rectangle: Top Corners Snipped 직사각형: 위쪽 두 모서리가 잘린형 +Rectangles 사각형 +Rectangular 사각형 +Red 빨강 +Red Pen 빨간색 펜 +Redo 다시 실행 +Redo (⌘Y) 다시 실행 (⌘Y) +Reflected Rounded Rectangle 반사 효과가 있는 모서리가 둥근 직사각형 +Reflection 반사 +Regroup 다시 그룹화 +Regular Pentagon 정오각형 +Rehearse Timings 예행 연습 +Rehearse\nTimings 예행\n연습 +Remove 제거 +Remove All Animations 모든 애니메이션 제거 +Remove All Sections 모든 구역 제거 +Remove Animation 애니메이션 제거 +Remove Background 배경 제거 +Remove Background (coming soon) 배경 제거 (지원 예정) +Remove Section 구역 제거 +Remove Section & Slides 구역 및 슬라이드 제거 +Remove stop 중지점 제거 +Remove\nBackground 배경\n제거 +Rename 이름 바꾸기 +Rename Layout 레이아웃 이름 바꾸기 +Rename Section 구역 이름 바꾸기 +Rename Section… 구역 이름 바꾸기… +Rename Slide 슬라이드 이름 바꾸기 +Rename… 이름 바꾸기… +Render Slide 슬라이드 렌더링 +Reopen 다시 열기 +Reorder Animation 애니메이션 순서 변경 +Reorder Objects 개체 순서 변경 +Replace straight quotes with smart quotes 직선 따옴표를 둥근 따옴표로 자동 변경 +Reply 답장 +Reply… 답장… +Reset 원래대로 +Reset Background 배경 원래대로 +Reset Crop 자르기 원래대로 +Reset Effects 효과 원래대로 +Reset Picture 그림 원래대로 +Reset Slide 슬라이드 원래대로 +Reset Slide Background 슬라이드 배경 원래대로 +Resize shape to fit text 텍스트에 맞춰 도형 크기 조정 +Resolve 해결됨으로 표시 +Resolve Thread 스레드 해결 +Return 돌아가기 +Reveal 드러내기 +Reverse the order 역순으로 +Revert 되돌리기 +Review 검토 +Rewind after Playing 재생 후 되감기 +Ribbon Tab 리본 탭 +Ribbon: Tilted Down 리본: 아래로 기울어짐 +Ribbon: Tilted Up 리본: 위로 기울어짐 +Right 오른쪽 +Right Brace 오른쪽 중괄호 +Right Bracket 오른쪽 대괄호 +Right Triangle 직각 삼각형 +Right-to-Left Text Direction 오른쪽에서 왼쪽으로 텍스트 방향 +Ring 고리 +Ripple 물결 +Rise Up 떠오르기 +Rotate 회전 +Rotate Left 90° 왼쪽으로 90° 회전 +Rotate Right 90° 오른쪽으로 90° 회전 +Rotate all text 270° 모든 텍스트 270° 회전 +Rotate all text 90° 모든 텍스트 90° 회전 +Rotate with shape 도형과 함께 텍스트 회전 +Rotation 회전 +Round Dot 원형 점선 +Rounded Diagonal Corner 대각선 모서리가 둥근 사각형 +Ruler 눈금자 +Saturation 채도 +Saturation {}% 채도 {}% +Save 저장 +Save (⌘S) 저장 (⌘S) +Save As… 다른 이름으로 저장… +Save AutoRecover Information 자동 복구 정보 저장 +Save as Template… 템플릿으로 저장… +Save to Data 데이터로 저장 +Saved 저장됨 +Saved {path} 저장됨: {path} +Scatter 분산형 +ScreenTip: 화면 설명: +Screenshot 스크린샷 +Screenshot (not available yet) 스크린샷 (지원 예정) +Scroll: Horizontal 두루마리: 가로 +Scroll: Vertical 두루마리: 세로 +Search commands (⇧⌘P) 명령 검색 (⇧⌘P) +Search commands… 명령 검색… +Section 구역 +Seek 탐색 +Select 선택 +Select All 모두 선택 +Select All Text 모든 텍스트 선택 +Select Cells 셀 선택 +Select Objects 개체 선택 +Select Paragraph 단락 선택 +Select Slides 슬라이드 선택 +Select Text 텍스트 선택 +Select Word 단어 선택 +Select a chart first. 먼저 차트를 선택하세요. +Select a shape to format it. 서식을 지정할 도형을 선택하세요. +Select an object on the slide and add an animation from the Animations tab. 슬라이드에서 개체를 선택한 뒤 [애니메이션] 탭에서 애니메이션을 추가하세요. +Selection 선택 +Selection Pane 선택 창 +Selection Pane… 선택 창… +Selection\nPane 선택\n창 +Send Backward 뒤로 보내기 +Send to Back 맨 뒤로 보내기 +Send\nBackward 뒤로\n보내기 +Sentence case. 문장 첫 글자만 대문자. +Set Text 텍스트 설정 +Set Up Show… 슬라이드 쇼 설정… +Set Up Slide Show 슬라이드 쇼 설정 +Set Up Slide Show… 슬라이드 쇼 설정… +Set Up\nSlide Show 슬라이드 쇼\n설정 +Set as Default Shape 기본 도형으로 설정 +Shading 음영 +Shadow 그림자 +Shape 도형 +Shape Effects 도형 효과 +Shape Fill 도형 채우기 +Shape Format 도형 서식 +Shape Gallery 도형 갤러리 +Shape Outline 도형 윤곽선 +Shape Styles 도형 스타일 +Shape {} 도형 {} +Shape\nStyles 도형\n스타일 +Shapes 도형 +Shift+ Shift+ +Shift+F9 Shift+F9 +Show 표시 +Show All 모두 표시 +Show Comments 메모 표시 +Show Media Controls 미디어 컨트롤 표시 +Show Slide 슬라이드 표시 +Show smart guides 스마트 안내선 표시 +Show type 쇼 형식 +Show without animation 애니메이션 없이 보기 +Show without narration 설명 없이 보기 +Show/Hide 표시/숨기기 +Show\nComments 메모\n표시 +Shred 조각내기 +Shrink & Turn 축소 및 회전 +Shrink text on overflow 넘칠 때 텍스트 크기 축소 +Simple Frame, White 단순 액자, 흰색 +Sink Down 가라앉기 +Size 크기 +Size & Properties 크기 및 속성 +Slide Master 슬라이드 마스터 +Slide Number 슬라이드 번호 +Slide Show 슬라이드 쇼 +Slide Size 슬라이드 크기 +Slide Sorter 슬라이드 여러 장 보기 +Slide number 슬라이드 번호 +Slide timings recorded. 슬라이드 진행 시간이 기록되었습니다. +Slide {:02}:{:04.1} Total {:02}:{:02} 슬라이드 {:02}:{:04.1} 전체 {:02}:{:02} +Slide {} of {} 슬라이드 {} / {} +Slide {}: {}{} 슬라이드 {}: {}{} +Slide {}: “{w}” 슬라이드 {}: “{w}” +Slide\nMaster 슬라이드\n마스터 +Slide\nNumber 슬라이드\n번호 +Slide\nSize 슬라이드\n크기 +Slide\nSorter 여러 장\n보기 +Slides (e.g. 1,3,5): 슬라이드 번호 (예: 1,3,5): +Slides from Outline 개요에서 슬라이드 가져오기 +Slides from Outline… 개요에서 슬라이드 가져오기… +Slides sized for: 슬라이드 크기 기준: +SmartArt SmartArt +Smile 미소 +Smiley Face 웃는 얼굴 +Smoothly 부드럽게 +Smoothly Left 부드럽게 왼쪽으로 +Smoothly Right 부드럽게 오른쪽으로 +Snap objects to grid 개체를 눈금에 맞춤 +Soft Edge Rectangle 부드러운 가장자리 직사각형 +Soft Edges 부드러운 가장자리 +Solid 실선 +Solid fill 단색 채우기 +Solid line 실선 +Sort 정렬 +Source on GitHub GitHub 소스 코드 +Space Space +Spacing after (pt): 단락 뒤 간격 (pt): +Spacing before (pt): 단락 앞 간격 (pt): +Special — hanging (in): 특수 — 내어쓰기 (인치): +Speech 말풍선 +Speech Bubble: Oval 말풍선: 타원형 +Speech Bubble: Rectangle 말풍선: 직사각형 +Speech Bubble: Rectangle with Corners Rounded 말풍선: 모서리가 둥근 직사각형 +Spelling 맞춤법 검사 +Spin 회전 +Spinner 스피너 회전 +Split 나누기 +Split Cells 셀 분할 +Split\nCells 셀\n분할 +Square 정사각형 +Square (1:1) 정사각형 (1:1) +Square Dot 사각 점선 +Stacked 쌓기 +Stacked Area 누적 영역형 +Stacked Bar 누적 가로 막대형 +Stacked Column 누적 세로 막대형 +Stacked Line 누적 꺾은선형 +Standard (4:3) 표준 (4:3) +Standard Colors 표준 색 +Star 별 +Star: 10 Points 별: 10각 +Star: 12 Points 별: 12각 +Star: 16 Points 별: 16각 +Star: 24 Points 별: 24각 +Star: 32 Points 별: 32각 +Star: 4 Points 별: 4각 +Star: 5 Points 별: 5각 +Star: 6 Points 별: 6각 +Star: 7 Points 별: 7각 +Star: 8 Points 별: 8각 +Stars and Banners 별 및 현수막 +Start After Previous 이전 효과 다음에 시작 +Start Automatically 자동 실행 +Start On Click 클릭할 때 시작 +Start With Previous 이전 효과와 함께 시작 +Start a conversation 대화를 시작하세요 +Start time (s): 시작 시간 (초): +Start: 시작: +Stealth end 스텔스 화살표 끝 +Stock 주식형 +Stop 중지 +Stop Editing Text 텍스트 편집 종료 +Strikethrough 취소선 +Strips 조각 무늬 +Style {i} 스타일 {i} +Subscript 아래 첨자 +Subtle 은은한 효과 +Sun 해 +Sunburst 선버스트 +Superscript 위 첨자 +Surface 표면형 +Switch 스위치 +Swivel 회전 +Symbol 기호 +System language 시스템 언어 +Tab Tab +Table 표 +Table Design 표 디자인 +Table Style Options 표 스타일 옵션 +Table Styles 표 스타일 +Table… 표… +Task Pane 작업창 +Teardrop 물방울 +Teeter 시소 +Text 텍스트 +Text Box 텍스트 상자 +Text Box Margins 텍스트 상자 여백 +Text Direction 텍스트 방향 +Text Effects 텍스트 효과 +Text Fill 텍스트 채우기 +Text Highlight Color 텍스트 강조 색 +Text Options 텍스트 옵션 +Text Outline 텍스트 윤곽선 +Text Shadow 텍스트 그림자 +Text/Background - Dark 1 텍스트/배경 - 어둡게 1 +Text/Background - Dark 2 텍스트/배경 - 어둡게 2 +Text/Background - Light 1 텍스트/배경 - 밝게 1 +Text/Background - Light 2 텍스트/배경 - 밝게 2 +Text\nBox 텍스트\n상자 +The spelling check is complete. No issues found. 맞춤법 검사를 완료했습니다. 발견된 오류가 없습니다. +Theme Colors 테마 색 +Theme Gallery 테마 갤러리 +Themes 테마 +Thesaurus 유의어 사전 +Thesaurus (coming soon) 유의어 사전 (지원 예정) +This Document 이 문서 +Thought 생각 풍선 +Thought Bubble: Cloud 생각 풍선: 구름형 +Through Black 검은색을 거쳐 +Tight 좁게 +Timing 진행 시간 +Top 위쪽 +Total Row 요약 행 +Transition 전환 +Transition Gallery 전환 갤러리 +Transitions 전환 +Translate 번역 +Translation needs a language model (not bundled) 번역 기능에는 언어 모델이 필요합니다 (기본 미포함) +Transparency 투명도 +Trapezoid 사다리꼴 +Treemap 트리맵 +Triangle 삼각형 +Trigger 트리거 +Trim 자르기 +Trim Media 미디어 자르기 +Turns 회전 +Type 종류 +Type Text 텍스트 입력 +Type an equation in linear form (e.g. a^2+b^2=c^2, x=(-b±√(b^2-4ac))/2a). 선형 형식으로 수식을 입력하세요 (예: a^2+b^2=c^2, x=(-b±√(b^2-4ac))/2a). +Uncover 나타내기(밀어내기) +Underline 밑줄 +Undo 실행 취소 +Undo (⌘Z) 실행 취소 (⌘Z) +Ungroup 그룹 해제 +Up 위로 +Use Timings 진행 시간 사용 +Use timings, if present 기록된 진행 시간 사용(있는 경우) +Username 사용자 이름 +Version 버전 +Version {} 버전 {} +Vert In 세로 안으로 +Vert Out 세로 밖으로 +Vertical 세로 +Vertical alignment 세로 맞춤 +Very Loose 매우 넓게 +Very Tight 매우 좁게 +Video 비디오 +Video from File… 파일에서 비디오… +View 보기 +Volume 볼륨 +Vortex 소용돌이 +Waterfall 폭포형 +Wave 물결 +Web Page or File 웹 페이지 또는 파일 +Wedge 쐐기 +Weight 두께 +Wheel 바퀴 +Widescreen (13.333x7.5 in) 와이드스크린 (13.333×7.5 인치) +Widescreen (16:9) 와이드스크린 (16:9) +Width 너비 +Width (in): 너비 (인치): +Width: 너비: +Width: 너비: +Wind 바람 +Window 창 +Wipe 닦아내기 +With Previous 이전 효과와 함께 +WordArt WordArt +WordArt style {} WordArt 스타일 {} +Words 단어 단위 +Wrap Text in Shape 도형에서 텍스트 줄 바꿈 +Wrap text in shape 도형에서 텍스트 줄 바꿈 +Yellow 노랑 +Your changes will be lost if you don't save them. 저장하지 않으면 변경 내용이 손실됩니다. +Your name (comments): 작성자 이름 (메모): +Zoom 확대/축소 +Zoom In 확대 +Zoom Out 축소 +Zoom… 확대/축소… +a) b) c) a) b) c) +a. b. c. a. b. c. +dropped file 드롭된 파일 +i. ii. iii. i. ii. iii. +no presentation open 열린 프레젠테이션 없음 +no slides 슬라이드 없음 +no text insertion point 텍스트 삽입 지점 없음 +nothing selected 선택된 개체 없음 +nothing to redo 다시 실행할 작업 없음 +nothing to undo 실행 취소할 작업 없음 +select text or a shape 텍스트 또는 도형을 선택하세요 +select two or more objects 두 개 이상의 개체를 선택하세요 +size (pt) 크기 (pt) +tOGGLE cASE 대/소문자 반전 +unknown interface language 알 수 없는 인터페이스 언어 +{ch} Bullet {ch} 글머리 기호 +{f} (Body) {f} (본문) +{f} (Headings) {f} (제목) +{name}: DeckCraft can't open this kind of file {name}: DeckCraft에서 열 수 없는 파일 형식입니다 +{n} Column{} {n}단{} +{r} pt {r} pt +{r} pt glow 네온 {r} pt +{w} pt {w} pt +{} ({} slides) {} ({}개 슬라이드) +{} Slide Master {} 슬라이드 마스터 +{} contributors · {} commits 기여자 {}명 · 커밋 {}건 +{} presentations {}개의 프레젠테이션 +{}x{} Table {}×{} 표 +“{}” is not available yet. “{}” 기능은 아직 지원되지 않습니다. +−Bin −구간 +▶ Play All ▶ 모두 재생 +◀ Previous ◀ 이전 +✏ on ✏ 켜짐 +Arrow: Circular 화살표: 원형 +Arrow: Circular Counter-clockwise 화살표: 반시계 방향 원형 +Arrow: Circular Two-headed 화살표: 양방향 원형 +Arrow: Curved Down 화살표: 아래로 휜 형태 +Arrow: Curved Left 화살표: 왼쪽으로 휜 형태 +Arrow: Curved Right 화살표: 오른쪽으로 휜 형태 +Arrow: Curved Up 화살표: 위로 휜 형태 +Arrow: Swoosh 화살표: 급회전 +Artistic Effects 꾸미기 효과 +AutoCorrect Options 자동 고침 옵션 +AutoCorrect Options… 자동 고침 옵션… +Bean 콩 모양 +Black and White: 25% 흑백: 25% +Black and White: 50% 흑백: 50% +Black and White: 75% 흑백: 75% +Borders 테두리 +Bounce Left 왼쪽으로 통통 튀기 +Bounce Right 오른쪽으로 통통 튀기 +Browse for Themes… 테마 찾아보기… +Callout: Bent Line 설명선: 꺾인 선 +Callout: Bent Line with Accent Bar 설명선: 강조 표시줄이 있는 꺾인 선 +Callout: Bent Line with Border and Accent Bar 설명선: 테두리와 강조 표시줄이 있는 꺾인 선 +Callout: Bent Line with No Border 설명선: 테두리 없는 꺾인 선 +Callout: Double Bent Line 설명선: 이중 꺾인 선 +Callout: Double Bent Line with Accent Bar 설명선: 강조 표시줄이 있는 이중 꺾인 선 +Callout: Double Bent Line with Border and Accent Bar 설명선: 테두리와 강조 표시줄이 있는 이중 꺾인 선 +Callout: Double Bent Line with No Border 설명선: 테두리 없는 이중 꺾인 선 +Callout: Left-Right Arrow 설명선: 좌우 화살표 +Callout: Line with Accent Bar 설명선: 강조 표시줄이 있는 선 +Callout: Line with Border and Accent Bar 설명선: 테두리와 강조 표시줄이 있는 선 +Callout: Line with No Border 설명선: 테두리 없는 선 +Callout: Quad Arrow 설명선: 사방 화살표 +Callout: Up-Down Arrow 설명선: 상하 화살표 +Cell Margins 셀 여백 +Change Colors 색 변경 +Change Layout 레이아웃 변경 +Change Picture… 그림 변경… +Change Spelling 맞춤법 변경 +Chart Marker: Cross Lines 차트 표식: X자 선 +Chart Marker: Plus Lines 차트 표식: 십자 선 +Chart Marker: Star Lines 차트 표식: 별 모양 선 +Commit Composition 조합 입력 확정 +Compare 비교 +Compare… 비교… +Compose Text 텍스트 조합 입력 +Compress Pictures 그림 압축 +Convert to Shapes 도형으로 변환 +Convert to SmartArt SmartArt로 변환 +Convert to Text 텍스트로 변환 +Corner Tabs: Quarter Circles 모서리 탭: 사분원 +Corner Tabs: Squares 모서리 탭: 정사각형 +Corner Tabs: Triangles 모서리 탭: 삼각형 +Flowchart: Offline Storage 순서도: 오프라인 저장소 +Gear: 6 Teeth 톱니바퀴: 6톱니 +Gear: 9 Teeth 톱니바퀴: 9톱니 +Line: Rising 선: 상승선 +Quarter Wedge 사분원 쐐기 +Ribbon: Curved and Tilted Down 리본: 아래로 기울어진 곡선 +Ribbon: Curved and Tilted Up 리본: 위로 기울어진 곡선 +Ribbon: Left-Right Fold 리본: 좌우 접기 +Trapezoid: Uneven Sides 사다리꼴: 비대칭 변 +Unlock 잠금 해제 +Print 인쇄 +Info 정보 +Print… 인쇄… +Counterclockwise 반시계 방향 +Crescent Moon 초승달 +Curved Square 곡선 정사각형 +Customize Quick Access Toolbar 빠른 실행 도구 모음 사용자 지정 +Decaying Wave 감쇠 파동 +Delete Guide 안내선 삭제 +Delete Point 점 삭제 +Delete Repeated Word 반복된 단어 삭제 +Demote 수준 낮추기 +Diagonal Down Right 오른쪽 아래 대각선 +Diagonal Up Right 오른쪽 위 대각선 +Diamonds From Bottom 아래에서 다이아몬드 +Diamonds From Left 왼쪽에서 다이아몬드 +Diamonds From Right 오른쪽에서 다이아몬드 +Diamonds From Top 위에서 다이아몬드 +Do not check spelling or grammar 맞춤법 또는 문법 검사 안 함 +dictionary 사전 +Double Left 왼쪽 이중 +Double Right 오른쪽 이중 +Draw Table 표 그리기 +Edit Photo Album… 사진 앨범 편집… +Edit Points 점 편집 +Edit SmartArt Text SmartArt 텍스트 편집 +End Review 검토 끝내기 +Enter a zoom percent 확대/축소 비율(%) 입력 +Find 찾기 +Fonts in Presentation 프레젠테이션의 글꼴 +Font… 글꼴… +Football 럭비공 +From Bottom-Left 왼쪽 아래에서 +From Bottom-Right 오른쪽 아래에서 +From Top-Left 왼쪽 위에서 +From Top-Right 오른쪽 위에서 +Grid and Guides 눈금 및 안내선 +Grid Options… 눈금 옵션… +Handout Placeholders 유인물 개체 틀 +Heartbeat 심장 박동 +Hexagons From Bottom 아래에서 육각형 +Hexagons From Left 왼쪽에서 육각형 +Hexagons From Right 오른쪽에서 육각형 +Hexagons From Top 위에서 육각형 +Horizontal Figure 8 가로 8자 모양 +Horizontal In 가로 안쪽으로 +Horizontal Out 가로 바깥쪽으로 +Icon Gallery 아이콘 갤러리 +Ignore All 모두 무시 +In with Bounce 안쪽으로 통통 튀기 +Ink to Shape: turn drawn lines, rectangles, ovals and triangles into shapes 잉크를 도형으로: 그린 선, 직사각형, 타원, 삼각형을 도형으로 변환 +Insert Picture in SmartArt SmartArt에 그림 삽입 +Insert Picture in SmartArt… SmartArt에 그림 삽입… +Insert Synonym 유의어 삽입 +Layout Placeholders 레이아웃 개체 틀 +Loop de Loop 연속 회전 고리 +Move Guide 안내선 이동 +Move Handle 조절점 이동 +Move Point 점 이동 +Neutron 중성자 궤도 +New 새로 만들기 +New Window 새 창 +Next Comment 다음 메모 +No comments. 메모가 없습니다. +No icons match. 일치하는 아이콘이 없습니다. +No Recolor 다시 칠하기 없음 +Notes Placeholders 슬라이드 노트 개체 틀 +Office Theme 기본 오피스 테마 +Open 열기 +Out with Bounce 바깥쪽으로 통통 튀기 +Particles In 입자 모이기 +Particles Out 입자 흩어지기 +Paste Special 선택하여 붙여넣기 +Paste Special… 선택하여 붙여넣기… +Peanut 땅콩 모양 +Percent: 비율(%): +Photo Album… 사진 앨범… +Pick a design for this slide. Ideas are generated on your device. 이 슬라이드의 디자인을 선택하세요. 디자인 아이디어는 기기에서 직접 생성됩니다. +Picture Border 그림 테두리 +Picture Style Gallery 그림 스타일 갤러리 +Point Type 점 유형 +Points 점 +Preserve 보존 +Preview Animations 애니메이션 미리 보기 +Previous Comment 이전 메모 +Promote 수준 높이기 +Quick Access Toolbar 빠른 실행 도구 모음 +Recolor 다시 칠하기 +Recolor Gallery 다시 칠하기 갤러리 +Remove from Quick Access Toolbar 빠른 실행 도구 모음에서 제거 +Remove Shape 도형 제거 +Repeated word 반복된 단어 +Replace 바꾸기 +Replace Fonts 글꼴 바꾸기 +Replace… 바꾸기… +Rescan Installed Fonts 설치된 글꼴 다시 검색 +Reset to Defaults 기본값으로 복원 +Reuse Slides… 슬라이드 다시 사용… +Revisions 수정 내역 +Right-click any ribbon button to add it. 리본 단추를 마우스 오른쪽 단추로 클릭하여 추가하세요. +S Curve 1 S자 곡선 1 +S Curve 2 S자 곡선 2 +Sepia 세피아 +Set Transparent Color 투명한 색 설정 +Set Zoom 확대/축소 설정 +Sine Wave 사인파 +Single Left 왼쪽 단일 +Single Right 오른쪽 단일 +Size and Position… 크기 및 위치… +SmartArt Styles SmartArt 스타일 +Smooth Corners 부드러운 모서리 +Smoothly From Left 왼쪽에서 부드럽게 +Smoothly From Right 오른쪽에서 부드럽게 +Sound 소리 +Spelling Dictionary 맞춤법 사전 +Spelling is checked for languages marked (dictionary). (사전)으로 표시된 언어에 대해 맞춤법을 검사합니다. +Spiral Left 왼쪽 나선형 +Spiral Right 오른쪽 나선형 +Spring 용수철 +Stairs Down 계단 내려가기 +Start from Beginning 처음부터 시작 +Starter Templates 시작 템플릿 +Strips In 안쪽으로 줄무늬 +Strips Out 바깥쪽으로 줄무늬 +Swoosh 급회전 +Table Eraser 표 지우개 +Tabs… 탭… +Text Pane 텍스트 창 +Text Transform 텍스트 변환 +Text Transparency 텍스트 투명도 +Themes and presentations 테마 및 프레젠테이션 +Through Black From Left 검은 화면을 거쳐 왼쪽에서 +Through Black From Right 검은 화면을 거쳐 오른쪽에서 +Variant Gallery 변형 갤러리 +Variants 변형 +Vertical Figure 8 세로 8자 모양 +Vertical In 세로 안쪽으로 +Vertical Out 세로 바깥쪽으로 +Washout 희미하게 +Zigzag 지그재그 +Zoom and Rotate 확대/축소 및 회전 +Zoom to 확대/축소 비율 +■ Stop ■ 정지 +circle 원 +diamond 다이아몬드 +plus 십자(+) +▶ Play From ▶ 선택한 위치부터 재생 +Pattern 패턴 +List Slides to Reuse 다시 사용할 슬라이드 목록 +Delete Segment 선분 삭제 +Segment Type 선분 유형 +Open Path 경로 열기 +Close Path 경로 닫기 +Reject Revision 수정 내용 거부 +Duration 재생 시간 +Delay 지연 시간 +Delete Surrounding Text 주변 텍스트 삭제 +{} options {} 옵션 +Missing slide title 슬라이드 제목 누락 +Missing alternative text 대체 텍스트 누락 +Hard-to-read text contrast ({}:1) 텍스트 대비가 낮아 읽기 어려움 ({}:1) +Apply to: 적용 대상: +Selected text 선택한 텍스트 +Whole presentation 전체 프레젠테이션 +Measurement units: 측정 단위: +Inches 인치(in) +Centimeters 센티미터(cm) +Select Slide Range 슬라이드 범위 선택 +Language for All Text 모든 텍스트의 언어 +Mark all text in the presentation as: 프레젠테이션의 모든 텍스트 언어 표시: +Check for Updates… 업데이트 확인… diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index c3497895b..54fea4204 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -1392,6 +1392,7 @@ impl SlideApp { "en" => i18n::Language::En, "uk" => i18n::Language::Uk, "pl" => i18n::Language::Pl, + "ko" => i18n::Language::Ko, _ => return Err("unknown interface language".into()), }; i18n::set_current(self.ui.interface_language.resolve(self.services.system_locale.as_deref())); diff --git a/docs/localization-parity.md b/docs/localization-parity.md index f07a9e4a4..8beeddbe5 100644 --- a/docs/localization-parity.md +++ b/docs/localization-parity.md @@ -3,8 +3,8 @@ > **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-25 · **Change:** minor (Polish UI catalog; previous: minor, opt-in native LXGW WenKai Lite fallback and interface CJK tests; local implementation only, managed validation pending; previous: minor, IME composition underlined in table cells; previous: minor, IME composition and CJK interface text, #101; previous: major, created) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) > **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-21 · **Change:** minor (one proofing language for the whole presentation; previous: minor, IME composition underlined in table cells; previous: minor, IME composition and CJK interface text, #101; previous: major, created) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) -DeckCraft's interface supports English, Ukrainian and Polish through `crates/ui-egui/src/i18n/uk.tsv` -and `crates/ui-egui/src/i18n/pl.tsv`. +DeckCraft's interface supports English, Ukrainian, Polish and Korean through `crates/ui-egui/src/i18n/uk.tsv`, +`crates/ui-egui/src/i18n/pl.tsv` and `crates/ui-egui/src/i18n/ko.tsv`. Preferences offers the system language or an explicit choice; desktop choices persist across restarts. Menus, ribbon, command palette, panels, dialogs and presenter controls are translated, with English fallback. Presentation content, custom placeholder prompts and command IDs keep @@ -40,7 +40,7 @@ support partly there), **55–90 h** to parity plus native-speaker review. | Indonesian | id | 0 (0%) | no | full (Latin) | no | none | yes | 3–5 | | Japanese | ja | 0 (0%) | no | renders with system or craft-fonts Japanese faces; IME composition (#101); vertical text is rotated, not laid out vertically | no | none | yes | 4–7 | | German | de | 0 (0%) | no | full (Latin) | no | none | yes | 3–5 | -| Korean | ko | 0 (0%) | no | system Hangul fonts, in the interface too; IME composition (#101) | no | none | yes | 4–7 | +| Korean | ko | UI catalog (1,486 entries); coverage checked by tests | UI captions; diagnostics retain original wording | system Hangul fonts, in the interface too; IME composition (#101) | yes | available; English fallback | yes | — | | Vietnamese | vi | 0 (0%) | no | stacked diacritics through HarfRust | no | none | **no** (Windows only) | 3–5 | Other languages shipped: **2 (Ukrainian, Polish)**. The Polish catalog (`i18n/pl.tsv`) has the same 1,232 keys as the Ukrainian one, its own integer-count forms, and no second native-speaker review yet. PowerPoint for Mac ships 16 more