Add Korean translation (#234)

* Create ko.json

* Update ko.json

* Update i18n.rs

* Fonts: add a Hangul fallback so Korean doesn't render as boxes

Only the first CJK font found was loaded, and on Windows and macOS that is
a Japanese font without Hangul (Meiryo, Hiragino). Load the first readable
font of each group: the existing CJK list, then Malgun Gothic (Windows),
Apple SD Gothic Neo (macOS) or Nanum Gothic (Linux). Also rustfmt the
Korean catalog test.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01RjGw4gA5ZvKrU8MQP6sBgT

---------

Co-authored-by: bherbruck <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
This commit is contained in:
authored and GitHub committed 2026-10-12 11:06:03 -04:00
1 parent 59fabc16ac
commit db6ecaf446
3 files changed
+377 -11

No files matched your search

+58 -10
View File
@@ -1,13 +1,18 @@
//! System font loading and fallback setup for multi-language (CJK/Japanese) support.
//! System font loading and fallback setup for multi-language (CJK: Japanese, Chinese, Korean) support.
//!
//! The UI's Latin font always comes first; a CJK font is only ever appended as a fallback, so
//! English text renders exactly as before and only glyphs the Latin font lacks use the CJK face.
//! The UI's Latin font always comes first; a CJK font (then a Hangul font) is only ever appended as
//! a fallback, so English text renders exactly as before and only glyphs the Latin font lacks use
//! the CJK face. The Hangul fallback covers Korean where the CJK font found (Meiryo, Hiragino)
//! lacks Hangul.
use std::sync::Arc;
/// Name of the CJK fallback entry in [`egui::FontDefinitions`].
pub const CJK_FALLBACK: &str = "cjk_fallback";
/// Name of the Hangul fallback entry in [`egui::FontDefinitions`], after [`CJK_FALLBACK`].
pub const HANGUL_FALLBACK: &str = "hangul_fallback";
/// UI languages whose text needs the CJK font. The web app's bundled font covers Japanese only;
/// add Chinese or Korean here together with a font that covers them.
pub const CJK_LANGUAGES: &[&str] = &["ja"];
@@ -19,13 +24,19 @@ pub fn needs_cjk_font(language: &str) -> bool {
/// `fonts` with `bytes` appended as the last fallback of the Proportional and Monospace
/// families, after the Latin UI font. `None` when the bytes are not a font.
pub fn with_cjk_fallback(mut fonts: egui::FontDefinitions, bytes: Vec<u8>) -> Option<egui::FontDefinitions> {
pub fn with_cjk_fallback(fonts: egui::FontDefinitions, bytes: Vec<u8>) -> Option<egui::FontDefinitions> {
with_fallback(fonts, CJK_FALLBACK, bytes)
}
/// `fonts` with `bytes` appended under `name` as the last fallback of the Proportional and
/// Monospace families. `None` when the bytes are not a font.
fn with_fallback(mut fonts: egui::FontDefinitions, name: &str, bytes: Vec<u8>) -> Option<egui::FontDefinitions> {
ttf_parser::Face::parse(&bytes, 0).ok()?;
fonts.font_data.insert(CJK_FALLBACK.to_owned(), Arc::new(egui::FontData::from_owned(bytes)));
fonts.font_data.insert(name.to_owned(), Arc::new(egui::FontData::from_owned(bytes)));
for family in [egui::FontFamily::Proportional, egui::FontFamily::Monospace] {
let list = fonts.families.entry(family).or_default();
list.retain(|name| name != CJK_FALLBACK);
list.push(CJK_FALLBACK.to_owned());
list.retain(|entry| entry != name);
list.push(name.to_owned());
}
Some(fonts)
}
@@ -183,6 +194,20 @@ const FONT_CANDIDATES: &[&str] = &[
"/usr/share/fonts/cjkuni-uming/uming.ttc",
];
/// Hangul fonts, appended after the CJK fallback so Korean renders even when the CJK font found
/// is a Japanese one without Hangul (Meiryo, Hiragino).
#[cfg(not(target_arch = "wasm32"))]
const HANGUL_FONT_CANDIDATES: &[&str] = &[
// Windows: Malgun Gothic
"C:\\Windows\\Fonts\\malgun.ttf",
// macOS: Apple SD Gothic Neo
"/System/Library/Fonts/AppleSDGothicNeo.ttc",
"/System/Library/Fonts/Supplemental/AppleGothic.ttf",
// Linux: Nanum Gothic (Noto Sans CJK already covers Hangul)
"/usr/share/fonts/truetype/nanum/NanumGothic.ttf",
"/usr/share/fonts/nanum/NanumGothic.ttf",
];
#[cfg(not(target_arch = "wasm32"))]
fn try_read_font(path: &std::path::Path) -> Option<Vec<u8>> {
if let Ok(bytes) = std::fs::read(path)
@@ -298,11 +323,22 @@ fn find_system_cjk_font() -> Option<Vec<u8>> {
None
}
/// Configure font definitions with system CJK fallback if available.
/// Configure font definitions with system CJK and Hangul fallbacks where available.
#[cfg(not(target_arch = "wasm32"))]
pub fn setup_fonts(ctx: &egui::Context) {
if let Some(bytes) = find_system_cjk_font() {
install_cjk_font(ctx, bytes);
let mut fonts = egui::FontDefinitions::default();
let mut found = false;
if let Some(cjk) = find_system_cjk_font().and_then(|bytes| with_cjk_fallback(fonts.clone(), bytes)) {
fonts = cjk;
found = true;
}
let hangul = HANGUL_FONT_CANDIDATES.iter().find_map(|path| try_read_font(std::path::Path::new(path)));
if let Some(with_hangul) = hangul.and_then(|bytes| with_fallback(fonts.clone(), HANGUL_FALLBACK, bytes)) {
fonts = with_hangul;
found = true;
}
if found {
ctx.set_fonts(fonts);
}
}
@@ -339,6 +375,18 @@ mod tests {
assert!(with_cjk_fallback(defaults, b"not a font".to_vec()).is_none());
}
#[test]
fn hangul_font_falls_back_after_the_cjk_font() {
let defaults = egui::FontDefinitions::default();
let cjk = with_cjk_fallback(defaults.clone(), latin_font_bytes()).unwrap();
let fonts = with_fallback(cjk, HANGUL_FALLBACK, latin_font_bytes()).unwrap();
for family in [egui::FontFamily::Proportional, egui::FontFamily::Monospace] {
let list = &fonts.families[&family];
assert_eq!(list.first(), defaults.families[&family].first(), "the Latin UI font stays first");
assert_eq!(&list[list.len() - 2..], [CJK_FALLBACK, HANGUL_FALLBACK], "Hangul follows the CJK fallback");
}
}
#[test]
fn directory_scan_matches_cjk_fonts_only() {
for cjk in [
+23 -1
View File
@@ -34,7 +34,8 @@ impl Catalog {
static ENGLISH: Catalog = Catalog::new("en", "English", include_str!("locales/en.json"));
static JAPANESE: Catalog = Catalog::new("ja", "日本語", include_str!("locales/ja.json"));
static GERMAN: Catalog = Catalog::new("de", "Deutsch", include_str!("locales/de.json"));
pub const LOCALES: &[&Catalog] = &[&ENGLISH, &JAPANESE, &GERMAN];
static KOREAN: Catalog = Catalog::new("ko", "한국어", include_str!("locales/ko.json"));
pub const LOCALES: &[&Catalog] = &[&ENGLISH, &JAPANESE, &GERMAN, &KOREAN];
pub fn is_supported(id: &str) -> bool {
LOCALES.iter().any(|catalog| catalog.id == id)
@@ -250,6 +251,27 @@ mod tests {
assert_eq!(Locale::for_language("en").dynamic("Sketch1"), "Sketch1");
}
#[test]
fn korean_catalog_translates_keys_labels_and_browser_patterns() {
let ko = Locale::for_language("ko");
assert_eq!(ko.name(), "한국어");
assert!(is_supported("ko"));
assert_eq!(ko.text("ui.preferences"), "환경 설정");
assert_eq!(ko.label("Extrude"), "돌출");
assert_eq!(ko.label("CREATE SKETCH"), "스케치 만들기");
assert_eq!(ko.label("NonExistentKey"), "NonExistentKey");
assert_eq!(ko.format("document.save_changes", &[("name", "My {design}")]), "My {design}의 변경 사항을 저장하시겠습니까?");
assert_eq!(ko.format("dialog.selected_count", &[("count", "2")]), "↖ 2개 선택됨");
assert_eq!(ko.dynamic("Sketch1"), "스케치1");
assert_eq!(ko.dynamic("Body2"), "바디2");
assert_eq!(ko.dynamic("Component12"), "컴포넌트12");
assert_eq!(ko.dynamic("Units: mm"), "단위: mm");
assert_eq!(ko.dynamic("Precision: 3"), "정밀도: 3");
assert_eq!(ko.dynamic("Parameters (5)"), "매개변수 (5)");
assert_eq!(ko.dynamic("4 degrees of freedom"), "자유도 4개");
assert_eq!(ko.dynamic("Sketch of bracket"), "Sketch of bracket");
}
#[test]
fn render_scopes_restore_the_previous_language() {
let _english = scope("en");
+296
View File
@@ -0,0 +1,296 @@
{
"browser.body_name": "바디{number}",
"browser.component_name": "컴포넌트{number}",
"browser.parameters": "매개변수 ({count})",
"browser.precision": "정밀도: {precision}",
"browser.sketch_name": "스케치{number}",
"browser.units": "단위: {units}",
"dialog.custom_axis": "↖ 사용자 정의 축",
"dialog.edit_title": "{title} 편집",
"dialog.select": "↖ 선택",
"dialog.selected_count": "↖ {count}개 선택됨",
"document.save_changes": "{name}의 변경 사항을 저장하시겠습니까?",
"document.unsaved_changes": "{name}에 저장되지 않은 변경 사항이 있습니다. 저장하지 않으면 변경 사항이 사라집니다.",
"sketch.degrees_of_freedom": "자유도 {count}개",
"sketch.draw_curves": "{linetype} 곡선 그리기",
"sketch.make_selection": "선택 항목을 {linetype}(으)로 변경",
"ui.1_degree_of_freedom": "자유도 1개",
"ui.3d_sketch": "3D 스케치",
"ui.about": "정보",
"ui.activate": "활성화",
"ui.active_component": "활성 컴포넌트",
"ui.agent_cursor": "에이전트 커서",
"ui.agent_cursor_speed": "에이전트 커서 속도",
"ui.all": "모두",
"ui.angle": "각도",
"ui.angle_decimals": "각도 소수점 자릿수",
"ui.animate_view_changes": "뷰 변경 애니메이션",
"ui.animation": "애니메이션",
"ui.animation_speed": "애니메이션 속도",
"ui.apply": "적용",
"ui.arc": "호",
"ui.assemble": "조립",
"ui.autosave_every": "자동 저장 간격",
"ui.axes": "축",
"ui.axis": "축",
"ui.bodies": "바디",
"ui.box": "박스",
"ui.break": "끊기",
"ui.cancel": "취소",
"ui.canvases": "캔버스",
"ui.centerline": "중심선",
"ui.chamfer": "모따기",
"ui.change_workspace": "작업 공간 변경",
"ui.chord_length": "현 길이",
"ui.circle": "원",
"ui.circular_pattern": "원형 패턴",
"ui.clear_recent": "최근 항목 지우기",
"ui.clear_the_selection": "선택 해제",
"ui.close": "닫기",
"ui.coil": "코일",
"ui.coincident": "일치",
"ui.collapse": "접기",
"ui.collinear": "동일 선상",
"ui.combine": "결합",
"ui.coming_later": "추후 지원 예정",
"ui.concentric": "동심",
"ui.conic_curve": "원뿔 곡선",
"ui.constant": "상수",
"ui.constraints_conflict": "구속 조건 충돌",
"ui.construct": "구성",
"ui.construction": "구성선",
"ui.copy": "복사",
"ui.counterbore": "카운터보어",
"ui.countersink": "카운터싱크",
"ui.create": "만들기",
"ui.create_sketch": "스케치 만들기",
"ui.current": "현재",
"ui.curvature": "곡률",
"ui.cut": "잘라내기",
"ui.cylinder": "원통",
"ui.dark": "어둡게",
"ui.default_modelling_orientation": "기본 모델링 방향",
"ui.degrees_of_freedom": "자유도",
"ui.delete": "삭제",
"ui.depth": "깊이",
"ui.design": "디자인",
"ui.diameter": "지름",
"ui.dimension": "치수",
"ui.direction": "방향",
"ui.distance": "거리",
"ui.distance_1": "거리 1",
"ui.distance_2": "거리 2",
"ui.document_settings": "문서 설정",
"ui.don_t_save": "저장 안 함",
"ui.draft": "구배",
"ui.drawing": "도면",
"ui.drill_point": "드릴 선단",
"ui.edit": "편집",
"ui.ellipse": "타원",
"ui.end_radius": "끝 반지름",
"ui.equal": "동일",
"ui.expand": "펼치기",
"ui.export_ellipsis": "내보내기...",
"ui.extend": "연장",
"ui.extent_type": "범위 유형",
"ui.extrude": "돌출",
"ui.features": "피처",
"ui.file": "파일",
"ui.fillet": "필렛",
"ui.finish_sketch": "스케치 종료",
"ui.fix_unfix": "고정/고정 해제",
"ui.flat": "플랫",
"ui.flip": "뒤집기",
"ui.front": "정면",
"ui.fully_constrained": "완전 구속",
"ui.general": "일반",
"ui.generative_design": "제너레이티브 디자인",
"ui.graphics": "그래픽",
"ui.heading_assemble": "조립",
"ui.heading_box": "박스",
"ui.heading_chamfer": "모따기",
"ui.heading_circular_pattern": "원형 패턴",
"ui.heading_coil": "코일",
"ui.heading_combine": "결합",
"ui.heading_construct": "구성",
"ui.heading_create": "만들기",
"ui.heading_create_sketch": "스케치 만들기",
"ui.heading_cylinder": "원통",
"ui.heading_design": "디자인",
"ui.heading_draft": "구배",
"ui.heading_edit": "편집",
"ui.heading_extrude": "돌출",
"ui.heading_fillet": "필렛",
"ui.heading_finish_sketch": "스케치 종료",
"ui.heading_front": "정면",
"ui.heading_hole": "구멍",
"ui.heading_home": "홈",
"ui.heading_insert": "삽입",
"ui.heading_inspect": "검사",
"ui.heading_loft": "로프트",
"ui.heading_measure": "측정",
"ui.heading_mesh": "메시",
"ui.heading_mirror": "대칭 복사",
"ui.heading_modify": "수정",
"ui.heading_move": "이동",
"ui.heading_offset_plane": "오프셋 평면",
"ui.heading_ok": "확인",
"ui.heading_parameters": "매개변수",
"ui.heading_pattern_on_path": "경로 패턴",
"ui.heading_pipe": "파이프",
"ui.heading_plane_at_angle": "각도 평면",
"ui.heading_plastic": "플라스틱",
"ui.heading_rectangular_pattern": "직사각형 패턴",
"ui.heading_revolve": "회전",
"ui.heading_right": "우측면",
"ui.heading_scale": "축척",
"ui.heading_section_analysis": "단면 분석",
"ui.heading_select": "선택",
"ui.heading_sheet_metal": "판금",
"ui.heading_shell": "쉘",
"ui.heading_sketch": "스케치",
"ui.heading_sketch_palette": "스케치 팔레트",
"ui.heading_solid": "솔리드",
"ui.heading_sphere": "구",
"ui.heading_split_body": "바디 분할",
"ui.heading_surface": "서피스",
"ui.heading_sweep": "스윕",
"ui.heading_thread": "나사산",
"ui.heading_top": "상면",
"ui.heading_torus": "토러스",
"ui.help": "도움말",
"ui.hide": "숨기기",
"ui.hole": "구멍",
"ui.hole_tap_type": "탭 유형",
"ui.hole_type": "구멍 유형",
"ui.home": "홈",
"ui.horizontal_vertical": "수평/수직",
"ui.insert": "삽입",
"ui.inspect": "검사",
"ui.intersect": "교차",
"ui.join": "접합",
"ui.joints": "조인트",
"ui.keyboard_shortcuts": "키보드 단축키",
"ui.language": "언어",
"ui.length_decimals": "길이 소수점 자릿수",
"ui.light": "밝게",
"ui.line": "선",
"ui.linetype": "선 종류",
"ui.loft": "로프트",
"ui.look_at": "바라보기",
"ui.manufacture": "제조",
"ui.measure": "측정",
"ui.midpoint": "중간점",
"ui.mirror": "대칭 복사",
"ui.modify": "수정",
"ui.move": "이동",
"ui.multiple": "다중",
"ui.named_views": "명명된 뷰",
"ui.navigation": "탐색",
"ui.new_body": "새 바디",
"ui.new_design": "새 디자인",
"ui.normal": "일반",
"ui.object_type": "객체 유형",
"ui.objects": "객체",
"ui.offer_to_stand_up_y_up_files": "Y-up 파일을 Z-up으로 세우도록 제안",
"ui.offset": "오프셋",
"ui.offset_plane": "오프셋 평면",
"ui.one_side": "한 방향",
"ui.open": "열기",
"ui.open_ellipsis": "열기...",
"ui.open_recent": "최근 항목 열기",
"ui.operation": "작업",
"ui.options": "옵션",
"ui.orbit_around": "궤도 회전 기준",
"ui.origin": "원점",
"ui.pan_zoom_orbit": "이동, 확대/축소, 궤도 회전",
"ui.parallel": "평행",
"ui.parameters": "매개변수",
"ui.paste": "붙여넣기",
"ui.path": "경로",
"ui.pattern_on_path": "경로 패턴",
"ui.perpendicular": "수직",
"ui.pipe": "파이프",
"ui.placement": "배치",
"ui.plane": "평면",
"ui.plane_at_angle": "각도 평면",
"ui.point": "점",
"ui.point_under_the_cursor": "커서 아래의 점",
"ui.points": "점",
"ui.polygon": "다각형",
"ui.preferences": "환경 설정",
"ui.profile": "프로파일",
"ui.profile_plane": "프로파일 평면",
"ui.profiles": "프로파일",
"ui.project": "투영",
"ui.quit": "종료",
"ui.radius": "반지름",
"ui.radius_type": "반지름 유형",
"ui.rectangle": "직사각형",
"ui.rectangular_pattern": "직사각형 패턴",
"ui.redo": "다시 실행",
"ui.render": "렌더",
"ui.reset_the_settings_on_this_page": "이 페이지의 설정 초기화",
"ui.restore_defaults": "기본값 복원",
"ui.reverse_zoom_direction": "확대/축소 방향 반전",
"ui.revolve": "회전",
"ui.right": "우측면",
"ui.save": "저장",
"ui.save_as": "다른 이름으로 저장",
"ui.save_as_ellipsis": "다른 이름으로 저장...",
"ui.scale": "축척",
"ui.section_analysis": "단면 분석",
"ui.select": "선택",
"ui.selection": "선택",
"ui.shell": "쉘",
"ui.show": "표시",
"ui.show_constraints": "구속 조건 표시",
"ui.show_dimensions": "치수 표시",
"ui.show_points": "점 표시",
"ui.show_profile": "프로파일 표시",
"ui.show_projected_geometries": "투영된 형상 표시",
"ui.simple": "단순",
"ui.simulation": "시뮬레이션",
"ui.single": "단일",
"ui.sketch": "스케치",
"ui.sketch_dimension": "스케치 치수",
"ui.sketch_grid": "스케치 격자",
"ui.sketch_scale": "스케치 축척",
"ui.sketches": "스케치",
"ui.slice": "슬라이스",
"ui.slot": "슬롯",
"ui.smooth": "부드럽게",
"ui.snap": "스냅",
"ui.sphere": "구",
"ui.spline": "스플라인",
"ui.split_body": "바디 분할",
"ui.start": "시작",
"ui.start_radius": "시작 반지름",
"ui.sweep": "스윕",
"ui.symmetric": "대칭",
"ui.symmetry": "대칭",
"ui.system": "시스템",
"ui.tangent": "접선",
"ui.taper_angle": "테이퍼 각도",
"ui.tapped": "탭",
"ui.text": "텍스트",
"ui.theme": "테마",
"ui.thickness": "두께",
"ui.thread": "나사산",
"ui.tip_angle": "선단 각도",
"ui.to_object": "객체까지",
"ui.top": "상면",
"ui.torus": "토러스",
"ui.trim": "트림",
"ui.two_sides": "양방향",
"ui.ui_scale": "UI 배율",
"ui.undo": "실행 취소",
"ui.units": "단위",
"ui.units_of_new_designs": "새 디자인의 단위",
"ui.untitled": "제목 없음",
"ui.variable": "가변",
"ui.view_centre": "뷰 중심",
"ui.visual_style": "시각 스타일",
"ui.workspace": "작업 공간",
"ui.zoom_to_the_cursor": "커서 위치로 확대/축소"
}