i18n: add Korean UI catalog and font fallback (#1216)

Korean interface catalog and Hangul font fallback by @heogun (#1216), merged with main.

Co-authored-by: heogun <mumal@MP.local>
This commit is contained in:
Brandon Thomasandheogun authored and GitHub committed 2026-10-14 23:44:26 -04:00
1 parent 3292ba05d3
commit e5d9b3e7cf
11 files changed
+2705 -50

No files matched your search

+2 -1
View File
@@ -109,7 +109,7 @@ Weights and evidence per area: [docs/target-app-parity.md](docs/target-app-parit
| Indonesian | id | none | 0% |
| Japanese | ja | partial | ~95% |
| German | de | partial (#574) | ~95% |
| Korean | ko | none | 0% |
| Korean | ko | partial (interface catalog; proofing unavailable) | 100% (2,463 entries) |
| Vietnamese | vi | partial (#1434) | ~95% |
Also shipped: Traditional Chinese (`zh-hant`), Ukrainian (`uk`), Serbian (`sr`, `sr-latn`, #250), Russian (`ru`) and Polish (`pl`), all partial (~95%). Bulgarian (`bg`) covers everything the interface audit finds (2,419 entries). Estonian (`et`), Italian (`it`, #594) and Czech (`cs`) now have complete interface catalogs, and Turkish (`tr`) a complete 2,325-entry one (every key of the Russian catalog), all with
@@ -276,6 +276,7 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm
| 2026-10-11 | Hide White Space (#645): `view.hideWhiteSpace` (View › Show checkbox, or double-click the gap between pages) collapses margins, headers and footers between pages to a thin line in Print Layout; screen only, page count unchanged |
| 2026-10-11 | Fonts: Palatino Linotype and Book Antiqua fall back to Palatino (macOS) or TeX Gyre Pagella / P052 / URW Palladio (Linux) before generic serif (#670) |
|---|---|
| 2026-10-13 | Korean interface catalog completed: all 2,463 entries translated; the ko-KR system locale and saved language choice select the interface. The UI prefers Korean embedded/system font faces so Hangul renders instead of boxes, and print settings and printer feedback are translated. Korean proofing remains unavailable; macOS Korean IME fixes are still pending (#163, #164) |
| 2026-10-12 | Word default shortcuts expanded (gap #24): 140 commands bound on macOS (169 combinations), 158 on Linux/Windows (188 combinations). Bound Style Inspector (Shift+F1), Macros (Alt+F8), Strikethrough (Mod+Shift+X), Find Next (Shift+F4), Copy/Paste (Mod+Insert, Shift+Insert), Go Back (Mod+Alt+Z), File Open/Save/Save As/Print (Mod+Alt+F2, Alt+Shift+F2, Alt+F2, Mod+Alt+I), Field navigation (Alt+F1, Alt+Shift+F1), Outline show first line (Alt+Shift+L), Next Footnote (Alt+Shift+>), Link to Previous (Alt+Shift+R), AutoText (Alt+F3), Spelling on Mac (Mod+Alt+L) |
| 2026-10-13 | Web and Draft Layout now render visible full-resolution raster tiles for exceptionally tall pages rather than downsampling the entire page into one texture; verified with a 440-paragraph headless UI reproduction |
| 2026-10-12 | Track Changes: Accept All after a tracked deletion from the end of one paragraph into a later one no longer joins the paragraph that follows the range, and Reject All after typing into an empty paragraph keeps that paragraph (#864) |
+23 -6
View File
@@ -24,7 +24,7 @@ pub use kurbo::BezPath;
pub use script::{fallback_rank, needs_layout_tables, script_fallbacks};
use skrifa::MetadataProvider;
use skrifa::instance::Size;
pub use ui_fallback::{SystemUiFont, is_cjk, system_cjk_ui_font, ui_needs_system_cjk, ui_uncovered_cjk};
pub use ui_fallback::{SystemUiFont, is_cjk, system_cjk_ui_font, system_cjk_ui_font_for, ui_needs_system_cjk, ui_uncovered_cjk};
/// A font from the optional craft-fonts build input (https://github.com/storytold/craft-fonts;
/// empty unless built with `CRAFT_FONTS_DIR`, see `build.rs`).
@@ -65,15 +65,32 @@ pub fn chinese_fonts() -> impl Iterator<Item = &'static CraftFont> {
CRAFT_FONTS.iter().filter(|f| f.scripts.contains(&"Hans"))
}
/// The CJK interface faces, Regular styles only (the UI fakes no bold): the Japanese UI face and
/// the Chinese face, Chinese first when `prefer_hans` (a Chinese interface), Japanese first
/// otherwise. Mixing the two within a line would mix glyph styles, so the first face should
/// cover the interface language. Empty without craft-fonts.
/// The craft-fonts faces for Korean (`"Kore"`), in manifest order. Empty without craft-fonts.
pub fn korean_fonts() -> impl Iterator<Item = &'static CraftFont> {
CRAFT_FONTS.iter().filter(|f| f.scripts.contains(&"Kore"))
}
/// The CJK interface faces, Regular styles only (the UI fakes no bold), with the Japanese UI,
/// Chinese, and Korean faces ordered for the selected interface language. Mixing the faces within
/// a line would mix glyph styles, so the first face should cover the interface language. Empty
/// without craft-fonts.
pub fn ui_cjk_fonts(prefer_hans: bool) -> Vec<&'static CraftFont> {
ui_cjk_fonts_for(prefer_hans, false)
}
/// The CJK interface faces in the preferred writing system's order.
pub fn ui_cjk_fonts_for(prefer_hans: bool, prefer_korean: bool) -> Vec<&'static CraftFont> {
let regular = |f: &&CraftFont| f.style == "Regular";
let ja = japanese_ui_fonts(false).into_iter().filter(regular).take(1);
let zh = chinese_fonts().filter(regular).take(1);
if prefer_hans { zh.chain(ja).collect() } else { ja.chain(zh).collect() }
let ko = korean_fonts().filter(regular).take(1);
if prefer_korean {
ko.chain(ja).chain(zh).collect()
} else if prefer_hans {
zh.chain(ja).chain(ko).collect()
} else {
ja.chain(zh).chain(ko).collect()
}
}
/// The bundled serif used when a document asks for nothing better.
+56 -11
View File
@@ -70,16 +70,25 @@ const MAX_NEEDED: usize = 4096;
/// as blank space.
const OUTLINE_TABLES: [&[u8; 4]; 4] = [b"glyf", b"CFF ", b"CFF2", b"VARC"];
/// A character only a font for the language has: a simplified-only hanzi, or kana.
fn probe(hans: bool) -> char {
if hans { '页' } else { 'の' }
/// A character only a font for the language has: a simplified-only hanzi, kana, or Hangul.
fn probe(hans: bool, korean: bool) -> char {
if korean {
'한'
} else if hans {
'页'
} else {
'の'
}
}
/// Font files to look for, relative to a font folder (or absolute), best first. Chinese faces
/// cover kana too, so the Japanese list ends with the Chinese one.
fn candidates(hans: bool) -> Vec<&'static str> {
let (zh, ja): (&[&str], &[&str]) = if cfg!(windows) {
(&["Deng.ttf", "msyh.ttc", "simhei.ttf", "simsun.ttc", "NotoSansSC-VF.ttf"], &["YuGothR.ttc", "meiryo.ttc", "msgothic.ttc"])
/// Font files to look for, relative to a font folder (or absolute), best first.
fn candidates(hans: bool, korean: bool) -> Vec<&'static str> {
let (zh, ja, ko): (&[&str], &[&str], &[&str]) = if cfg!(windows) {
(
&["Deng.ttf", "msyh.ttc", "simhei.ttf", "simsun.ttc", "NotoSansSC-VF.ttf"],
&["YuGothR.ttc", "meiryo.ttc", "msgothic.ttc"],
&["malgun.ttf", "malgunsl.ttf", "gulim.ttc", "batang.ttc"],
)
} else if cfg!(target_os = "macos") {
(
&[
@@ -92,6 +101,7 @@ fn candidates(hans: bool) -> Vec<&'static str> {
"/System/Library/PrivateFrameworks/FontServices.framework/Resources/Reserved/PingFangUI.ttc",
],
&["ヒラギノ角ゴシック W3.ttc", "Hiragino Sans W3.ttc"],
&["/System/Library/Fonts/AppleSDGothicNeo.ttc", "/System/Library/Fonts/Supplemental/AppleGothic.ttf"],
)
} else {
(
@@ -110,18 +120,37 @@ fn candidates(hans: bool) -> Vec<&'static str> {
"truetype/droid/DroidSansFallbackFull.ttf",
],
&["noto-cjk/NotoSansCJKjp-Regular.otf", "opentype/noto/NotoSansCJKjp-Regular.otf"],
&[
"noto-cjk/NotoSansCJKkr-Regular.otf",
"opentype/noto/NotoSansCJKkr-Regular.otf",
"google-noto-sans-cjk-fonts/NotoSansCJKkr-Regular.otf",
"adobe-source-han-sans/SourceHanSansK-Regular.otf",
"opentype/adobe-source-han-sans/SourceHanSansK-Regular.otf",
"truetype/nanum/NanumGothic.ttf",
],
)
};
if hans { zh.to_vec() } else { ja.iter().chain(zh).copied().collect() }
if korean {
ko.iter().chain(ja).chain(zh).copied().collect()
} else if hans {
zh.to_vec()
} else {
ja.iter().chain(zh).copied().collect()
}
}
/// The installed CJK font for a Chinese (`hans`) or other interface that best covers `needed`
/// (the characters the embedded faces lack; empty: a character typical of the language), if one
/// is found. Never panics on damaged files.
pub fn system_cjk_ui_font(hans: bool, needed: &[char]) -> Option<SystemUiFont> {
let probe = [probe(hans)];
system_cjk_ui_font_for(hans, false, needed)
}
/// The installed CJK font for a Chinese, Korean or Japanese interface.
pub fn system_cjk_ui_font_for(hans: bool, korean: bool, needed: &[char]) -> Option<SystemUiFont> {
let probe = [probe(hans, korean)];
let needed = if needed.is_empty() { &probe[..] } else { needed.get(..MAX_NEEDED).unwrap_or(needed) };
find_ui_font(&with_asset_data(crate::system_font_dirs()), &candidates(hans), needed, hans)
find_ui_font(&with_asset_data(crate::system_font_dirs()), &candidates(hans, korean), needed, hans)
}
/// `dirs` and, for each macOS font asset folder among them (`com_apple_MobileAsset_Font<N>`),
@@ -210,6 +239,22 @@ pub(crate) fn pick_face(data: &[u8], needed: &[char], hans: bool) -> Option<(u32
mod tests {
use super::*;
#[test]
fn korean_interface_uses_korean_first_system_font_candidates() {
let names = candidates(false, true);
assert_eq!(
names.first(),
Some(&if cfg!(windows) {
"malgun.ttf"
} else if cfg!(target_os = "macos") {
"/System/Library/Fonts/AppleSDGothicNeo.ttc"
} else {
"noto-cjk/NotoSansCJKkr-Regular.otf"
})
);
assert_eq!(probe(false, true), '한');
}
#[test]
fn needs_system_cjk_when_an_embedded_face_lacks_an_interface_character() {
// A face without CJK glyphs stands in for a craft-fonts face that lacks some (#486).
+7 -1
View File
@@ -1389,7 +1389,13 @@ pub fn show(app: &mut WordApp, ui: &mut Ui) {
);
if !new.is_empty() {
app.canvas.ime_faces.extend(new);
crate::theme::install_fonts_extra(ui.ctx(), app.fonts_hans, app.fonts_system_cjk, &app.canvas.ime_faces);
crate::theme::install_fonts_language_extra(
ui.ctx(),
app.fonts_hans,
app.fonts_korean,
app.fonts_system_cjk,
&app.canvas.ime_faces,
);
ui.ctx().request_repaint();
}
let caret_color = caret_color(app);
File diff suppressed because it is too large. Load diff
+19 -1
View File
@@ -52,7 +52,7 @@ pub struct LangInfo {
}
/// The registry. English first: it is the fallback and the source language.
pub static LANGUAGES: [LangInfo; 20] = [
pub static LANGUAGES: [LangInfo; 21] = [
LangInfo { code: "en", name: "English", source: "", prefer_hans: false, editing_lang: None, catalog: OnceLock::new() },
// Simplified Chinese; `zh`, `zh-CN`, `zh-SG` and `zh-Hans-*` resolve here (see `candidates`).
LangInfo {
@@ -73,6 +73,14 @@ pub static LANGUAGES: [LangInfo; 20] = [
catalog: OnceLock::new(),
},
LangInfo { code: "ja", name: "日本語", source: include_str!("ja.tsv"), prefer_hans: false, editing_lang: None, catalog: OnceLock::new() },
LangInfo {
code: "ko",
name: "한국어",
source: include_str!("ko.tsv"),
prefer_hans: false, // 일본어처럼 CJK지만 한자 우선순위는 필요 없으므로 false
editing_lang: None, // 현재 한국어 맞춤법(proofing) 지원이 없으므로 None
catalog: OnceLock::new(),
},
LangInfo {
code: "uk", name: "Українська", source: include_str!("uk.tsv"), prefer_hans: false, editing_lang: None, catalog: OnceLock::new()
},
@@ -193,6 +201,10 @@ impl Lang {
self.0.prefer_hans
}
pub fn prefers_korean(self) -> bool {
self.0.code == "ko"
}
/// The language new blank documents are written in with this interface ([`LangInfo::editing_lang`]).
pub fn editing_lang(self) -> Option<&'static str> {
self.0.editing_lang
@@ -211,6 +223,12 @@ impl Lang {
(catalogs, Lang::all().map(Lang::name).collect::<Vec<_>>().join(" "))
}
/// The CJK interface catalogs using the Korean or Chinese writing-system preference.
pub fn cjk_font_text_for(hans: bool, korean: bool) -> (Vec<&'static str>, String) {
let catalogs = Lang::all().filter(|l| l.uses_cjk() && l.prefers_hans() == hans && l.prefers_korean() == korean).map(|l| l.0.source).collect();
(catalogs, Lang::all().map(Lang::name).collect::<Vec<_>>().join(" "))
}
/// A language by its exact code (any case).
pub fn from_code(code: &str) -> Option<Lang> {
LANGUAGES.iter().find(|l| l.code.eq_ignore_ascii_case(code)).map(Lang)
+60 -6
View File
@@ -19,6 +19,8 @@ fn system_locales_map_to_languages() {
assert_eq!(lang_from_tag("zh-Hant"), Some(lang("zh-hant")));
assert_eq!(lang_from_tag("ja-JP"), Some(lang("ja")));
assert_eq!(lang_from_tag("ja_JP.eucJP@euro"), Some(lang("ja")));
assert_eq!(lang_from_tag("ko-KR"), Some(lang("ko")));
assert_eq!(lang_from_tag("ko_KR.UTF-8"), Some(lang("ko")));
assert_eq!(lang_from_tag("en-GB"), Some(Lang::EN));
assert_eq!(lang_from_tag("C"), Some(Lang::EN));
assert_eq!(lang_from_tag("POSIX"), Some(Lang::EN));
@@ -150,6 +152,40 @@ fn every_tab_and_command_is_translated() {
assert!(missing.is_empty(), "untranslated: {missing:#?}");
}
#[test]
fn korean_print_settings_and_printer_feedback_are_translated() {
let korean = lang("ko");
for source in [
"Printer",
"Select a printer",
"Refresh printers",
"Copies",
"Two-sided printing",
"Printer default",
"One-sided",
"Two-sided: long edge",
"Two-sided: short edge",
"Grayscale",
"Finding printers…",
"No printers found. Configure a system printer or export a PDF.",
"Printer discovery failed.",
"Submitting print job…",
"Print job submitted.",
"Print job submitted: {receipt}",
"Print failed: the printing worker stopped.",
"Print failed: {error}",
] {
assert!(has(korean, source), "missing Korean print translation: {source}");
}
assert_eq!(korean.catalog().plain("Printer"), Some("프린터"));
assert_eq!(korean.catalog().plain("Copies"), Some("매수"));
assert_eq!(korean.catalog().plain("Two-sided printing"), Some("양면 인쇄"));
assert_eq!(
korean.catalog().plain("No printers found. Configure a system printer or export a PDF."),
Some("프린터를 찾을 수 없습니다. 시스템 프린터를 설정하거나 PDF로 내보내세요.")
);
}
#[test]
fn the_language_setting_is_a_ui_command_and_persists() {
let mut app = crate::WordApp::new(wordcraft_engine::Session::new(wordcraft_doc::Document::new()), Default::default());
@@ -439,9 +475,24 @@ fn chinese_interface_fonts_load_and_cover_simplified_hanzi() {
}
}
#[test]
fn korean_interface_fonts_load_and_cover_hangul() {
let ctx = egui::Context::default();
ctx.set_fonts(crate::theme::font_definitions_language(false, true, true));
ctx.run_ui(Default::default(), |_| {}).textures_delta.clear();
let covered = ctx.fonts_mut(|f| f.has_glyphs(&egui::FontId::proportional(14.0), "한글"));
let (catalogs, names) = Lang::cjk_font_text_for(false, true);
let mut texts = catalogs;
texts.push(&names);
let embedded = !wordcraft_fonts::ui_needs_system_cjk(&texts, &wordcraft_fonts::ui_cjk_fonts_for(false, true));
if embedded || wordcraft_fonts::system_cjk_ui_font_for(false, true, &['한']).is_some() {
assert!(covered, "a Korean face is available but the interface lacks Hangul glyphs");
}
}
#[test]
fn cjk_catalogs_are_covered_by_the_embedded_interface_faces() {
// #486: every CJK character of the Chinese and Japanese catalogs and of the language names
// #486: every CJK character of the Chinese, Japanese and Korean catalogs and language names
// has a glyph in the interface font chain built from the embedded faces alone, in every
// interface weight; `?` and boxes showed where the Japanese face was the only CJK one. Needs
// a craft-fonts build (CRAFT_FONTS_DIR); without one there is nothing embedded to check, and
@@ -451,10 +502,10 @@ fn cjk_catalogs_are_covered_by_the_embedded_interface_faces() {
return;
}
let names: String = Lang::all().map(Lang::name).collect();
for code in ["zh-hans", "zh-hant", "ja"] {
for code in ["zh-hans", "zh-hant", "ja", "ko"] {
let l = lang(code);
let ctx = egui::Context::default();
ctx.set_fonts(crate::theme::font_definitions(l.prefers_hans(), false));
ctx.set_fonts(crate::theme::font_definitions_language(l.prefers_hans(), l.prefers_korean(), false));
ctx.run_ui(Default::default(), |_| {}).textures_delta.clear();
let (entries, errors) = parse_entries(l.0.source);
assert!(errors.is_empty(), "{code}: {errors:?}");
@@ -466,7 +517,10 @@ fn cjk_catalogs_are_covered_by_the_embedded_interface_faces() {
assert!(missing.is_empty(), "{code}: no glyph in {font:?} for {missing}");
}
// So the interface reads no installed CJK font for these languages.
assert_eq!(wordcraft_fonts::ui_uncovered_cjk(&[l.0.source, &names], &wordcraft_fonts::ui_cjk_fonts(l.prefers_hans())), Vec::<char>::new());
assert_eq!(
wordcraft_fonts::ui_uncovered_cjk(&[l.0.source, &names], &wordcraft_fonts::ui_cjk_fonts_for(l.prefers_hans(), l.prefers_korean())),
Vec::<char>::new()
);
}
}
@@ -474,7 +528,7 @@ fn cjk_catalogs_are_covered_by_the_embedded_interface_faces() {
fn only_cjk_languages_ask_for_an_installed_cjk_font() {
// #241 follow-up: an English interface must not read a large system CJK font on startup.
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", "ko"]);
}
#[test]
@@ -1152,7 +1206,7 @@ fn bulgarian_locales_and_saved_preference_work_without_changing_the_document() {
for tag in ["bg", "bg-BG", "bg_BG.UTF-8", "BG-bg", "bg-Cyrl-BG"] {
assert_eq!(lang_from_tag(tag), Some(bg), "{tag}");
}
assert_eq!(first_supported(["ko-KR", "bg-BG", "en-US"]), Some(bg));
assert_eq!(first_supported([UNSUPPORTED, "bg-BG", "en-US"]), Some(bg));
let mut app = crate::WordApp::new(wordcraft_engine::Session::new(wordcraft_doc::Document::new()), Default::default());
let before = serde_json::to_value(&app.session.doc).unwrap();
let result = app.run("ui.language", serde_json::json!({"value": "BG"})).unwrap();
+7 -2
View File
@@ -460,6 +460,8 @@ pub struct WordApp {
styled: bool,
/// The CJK face order the installed UI fonts use (Chinese first, or Japanese first).
fonts_hans: bool,
/// Whether the installed UI fonts use the Korean CJK face order.
fonts_korean: bool,
/// Whether the installed UI fonts include an installed CJK fallback font (#241).
fonts_system_cjk: bool,
/// CJK text is (about to be) on screen in a non-CJK interface, e.g. the language names in
@@ -588,6 +590,7 @@ impl WordApp {
nav_search_focus: false,
styled: false,
fonts_hans: false,
fonts_korean: false,
fonts_system_cjk: false,
want_system_cjk: false,
fonts_frames: 0,
@@ -1830,9 +1833,11 @@ impl WordApp {
/// face has changed since the last install.
fn install_ui_fonts(&mut self, ctx: &egui::Context, lang: i18n::Lang) {
let system_cjk = self.fonts_system_cjk || self.want_system_cjk || lang.uses_cjk();
if !self.styled || lang.prefers_hans() != self.fonts_hans || system_cjk != self.fonts_system_cjk {
theme::install_fonts_extra(ctx, lang.prefers_hans(), system_cjk, &self.canvas.ime_faces);
if !self.styled || lang.prefers_hans() != self.fonts_hans || lang.prefers_korean() != self.fonts_korean || system_cjk != self.fonts_system_cjk
{
theme::install_fonts_language_extra(ctx, lang.prefers_hans(), lang.prefers_korean(), system_cjk, &self.canvas.ime_faces);
self.fonts_hans = lang.prefers_hans();
self.fonts_korean = lang.prefers_korean();
self.fonts_system_cjk = system_cjk;
// Mod with -, = and 0 are Word shortcuts (optional hyphen, subscript, paragraph spacing);
// egui's keyboard zoom would also scale the whole window on them. Zoom is View › Zoom.
+52 -19
View File
@@ -376,12 +376,17 @@ pub fn install_fonts(ctx: &egui::Context) {
install_fonts_for(ctx, false);
}
/// [`install_fonts`] with the CJK fallback order for the interface language: the Chinese face
/// first for Chinese (#8), the Japanese one otherwise. The new fonts apply from the next frame.
/// [`install_fonts`] with the Chinese or Japanese CJK fallback order. The new fonts apply from the
/// next frame.
pub fn install_fonts_for(ctx: &egui::Context, prefer_hans: bool) {
install_fonts_with(ctx, prefer_hans, false);
}
/// [`install_fonts_for`] with the CJK fallback order for a Korean interface.
pub fn install_fonts_language(ctx: &egui::Context, prefer_hans: bool, prefer_korean: bool) {
install_fonts_language_with(ctx, prefer_hans, prefer_korean, false);
}
/// [`install_fonts_for`], also adding an installed CJK font after the embedded faces when
/// `system_cjk` is set and the embedded faces leave CJK characters of the interface text without
/// a glyph (#241, #486). Reading that font is a large file read, so callers ask for it only when
@@ -390,11 +395,27 @@ pub fn install_fonts_with(ctx: &egui::Context, prefer_hans: bool, system_cjk: bo
install_fonts_extra(ctx, prefer_hans, system_cjk, &[]);
}
/// [`install_fonts_with`] with the CJK font order selected for the interface language.
pub fn install_fonts_language_with(ctx: &egui::Context, prefer_hans: bool, prefer_korean: bool, system_cjk: bool) {
install_fonts_language_extra(ctx, prefer_hans, prefer_korean, system_cjk, &[]);
}
/// [`install_fonts_with`], also adding document-font `faces` as last-resort fallbacks (#565): the
/// faces that cover text the interface fonts cannot draw (an input method's composition, or a
/// file name in the window caption).
pub fn install_fonts_extra(ctx: &egui::Context, prefer_hans: bool, system_cjk: bool, faces: &[Arc<wordcraft_fonts::FontFace>]) {
let mut fonts = font_definitions(prefer_hans, system_cjk);
install_fonts_language_extra(ctx, prefer_hans, false, system_cjk, faces);
}
/// [`install_fonts_extra`] with the CJK font order selected for the interface language.
pub fn install_fonts_language_extra(
ctx: &egui::Context,
prefer_hans: bool,
prefer_korean: bool,
system_cjk: bool,
faces: &[Arc<wordcraft_fonts::FontFace>],
) {
let mut fonts = font_definitions_language(prefer_hans, prefer_korean, system_cjk);
add_fallback_faces(&mut fonts, faces);
ctx.set_fonts(fonts);
}
@@ -445,6 +466,11 @@ pub fn faces_for_preedit(
/// The interface fonts; see [`install_fonts_with`].
pub fn font_definitions(prefer_hans: bool, system_cjk: bool) -> FontDefinitions {
font_definitions_language(prefer_hans, false, system_cjk)
}
/// The interface fonts, ordered for the preferred CJK writing system.
pub fn font_definitions_language(prefer_hans: bool, prefer_korean: bool, system_cjk: bool) -> FontDefinitions {
let mut fonts = FontDefinitions::default();
let add = |fonts: &mut FontDefinitions, name: &str, bytes: &'static [u8]| {
fonts.font_data.insert(name.into(), Arc::new(FontData::from_static(bytes)));
@@ -471,7 +497,7 @@ pub fn font_definitions(prefer_hans: bool, system_cjk: bool) -> FontDefinitions
}
}
};
let cjk = wordcraft_fonts::ui_cjk_fonts(prefer_hans);
let cjk = wordcraft_fonts::ui_cjk_fonts_for(prefer_hans, prefer_korean);
for f in &cjk {
// The same static bytes the document fonts use: one copy in the binary.
add_cjk(&mut fonts, format!("{} {}", f.family, f.style), FontData::from_static(f.bytes));
@@ -480,7 +506,7 @@ pub fn font_definitions(prefer_hans: bool, system_cjk: bool) -> FontDefinitions
// without its Chinese face, #241, or with faces lacking catalog characters, #486): an
// installed CJK font after them, so the menus don't show boxes or `?`.
#[cfg(not(target_arch = "wasm32"))]
if system_cjk && let Some(f) = system_cjk_font(prefer_hans) {
if system_cjk && let Some(f) = system_cjk_font(prefer_hans, prefer_korean) {
let mut data = FontData::from_static(&f.bytes);
data.index = f.index;
add_cjk(&mut fonts, format!("system {}", f.family), data);
@@ -496,24 +522,31 @@ pub fn font_definitions(prefer_hans: bool, system_cjk: bool) -> FontDefinitions
/// ([`cjk_font_text`](crate::i18n::Lang::cjk_font_text)). The font is picked for the catalogs'
/// missing characters, or for the language names when only those are missing.
#[cfg(not(target_arch = "wasm32"))]
fn system_cjk_font(hans: bool) -> Option<&'static wordcraft_fonts::SystemUiFont> {
fn system_cjk_font(hans: bool, korean: bool) -> Option<&'static wordcraft_fonts::SystemUiFont> {
static ZH: std::sync::OnceLock<Option<wordcraft_fonts::SystemUiFont>> = std::sync::OnceLock::new();
static OTHER: std::sync::OnceLock<Option<wordcraft_fonts::SystemUiFont>> = std::sync::OnceLock::new();
(if hans { &ZH } else { &OTHER })
.get_or_init(|| {
let embedded = wordcraft_fonts::ui_cjk_fonts(hans);
let (catalogs, names) = crate::i18n::Lang::cjk_font_text(hans);
let mut needed = wordcraft_fonts::ui_uncovered_cjk(&catalogs, &embedded);
static KO: std::sync::OnceLock<Option<wordcraft_fonts::SystemUiFont>> = std::sync::OnceLock::new();
(if hans {
&ZH
} else if korean {
&KO
} else {
&OTHER
})
.get_or_init(|| {
let embedded = wordcraft_fonts::ui_cjk_fonts_for(hans, korean);
let (catalogs, names) = crate::i18n::Lang::cjk_font_text_for(hans, korean);
let mut needed = wordcraft_fonts::ui_uncovered_cjk(&catalogs, &embedded);
if needed.is_empty() {
needed = wordcraft_fonts::ui_uncovered_cjk(&[&names], &embedded);
if needed.is_empty() {
needed = wordcraft_fonts::ui_uncovered_cjk(&[&names], &embedded);
if needed.is_empty() {
return None;
}
return None;
}
log::debug!("interface: {} CJK characters lack an embedded glyph", needed.len());
wordcraft_fonts::system_cjk_ui_font(hans, &needed)
})
.as_ref()
}
log::debug!("interface: {} CJK characters lack an embedded glyph", needed.len());
wordcraft_fonts::system_cjk_ui_font_for(hans, korean, &needed)
})
.as_ref()
}
pub fn medium(size: f32) -> FontId {
+1 -1
View File
@@ -33,7 +33,7 @@ Document renaming: desktop renaming requires a filesystem that supports hard lin
| 9 | **Password-protected `.doc` files** (AES) can't be opened; password-protected `.docx` opens and saves (agile encryption, #55 closed by #281) | | `crates/docx/src/crypt.rs`; `crates/docbin/src/lib.rs` opens XOR/RC4/RC4 CryptoAPI `.doc` with a password (#1079, untested on Word-written files; AES `.doc` refused) | Corporate and legal users with old files | 3–5 | [file-format-parity.md](file-format-parity.md) |
| 8 | **Proofing covers English, Polish, Portuguese, Italian, Croatian, Turkish and Russian only**; no dictionaries, grammar or hyphenation for the other ~43 languages Word proofs (text in them is left unmarked) | | `crates/proof` `ProofLang::PROOFED`: English, Polish, Portuguese (pt-BR via the installed VERO dictionary, #797), Italian (it-IT via the installed Hunspell dictionary), Croatian (hr-HR, 2026-10-13), Turkish (tr_TR spelling, grammar, TDK hyphenation, 2026-10-13) and Russian (the installed `ru_RU` dictionary, 2026-10-12), a language per run; Bulgarian has hyphenation only (#826); open PRs add Danish (#723), Russian (#894) and Czech (#1048); Word ships 120 proofing tools; issues #25, #40, #100 open | Every writer outside English, Polish, Portuguese, Italian, Croatian, Turkish and Russian | 20–40 | [localization-parity.md](localization-parity.md) |
| 10 | **Objects**: text in a freely rotated text box (free rotation and flips landed, #332; text direction, alignment and linked text boxes landed, #369; contour wrap and wrap side landed, #354), shape effects, WordArt 3-D effects and connector routing around shapes (WordArt effects and warps and glued connectors landed, #416), dragging members inside a Drawing Canvas (the canvas landed, #344) | | [parity-checklist.md](parity-checklist.md) (Shape Format 11/11, Layout 22/22); group and ungroup landed (#267) | Newsletters, flyers, reports with diagrams | 20–30 | [target-app-parity.md](target-app-parity.md) |
| 11 | **Interface languages**: 5 of the 12 key languages missing (hi, ar, id, ko, vi) and pt-PT (French landed, #729; 19 interface languages in `crates/ui-egui/src/i18n`); open PRs for Arabic (#737, #1042, #1032), Indonesian (#572) and pt-PT (#1028); no mirrored RTL interface yet; ~5% of strings still English in the translated catalogs | | [localization-parity.md](localization-parity.md) | Most of the world's writers | 50–80 | [localization-parity.md](localization-parity.md) |
| 11 | **Interface languages**: 3 of the 12 key languages missing (hi, ar, id) and pt-PT (French landed, #729; Vietnamese, #1434; Korean, #1216; 21 interface languages in `crates/ui-egui/src/i18n`); open PRs for Arabic (#737, #1042, #1032), Indonesian (#572) and pt-PT (#1028); no mirrored RTL interface yet; ~5% of strings still English in the translated catalogs | | [localization-parity.md](localization-parity.md) | Most of the world's writers | 50–80 | [localization-parity.md](localization-parity.md) |
| 12 | **Draw tab / ink**: pens, pencil, highlighter, eraser, Select and Hide Ink landed (#307); Lasso Select, Add Pen, Ink to Shape and Ink Replay landed (#395); Ink to Shape also recognises single-stroke arrows (a line plus a V head drawn in one stroke becomes a straight connector with an arrowhead); missing: Ink to Math, two-stroke arrows and arrows with a closed triangle head, pressure, rendering/editing Word's own ink (drawing-level and direct run-level InkML content parts now survive DOCX save) | | [parity-checklist.md](parity-checklist.md) (Draw 10/11) | Pen and tablet users | 4–8 | [hardware-parity.md](hardware-parity.md) |
| 13 | **Charts and SmartArt editing** (editing charts and SmartArt from Word, more chart types and formatting) | | `insert.chart` and Chart Design basics landed (#409): WordCraft's own charts are editable; charts from Word stay read-only (no embedded-workbook editing), no combo/3D/radar/stock charts (charts of those types show a placeholder with their title and type, #998), axis or series formatting; Insert › SmartArt landed (#497) for graphics WordCraft makes | Reports and business documents | 35–55 | [target-app-parity.md](target-app-parity.md) |
| 14 | **`.doc` can't be written; Word 6/95 files refused** | | `crates/docbin` is read-only | Users exchanging files with old Word or legacy systems | 25–40 | [file-format-parity.md](file-format-parity.md) |
+2 -2
View File
@@ -19,7 +19,7 @@ native-speaker review (human).
## What WordCraft ships (measured)
- **20 interface languages**: English plus `zh-hans`, `zh-hant`, `ja`, `uk`, `es`, `pt-br`,
- **21 interface languages**: English plus `zh-hans`, `zh-hant`, `ja`, Korean `ko` (#1216), `uk`, `es`, `pt-br`,
Serbian `sr`/`sr-latn` (#250), Estonian `et`, Turkish `tr`, Russian `ru`, Italian `it` (#594), German `de` (#574), Polish `pl`, Czech `cs`, Hungarian `hu`, French `fr`, Bulgarian `bg` and Vietnamese `vi` (#1434). Catalogs have 2,061–2,463 entries
each; Polish has every key of every other catalog, including the built-in style names and the equation gallery, except the citation style names only Hungarian and Bulgarian translate so far (a test checks it). A test (`every_tab_and_command_is_translated`) enforces that every ribbon tab, command
label and ribbon location is translated in every language.
@@ -56,7 +56,7 @@ shared items below.
| Indonesian | id | 0 | ❌ | Latin ✅ | no | none | 3–4 |
| Japanese | ja | ~930; 98% / ~95% | most | CJK ✅, IME ✅; no vertical text, ruby or document grid | no | partial | 3–5 |
| German | de | 2,021 entries; same key set as zh-hans (#574); 98% of extracted, ~95% of visible | most | Latin ✅ | no | partial | 1–2 |
| Korean | ko | 0 | ❌ | Hangul ✅; macOS Korean IME fixes pending (#163, #164) | no | none | 3–5 |
| Korean | ko | 2,463 entries (all catalog keys and preview context) | most; newer panes English | Hangul ✅ (Korean-first embedded/system UI font fallback); macOS Korean IME fixes pending (#163, #164) | no | partial | 3–5 |
| Vietnamese | vi | 2,448 entries; every key of the reference catalogs (#1434); 98% of extracted, ~95% of visible | most | Latin ✅ | no | partial | 0–1 (review) |
Other shipped languages: **14** — Traditional Chinese (`zh-hant`), Ukrainian (`uk`), Serbian