Add Estonian interface translation (#294)

* I18N.et: add Estonian interface translation

* I18N.et: record the verified headless fork push step

* i18n: send interface text that skipped translation through the catalog

A test-only pseudo-language wraps every catalog lookup in ⟦…⟧ marks; a new
audit test lays the app out in it (every tab, File page, dialog and pane,
the right-click menu, and a hover and a click on every ribbon control,
including the Table and Equation tabs) and fails on any visible text
outside the marks. The document is drawn as an image, so only interface
text is checked; document text, font and style names, shortcuts, icon
lettering, contributor handles and command error messages are allowed.

What it found, now translated in every language that has the strings:
- Right-click menu items (Cut, Paste, Font…, Paragraph…, Link…, New Comment).
- Paragraph dialog: the alignment choices showed code values (`left`);
  the label now sits before the dropdown.
- Table Design › Table Style Options checkboxes.
- Table Layout › Alignment tooltips showed code values (`topLeft`).
- Design › Fonts: the theme's name is translated, its font names are not.
- `pt` units, the Insert › Table grid label, the default watermark text,
  the font dialog's preview sample, and the untitled document's name.
- Contributors and Models dialogs: headings, sort and name choices, summary.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WtHKQ7wrZm3uBuaG1Ye8po

* Draw Table and Eraser: draw tables and split or merge cells with a pen (#303)

Insert › Tables › Draw Table and Table Layout › Draw › Draw Table / Eraser.
table.draw takes a pen stroke in page points: a rectangle outside a table
inserts a one-cell table of that size at the paragraph under its start; a
straight vertical or horizontal line inside a table splits the cells it
crosses (new grid column or row; untouched cells span it). table.eraser
merges the two cells beside the border nearest a point. Each stroke is one
undo step. In the app the buttons toggle pen and eraser modes with a live
preview, a crosshair with the tool's icon, and Esc to stop; canvas clicks
don't move the caret while a tool is on. Splitting through part of a
vertically merged cell and erasing beside one are refused with a clear
error. Catalog 384/430 -> 387/430.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Draw tab: pen, pencil and highlighter ink, stroke eraser and Hide Ink (#307)

Ink strokes are floating freeform shapes: a new Freeform geometry (paths in
shape-local coordinates, opacity, and the pen that drew it) on
InlineObject::Shape with ShapeKind::Freeform. draw.stroke inserts one
anchored to the paragraph under the first point (one undo step; points
capped per stroke, strokes capped per page, NaN dropped); draw.erase /
draw.eraser with a point deletes the stroke under it; draw.pen, draw.pencil,
draw.highlighter, draw.eraser and draw.select set the Draw tab's tool in the
session view state; review.hideInk toggles showing ink on screen.

The canvas draws a live stroke while a pen is dragged and commits it on
release; the eraser deletes strokes it passes over (one undo step per drag);
Esc or Select goes back to selecting. Draw tab buttons show the tool in use,
with colour and thickness menus; Review gets an Ink group with Hide Ink.
Ink renders with round caps and joins on screen and in PDF; highlighters are
translucent and behind the text. DOCX writes freeforms as wps:wsp with
a:custGeom (moveTo/lnTo) in a wp:anchor and reads them back, along with
simple custom-geometry freeforms from other files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* #315 review fixes: eraser undo test, sanitized tap dots, custGeom path cap, ink docs

- draw.eraser with a point erases through draw.erase: test that it is one
  undo step (undo/redo) and marks the document dirty.
- Render and PDF: a single-point stroke's dot is drawn at the sanitized
  point, not the raw first one.
- DOCX custGeom reading stops once MAX_PATHS paths (subpaths included) are
  collected, across a:path elements.
- docs/file-format-parity.md: ink is saved as custom geometry, not InkML, so
  Word shows it as a freeform.
- widgets test: theme::apply takes an Appearance (#317).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* #313: catalog rows for the newly translated interface text

Rows in all eight catalogs for the strings #313 now sends through the
catalog (cell alignment tooltips, credits columns and summary, theme names,
context-menu items, table picker, pt suffix, Document1, the Font dialog
sample) and for the Table Style preview's sample table, which the
untranslated-text audit flagged after main's table style editor landed.
The audit runs headless (no host services: no file pickers or speech) in
about 20 s, so it stays enabled.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fmt

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* i18n: Borders and Shading line widths through the catalog ({n} pt)

#313's untranslated-text audit flagged main's new dialog (#327).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* land-294: Estonian rows for main's newer dialogs

Define New Multilevel List and Track Changes Options (#342), rotation (#332),
Tabs and Borders and Shading (#327), Columns, Symbol and Field (#335): et
keys equal es again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* i18n audit: run on demand (#[ignore]) until its rare failure under load is understood

It failed once in ~46 runs on a heavily loaded machine and passed 35 straight reruns,
so CI shouldn't depend on it yet. Run it with --ignored.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
Henno Täht 2026-10-11 02:06:45 +03:00 committed by GitHub
parent 25b025b1ea
commit afb4720644
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 1397 additions and 9 deletions

View File

@ -61,4 +61,5 @@ Generated-in-code art is original and has no file to list: the UI icon set (`cra
| `docs/brand/artcraft-mark.svg` | ArtCraft Team | craftrules `assets/brand/` | ArtCraft trademark, `docs/brand/LICENSE-brand.txt` | Not open source; usable unmodified only | | `docs/brand/artcraft-mark.svg` | ArtCraft Team | craftrules `assets/brand/` | ArtCraft trademark, `docs/brand/LICENSE-brand.txt` | Not open source; usable unmodified only |
| craft-fonts (optional build input, not files in this repo): BIZ UDPGothic, BIZ UDMincho, Shippori Mincho, Noto Sans CJK SC, Noto Sans Arabic | see https://github.com/storytold/craft-fonts/blob/main/ATTRIBUTION.md | embedded only when built with `CRAFT_FONTS_DIR` (all official releases) | OFL-1.1 | Japanese and Chinese UI, Japanese document fallback | | craft-fonts (optional build input, not files in this repo): BIZ UDPGothic, BIZ UDMincho, Shippori Mincho, Noto Sans CJK SC, Noto Sans Arabic | see https://github.com/storytold/craft-fonts/blob/main/ATTRIBUTION.md | embedded only when built with `CRAFT_FONTS_DIR` (all official releases) | OFL-1.1 | Japanese and Chinese UI, Japanese document fallback |
| `crates/ui-egui/src/i18n/uk.tsv` | @dmatviichuk with Codex | Original translation from the English interface labels in this repository; no other product localisation copied | MIT OR Apache-2.0 | Ukrainian interface catalog; spelling checked with external VESUM and ULIF dictionaries, neither bundled | | `crates/ui-egui/src/i18n/uk.tsv` | @dmatviichuk with Codex | Original translation from the English interface labels in this repository; no other product localisation copied | MIT OR Apache-2.0 | Ukrainian interface catalog; spelling checked with external VESUM and ULIF dictionaries, neither bundled |
| `crates/ui-egui/src/i18n/et.tsv` | @henno with Codex | Original translation from the English interface labels in this repository; no other product localisation copied | MIT OR Apache-2.0 | Estonian interface catalog |
| `crates/ui-egui/src/i18n/pt-br.tsv` | @edurbs with ZCode | Original translation from the English interface labels in this repository; no other product localisation copied | MIT OR Apache-2.0 | Brazilian Portuguese interface catalog; spelling checked with the external pt_BR Hunspell dictionary, not bundled | | `crates/ui-egui/src/i18n/pt-br.tsv` | @edurbs with ZCode | Original translation from the English interface labels in this repository; no other product localisation copied | MIT OR Apache-2.0 | Brazilian Portuguese interface catalog; spelling checked with the external pt_BR Hunspell dictionary, not bundled |

View File

@ -117,7 +117,7 @@ Every screenshot below is WordCraft itself, rendered offscreen by its own UI tes
| **References** | Table of contents, footnotes and endnotes, citations and bibliography (APA, MLA, Chicago, IEEE), captions, table of figures, cross-references, index, table of authorities | | **References** | Table of contents, footnotes and endnotes, citations and bibliography (APA, MLA, Chicago, IEEE), captions, table of figures, cross-references, index, table of authorities |
| **Review** | Spelling and grammar with suggestions, thesaurus, word count, comments in margin balloons or a pane, track changes, accept/reject, compare documents, restrict editing, accessibility checker, document inspector | | **Review** | Spelling and grammar with suggestions, thesaurus, word count, comments in margin balloons or a pane, track changes, accept/reject, compare documents, restrict editing, accessibility checker, document inspector |
| **Mailings** | Mail merge from CSV, merge fields, address block, greeting line, rules, preview, finish to a document; envelopes and labels | | **Mailings** | Mail merge from CSV, merge fields, address block, greeting line, rules, preview, finish to a document; envelopes and labels |
| **View** | Print layout, web layout, draft, read mode, focus, zoom, one/multiple pages, page width, Navigation pane, rulers, gridlines, dark mode; interface in English, 简体中文, 繁體中文, 日本語, Português (Brasil), Español or Українська (follows the system language by default) | | **View** | Print layout, web layout, draft, read mode, focus, zoom, one/multiple pages, page width, Navigation pane, rulers, gridlines, dark mode; interface in English, 简体中文, 繁體中文, 日本語, Português (Brasil), Español, Українська, Српски / Srpski or Eesti (follows the system language by default) |
| **Files** | .docx read/write (opens in Word), Word 97-2003 .doc import, PDF export, .odt, .rtf, .html, .md, .tex, .txt import/export, page images | | **Files** | .docx read/write (opens in Word), Word 97-2003 .doc import, PDF export, .odt, .rtf, .html, .md, .tex, .txt import/export, page images |
The honest picture, area by area, is in [ROADMAP.md](ROADMAP.md), the The honest picture, area by area, is in [ROADMAP.md](ROADMAP.md), the

View File

@ -110,7 +110,8 @@ Weights and evidence per area: [docs/target-app-parity.md](docs/target-app-parit
| Korean | ko | none | 0% | | Korean | ko | none | 0% |
| Vietnamese | vi | none | 0% | | Vietnamese | vi | none | 0% |
Also shipped: Traditional Chinese (`zh-hant`), Ukrainian (`uk`) and Serbian (`sr`, `sr-latn`, #250), all partial (~95%). Word ships Also shipped: Traditional Chinese (`zh-hant`), Ukrainian (`uk`) and Serbian (`sr`, `sr-latn`, #250), all partial (~95%). Estonian (`et`) now has a 1,161-entry interface catalog with system-locale
selection and a saved language choice; Estonian proofing is not included. Word ships
30 interface languages and proofing for ~50. Detail: [docs/localization-parity.md](docs/localization-parity.md). 30 interface languages and proofing for ~50. Detail: [docs/localization-parity.md](docs/localization-parity.md).
## Upcoming ## Upcoming
@ -130,6 +131,7 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm
| Date | What landed | | Date | What landed |
|---|---| |---|---|
| 2026-10-10 | Estonian (`et`) interface (#294): 1,161 translated entries, locale and saved-preference tests, bundled-font checks; no Estonian proofing resources |
| 2026-10-10 | Draw tab: pen, pencil and highlighter ink, stroke eraser, Select, Review › Hide Ink; ink saved to DOCX as DrawingML freeforms (#307); catalog 399/431 (92.6%) | | 2026-10-10 | Draw tab: pen, pencil and highlighter ink, stroke eraser, Select, Review › Hide Ink; ink saved to DOCX as DrawingML freeforms (#307); catalog 399/431 (92.6%) |
| 2026-10-10 | Draw Table and Eraser (#303): a pen draws one-cell tables and splits cells along drawn lines; the eraser merges the cells beside a border. Catalog 393/431 (91.2%) | | 2026-10-10 | Draw Table and Eraser (#303): a pen draws one-cell tables and splits cells along drawn lines; the eraser merges the cells beside a border. Catalog 393/431 (91.2%) |
| 2026-10-10 | DOCX charts, SmartArt and OLE objects survive open and save with their parts (#319) | | 2026-10-10 | DOCX charts, SmartArt and OLE objects survive open and save with their parts (#319) |

View File

@ -37,3 +37,6 @@ sync_github_name = true
real_name = "Dmitrii Khlapov" real_name = "Dmitrii Khlapov"
display_name = "Dima Khlapov" display_name = "Dima Khlapov"
sync_github_name = false sync_github_name = false
[people.henno]
sync_github_name = true

File diff suppressed because it is too large Load Diff

View File

@ -47,7 +47,7 @@ pub struct LangInfo {
} }
/// The registry. English first: it is the fallback and the source language. /// The registry. English first: it is the fallback and the source language.
pub static LANGUAGES: [LangInfo; 9] = [ pub static LANGUAGES: [LangInfo; 10] = [
LangInfo { code: "en", name: "English", source: "", prefer_hans: false, catalog: OnceLock::new() }, LangInfo { code: "en", name: "English", source: "", prefer_hans: false, catalog: OnceLock::new() },
// Simplified Chinese; `zh`, `zh-CN`, `zh-SG` and `zh-Hans-*` resolve here (see `candidates`). // Simplified Chinese; `zh`, `zh-CN`, `zh-SG` and `zh-Hans-*` resolve here (see `candidates`).
LangInfo { code: "zh-hans", name: "简体中文", source: include_str!("zh-hans.tsv"), prefer_hans: true, catalog: OnceLock::new() }, LangInfo { code: "zh-hans", name: "简体中文", source: include_str!("zh-hans.tsv"), prefer_hans: true, catalog: OnceLock::new() },
@ -66,6 +66,7 @@ pub static LANGUAGES: [LangInfo; 9] = [
// Serbian, Latin script; `sr-Latn-*` resolves here (see `candidates`'s generic prefix // Serbian, Latin script; `sr-Latn-*` resolves here (see `candidates`'s generic prefix
// matching — no special-casing needed, unlike Chinese's script-by-region fallback). // matching — no special-casing needed, unlike Chinese's script-by-region fallback).
LangInfo { code: "sr-latn", name: "Srpski (latinica)", source: include_str!("sr-latn.tsv"), prefer_hans: false, catalog: OnceLock::new() }, LangInfo { code: "sr-latn", name: "Srpski (latinica)", source: include_str!("sr-latn.tsv"), prefer_hans: false, catalog: OnceLock::new() },
LangInfo { code: "et", name: "Eesti", source: include_str!("et.tsv"), prefer_hans: false, catalog: OnceLock::new() },
]; ];
impl LangInfo { impl LangInfo {

View File

@ -361,3 +361,54 @@ fn only_cjk_languages_ask_for_an_installed_cjk_font() {
let cjk: Vec<&str> = Lang::all().filter(|l| l.uses_cjk()).map(Lang::code).collect(); let cjk: Vec<&str> = Lang::all().filter(|l| l.uses_cjk()).map(Lang::code).collect();
assert_eq!(cjk, ["zh-hans", "zh-hant", "ja"]); assert_eq!(cjk, ["zh-hans", "zh-hant", "ja"]);
} }
#[test]
fn estonian_locales_and_saved_preference_keep_document_content() {
let et = lang("et");
for tag in ["et", "et-EE", "ET_ee.UTF-8", "et-Latn-EE", "et_EE.UTF-8@euro"] {
assert_eq!(lang_from_tag(tag), Some(et), "{tag}");
}
assert_eq!(first_supported(["fr-FR", "et-EE", "en-US"]), Some(et));
assert_eq!(normalize_pref("ET"), Some("et"));
assert_eq!(et.name(), "Eesti");
let mut app = crate::WordApp::new(wordcraft_engine::Session::new(wordcraft_engine::sample::sample_document()), Default::default());
let before = serde_json::to_value(&app.session.doc).unwrap();
let result = app.run("ui.language", serde_json::json!({"value": "ET"})).unwrap();
assert_eq!(result["effective"], "et");
assert_eq!(app.ui.language, "et");
assert_eq!(serde_json::to_value(&app.session.doc).unwrap(), before);
let saved = serde_json::to_string(&app.ui).unwrap();
let restored: crate::UiState = serde_json::from_str(&saved).unwrap();
assert_eq!(Lang::from_pref(&restored.language), et);
}
#[test]
fn estonian_covers_the_interface_catalog_and_keeps_count_labels_neutral() {
use std::collections::HashSet;
let keys = |source| parse_entries(source).0.into_iter().map(|e| e.source).collect::<HashSet<_>>();
let et = lang("et");
assert_eq!(keys(et.0.source), keys(lang("es").0.source));
assert!(keys(lang("zh-hans").0.source).is_subset(&keys(et.0.source)));
assert_eq!(tr(et, "Home"), "Avaleht");
assert_eq!(tr(et, "Save"), "Salvesta");
assert_eq!(tr(et, "Spelling & Grammar"), "Õigekiri ja grammatika");
assert_eq!(tr(et, "unknown future label"), "unknown future label");
assert_eq!(fmt(tr(et, "Exported {path}"), &[("path", "draft-{words}.docx")]), "Eksporditud: draft-{words}.docx");
for count in [0, 1, 2, 11, 21, 101] {
let words = count.to_string();
assert_eq!(fmt(tr(et, "{words} words"), &[("words", &words)]), format!("Sõnu: {count}"));
assert_eq!(fmt(tr(et, "{selected} of {words} words"), &[("selected", "1"), ("words", &words)]), format!("Sõnu: {count}; valitud: 1"));
}
}
#[test]
fn bundled_interface_fonts_cover_estonian_letters() {
let db = wordcraft_fonts::FontDb::with_font_dirs(Vec::new());
for (family, style) in [("Inter", "Regular"), ("Inter", "Medium"), ("Inter", "SemiBold"), ("JetBrains Mono", "Regular")] {
let face = db.face(family, style);
assert_eq!(face.family, family, "must use the bundled interface face");
for ch in "ÕõÄäÖöÜüŠšŽž".chars() {
assert_ne!(face.glyph_for(ch), 0, "{family} {style} lacks {ch}");
}
}
}

View File

@ -50,7 +50,7 @@ and set `WORDCRAFT_CONTROL_KEY` to that app's key.
| Method | Params | Result | | Method | Params | Result |
|---|---|---| |---|---|---|
| `engine.execute` | `command`, `params` | the command's result (any id from `engine.commands`, including UI commands `ui.tab`, `ui.dialog`, `ui.backstage`, `ui.zotero.*` (see `docs/zotero.md`), `ui.language` (`{"value": "auto"|"en"|"zh-hans"|"zh-hant"|"ja"|"pt-br"|"es"|"uk"}`), `ui.theme` (`{"value": "system"|"light"|"dark"}`; `system` follows the OS appearance, light when it reports none)…) | | `engine.execute` | `command`, `params` | the command's result (any id from `engine.commands`, including UI commands `ui.tab`, `ui.dialog`, `ui.backstage`, `ui.zotero.*` (see `docs/zotero.md`), `ui.language` (`{"value": "auto"|"en"|"zh-hans"|"zh-hant"|"ja"|"pt-br"|"es"|"uk"|"sr"|"sr-latn"|"et"}`), `ui.theme` (`{"value": "system"|"light"|"dark"}`; `system` follows the OS appearance, light when it reports none)…) |
| `engine.commands` | — | every command: id, label, location, shortcut, params, enabled | | `engine.commands` | — | every command: id, label, location, shortcut, params, enabled |
| `document.inspect` | `text?` | blocks (text, style, runs, lists, tables), parts, sections, selection, pages | | `document.inspect` | `text?` | blocks (text, style, runs, lists, tables), parts, sections, selection, pages |
| `ui.inspect` | — | UI state, view, dialog, window size, page rects on screen, caret, comment balloons (`balloons`: id + screen rect; `balloon`: the selected one), perf | | `ui.inspect` | — | UI state, view, dialog, window size, page rects on screen, caret, comment balloons (`balloons`: id + screen rect; `balloon`: the selected one), perf |

View File

@ -18,9 +18,10 @@ other ~18 languages Word ships, plus native-speaker review (human).
## What WordCraft ships (measured) ## What WordCraft ships (measured)
- **9 interface languages**: English plus `zh-hans`, `zh-hant`, `ja`, `uk`, `es`, `pt-br`, and - **10 interface languages**: English plus `zh-hans`, `zh-hant`, `ja`, `uk`, `es`, `pt-br`,
Serbian `sr`/`sr-latn` (#250) (catalogs of ~930–1,010 entries each). A test (`every_tab_and_command_is_translated`) enforces that every ribbon Serbian `sr`/`sr-latn` (#250), and Estonian `et` (1,161 entries). Catalogs have ~1,100–1,160 entries
tab, command label and ribbon location is translated in every language. each. A test (`every_tab_and_command_is_translated`) enforces that every ribbon tab, command
label and ribbon location is translated in every language.
- Of the 675 English strings a scan extracts from `tl!(…)` calls and command labels/locations, each - Of the 675 English strings a scan extracts from `tl!(…)` calls and command labels/locations, each
catalog covers **661 (98%)**. Some newer dialog and pane strings (Equation tab tooltips, Zotero, catalog covers **661 (98%)**. Some newer dialog and pane strings (Equation tab tooltips, Zotero,
Read Aloud, Backstage greetings) aren't wrapped in `tl!` yet, so they show in English in every Read Aloud, Backstage greetings) aren't wrapped in `tl!` yet, so they show in English in every
@ -51,8 +52,11 @@ shared items below.
| Korean | ko | 0 | ❌ | Hangul ✅; macOS Korean IME fixes pending (#163, #164) | no | none | 3–5 | | Korean | ko | 0 | ❌ | Hangul ✅; macOS Korean IME fixes pending (#163, #164) | no | none | 3–5 |
| Vietnamese | vi | 0 | ❌ | Latin with stacked diacritics, untested | no | none | 3–4 | | Vietnamese | vi | 0 | ❌ | Latin with stacked diacritics, untested | no | none | 3–4 |
Other shipped languages: **4** — Traditional Chinese (`zh-hant`), Ukrainian (`uk`) and Serbian Other shipped languages: **5** — Traditional Chinese (`zh-hant`), Ukrainian (`uk`), Serbian
Cyrillic and Latin (`sr`, `sr-latn`, #250), all partial (~95%). Cyrillic and Latin (`sr`, `sr-latn`, #250), and Estonian (`et`), all partial.
Estonian uses the existing fonts, follows `et-EE` system locales, and is available in Options or
through `ui.language` with `{"value":"et"}`. Its 1,161 entries cover the existing interface catalog.
Estonian spelling, grammar, dates and document templates are not added by the interface catalog.
## Shared work (not in the per-language hours) ## Shared work (not in the per-language hours)
@ -68,5 +72,6 @@ Cyrillic and Latin (`sr`, `sr-latn`, #250), all partial (~95%).
| Date | Change | Summary | | Date | Change | Summary |
|---|---|---| |---|---|---|
| 2026-10-10 | minor | Estonian interface catalog, locale selection, saved preference and font coverage checks |
| 2026-10-10 | minor | Serbian (Cyrillic and Latin) interface merged (#250) | | 2026-10-10 | minor | Serbian (Cyrillic and Latin) interface merged (#250) |
| 2026-10-10 | major | First version: twelve-language table, catalog coverage measured, Word's 30 UI languages and 120 proofing tools listed | | 2026-10-10 | major | First version: twelve-language table, catalog coverage measured, Word's 30 UI languages and 120 proofing tools listed |