diff --git a/crates/ui-egui/src/fonts.rs b/crates/ui-egui/src/fonts.rs index 7e61c0d5..67be7d4e 100644 --- a/crates/ui-egui/src/fonts.rs +++ b/crates/ui-egui/src/fonts.rs @@ -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) -> Option { +pub fn with_cjk_fallback(fonts: egui::FontDefinitions, bytes: Vec) -> Option { + 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) -> Option { 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> { if let Ok(bytes) = std::fs::read(path) @@ -298,11 +323,22 @@ fn find_system_cjk_font() -> Option> { 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 [ diff --git a/crates/ui-egui/src/i18n.rs b/crates/ui-egui/src/i18n.rs index c0291332..40f60270 100644 --- a/crates/ui-egui/src/i18n.rs +++ b/crates/ui-egui/src/i18n.rs @@ -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"); diff --git a/crates/ui-egui/src/locales/ko.json b/crates/ui-egui/src/locales/ko.json new file mode 100644 index 00000000..44641acc --- /dev/null +++ b/crates/ui-egui/src/locales/ko.json @@ -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": "커서 위치로 확대/축소" +}