diff --git a/ROADMAP.md b/ROADMAP.md index a993692560..12575f6e27 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -16,6 +16,8 @@ > **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-25 · **Change:** trivial (progress log: a picture in a picture placeholder is cut into the layout's shape; previous: trivial, progress log: picture and embedded object fills; previous: trivial, scroll bars on the zoomed slide pane, #1157; previous: minor, native chart payloads stored in bounded ZIP members; previous: major, community PR wave 2026-10-14: ~60 landed items in the progress log, among them the Font, Paragraph, Photo Album, theme colour and font and Date and Time dialogs, presenter and show controls, the SmartArt Text Pane and PPTX corpus slices; breadth 84% after `cargo xtask parity`, ready stays ~55%, stage stays alpha; duplicated rows and headers folded; previous: minor, hours to ~95% per audience added) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) > **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-22 · **Change:** trivial (fill and outline edits reach group members, #1079; stage and scores unchanged) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) > **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-20 · **Change:** trivial (empty chart number-format sections hide values; previous: minor, native chart payloads stored in bounded ZIP members; previous: major, community PR wave 2026-10-14: ~60 landed items in the progress log, among them the Font, Paragraph, Photo Album, theme colour and font and Date and Time dialogs, presenter and show controls, the SmartArt Text Pane and PPTX corpus slices; breadth 84% after `cargo xtask parity`, ready stays ~55%, stage stays alpha; duplicated rows and headers folded; previous: minor, hours to ~95% per audience added) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) +> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-27 · **Change:** minor (Insert › Date and Time: Japanese date and time fields corrected to PowerPoint 16.0's measured `ja-JP` formats — the system short and long date for `datetime1`/`datetime2`, the 令和 era for `datetime3`/`datetime6`/`datetime7`, `時`/`分`/`秒` for `datetime8`/`datetime9`/`datetime12`/`datetime13`; a bare `ja` tag is not expanded; previous: minor, Insert › Date and Time: Japanese date and time fields; +> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-20 · **Change:** minor (Insert › Date and Time: Japanese date and time fields; previous: minor, native chart payloads stored in bounded ZIP members; previous: major, community PR wave 2026-10-14: ~60 landed items in the progress log, among them the Font, Paragraph, Photo Album, theme colour and font and Date and Time dialogs, presenter and show controls, the SmartArt Text Pane and PPTX corpus slices; breadth 84% after `cargo xtask parity`, ready stays ~55%, stage stays alpha; duplicated rows and headers folded; previous: minor, hours to ~95% per audience added) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) > **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-18 · **Change:** minor (Symbol-font characters (U+F0xx) remap to the installed Symbol face's real Unicode on macOS, so they draw from Symbol instead of a look-alike; previous: minor, native chart payloads stored in bounded ZIP members; previous: major, community PR wave 2026-10-14: ~60 landed items in the progress log, among them the Font, Paragraph, Photo Album, theme colour and font and Date and Time dialogs, presenter and show controls, the SmartArt Text Pane and PPTX corpus slices; breadth 84% after `cargo xtask parity`, ready stays ~55%, stage stays alpha; duplicated rows and headers folded; previous: minor, hours to ~95% per audience added) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) > **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-21 · **Change:** minor (proofing language for the whole presentation; previous: minor, native chart payloads stored in bounded ZIP members; previous: major, community PR wave 2026-10-14: ~60 landed items in the progress log, among them the Font, Paragraph, Photo Album, theme colour and font and Date and Time dialogs, presenter and show controls, the SmartArt Text Pane and PPTX corpus slices; breadth 84% after `cargo xtask parity`, ready stays ~55%, stage stays alpha; duplicated rows and headers folded; previous: minor, hours to ~95% per audience added) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) @@ -148,12 +150,14 @@ app's JSON control channel (`deckcraft --control PORT`). |---|---| | 2026-10-29 | Slide Show: going back to the previous slide plays the transition of the slide being left, reversed, as PowerPoint does, instead of the previous slide's own entrance transition (#1531); a jump (Go to Slide, links) still plays the target slide's transition | | 2026-10-27 | Set Up Slide Show ▸ Keep mouse cursor visible: a full-screen show no longer hides a still pointer, so the choices of a branching deck stay visible and clickable; the setting is kept with the user's preferences (`ui.json`, `showKeepCursor`), off by default (#1445) | +| 2026-10-27 | Insert › Date and Time: Japanese date and time fields follow PowerPoint 16.0 (`lang="ja-JP"`, measured 2026-10-27 19:43:50 on an en-US Windows): `datetime1`/`datetime2` are the system regional short and long date, `datetime3`/`datetime6`/`datetime7` use the 令和 era (kanji numerals for `datetime7`), `datetime8`/`datetime9`/`datetime12`/`datetime13` use `時`/`分`/`秒`, and `datetime5` keeps `27-Oct-26`. No kind shows a Japanese weekday, a bare `lang="ja"` is not expanded, and English text is unchanged | | 2026-10-25 | A photo in a picture placeholder is cut into the shape its layout gives the placeholder, as PowerPoint draws it: a layout's round picture placeholder keeps a slide's photo in a circle with the layout's white border, where it came out as a rectangle (the reader only passed `p:sp` placeholders to `inherit_placeholder_geometry`, not `p:pic`) | | 2026-10-28 | CLI `run --cmd-file FILE` reads UTF-8 commands in order alongside `--cmd`, skips blank/comment lines, and reports file/line errors before final save/export (#1098) | | 2026-10-28 | MCP accepts repeated `--automation-read-root` and `--automation-write-root` flags for access to multiple folders (#1154) | | 2026-10-26 | Slide reorder: dragging one of several selected slides in the thumbnail pane or the Slide Sorter moves the whole selection, as one run in deck order and one undo step; the slides stay selected and join the section they land in. `slide.move {ids, to}` does the same from the control channel and MCP; dragging an unselected slide still moves only that slide | | 2026-10-26 | A space, digit or punctuation after a symbol-font character (`a:sym`, e.g. a Wingdings arrow typed in a run) draws in the run's own font, as PowerPoint does; it stayed in the symbol font, whose space is about four times as wide (synthetic probe) | | 2026-10-29 | Review ▸ Accessibility checks text runs in every paragraph and nested group descendants for low contrast, reporting each affected shape once; the all-black control stays clear | + | 2026-10-25 | Windows installer: the Start menu shortcut takes the icon embedded in `deckcraft.exe` instead of an MSI ``, so a taskbar pin keeps its icon after an update (#1114); a pin that already lost it has to be unpinned and pinned again once | | 2026-10-29 | Picture Format: the Picture Styles row shows as many styles as leave room for the groups after it, and its arrow opens all 24. With every style in the row, Crop and Size sat past the ribbon's edge in any window narrower than about 2500 px, so Crop could not be seen without scrolling the ribbon (#1232). In the default 1440 px window Crop, Height and Width are now in view, and from 1600 px the whole tab fits | | 2026-10-24 | AutoFormat as you type: a paragraph that starts with `*`, `-`, `--`, `>`, `<>`, `1.`, `1)`, `I.`, `i.`, `A.`, `a.` or `a)` and a space or tab becomes a bulleted or numbered list in that style; Backspace right after puts the typed text back, and Tools ▸ AutoCorrect and `text.autocorrect {lists}` turn it off | @@ -316,6 +320,7 @@ app's JSON control channel (`deckcraft --control PORT`). |---|---|---| | 2026-10-29 | trivial | Progress log: going back in the slide show reverses the left slide's transition (#1531). Scores and milestone names unchanged | | 2026-10-29 | trivial | Progress log: the Picture Styles row leaves room for Crop and Size (#1232). Scores and milestone names unchanged | +| 2026-10-27 | minor | Insert › Date and Time: the `ja-JP` formats follow PowerPoint 16.0 (`datetime1`/`2` system dates, `datetime3`/`6`/`7` the 令和 era, `datetime8`/`9`/`12`/`13` `時`/`分`/`秒`); a bare `ja` tag is not expanded; English text unchanged | | 2026-10-25 | trivial | Progress log: a picture in a picture placeholder takes the layout's shape. Scores and milestone names unchanged | | 2026-10-28 | trivial | Progress log: CLI command-file input (#1098). Scores and milestone scope unchanged. | | 2026-10-28 | trivial | Progress log: MCP accepts multiple automation read and write roots (#1154). Scores and milestone names unchanged | diff --git a/crates/engine/src/cmd/insert/text_fields.rs b/crates/engine/src/cmd/insert/text_fields.rs index e90c7c898f..97ed37b477 100644 --- a/crates/engine/src/cmd/insert/text_fields.rs +++ b/crates/engine/src/cmd/insert/text_fields.rs @@ -110,7 +110,8 @@ fn date_opts(p: &Value) -> Result { } /// The text `format` shows now in `lang`. Errors when that language has no names for the format -/// (month names and AM/PM are English only). An empty string where there is no clock (wasm). +/// (English and `ja-JP` compute every kind; other languages only the numeric date and the +/// 24-hour time). An empty string where there is no clock (wasm). fn date_sample(format: &str, lang: Option<&str>) -> Result { use deckcraft_text::datetime::{DateTime, field_text}; let probe = DateTime::new(2026, 10, 9, 20, 5, 7); diff --git a/crates/text/src/datetime.rs b/crates/text/src/datetime.rs index e2f6b23cdc..c5fbabfacb 100644 --- a/crates/text/src/datetime.rs +++ b/crates/text/src/datetime.rs @@ -3,9 +3,10 @@ //! //! The field types and their formats follow ECMA-376 Part 1, §21.1.2.2.4 (`fld`): `datetime1` //! to `datetime13`, written as in PowerPoint's English (United States) Date and Time list. -//! `datetime` and `datetimeFigureOut` show the language's short date. Numeric dates follow the -//! run's language; formats with month or day names, or AM/PM, are only computed for English, so -//! other languages keep the text the file was saved with. +//! `datetime` and `datetimeFigureOut` show the language's short date. English keeps its exact +//! text; `ja-JP` follows what PowerPoint 16.0 on Windows showed with `lang="ja-JP"` (see +//! [`japanese_field_text`]); every other language keeps the text the file was saved with when +//! the format needs names. /// A local date and time: what the clock read, or a fixed moment (tests, deterministic output). #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -85,24 +86,39 @@ pub const DATE_KINDS: [&str; 13] = [ /// The text date/time field `kind` shows at `now` in a run of language `lang` (BCP 47, e.g. /// `en-US`). `None` when `kind` isn't a date/time type this knows, `now` isn't a real date, or -/// the format needs names in a language other than English: the field keeps its saved text. +/// the format needs names in a language other than English or Japanese: the field keeps its +/// saved text. pub fn field_text(kind: &str, lang: Option<&str>, now: DateTime) -> Option { if !now.is_valid() { return None; } let lang = lang.map(str::trim).filter(|l| !l.is_empty()).unwrap_or("en-US").to_ascii_lowercase(); + if is_ja_jp(&lang) { + return japanese_field_text(kind, now); + } let english = lang == "en" || lang.starts_with("en-"); + let (h, mi, s) = (now.hour, now.minute, now.second); + if !english { + // A language without month, weekday and AM/PM names keeps the field's saved text for + // every format that needs them. The numeric date follows the language's date order and + // the 24-hour clock is language-neutral, so those are still computed. + return match kind { + "datetime" | "datetimeFigureOut" | "datetime1" => Some(short_date(&lang, now)), + "datetime10" => Some(format!("{h}:{mi:02}")), + "datetime11" => Some(format!("{h}:{mi:02}:{s:02}")), + _ => None, + }; + } let month = MONTHS.get(now.month.checked_sub(1)? as usize)?; let mon = month.get(..3)?; let day = DAYS.get(now.weekday())?; let (d, y) = (now.day, now.year); let yy = y.rem_euclid(100); - let h12 = match now.hour % 12 { + let h12 = match h % 12 { 0 => 12, h => h, }; - let ampm = if now.hour < 12 { "AM" } else { "PM" }; - let (mi, s) = (now.minute, now.second); + let ampm = if h < 12 { "AM" } else { "PM" }; let short = short_date(&lang, now); // British-style English writes the day before the month ("9 October 2026"). The punctuation // follows PowerPoint on Windows per locale: Irish English has no comma after the weekday, @@ -128,10 +144,100 @@ pub fn field_text(kind: &str, lang: Option<&str>, now: DateTime) -> Option Some(format!("{}:{mi:02}:{s:02}", now.hour)), "datetime12" => named(format!("{h12}:{mi:02} {ampm}")), "datetime13" => named(format!("{h12}:{mi:02}:{s:02} {ampm}")), + + _ => None, + } +} + +/// `kind` in Japanese, as PowerPoint 16.0 on Windows showed it with `lang="ja-JP"` at +/// 2026-10-27 19:43:50 on an en-US machine (measured 2026-10-27): +/// +/// ```text +/// datetime1 10/27/2026 +/// datetime2 Tuesday, October 27, 2026 +/// datetime3 令和8年10月27日 +/// datetime4 2026年10月27日 +/// datetime5 27-Oct-26 +/// datetime6 令和8年10月 +/// datetime7 令和八年十月二十七日 +/// datetime8 26/10/27 19時43分 +/// datetime9 26/10/27 19時43分50秒 +/// datetime10 19:43 +/// datetime11 19:43:50 +/// datetime12 19時43分 +/// datetime13 19時43分50秒 +/// ``` +/// +/// No kind showed a Japanese weekday. `datetime1` and `datetime2` are the operating system's +/// regional short and long dates rather than Japanese patterns: on the measured en-US machine +/// they came out as the English ones, and they would change with the regional settings of the +/// machine, not with the run's language. DeckCraft has no locale-aware date formatter (`chrono` +/// carries no month names) and no view of the OS regional settings, so the two use the measured +/// en-US shapes; the rest use the measured Japanese patterns. A bare `lang="ja"` did not get +/// these formats from PowerPoint and does not get them here (see [`is_ja_jp`]). +fn japanese_field_text(kind: &str, now: DateTime) -> Option { + let (y, mo, d) = (now.year, now.month, now.day); + let (h, mi, s) = (now.hour, now.minute, now.second); + let month_en = MONTHS.get(mo.checked_sub(1)? as usize)?; + let mon = month_en.get(..3)?; + let yy = y.rem_euclid(100); + match kind { + // The OS regional dates: measured as the en-US short and long date. + "datetime" | "datetimeFigureOut" | "datetime1" => Some(system_short_date(now)), + "datetime2" => system_long_date(now), + // The Japanese era (令和) in Arabic numerals. + "datetime3" => { + let era = era_year_text(now)?; + Some(format!("令和{era}年{mo}月{d}日")) + } + // The Gregorian date, unchanged by the era. + "datetime4" => Some(format!("{y}年{mo}月{d}日")), + // PowerPoint kept the English month abbreviation for this one. + "datetime5" => Some(format!("{d}-{mon}-{yy:02}")), + "datetime6" => { + let era = era_year_text(now)?; + Some(format!("令和{era}年{mo}月")) + } + // The era date in kanji numerals: 令和八年十月二十七日. + "datetime7" => { + let era = era_year_kanji(now)?; + Some(format!("令和{era}年{}月{}日", kanji_number(mo)?, kanji_number(d)?)) + } + // Two-digit year, then 時/分/秒; the hour is not zero-padded, minute and second are. + "datetime8" => Some(format!("{yy:02}/{mo:02}/{d:02} {h}時{mi:02}分")), + "datetime9" => Some(format!("{yy:02}/{mo:02}/{d:02} {h}時{mi:02}分{s:02}秒")), + "datetime10" => Some(format!("{h}:{mi:02}")), + "datetime11" => Some(format!("{h}:{mi:02}:{s:02}")), + "datetime12" => Some(format!("{h}時{mi:02}分")), + "datetime13" => Some(format!("{h}時{mi:02}分{s:02}秒")), _ => None, } } +/// Is `lang` (lower case) the Japanese tag PowerPoint expands date/time fields for? Only +/// `ja-JP`: a bare `lang="ja"` was not treated like `lang="ja-JP"` in the measurement, so it +/// falls through to the generic rules (a numeric date, otherwise the saved text) instead of +/// taking the Japanese patterns. Case and `_`/`-` are normalised because OOXML writers differ. +fn is_ja_jp(lang: &str) -> bool { + let mut parts = lang.split(['-', '_']); + parts.next() == Some("ja") && parts.next() == Some("jp") +} + +/// The short (numeric) date Windows' en-US regional settings give (`M/d/yyyy`, no leading +/// zeros): what `datetime1` showed in the ja-JP measurement, and this crate's default for a +/// language whose date order it doesn't know. +fn system_short_date(t: DateTime) -> String { + format!("{}/{}/{}", t.month, t.day, t.year) +} + +/// The long date Windows' en-US regional settings give (`dddd, MMMM d, yyyy`): the English +/// `datetime2`, and what `datetime2` showed in the ja-JP measurement. +fn system_long_date(t: DateTime) -> Option { + let month = MONTHS.get(t.month.checked_sub(1)? as usize)?; + let day = DAYS.get(t.weekday())?; + Some(format!("{day}, {month} {}, {}", t.day, t.year)) +} + /// The short (numeric) date of language `lang` (lower case), US order unless the language is /// known to write its dates another way. fn short_date(lang: &str, t: DateTime) -> String { @@ -148,8 +254,65 @@ fn short_date(lang: &str, t: DateTime) -> String { "nl" => format!("{d}-{m}-{y}"), "ja" | "zh" => format!("{y}/{m}/{d}"), "ko" | "sv" | "lt" => format!("{y}-{m:02}-{d:02}"), - _ => format!("{m}/{d}/{y}"), + _ => system_short_date(t), + } +} + +/// The Reiwa year of `t`: 1 on 2019-05-01. `None` before the era began, where PowerPoint writes +/// 平成 or an older era: the measurement covers 令和 only, so those fields keep their saved text +/// rather than show a wrong era. +fn reiwa_year(t: DateTime) -> Option { + if (t.year, t.month, t.day) < (2019, 5, 1) { + return None; + } + Some(t.year - 2018) +} + +/// The era year as `datetime3`/`datetime6` write it: 元 in the first year, the number after (`8`). +fn era_year_text(t: DateTime) -> Option { + Some(match reiwa_year(t)? { + 1 => "元".to_string(), + year => year.to_string(), + }) +} + +/// The era year in kanji numerals (元, 八, 十八 …) for `datetime7`. +fn era_year_kanji(t: DateTime) -> Option { + Some(match reiwa_year(t)? { + 1 => "元".to_string(), + year => kanji_number(u32::try_from(year).ok()?)?, + }) +} + +/// 1…999 in kanji numerals, as `datetime7` writes the era year, month and day (`八`, `十`, +/// `二十七`). Zero and four-digit numbers have no form here. +fn kanji_number(n: u32) -> Option { + const DIGITS: [&str; 10] = ["", "一", "二", "三", "四", "五", "六", "七", "八", "九"]; + if n == 0 || n > 999 { + return None; + } + let hundreds = usize::try_from(n / 100).ok()?; + let tens = usize::try_from((n / 10) % 10).ok()?; + let ones = usize::try_from(n % 10).ok()?; + let mut out = String::new(); + match hundreds { + 0 => {} + 1 => out.push('百'), + _ => { + out.push_str(DIGITS.get(hundreds)?); + out.push('百'); + } } + match tens { + 0 => {} + 1 => out.push('十'), + _ => { + out.push_str(DIGITS.get(tens)?); + out.push('十'); + } + } + out.push_str(DIGITS.get(ones)?); + Some(out) } #[cfg(test)] @@ -157,6 +320,13 @@ mod tests { use super::*; const FRI: DateTime = DateTime { year: 2026, month: 10, day: 9, hour: 20, minute: 5, second: 7 }; + /// The moment the Japanese formats were measured at: PowerPoint 16.0, 2026-10-27 19:43:50, + /// `lang="ja-JP"`, en-US Windows. + const MEASURED: DateTime = DateTime { year: 2026, month: 10, day: 27, hour: 19, minute: 43, second: 50 }; + /// A Sunday (weekday 0). + const SUN: DateTime = DateTime { year: 2026, month: 2, day: 1, hour: 9, minute: 5, second: 7 }; + /// A leap day, a Thursday, at midnight. + const LEAP: DateTime = DateTime { year: 2024, month: 2, day: 29, hour: 0, minute: 0, second: 0 }; #[test] fn english_formats_of_every_type() { @@ -182,11 +352,105 @@ mod tests { assert_eq!(field_text("datetime2", None, DateTime::new(2000, 1, 1, 0, 0, 0)).as_deref(), Some("Saturday, January 1, 2000")); } + #[test] + fn japanese_formats_of_every_type_as_measured() { + let f = |k: &str| field_text(k, Some("ja-JP"), MEASURED); + assert_eq!(f("datetime1").as_deref(), Some("10/27/2026")); + assert_eq!(f("datetime2").as_deref(), Some("Tuesday, October 27, 2026")); + assert_eq!(f("datetime3").as_deref(), Some("令和8年10月27日")); + assert_eq!(f("datetime4").as_deref(), Some("2026年10月27日")); + assert_eq!(f("datetime5").as_deref(), Some("27-Oct-26")); + assert_eq!(f("datetime6").as_deref(), Some("令和8年10月")); + assert_eq!(f("datetime7").as_deref(), Some("令和八年十月二十七日")); + assert_eq!(f("datetime8").as_deref(), Some("26/10/27 19時43分")); + assert_eq!(f("datetime9").as_deref(), Some("26/10/27 19時43分50秒")); + assert_eq!(f("datetime10").as_deref(), Some("19:43")); + assert_eq!(f("datetime11").as_deref(), Some("19:43:50")); + assert_eq!(f("datetime12").as_deref(), Some("19時43分")); + assert_eq!(f("datetime13").as_deref(), Some("19時43分50秒")); + assert_eq!(f("datetimeFigureOut").as_deref(), Some("10/27/2026")); + assert_eq!(f("datetime").as_deref(), Some("10/27/2026")); + // No kind showed a Japanese weekday. + assert!(!f("datetime2").unwrap().contains('曜')); + } + + #[test] + fn japanese_era_years_and_kanji_numerals() { + // 令和 began on 2019-05-01; the first year is written 元年. + assert_eq!(field_text("datetime3", Some("ja-JP"), DateTime::new(2019, 5, 1, 0, 0, 0)).as_deref(), Some("令和元年5月1日")); + assert_eq!(field_text("datetime7", Some("ja-JP"), DateTime::new(2019, 12, 31, 0, 0, 0)).as_deref(), Some("令和元年十二月三十一日")); + assert_eq!(field_text("datetime7", Some("ja-JP"), LEAP).as_deref(), Some("令和六年二月二十九日")); + assert_eq!(field_text("datetime6", Some("ja-JP"), DateTime::new(2026, 1, 5, 0, 0, 0)).as_deref(), Some("令和8年1月")); + assert_eq!(field_text("datetime7", Some("ja-JP"), DateTime::new(2026, 1, 5, 0, 0, 0)).as_deref(), Some("令和八年一月五日")); + // Before 令和 the era is not modelled, so the era fields keep their saved text, while + // the Gregorian `datetime4` still computes. + assert_eq!(field_text("datetime3", Some("ja-JP"), DateTime::new(2019, 4, 30, 0, 0, 0)), None); + assert_eq!(field_text("datetime6", Some("ja-JP"), DateTime::new(2019, 4, 30, 0, 0, 0)), None); + assert_eq!(field_text("datetime7", Some("ja-JP"), DateTime::new(2019, 4, 30, 0, 0, 0)), None); + assert_eq!(field_text("datetime4", Some("ja-JP"), DateTime::new(2019, 4, 30, 0, 0, 0)).as_deref(), Some("2019年4月30日")); + } + + #[test] + fn japanese_times_use_measured_padding() { + let early = DateTime::new(2026, 1, 5, 9, 7, 5); + assert_eq!(field_text("datetime8", Some("ja-JP"), early).as_deref(), Some("26/01/05 9時07分")); + assert_eq!(field_text("datetime9", Some("ja-JP"), early).as_deref(), Some("26/01/05 9時07分05秒")); + assert_eq!(field_text("datetime12", Some("ja-JP"), early).as_deref(), Some("9時07分")); + assert_eq!(field_text("datetime13", Some("ja-JP"), early).as_deref(), Some("9時07分05秒")); + // The 24-hour clock forms keep colons and no leading zero on the hour. + assert_eq!(field_text("datetime10", Some("ja-JP"), early).as_deref(), Some("9:07")); + assert_eq!(field_text("datetime11", Some("ja-JP"), early).as_deref(), Some("9:07:05")); + } + + #[test] + fn only_ja_jp_reaches_the_japanese_formats() { + for tag in ["ja-JP", "ja_JP", "JA-jp", " ja-JP "] { + assert_eq!(field_text("datetime3", Some(tag), MEASURED).as_deref(), Some("令和8年10月27日"), "{tag}"); + assert_eq!(field_text("datetime8", Some(tag), MEASURED).as_deref(), Some("26/10/27 19時43分"), "{tag}"); + } + // A bare `lang="ja"` is not expanded by PowerPoint, so the named and Japanese-time + // formats keep the field's saved text here too. + for kind in ["datetime2", "datetime3", "datetime6", "datetime7", "datetime8", "datetime13"] { + assert_eq!(field_text(kind, Some("ja"), MEASURED), None, "{kind}"); + } + // The numeric date and the 24-hour clock need no names and still compute. + assert_eq!(field_text("datetime1", Some("ja"), FRI).as_deref(), Some("2026/10/9")); + assert_eq!(field_text("datetime10", Some("ja"), FRI).as_deref(), Some("20:05")); + // A tag that merely starts with `ja` is not Japanese either. + assert_eq!(field_text("datetime2", Some("jam"), FRI), None); + } + + #[test] + fn japanese_weekday_zero_and_a_leap_day() { + assert_eq!(field_text("datetime2", Some("ja-JP"), SUN).as_deref(), Some("Sunday, February 1, 2026")); + assert_eq!(field_text("datetime3", Some("ja-JP"), SUN).as_deref(), Some("令和8年2月1日")); + assert_eq!(field_text("datetime2", Some("ja-JP"), LEAP).as_deref(), Some("Thursday, February 29, 2024")); + // Japanese keeps the 24-hour clock through midnight, where English uses 12 AM. + assert_eq!(field_text("datetime8", Some("ja-JP"), LEAP).as_deref(), Some("24/02/29 0時00分")); + assert_eq!(field_text("datetime12", Some("ja-JP"), LEAP).as_deref(), Some("0時00分")); + assert_eq!(field_text("datetime2", Some("ja-JP"), DateTime::new(2026, 2, 30, 0, 0, 0)), None); + } + + #[test] + fn languages_without_names_still_keep_their_saved_text() { + // Japanese is a second language with names; English keeps every string it had. + assert_eq!(field_text("datetime2", Some("en-US"), FRI).as_deref(), Some("Friday, October 9, 2026")); + assert_eq!(field_text("datetime5", Some("en-GB"), FRI).as_deref(), Some("9-Oct-26")); + assert_eq!(field_text("datetime7", Some("en-US"), FRI).as_deref(), Some("Oct-26")); + assert_eq!(field_text("datetime13", Some("en-US"), FRI).as_deref(), Some("8:05:07 PM")); + assert_eq!(field_text("datetime2", Some("de-DE"), FRI), None); + assert_eq!(field_text("datetime7", Some("ko-KR"), FRI), None); + assert_eq!(field_text("datetime12", Some("fr-FR"), FRI), None); + // The numeric kinds need no names and are unchanged. + assert_eq!(field_text("datetime10", Some("de-DE"), FRI).as_deref(), Some("20:05")); + assert_eq!(field_text("datetime1", Some("ja-JP"), FRI).as_deref(), Some("10/9/2026")); + } + #[test] fn language_orders_numeric_dates_and_keeps_names_english_only() { assert_eq!(field_text("datetime1", Some("en-GB"), FRI).as_deref(), Some("09/10/2026")); assert_eq!(field_text("datetimeFigureOut", Some("de-DE"), FRI).as_deref(), Some("09.10.2026")); - assert_eq!(field_text("datetime1", Some("ja-JP"), FRI).as_deref(), Some("2026/10/9")); + assert_eq!(field_text("datetime1", Some("ja-JP"), FRI).as_deref(), Some("10/9/2026")); assert_eq!(field_text("datetime1", Some("sv-SE"), FRI).as_deref(), Some("2026-10-09")); assert_eq!(field_text("datetime10", Some("fr-FR"), FRI).as_deref(), Some("20:05")); // Month names in another language: not computed (the field keeps its saved text). diff --git a/crates/ui-egui/src/datetime_tests.rs b/crates/ui-egui/src/datetime_tests.rs index 8a92c6379a..c991b60468 100644 --- a/crates/ui-egui/src/datetime_tests.rs +++ b/crates/ui-egui/src/datetime_tests.rs @@ -12,7 +12,7 @@ use crate::{Services, SlideApp}; fn harness() -> Harness<'static, SlideApp> { let mut app = SlideApp::new(Session::with_new(), Services::default()); app.ui.tab = "Insert".into(); - let mut h = Harness::builder().with_size(egui::vec2(1800.0, 1100.0)).build_ui_state( + let mut h = Harness::builder().with_size(egui::vec2(1800.0, 1100.0)).allow_missing_glyphs().build_ui_state( |ui, app: &mut SlideApp| { app.logic(ui.ctx()); app.ui(ui); @@ -66,6 +66,59 @@ fn insert_opens_the_dialog_and_ok_inserts_one_field() { assert!(date_run(&h).is_none()); } +#[test] +fn choosing_japanese_lists_and_inserts_a_japanese_date() { + let mut h = harness(); + h.get_by_label("Date & Time").click(); + h.run_steps(2); + h.get_by_role_and_label(egui::accesskit::Role::ComboBox, "Language").click(); + h.run_steps(2); + h.get_by_label("Japanese").click(); + h.run_steps(2); + // `datetime2` is the system long date even in Japanese (measured: en-US Windows showed + // "Tuesday, October 27, 2026"), so pick the Gregorian Japanese date. + let now = deckcraft_text::datetime::DateTime::now_local().unwrap(); + let sample = deckcraft_text::datetime::field_text("datetime4", Some("ja-JP"), now).unwrap(); + assert!(sample.ends_with('日'), "the dialog lists the Japanese date: {sample}"); + h.get_by_label(&sample).click(); + h.run_steps(1); + h.get_by_label("OK").click(); + h.run_steps(2); + assert!(h.state().dialog.is_none()); + let run = date_run(&h).unwrap(); + assert!(matches!(run.kind, RunKind::Field { ref field } if field == "datetime4"), "{run:?}"); + assert_eq!(run.text, sample); + assert_eq!(run.props.lang.as_deref(), Some("ja-JP")); +} + +#[test] +fn japanese_formats_are_listed_and_distinct() { + let mut h = harness(); + h.get_by_label("Date & Time").click(); + h.run_steps(2); + h.get_by_role_and_label(egui::accesskit::Role::ComboBox, "Language").click(); + h.run_steps(2); + h.get_by_label("Japanese").click(); + h.run_steps(2); + // The measured Japanese list has no two kinds reading the same text: the era, the Gregorian + // date and the English-style `27-Oct-26` all differ, so no row needs its kind appended. + let now = deckcraft_text::datetime::DateTime::now_local().unwrap(); + let samples: Vec = + deckcraft_text::datetime::DATE_KINDS.iter().map(|k| deckcraft_text::datetime::field_text(k, Some("ja-JP"), now).unwrap()).collect(); + let mut seen = std::collections::HashSet::new(); + for sample in &samples { + assert!(seen.insert(sample.as_str()), "two Japanese kinds read the same: {sample}"); + } + let kanji = deckcraft_text::datetime::field_text("datetime7", Some("ja-JP"), now).unwrap(); + assert!(kanji.starts_with("令和") && kanji.ends_with('日'), "the dialog lists the kanji era date: {kanji}"); + h.get_by_label(&kanji).click(); + h.run_steps(1); + h.get_by_label("OK").click(); + h.run_steps(2); + let run = date_run(&h).unwrap(); + assert!(matches!(run.kind, RunKind::Field { ref field } if field == "datetime7"), "{run:?}"); +} + #[test] fn cancel_closes_the_dialog_without_a_date() { let mut h = harness(); diff --git a/crates/ui-egui/src/dialogs/date_time.rs b/crates/ui-egui/src/dialogs/date_time.rs index 3ba1c3d7ef..4c6ad29ff6 100644 --- a/crates/ui-egui/src/dialogs/date_time.rs +++ b/crates/ui-egui/src/dialogs/date_time.rs @@ -8,8 +8,8 @@ const DATE_LANGS: &[(&str, &str)] = &[ ("sv-SE", "Swedish"), ]; -/// Samples `field_text` can compute for `lang`. Named months and AM/PM exist for English only; -/// other languages keep the numeric date and the 24-hour times. +/// Samples `field_text` can compute for `lang`. English and Japanese compute their own forms; +/// other languages list the numeric date and the 24-hour times. pub(super) fn date_rows(lang: &str) -> Vec<(&'static str, String)> { let now = deckcraft_text::datetime::DateTime::now_local(); let mut rows = Vec::new(); @@ -41,7 +41,11 @@ pub(super) fn date_time_dialog(app: &mut SlideApp, ui: &mut Ui, d: &mut Dialog) let format = d.get("format", "datetime1"); egui::ScrollArea::vertical().max_height(280.0).min_scrolled_height(220.0).auto_shrink([false, false]).show(ui, |ui| { for (kind, sample) in &rows { - if ui.selectable_label(format == *kind, sample).clicked() { + // Two kinds can compute the same text in some language. The row inserts its kind, so + // name the kind whenever the sample alone is ambiguous. + let ambiguous = rows.iter().filter(|(_, other)| other == sample).count() > 1; + let label = if ambiguous { format!("{sample} ({kind})") } else { sample.clone() }; + if ui.selectable_label(format == *kind, label).clicked() { d.fields.insert("format".into(), (*kind).to_string()); } } @@ -64,7 +68,7 @@ pub(super) fn date_time_dialog(app: &mut SlideApp, ui: &mut Ui, d: &mut Dialog) }); }); ui.label( - egui::RichText::new("Month names and AM/PM are listed for English. Other languages list the numeric date and 24-hour time.") + egui::RichText::new("English and Japanese list their own date and time forms. Other languages list the numeric date and the 24-hour time.") .size(11.0) .color(Tokens::get(ui.ctx()).text_dim), ); diff --git a/docs/localization-parity.md b/docs/localization-parity.md index 5fb39d1e64..9112575733 100644 --- a/docs/localization-parity.md +++ b/docs/localization-parity.md @@ -1,5 +1,6 @@ # Localization parity with PowerPoint +> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-27 · **Change:** minor (Japanese date and time fields follow PowerPoint 16.0 with `lang="ja-JP"`: the system short and long date for `datetime1`/`datetime2`, the 令和 era for `datetime3`/`datetime6`/`datetime7`, `時`/`分`/`秒` for `datetime8`/`datetime9`/`datetime12`/`datetime13`; a bare `ja` tag is not expanded; 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-24 · **Change:** 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) DeckCraft's interface is English only today: strings are literals in the UI and engine code, with @@ -32,7 +33,7 @@ support partly there), **55–90 h** to parity plus native-speaker review. | French | fr | 0 (0%) | no | full (Latin) | no | none | yes (fr, fr-CA) | 3–5 | | Portuguese | pt | 0 (0%) | no | full (Latin) | no | none | yes (pt-BR, pt-PT) | 3–5 | | 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 | +| Japanese | ja | 0 (0%) | no | renders with system or craft-fonts Japanese faces; date and time fields (`datetime1`–`datetime13`) follow PowerPoint 16.0 with `lang="ja-JP"` (system short and long date for `datetime1`/`datetime2`, the 令和 era for `datetime3`/`datetime6`/`datetime7`, `時`/`分`/`秒` for `datetime8`/`datetime9`/`datetime12`/`datetime13`); 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 | | Vietnamese | vi | 0 (0%) | no | stacked diacritics through HarfRust | no | none | **no** (Windows only) | 3–5 | @@ -60,6 +61,7 @@ of every text in the file and the default for new text (`format.languageAll`). | Date | Change | Summary | |---|---|---| +| 2026-10-27 | minor | Japanese date and time fields follow PowerPoint 16.0 measured with `lang="ja-JP"` on an en-US Windows: `datetime1`/`datetime2` keep the system regional short and long date, `datetime3`/`datetime6`/`datetime7` use the 令和 era (kanji numerals for `datetime7`), `datetime8`/`datetime9`/`datetime12`/`datetime13` use `時`/`分`/`秒`, `datetime5` keeps `27-Oct-26`, no kind shows a Japanese weekday, and a bare `ja` tag is not expanded | | 2026-10-24 | minor | Local 0.37 candidate adds an opt-in native LXGW WenKai Lite glyph fallback and CJK interface-test coverage. This records local implementation only: managed tests, formatting, clippy, wasm-size and native GUI qualification remain pending. Browser font budget and uniform Chinese typography are unchanged. | | 2026-10-21 | minor | Review › Language can set the proofing language of the whole presentation | | 2026-10-14 | minor | IME composition in a table cell is underlined in place; the candidate window sits on that cell's caret |