i18n: Korean (ko) interface catalog (#508)

Adds Korean as an interface language: Edit ▸ Language ▸ 한국어, Settings ▸
General ▸ Interface Language, and System Language for ko / ko-KR locales.

- crates/ui-egui/src/i18n/ko.tsv: 3,573 entries, the Spanish catalog's sources
  plus the macOS Settings item, written clean-room from the plain meaning of
  the English text (sidecar ko.tsv.attribution, ATTRIBUTION.md row).
- Hangul is drawn with the installed system face cjk.rs already adds (Malgun
  Gothic, Apple SD Gothic Neo, Noto Sans CJK KR…); switching is refused with a
  message when there is none, like Chinese.
- Test: korean_covers_the_menus_and_has_no_dead_entries.
- Docs: localization-parity, ROADMAP, ui-egui README.

Native-speaker review welcome.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_013XWUReif4gqcjU5GsjYioc
This commit is contained in:
CokeRoachandClaude Opus 5.5 committed 2026-10-13 08:44:01 +09:00
1 parent 71438c627b
commit b97bb74330
11 files changed
+3657 -5

No files matched your search

+1
View File
@@ -10,6 +10,7 @@ iconography, images or other Adobe assets; open licences only; every asset attri
| Asset | Author | Source | Licence |
|---|---|---|---|
| `crates/ui-egui/src/i18n/id.tsv` | FilmCraft contributors | Original work: clean-room Indonesian interface translations | MIT OR Apache-2.0 |
| `crates/ui-egui/src/i18n/ko.tsv` | FilmCraft contributors | Original work: clean-room Korean interface translations | MIT OR Apache-2.0 |
| `crates/ui-egui/src/i18n/fr.tsv` | FilmCraft contributors | Original work: clean-room French interface translations | MIT OR Apache-2.0 |
| `crates/ui-egui/src/i18n/pt-br.tsv` | FilmCraft contributors | Original work: clean-room Brazilian Portuguese interface translations | MIT OR Apache-2.0 |
| `crates/ui-egui/src/i18n/ru.tsv` | FilmCraft contributors | Original work: clean-room Russian interface translations | MIT OR Apache-2.0 |
+1
View File
@@ -84,6 +84,7 @@ Premiere ships 10 interface languages. Measured from our catalogs (3,498 strings
| French | partial (whole interface; native review welcome) | ~100% |
| Portuguese (Brazil) | partial (whole interface; native review welcome) | ~100% |
| Indonesian | partial (whole interface; native review welcome) | ~100% |
| Korean | partial (whole interface; needs an installed Hangul font; native review welcome) | ~100% |
| Japanese | partial (whole interface) | 99.7% |
| German | menus only (native-reviewed) | 10.6% |
| Korean | none | 0% |
+2
View File
@@ -131,6 +131,8 @@ pub fn install(app: &FilmcraftApp, ctx: egui::Context) -> (Receiver<String>, Men
filmcraft_ui_egui::i18n::Language::Fr
} else if checked("app.language.indonesian") {
filmcraft_ui_egui::i18n::Language::Id
} else if checked("app.language.korean") {
filmcraft_ui_egui::i18n::Language::Ko
} else {
filmcraft_ui_egui::i18n::Language::En
};
+1
View File
@@ -1007,6 +1007,7 @@ static CATEGORIES: &[Category] = &[
("ru", "Русский"),
("fr", "Français"),
("id", "Bahasa Indonesia"),
("ko", "한국어"),
]),
true,
),
+1 -1
View File
@@ -18,7 +18,7 @@ the committed span and undo. Set `FILMCRAFT_UI_SNAPSHOT_DIR` to render screensho
## Localisation
Edit > Language offers English, Japanese, Spanish, Brazilian Portuguese, Ukrainian, Simplified
Chinese, German, Russian, French and Indonesian (which covers the whole interface). The language is stored in the engine's `general.interfaceLanguage` preference,
Chinese, German, Russian, French, Indonesian and Korean (which cover the whole interface). The language is stored in the engine's `general.interfaceLanguage` preference,
restored on startup, and also available in Settings > General.
Its default, System Language (`system`), follows the operating system: the first of the user's
preferred languages that the interface has (the host supplies them through
+6
View File
@@ -55,6 +55,12 @@ fn find_system_font(sample: &str, preferred: &[&str]) -> Option<Arc<FontData>> {
/// Whether [`install`] has a Chinese face to add: craft-fonts Hans/Hant faces, or an installed
/// system face (scanned once per process). The Simplified Chinese interface needs one.
/// Whether [`install`] has a Hangul face to add (an installed system face; none is bundled).
/// The Korean interface needs one.
pub(crate) fn korean_available() -> bool {
korean_font().is_some()
}
pub(crate) fn available() -> bool {
craft_fonts().next().is_some() || system_font().is_some()
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,8 @@
asset: ko.tsv
title: Korean interface translation catalog
author: FilmCraft contributors
source: original work
license: MIT OR Apache-2.0
license-file: LICENSE-MIT and LICENSE-APACHE
added: 2026-10-13
notes: Original English-to-Korean interface translations using clean-room NLE vocabulary.
+44 -2
View File
@@ -46,6 +46,8 @@ pub enum Language {
Ru,
Fr,
Id,
/// Korean; Hangul needs an installed system font ([`korean_font_available`]).
Ko,
}
static JAPANESE: OnceLock<Catalog> = OnceLock::new();
@@ -56,10 +58,11 @@ static CHINESE: OnceLock<Catalog> = OnceLock::new();
static GERMAN: OnceLock<Catalog> = OnceLock::new();
static RUSSIAN: OnceLock<Catalog> = OnceLock::new();
static FRENCH: OnceLock<Catalog> = OnceLock::new();
static KOREAN: OnceLock<Catalog> = OnceLock::new();
static INDONESIAN: OnceLock<Catalog> = OnceLock::new();
impl Language {
pub const ALL: [Self; 10] = [Self::En, Self::Ja, Self::Es, Self::PtBr, Self::Uk, Self::ZhCn, Self::De, Self::Ru, Self::Fr, Self::Id];
pub const ALL: [Self; 11] = [Self::En, Self::Ja, Self::Es, Self::PtBr, Self::Uk, Self::ZhCn, Self::De, Self::Ru, Self::Fr, Self::Id, Self::Ko];
pub fn name(self) -> &'static str {
match self {
@@ -73,6 +76,7 @@ impl Language {
Self::Ru => "Русский",
Self::Fr => "Français",
Self::Id => "Bahasa Indonesia",
Self::Ko => "한국어",
}
}
@@ -88,6 +92,7 @@ impl Language {
"ru" => Some(Self::Ru),
"fr" => Some(Self::Fr),
"id" => Some(Self::Id),
"ko" => Some(Self::Ko),
_ => None,
}
}
@@ -101,7 +106,7 @@ impl Language {
/// only for Simplified locales (`zh`, `zh-CN`, `zh-SG`, `zh-Hans…`); Traditional ones (`zh-TW`,
/// `zh-HK`, `zh-MO`, `zh-Hant…`) are skipped.
pub fn from_locales(tags: &[String]) -> Self {
const PRIMARY: [(&str, Language); 10] = [
const PRIMARY: [(&str, Language); 11] = [
("en", Language::En),
("ja", Language::Ja),
("es", Language::Es),
@@ -112,6 +117,7 @@ impl Language {
("fr", Language::Fr),
("id", Language::Id),
("in", Language::Id),
("ko", Language::Ko),
];
tags.iter()
.find_map(|tag| {
@@ -142,6 +148,7 @@ impl Language {
Self::Ru => "ru",
Self::Fr => "fr",
Self::Id => "id",
Self::Ko => "ko",
}
}
@@ -158,6 +165,7 @@ impl Language {
Self::Ru => Some(RUSSIAN.get_or_init(|| Catalog::parse(include_str!("ru.tsv")))),
Self::Fr => Some(FRENCH.get_or_init(|| Catalog::parse(include_str!("fr.tsv")))),
Self::Id => Some(INDONESIAN.get_or_init(|| Catalog::parse(include_str!("id.tsv")))),
Self::Ko => Some(KOREAN.get_or_init(|| Catalog::parse(include_str!("ko.tsv")))),
}
}
@@ -313,6 +321,13 @@ pub fn chinese_font_available() -> bool {
crate::cjk::available()
}
/// Whether the interface can show Korean: `theme::install` adds an installed system Hangul face
/// (Malgun Gothic, Apple SD Gothic Neo, Noto Sans CJK KR…, see `cjk.rs`) when there is one; none
/// is bundled. False on systems (and the web) without one.
pub fn korean_font_available() -> bool {
crate::cjk::korean_available()
}
#[cfg(test)]
mod tests {
use super::*;
@@ -329,6 +344,7 @@ mod tests {
("ru", include_str!("ru.tsv")),
("fr", include_str!("fr.tsv")),
("id", include_str!("id.tsv")),
("ko", include_str!("ko.tsv")),
] {
let (entries, errors) = catalog::parse_entries(text);
assert!(errors.is_empty(), "{code}: {errors:?}");
@@ -1094,6 +1110,32 @@ mod tests {
set_current(Language::En);
}
/// Korean covers the whole interface (the Spanish catalog's entries and the native macOS
/// Settings menu); like French, the tests require the core menus, and every entry must still
/// have a Spanish counterpart so a renamed or removed source can't leave a dead Korean entry.
#[test]
fn korean_covers_the_menus_and_has_no_dead_entries() {
let (entries, errors) = catalog::parse_entries(include_str!("ko.tsv"));
assert!(errors.is_empty(), "{errors:?}");
for (ctx, en, _) in &entries {
assert!(ctx.is_empty() && (Language::Es.has(en) || en == "Settings"), "Korean entry without a Spanish one: {ctx:?} {en:?}");
}
for en in core_menu_labels() {
assert!(Language::Ko.has(&en), "missing Korean menu label: {en}");
}
for en in crate::menus::MENUS.into_iter().chain(["Audio", "Settings"]) {
assert!(Language::Ko.has(en), "untranslated Korean menu: {en}");
}
for (en, ko) in [("File", "파일"), ("Edit", "편집"), ("Sequence", "시퀀스"), ("Settings", "설정")] {
assert_eq!(Language::Ko.tr(en), ko);
}
assert_eq!(Language::Ko.name(), "한국어");
assert_eq!(Language::parse("ko"), Some(Language::Ko));
for tag in ["ko", "ko-KR", "ko_KR.UTF-8"] {
assert_eq!(Language::from_locales(&[tag.to_string()]), Language::Ko, "{tag}");
}
}
#[test]
fn russian_entries_cover_the_original_menu_catalog() {
let app = crate::FilmcraftApp::new(filmcraft_engine::Session::default());
+8
View File
@@ -36,6 +36,7 @@ pub const UI_COMMANDS: &[UiCommand] = &[
uic!("app.language.russian", "Русский", ["Edit", "Language"], None),
uic!("app.language.french", "Français", ["Edit", "Language"], None),
uic!("app.language.indonesian", "Bahasa Indonesia", ["Edit", "Language"], None),
uic!("app.language.korean", "한국어", ["Edit", "Language"], None),
uic!("source.playback.toggle", "Source Play/Stop", [], None),
uic!("source.playback.play", "Play Source", [], None),
uic!("source.playback.stop", "Stop Source", [], None),
@@ -195,6 +196,7 @@ pub fn invoke(app: &mut FilmcraftApp, ctx: &egui::Context, id: &str, mut params:
| "app.language.russian"
| "app.language.french"
| "app.language.indonesian"
| "app.language.korean"
) {
// Japanese needs the craft-fonts (built with CRAFT_FONTS_DIR) or a font installed on the system
if id == "app.language.japanese" && !crate::i18n::install_japanese_font(ctx) {
@@ -204,6 +206,10 @@ pub fn invoke(app: &mut FilmcraftApp, ctx: &egui::Context, id: &str, mut params:
if id == "app.language.chinese" && !crate::i18n::chinese_font_available() {
return Err("no Chinese font is installed on this system (for example Noto Sans CJK SC or Microsoft YaHei); the interface stays in English".into());
}
// Korean needs an installed Hangul font (none is bundled; see cjk.rs)
if id == "app.language.korean" && !crate::i18n::korean_font_available() {
return Err("no Korean font is installed on this system (for example Malgun Gothic or Noto Sans CJK KR); the interface stays in English".into());
}
let language = match id {
"app.language.japanese" => crate::i18n::Language::Ja,
"app.language.spanish" => crate::i18n::Language::Es,
@@ -214,6 +220,7 @@ pub fn invoke(app: &mut FilmcraftApp, ctx: &egui::Context, id: &str, mut params:
"app.language.russian" => crate::i18n::Language::Ru,
"app.language.french" => crate::i18n::Language::Fr,
"app.language.indonesian" => crate::i18n::Language::Id,
"app.language.korean" => crate::i18n::Language::Ko,
_ => crate::i18n::Language::En,
};
// The preference is updated in memory before it is written, so a failed write (read-only
@@ -674,6 +681,7 @@ pub fn menu_items(app: &FilmcraftApp) -> Vec<MenuItem> {
"app.language.russian" => it.checked = Some(app.ui.language == crate::i18n::Language::Ru),
"app.language.french" => it.checked = Some(app.ui.language == crate::i18n::Language::Fr),
"app.language.indonesian" => it.checked = Some(app.ui.language == crate::i18n::Language::Id),
"app.language.korean" => it.checked = Some(app.ui.language == crate::i18n::Language::Ko),
_ => {}
}
if it.id.starts_with("view.") {
+2 -2
View File
@@ -1,6 +1,6 @@
# Localization parity
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (German ships the menus, #630) · **Target:** Adobe Premiere Pro 2026 (26.5.2)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (Korean covers the whole interface, #508) · **Target:** Adobe Premiere Pro 2026 (26.5.2)
Per-language status of FilmCraft's interface. Premiere 26.5.2 ships 10 interface languages (the
installed bundle's `.lproj` folders): German, English, Spanish, French, Italian, Japanese, Korean,
@@ -51,7 +51,7 @@ whole interface, one PR) at ~4–8 h per language for the catalog plus tests.
| Indonesian | id | 3,541 (same entries as Spanish, plus the macOS Settings item); must cover the core menus, no entry without a Spanish one | as Spanish | Latin (bundled fonts); System Language also maps the legacy `in` code | not recorded; welcome | partial (#867) | review |
| Japanese | ja | 3,487 (99.7%) | as zh-cn; system-font kanji may pick the Chinese fallback (#547) | CJK fonts, vertical title text | not recorded | partial | 4–8 h + review |
| German | de | 371 (10.6%) | menus only | Latin | yes (#630) | menus only | 5–9 h + review |
| Korean | ko | 0 (0%) | — | needs Hangul fonts in craft-fonts and IME testing | — | none (requested #508) | 10–20 h + review |
| Korean | ko | 3,573 (same entries as Spanish, plus the macOS Settings item) | as Spanish | installed system Hangul font (Malgun Gothic, Apple SD Gothic Neo, Noto Sans CJK KR…); refused without one | not recorded; welcome | partial (#508) | review; Hangul in craft-fonts for the web |
| Vietnamese | vi | 0 (0%) | — | Latin with stacked diacritics (font coverage to check) | — | none | 6–12 h + review |
| *Other shipped:* Ukrainian | uk | 351 (10.0%) | menus only | Cyrillic (bundled) | not recorded | menus only | 5–9 h |