mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-26 19:26:46 -06:00
Add Ukrainian interface translation (#236)
* UK1: add Ukrainian interface translation Translate existing interface catalogs and wire Ukrainian menu selection, contextual publishing labels and grammatical counters. Validate persisted selection, untouched document data, placeholder/key coverage and bundled Cyrillic glyphs. Co-Authored-By: Codex <noreply@openai.com> * i18n uk: translate the open and place alert titles Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Codex <noreply@openai.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Codex
Claude Opus 5.5
Brandon Thomas
parent
c30340f60f
commit
e904bd6057
@@ -8,6 +8,10 @@ Generated-in-code art is original and has no file to list: the UI icon set (`cra
|
||||
|
||||
| Asset | Author | Source | Licence | Notes |
|
||||
|---|---|---|---|---|
|
||||
| `crates/ui-egui/src/i18n/uk.rs` (332 common phrases) | VectorCraft contributors | https://github.com/storytold/vectorcraft/blob/01f329aa99fa0c9517311c7b6cf7233ffc207742/crates/ui-egui/src/i18n/uk.tsv | MIT OR Apache-2.0 | Adapted for DesignCraft context; remaining Ukrainian phrases are original or credited below |
|
||||
| `crates/ui-egui/src/i18n/uk.rs` (68 common phrases) | PhotoCraft contributors | https://github.com/storytold/photocraft/blob/ed057bdc6fd89a858c5e0ea4eab158a2a7052dde/crates/ui-egui/src/i18n/uk.tsv | MIT OR Apache-2.0 | Adapted interface vocabulary |
|
||||
| `crates/ui-egui/src/i18n/uk.rs` (37 common phrases) | WordCraft contributors | https://github.com/storytold/wordcraft/blob/7fb7b013591e2593fbd038f90bbc535be318e5a3/crates/ui-egui/src/i18n/uk.tsv | MIT OR Apache-2.0 | Adapted interface vocabulary |
|
||||
| `crates/ui-egui/src/i18n/uk.rs` (15 common phrases) | DeckCraft contributors | https://github.com/storytold/deckcraft/blob/3a45c614a409349fc13d449f35bd98f3321f0df5/crates/ui-egui/src/i18n/uk.tsv | MIT OR Apache-2.0 | Adapted interface vocabulary; validation dictionaries are external and are not bundled |
|
||||
| `assets/hyphenation/en-us.dic` | Grady Ward (Moby Hyphenator II word list); converted by DesignCraft contributors | https://www.gutenberg.org/ebooks/3204 (file `https://www.gutenberg.org/files/3204/files/mhyph.txt`, SHA-256 `eeb30474c86b8af3469035ec1a0913e35905325ca885290db9dfed9881e230ac`) | Public Domain ("Public Domain material by grant from the author, January, 2001"; Project Gutenberg: "Public domain in the USA") | ~165k words with break points, front-coded + deflate; regenerate with `cargo run --release -p designcraft-compose --example hyphgen -- mhyph.txt assets/hyphenation` |
|
||||
| `assets/hyphenation/en-us.pat` | DesignCraft contributors (generated) | trained from `en-us.dic`'s source list by `crates/compose/src/hyphen/patgen.rs` (`examples/hyphgen.rs`) | Public Domain (CC0-1.0) | Our own Liang hyphenation patterns (no TeX/hyph-utf8 patterns used) |
|
||||
| `assets/fonts/Inter-Medium.ttf` | Rasmus Andersson / The Inter Project Authors | https://github.com/rsms/inter | OFL-1.1 (`assets/fonts/OFL-Inter.txt`) | Open-source typeface |
|
||||
|
||||
+1
-1
@@ -107,7 +107,7 @@ DesignCraft aims at full Adobe InDesign parity — and to be better: faster, ope
|
||||
- Transparency flattener (`file.exportPdf {flatten: high|medium|low|ppi}`, Edit ▸ Transparency Flattener Presets): each object involving transparency becomes an opaque image of its area over what's beneath it, so everything else stays vector (spreads with transparent parent items are rasterised whole).
|
||||
- Video and sound (place .mp4/.mov/.webm/.mp3/.wav…, `media.options/get`, Media panel): media frames show a poster or placeholder on screen, in print and in PDF, and export as HTML5 `<video>`/`<audio>` in EPUB. Interactive PDF export (`file.exportPdf {media: true}`, File ▸ Export PDF (Interactive)) embeds each file and plays it in a Screen annotation through a rendition action (controls, loop, play on page load). No in-app playback.
|
||||
- Right-to-left text (World-Ready basics): right-to-left runs are shaped right to left, lines are reordered by the Unicode Bidirectional Algorithm (matched against the reference implementation), Paragraph Direction (`type.para {direction}`, Paragraph panel, IDML ParagraphDirection, EPUB `dir`). Digits (`digits`: Default, Arabic, Hindi, Farsi, Native by language; Character panel for right-to-left text; IDML DigitsType) draw 0–9 in another script. Kashidas (`kashidas` paragraph attribute, on by default): justified Arabic lines stretch at one join per word (after seen/sad when possible, never inside lam–alef) with a drawn tatweel before word spaces widen. Carets follow the drawing: the caret before a right-to-left glyph sits at its right edge, arrow keys move as drawn (by words in the line's direction), clicks find the nearest drawn caret position and selections highlight the selected glyphs. Binding: Right to Left (Document Setup, `layout.documentSetup {binding}`, IDML PageBinding) makes page 1 a left page and lays spreads out right to left.
|
||||
- Interface language (Edit ▸ Interface Language, `app.language`): German, French, Spanish, Japanese, Simplified Chinese and Arabic menu titles, common menu items and panel names (our own translations). Common dialog, panel and tooltip labels are also localized, with English fallback for untranslated strings. Arabic translations cover every key in the shared interface translation table, including menu commands and utility labels. The Type menu's Story Direction, Horizontal/Vertical, Tate-Chu-Yoko, Ruby… and Kenten are translated too. Arabic labels use Unicode bidi run ordering and contextual shaping; welcome and new-document form layouts follow RTL direction independently of document binding. CJK and Arabic glyphs come from [craft-fonts](https://github.com/storytold/craft-fonts) when built with `CRAFT_FONTS_DIR` (all releases): BIZ UDPGothic and Noto Sans CJK SC for the UI (Chinese forms first in the Chinese interface), Noto Sans Arabic, and Mincho for Japanese documents (the web build embeds BIZ UDPGothic only); otherwise from system fonts.
|
||||
- Interface language (Edit ▸ Interface Language, `app.language`): German, French, Spanish, Japanese, Simplified Chinese and Arabic menu titles, common menu items and panel names (our own translations). Common dialog, panel and tooltip labels are also localized, with English fallback for untranslated strings. Arabic translations cover every key in the shared interface translation table, including menu commands and utility labels. The Type menu's Story Direction, Horizontal/Vertical, Tate-Chu-Yoko, Ruby… and Kenten are translated too. Arabic labels use Unicode bidi run ordering and contextual shaping; welcome and new-document form layouts follow RTL direction independently of document binding. CJK and Arabic glyphs come from [craft-fonts](https://github.com/storytold/craft-fonts) when built with `CRAFT_FONTS_DIR` (all releases): BIZ UDPGothic and Noto Sans CJK SC for the UI (Chinese forms first in the Chinese interface), Noto Sans Arabic, and Mincho for Japanese documents (the web build embeds BIZ UDPGothic only); otherwise from system fonts. Ukrainian covers all 1,430 existing catalog keys, including publishing dialogs and built-in names, with persisted selection, grammatical count captions, English fallback and bundled Cyrillic UI glyph coverage.
|
||||
- Page transitions (`page.transition/transitions`, Page Transitions panel): twelve transition types with speed and direction per spread, written into exported PDFs as `/Trans` entries through an incremental update.
|
||||
- PDF/X-4 export (`file.exportPdf {standard: "x4"}`): an output intent with our own Generic CMYK ICC profile (lut16 tables generated from the parametric press model), GTS_PDFXVersion in Info and XMP, and built-in PDF/X-4 checks reported as warnings. qpdf finds no errors in the result.
|
||||
- EPS place: placed at its (high-resolution) bounding box and shown, printed and exported through its TIFF preview, or a placeholder when it has none. PostScript itself isn't interpreted.
|
||||
|
||||
@@ -589,7 +589,14 @@ pub fn status_bar(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
egui::RichText::new(if errors == 0 {
|
||||
format!("{} ▾", crate::i18n::tr(&app.ui.language, "No errors"))
|
||||
} else {
|
||||
format!("{errors} {} ▾", crate::i18n::tr(&app.ui.language, if errors == 1 { "error" } else { "errors" }))
|
||||
format!(
|
||||
"{} ▾",
|
||||
crate::i18n::count_label(
|
||||
&app.ui.language,
|
||||
if errors == 1 && app.ui.language != "uk" { "error" } else { "errors" },
|
||||
errors
|
||||
)
|
||||
)
|
||||
})
|
||||
.font(small.clone()),
|
||||
);
|
||||
|
||||
@@ -1344,7 +1344,7 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) {
|
||||
egui::Grid::new("ins_table").num_columns(4).spacing([8.0, 6.0]).show(ui, |ui| {
|
||||
crate::rtl::label(ui, crate::i18n::tr(&app.ui.language, "Body Rows"));
|
||||
text_field(ui, &mut d, "bodyRows", 60.0);
|
||||
crate::rtl::label(ui, crate::i18n::tr(&app.ui.language, "Columns"));
|
||||
crate::rtl::label(ui, crate::i18n::tr_context(&app.ui.language, "Columns", "table"));
|
||||
text_field(ui, &mut d, "columns", 60.0);
|
||||
ui.end_row();
|
||||
crate::rtl::label(ui, crate::i18n::tr(&app.ui.language, "Header Rows"));
|
||||
@@ -1837,13 +1837,25 @@ fn paragraph_style_options(app: &mut DesignApp, ui: &mut egui::Ui, d: &mut Dialo
|
||||
}
|
||||
});
|
||||
let mut n = ns["count"].as_u64().unwrap_or(1) as u32;
|
||||
if ui.add(egui::DragValue::new(&mut n).range(1..=999)).changed() {
|
||||
ns["count"] = json!(n);
|
||||
changed = true;
|
||||
let mut count_value = |ui: &mut egui::Ui, ns: &mut Value, changed: &mut bool| {
|
||||
if ui.add(egui::DragValue::new(&mut n).range(1..=999)).changed() {
|
||||
ns["count"] = json!(n);
|
||||
*changed = true;
|
||||
}
|
||||
};
|
||||
if app.ui.language != "uk" {
|
||||
count_value(ui, ns, &mut changed);
|
||||
}
|
||||
let kind = ns["until"]["kind"].as_str().unwrap_or("words").to_string();
|
||||
egui::ComboBox::from_id_salt(("ns_until", i))
|
||||
.selected_text(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, &kind)))
|
||||
.selected_text(crate::rtl::widget(
|
||||
ui,
|
||||
if app.ui.language == "uk" {
|
||||
format!("{}:", crate::i18n::tr("uk", &kind))
|
||||
} else {
|
||||
crate::i18n::tr(&app.ui.language, &kind).to_owned()
|
||||
},
|
||||
))
|
||||
.width(100.0)
|
||||
.show_ui(ui, |ui| {
|
||||
for k in
|
||||
@@ -1855,6 +1867,9 @@ fn paragraph_style_options(app: &mut DesignApp, ui: &mut egui::Ui, d: &mut Dialo
|
||||
}
|
||||
}
|
||||
});
|
||||
if app.ui.language == "uk" {
|
||||
count_value(ui, ns, &mut changed);
|
||||
}
|
||||
if kind == "chars" {
|
||||
let mut c = ns["until"]["chars"].as_str().unwrap_or("").to_string();
|
||||
if ui.add(egui::TextEdit::singleline(&mut c).desired_width(40.0)).changed() {
|
||||
@@ -1906,13 +1921,22 @@ fn paragraph_style_options(app: &mut DesignApp, ui: &mut egui::Ui, d: &mut Dialo
|
||||
}
|
||||
}
|
||||
});
|
||||
crate::rtl::label(ui, crate::i18n::tr(&app.ui.language, "for"));
|
||||
crate::rtl::label(
|
||||
ui,
|
||||
if app.ui.language == "uk" {
|
||||
format!("{}:", crate::i18n::tr("uk", "lines"))
|
||||
} else {
|
||||
crate::i18n::tr(&app.ui.language, "for").to_owned()
|
||||
},
|
||||
);
|
||||
let mut n = l["lines"].as_u64().unwrap_or(1) as u32;
|
||||
if ui.add(egui::DragValue::new(&mut n).range(1..=999)).changed() {
|
||||
l["lines"] = json!(n);
|
||||
changed = true;
|
||||
}
|
||||
crate::rtl::label(ui, crate::i18n::tr(&app.ui.language, if n == 1 { "line" } else { "lines" }));
|
||||
if app.ui.language != "uk" {
|
||||
crate::rtl::label(ui, crate::i18n::tr(&app.ui.language, if n == 1 { "line" } else { "lines" }));
|
||||
}
|
||||
if ui
|
||||
.small_button("×")
|
||||
.on_hover_ui(|ui| {
|
||||
|
||||
@@ -5,6 +5,7 @@ use std::{collections::HashMap, sync::OnceLock};
|
||||
|
||||
mod ar;
|
||||
mod pt_br;
|
||||
mod uk;
|
||||
|
||||
/// Supported interface languages: (code, name in that language).
|
||||
pub const LANGUAGES: &[(&str, &str)] = &[
|
||||
@@ -16,6 +17,7 @@ pub const LANGUAGES: &[(&str, &str)] = &[
|
||||
("zh", "简体中文"),
|
||||
("ar", "العربية"),
|
||||
("pt-br", "Português (Brasil)"),
|
||||
("uk", "Українська"),
|
||||
];
|
||||
|
||||
/// English → [German, French, Spanish, Japanese, Simplified Chinese].
|
||||
@@ -2752,6 +2754,7 @@ fn column(lang: &str) -> Option<usize> {
|
||||
pub fn tr<'a>(lang: &str, s: &'a str) -> &'a str {
|
||||
static TRANSLATIONS: OnceLock<HashMap<&'static str, [&'static str; 5]>> = OnceLock::new();
|
||||
static ARABIC: OnceLock<HashMap<&'static str, &'static str>> = OnceLock::new();
|
||||
static UKRAINIAN: OnceLock<HashMap<&'static str, &'static str>> = OnceLock::new();
|
||||
static PORTUGUESE_BR: OnceLock<HashMap<&'static str, &'static str>> = OnceLock::new();
|
||||
if lang == "ar" {
|
||||
return ARABIC.get_or_init(|| ar::TABLE.iter().copied().collect()).get(s).copied().unwrap_or(s);
|
||||
@@ -2759,10 +2762,32 @@ pub fn tr<'a>(lang: &str, s: &'a str) -> &'a str {
|
||||
if lang == "pt-br" {
|
||||
return PORTUGUESE_BR.get_or_init(|| pt_br::TABLE.iter().copied().collect()).get(s).copied().unwrap_or(s);
|
||||
}
|
||||
if lang == "uk" {
|
||||
return UKRAINIAN.get_or_init(|| uk::TABLE.iter().copied().collect()).get(s).copied().unwrap_or(s);
|
||||
}
|
||||
let Some(c) = column(lang) else { return s };
|
||||
TRANSLATIONS.get_or_init(|| TABLE.iter().copied().collect()).get(s).and_then(|row| row.get(c)).copied().unwrap_or(s)
|
||||
}
|
||||
|
||||
/// Context-sensitive UI captions; unsupported contexts keep the shared translation.
|
||||
pub fn tr_context<'a>(lang: &str, s: &'a str, context: &str) -> &'a str {
|
||||
if lang == "uk" {
|
||||
return uk::CONTEXT.iter().find(|(key, scope, _)| *key == s && *scope == context).map_or_else(|| tr(lang, s), |(_, _, text)| text);
|
||||
}
|
||||
tr(lang, s)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn ukrainian_catalog() -> Vec<&'static str> {
|
||||
uk::TABLE.iter().map(|(_, text)| *text).chain(uk::CONTEXT.iter().map(|(_, _, text)| *text)).collect()
|
||||
}
|
||||
|
||||
/// Count captions in Ukrainian put the category before the value so every count uses
|
||||
/// a grammatical form; other languages keep the existing value-first caption.
|
||||
pub fn count_label(lang: &str, noun: &str, count: usize) -> String {
|
||||
if lang == "uk" { format!("{}: {count}", tr(lang, noun)) } else { format!("{count} {}", tr(lang, noun)) }
|
||||
}
|
||||
|
||||
/// Localize reserved built-in style names only; user-defined names are document data.
|
||||
pub fn style_name<'a>(lang: &str, name: &'a str) -> &'a str {
|
||||
match name {
|
||||
@@ -2797,8 +2822,11 @@ mod tests {
|
||||
for (key, expected) in pt_br::TABLE {
|
||||
assert_eq!(tr("pt-br", key), *expected, "pt-br: {key}");
|
||||
}
|
||||
for (key, expected) in uk::TABLE {
|
||||
assert_eq!(tr("uk", key), *expected, "uk: {key}");
|
||||
}
|
||||
let unknown = String::from("A user-defined untranslated label");
|
||||
for lang in ["ar", "pt-br", "zh", "ja", "de", "fr", "es", "", "unknown"] {
|
||||
for lang in ["uk", "ar", "pt-br", "zh", "ja", "de", "fr", "es", "", "unknown"] {
|
||||
assert!(std::ptr::eq(tr(lang, &unknown), unknown.as_str()));
|
||||
}
|
||||
}
|
||||
@@ -2830,6 +2858,56 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ukrainian_catalog_covers_existing_keys_and_preserves_placeholders() {
|
||||
let catalog: std::collections::HashMap<_, _> = uk::TABLE.iter().copied().collect();
|
||||
assert_eq!(catalog.len(), uk::TABLE.len(), "duplicate Ukrainian keys");
|
||||
let mut keys = std::collections::HashSet::new();
|
||||
keys.extend(TABLE.iter().map(|(key, _)| *key));
|
||||
keys.extend(ar::TABLE.iter().map(|(key, _)| *key));
|
||||
keys.extend(pt_br::TABLE.iter().map(|(key, _)| *key));
|
||||
assert_eq!(keys, catalog.keys().copied().collect());
|
||||
let placeholders =
|
||||
|s: &str| s.split('{').skip(1).filter_map(|part| part.split_once('}').map(|(name, _)| name.to_owned())).collect::<Vec<_>>();
|
||||
for (key, translation) in uk::TABLE {
|
||||
assert!(!translation.trim().is_empty(), "empty {key}");
|
||||
let mut source = placeholders(key);
|
||||
let mut translated = placeholders(translation);
|
||||
source.sort();
|
||||
translated.sort();
|
||||
assert_eq!(source, translated, "placeholders: {key}");
|
||||
}
|
||||
assert_eq!(tr_context("uk", "Columns", "table"), "Стовпці");
|
||||
assert_eq!(tr("uk", "Columns"), "Колонки");
|
||||
assert_eq!(tr_context("uk", "Group", "selection"), "Група");
|
||||
assert_eq!(tr("uk", "Group"), "Згрупувати");
|
||||
assert_eq!(tr_context("", "Columns", "table"), "Columns");
|
||||
assert_eq!(tr_context("uk", "Custom caption", "custom"), "Custom caption");
|
||||
assert_eq!(tr("uk", "through"), "включно");
|
||||
assert_eq!(tr("uk", "up to"), "не включно");
|
||||
assert!(!is_rtl("uk"));
|
||||
assert_eq!(tr("uk", "Type"), "Текст");
|
||||
assert_eq!(tr("uk", "Spread"), "Розворот");
|
||||
assert_eq!(tr("uk", "Story"), "Текстовий матеріал");
|
||||
assert_eq!(tr("uk", "Footer Rows"), "Рядки нижнього колонтитула");
|
||||
assert_eq!(style_name("uk", "[Basic Paragraph]"), "[Основний абзац]");
|
||||
assert_eq!(style_name("uk", "My layout style"), "My layout style");
|
||||
assert_eq!(workspace_name("uk", "Essentials"), "Основне");
|
||||
assert_eq!(workspace_name("uk", "My workspace"), "My workspace");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ukrainian_counts_use_category_captions_for_every_number() {
|
||||
for n in [0, 1, 2, 4, 5, 11, 21, 22, 25] {
|
||||
assert_eq!(count_label("uk", "pages", n), format!("сторінок: {n}"));
|
||||
assert_eq!(count_label("uk", "objects", n), format!("об’єктів: {n}"));
|
||||
assert_eq!(count_label("uk", "errors", n), format!("помилок: {n}"));
|
||||
let pages = tr("uk", "{pages} Pages in {spreads} Spreads").replace("{pages}", &n.to_string()).replace("{spreads}", &n.to_string());
|
||||
assert_eq!(pages, format!("Сторінок: {n}; розворотів: {n}"));
|
||||
}
|
||||
assert_eq!(count_label("", "objects", 2), "2 objects");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn japanese_vertical_type_commands_are_translated() {
|
||||
for (en, ja) in [
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -38,7 +38,7 @@ pub const UI_COMMANDS: &[(&str, &str, Option<&str>, &str)] = &[
|
||||
"app.language",
|
||||
"Interface Language",
|
||||
None,
|
||||
"{lang: \"\"|de|fr|es|ja|zh|ar|pt-br} — menus and panel names (the macOS menu bar follows on the next launch)",
|
||||
"{lang: \"\"|de|fr|es|ja|zh|ar|pt-br|uk} — menus and panel names (the macOS menu bar follows on the next launch)",
|
||||
),
|
||||
(
|
||||
"app.flattener",
|
||||
@@ -234,6 +234,7 @@ pub const MENUS: &[(&str, &[&str])] = &[
|
||||
"ui:app.language|简体中文|{\"lang\": \"zh\"}",
|
||||
"ui:app.language|العربية|{\"lang\": \"ar\"}",
|
||||
"ui:app.language|Português (Brasil)|{\"lang\": \"pt-br\"}",
|
||||
"ui:app.language|Українська|{\"lang\": \"uk\"}",
|
||||
"<",
|
||||
">Transparency Flattener Presets",
|
||||
"ui:app.flattener|None (keep transparency)|{\"preset\": \"\"}",
|
||||
@@ -2206,6 +2207,30 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ukrainian_language_is_selectable_persisted_and_keeps_document_data() {
|
||||
let mut app = crate::DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
|
||||
app.run("file.new", json!({"pages": 4, "facingPages": true})).unwrap();
|
||||
app.run("frame.create", json!({"rect": [36, 36, 200, 200], "content": "text", "text": "My words · Мої слова"})).unwrap();
|
||||
let before = serde_json::to_value(&app.session.doc().unwrap().doc).unwrap();
|
||||
run_ui(&mut app, "app.language", &json!({"lang": "uk"})).unwrap().unwrap();
|
||||
assert_eq!(checked(&app, "app.language", &json!({"lang": "uk"})), Some(true));
|
||||
assert_eq!(before, serde_json::to_value(&app.session.doc().unwrap().doc).unwrap());
|
||||
for (title, _) in menu_tree() {
|
||||
assert_ne!(crate::i18n::tr("uk", title), title, "{title}");
|
||||
}
|
||||
let bytes = serde_json::to_vec(&app.ui).unwrap();
|
||||
let restored: crate::UiState = serde_json::from_slice(&bytes).unwrap();
|
||||
assert_eq!(restored.language, "uk");
|
||||
assert_eq!(crate::i18n::tr(&restored.language, "File"), "Файл");
|
||||
assert_eq!(crate::i18n::tr(&restored.language, "A custom name"), "A custom name");
|
||||
assert!(menu_tree().iter().flat_map(|(_, entries)| entries.iter()).any(|entry| {
|
||||
matches!(entry, Item::Sub(label, children) if label == "Interface Language" && children.iter().any(|child| {
|
||||
matches!(child, Item::Cmd { label, id, params, .. } if label == "Українська" && id == "app.language" && params["lang"] == "uk")
|
||||
}))
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arabic_dialog_and_language_switch_keep_finite_widget_geometry() {
|
||||
let mut app = crate::DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
|
||||
@@ -2233,7 +2258,7 @@ mod tests {
|
||||
crate::dialogs::confirm(&mut app).unwrap();
|
||||
frame(&mut app);
|
||||
assert_eq!(app.session.documents().len(), 1);
|
||||
for lang in ["zh", "", "ar", "pt-br"] {
|
||||
for lang in ["uk", "zh", "", "ar", "pt-br"] {
|
||||
app.run("app.language", json!({"lang": lang})).unwrap();
|
||||
frame(&mut app);
|
||||
}
|
||||
|
||||
@@ -135,7 +135,7 @@ pub fn articles(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
crate::rtl::label(ui, egui::RichText::new(&name).strong());
|
||||
crate::rtl::label(
|
||||
ui,
|
||||
egui::RichText::new(format!("{} {}", a["items"].as_array().map_or(0, Vec::len), crate::i18n::tr(&app.ui.language, "objects")))
|
||||
egui::RichText::new(crate::i18n::count_label(&app.ui.language, "objects", a["items"].as_array().map_or(0, Vec::len)))
|
||||
.size(10.5)
|
||||
.color(t.text_dim),
|
||||
);
|
||||
|
||||
@@ -56,7 +56,7 @@ fn selection_title(app: &DesignApp, info: &Option<SelInfo>) -> String {
|
||||
Some(i) if i.count > 1 => crate::i18n::tr(&app.ui.language, "Multiple Objects").into(),
|
||||
Some(i) => match i.kind {
|
||||
"<text frame>" => crate::i18n::tr(&app.ui.language, "Text Frame").into(),
|
||||
"<group>" => crate::i18n::tr(&app.ui.language, "Group").into(),
|
||||
"<group>" => crate::i18n::tr_context(&app.ui.language, "Group", "selection").into(),
|
||||
"<image>" => crate::i18n::tr(&app.ui.language, "Image").into(),
|
||||
"<rectangle>" => crate::i18n::tr(&app.ui.language, "Rectangle").into(),
|
||||
"<ellipse>" => crate::i18n::tr(&app.ui.language, "Ellipse").into(),
|
||||
@@ -1935,13 +1935,10 @@ pub fn info_panel(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
crate::rtl::label(
|
||||
ui,
|
||||
format!(
|
||||
"{} {} · {} {} · {} {}",
|
||||
st.doc.page_count(),
|
||||
crate::i18n::tr(&app.ui.language, "pages"),
|
||||
st.doc.stories.len(),
|
||||
crate::i18n::tr(&app.ui.language, "stories"),
|
||||
st.doc.all_items().len(),
|
||||
crate::i18n::tr(&app.ui.language, "items")
|
||||
"{} · {} · {}",
|
||||
crate::i18n::count_label(&app.ui.language, "pages", st.doc.page_count()),
|
||||
crate::i18n::count_label(&app.ui.language, "stories", st.doc.stories.len()),
|
||||
crate::i18n::count_label(&app.ui.language, "items", st.doc.all_items().len())
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -2055,10 +2052,9 @@ pub fn preflight_panel(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
crate::i18n::tr(&app.ui.language, "No errors").to_string()
|
||||
} else {
|
||||
format!(
|
||||
"{errors} {}, {} {}",
|
||||
crate::i18n::tr(&app.ui.language, "errors"),
|
||||
issues.len() - errors,
|
||||
crate::i18n::tr(&app.ui.language, "warnings")
|
||||
"{}, {}",
|
||||
crate::i18n::count_label(&app.ui.language, "errors", errors),
|
||||
crate::i18n::count_label(&app.ui.language, "warnings", issues.len() - errors)
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
@@ -100,7 +100,7 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
run(app, "table.deleteRow", json!({"table": tid, "rows": [n, rows.len() - 1]}));
|
||||
}
|
||||
}
|
||||
caption(ui, crate::i18n::tr(&app.ui.language, "Columns"));
|
||||
caption(ui, crate::i18n::tr_context(&app.ui.language, "Columns", "table"));
|
||||
if let Some(v) = number(ui, "tp_cols", Some(cols.len() as f64), "", 52.0, 0) {
|
||||
let n = v.round().max(1.0) as usize;
|
||||
if n > cols.len() {
|
||||
|
||||
@@ -424,3 +424,31 @@ mod japanese_font_tests {
|
||||
super::install_fonts(&egui::Context::default(), "ja");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod ukrainian_font_tests {
|
||||
#[test]
|
||||
fn every_catalog_cyrillic_glyph_exists_in_the_bundled_ui_fonts() {
|
||||
// Check the actual installed face data, without craft-fonts or system fonts.
|
||||
// egui's has_glyph can report false for the face providing its replacement glyph.
|
||||
let definitions = super::font_definitions(&[], "uk");
|
||||
let db = designcraft_fonts::FontDb::with_font_dirs(Vec::new());
|
||||
db.set_system_fallback(false);
|
||||
let characters: std::collections::HashSet<_> = crate::i18n::ukrainian_catalog()
|
||||
.iter()
|
||||
.flat_map(|text| text.chars())
|
||||
.chain("ҐґЄєІіЇї".chars())
|
||||
.filter(|c| matches!(*c, '\u{0400}'..='\u{052f}'))
|
||||
.collect();
|
||||
for (name, family, style) in
|
||||
[("ui", "Source Sans 3", "Regular"), ("ui-semibold", "Source Sans 3", "Semibold"), ("mono", "JetBrains Mono", "Regular")]
|
||||
{
|
||||
let data = &definitions.font_data[name].font;
|
||||
let face = db.face(family, style);
|
||||
assert_eq!(face.data(), data.as_ref(), "configured UI face {name}");
|
||||
for character in &characters {
|
||||
assert_ne!(face.glyph_for(*character), 0, "missing {character} in {name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user