i18n: add Chinese, Korean and Russian interface languages

This commit is contained in:
neko233-com 2026-10-10 10:20:59 +08:00 committed by luohaojun
parent e8e010b446
commit 12bfdb0798
12 changed files with 1005 additions and 82 deletions

1
Cargo.lock generated
View File

@ -1530,6 +1530,7 @@ dependencies = [
"serde_json",
"sys-locale",
"toml",
"web-sys",
]
[[package]]

View File

@ -38,7 +38,7 @@ pub fn start() {
let _ = session.execute("file.new", serde_json::json!({"sample": "sales"}));
}
let app = SheetApp::new(session, services(inbox.clone(), cc.egui_ctx.clone()));
SheetApp::setup_context(&cc.egui_ctx, false);
SheetApp::setup_context_for_language(&cc.egui_ctx, false, app.ui.language);
Ok(Box::new(WebShell { app, inbox }))
}),
)

View File

@ -191,7 +191,7 @@ fn main() -> eframe::Result<()> {
"GridCraft",
options,
Box::new(move |cc| {
SheetApp::setup_context(&cc.egui_ctx, app.ui.dark);
SheetApp::setup_context_for_language(&cc.egui_ctx, app.ui.dark, app.ui.language);
if let Some(port) = control_port {
match control_server::start(port, cc.egui_ctx.clone()) {
Ok(rx) => app.control_rx = Some(rx),

View File

@ -21,10 +21,13 @@ image = { workspace = true }
egui_kittest = { workspace = true, features = ["wgpu", "snapshot"] }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
# The desktop's preferred language, for the first-run interface language. The web build has no
# system locale, so the crate is native-only (see i18n::Language::system).
# The desktop's preferred language for the first-run interface language.
sys-locale = { workspace = true }
[target.'cfg(target_arch = "wasm32")'.dependencies]
# Read the browser's preferred language for the first-run web interface language.
web-sys = { version = "0.3", features = ["Window", "Navigator"] }
[build-dependencies]
serde_json = { workspace = true }
toml = "1"

View File

@ -4,43 +4,63 @@
//! The interface language is a preference ([`crate::UiState::language`]): on first run it defaults
//! to the system's language ([`Language::system`]), and the View tab switches it by hand.
//!
//! No Japanese font is bundled and none is installed here: the CJK faces [`crate::theme`] appends
//! to every family already cover kana and kanji, so Japanese interface text renders without help.
//! CJK fonts stay system-provided: [`crate::theme`] appends available CJK faces to every family so
//! Japanese, Chinese and Korean labels and workbook text can render without bundling font files.
use serde::{Deserialize, Serialize};
mod ko;
mod ru;
mod zh;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Language {
#[default]
En,
Zh,
Ja,
Ko,
Ru,
}
impl Language {
pub const ALL: [Self; 2] = [Self::En, Self::Ja];
pub const ALL: [Self; 5] = [Self::En, Self::Zh, Self::Ja, Self::Ko, Self::Ru];
/// The language's own name, shown in the switcher.
pub fn name(self) -> &'static str {
match self {
Self::En => "English",
Self::Zh => "简体中文",
Self::Ja => "日本語",
Self::Ko => "한국어",
Self::Ru => "Русский",
}
}
pub const fn code(self) -> &'static str {
match self {
Self::En => "en",
Self::Zh => "zh",
Self::Ja => "ja",
Self::Ko => "ko",
Self::Ru => "ru",
}
}
pub fn parse(code: &str) -> Option<Self> {
match code {
"en" => Some(Self::En),
"ja" => Some(Self::Ja),
_ => None,
}
Self::from_tag(code)
}
/// A language tag (`ja`, `ja-JP`, `en_US`) reduced to a supported language. Native only: the web
/// build has no system locale to read and tests exercise it on the host.
#[cfg(not(target_arch = "wasm32"))]
fn from_tag(tag: &str) -> Option<Self> {
Self::parse(&tag.split(['-', '_']).next().unwrap_or("").to_ascii_lowercase())
match tag.trim().split(['-', '_']).next()?.to_ascii_lowercase().as_str() {
"en" => Some(Self::En),
"zh" => Some(Self::Zh),
"ja" => Some(Self::Ja),
"ko" => Some(Self::Ko),
"ru" => Some(Self::Ru),
_ => None,
}
}
/// The system's preferred interface language, best effort; English when it is unknown or
@ -52,17 +72,21 @@ impl Language {
}
#[cfg(target_arch = "wasm32")]
{
Self::En
web_sys::window().and_then(|window| window.navigator().language()).and_then(|tag| Self::from_tag(&tag)).unwrap_or(Self::En)
}
}
pub fn tr(self, text: &str) -> &str {
if self == Self::Ja
&& let Some((_, japanese)) = JAPANESE.iter().find(|(english, _)| *english == text)
{
return japanese;
if self == Self::En {
return text;
}
match self {
Self::En => text,
Self::Ja => JAPANESE.iter().find(|(english, _)| *english == text).map(|(_, translated)| *translated).unwrap_or(text),
Self::Zh => zh::translate(text).unwrap_or(text),
Self::Ko => ko::translate(text).unwrap_or(text),
Self::Ru => ru::translate(text).unwrap_or(text),
}
text
}
}
@ -344,6 +368,8 @@ const JAPANESE: &[(&str, &str)] = &[
("Lookup &\nReference", "検索/行列"),
("Math &\nTrig", "数学/三角"),
("More\nFunctions", "その他の関数"),
("Screen", "画面"),
("Print", "印刷"),
];
#[cfg(test)]
@ -357,8 +383,19 @@ mod tests {
assert!(JAPANESE.iter().take(i).all(|(other, _)| en != other));
assert_eq!(Language::En.tr(en), *en);
}
for (english, _) in JAPANESE {
assert!(zh::translate(english).is_some_and(|text| !text.is_empty()), "missing Simplified Chinese: {english}");
assert!(ko::translate(english).is_some_and(|text| !text.is_empty()), "missing Korean: {english}");
assert!(ru::translate(english).is_some_and(|text| !text.is_empty()), "missing Russian: {english}");
}
assert_eq!(Language::Ja.tr("Data"), "データ");
assert_eq!(Language::Zh.tr("Data"), "数据");
assert_eq!(Language::Ko.tr("Data"), "데이터");
assert_eq!(Language::Ru.tr("Data"), "Данные");
assert_eq!(Language::Ja.tr("Sheet1!A1"), "Sheet1!A1");
assert_eq!(Language::Zh.tr("Sheet1!A1"), "Sheet1!A1");
assert_eq!(Language::Ko.tr("Sheet1!A1"), "Sheet1!A1");
assert_eq!(Language::Ru.tr("Sheet1!A1"), "Sheet1!A1");
assert_eq!(Language::parse("xx"), None);
}
@ -366,17 +403,26 @@ mod tests {
fn tags_reduce_to_a_supported_language() {
assert_eq!(Language::from_tag("ja-JP"), Some(Language::Ja));
assert_eq!(Language::from_tag("en_US"), Some(Language::En));
assert_eq!(Language::from_tag("zh-Hans-CN"), Some(Language::Zh));
assert_eq!(Language::from_tag("ko-KR"), Some(Language::Ko));
assert_eq!(Language::from_tag("RU_ru"), Some(Language::Ru));
assert_eq!(Language::from_tag("de-DE"), None);
}
#[test]
fn an_unset_language_preference_uses_the_system_default() {
let restored: crate::UiState = serde_json::from_str("{}").unwrap();
assert_eq!(restored.language, Language::system());
}
#[test]
fn language_commands_switch_and_persist() {
let mut app = crate::SheetApp::new(gridcraft_engine::Session::default(), Default::default());
app.run("app.language.japanese", serde_json::json!({})).unwrap();
assert_eq!(app.ui.language, Language::Ja);
let restored: crate::UiState = serde_json::from_str(&serde_json::to_string(&app.ui).unwrap()).unwrap();
assert_eq!(restored.language, Language::Ja);
app.run("app.language.english", serde_json::json!({})).unwrap();
assert_eq!(app.ui.language, Language::En);
for language in Language::ALL {
app.run("app.language.set", serde_json::json!({"language": language.code()})).unwrap();
assert_eq!(app.ui.language, language);
let restored: crate::UiState = serde_json::from_str(&serde_json::to_string(&app.ui).unwrap()).unwrap();
assert_eq!(restored.language, language);
}
}
}

View File

@ -0,0 +1,268 @@
//! Korean translations for the interface strings in i18n.rs.
pub(super) fn translate(text: &str) -> Option<&'static str> {
Some(match text {
"Paste" => "붙여넣기",
"Paste Values" => "값 붙여넣기",
"Paste Formulas" => "수식 붙여넣기",
"Paste Formatting" => "서식 붙여넣기",
"Transpose" => "행/열 바꾸기",
"Paste Link" => "연결하여 붙여넣기",
"Paste Special…" => "선택하여 붙여넣기…",
"Cut (⌘X)" => "잘라내기 (⌘X)",
"Copy (⌘C)" => "복사 (⌘C)",
"Format Painter (double-click to keep it on)" => "서식 복사 (두 번 클릭하여 유지)",
"Bold (⌘B)" => "굵게 (⌘B)",
"Italic (⌘I)" => "기울임꼴 (⌘I)",
"Underline (⌘U)" => "밑줄 (⌘U)",
"Double Underline" => "이중 밑줄",
"Strikethrough" => "취소선",
"Underline" => "밑줄",
"Increase Font Size" => "글꼴 크기 늘리기",
"Decrease Font Size" => "글꼴 크기 줄이기",
"Wrap Text" => "텍스트 줄 바꿈",
"Merge & Center" => "병합하고 가운데 맞춤",
"Merge Cells" => "셀 병합",
"Merge Across" => "행 방향 병합",
"Unmerge Cells" => "셀 병합 취소",
"Fill Color" => "채우기 색",
"Font Color" => "글꼴 색",
"Borders" => "테두리",
"Top\nAlign" => "위쪽\n맞춤",
"Middle\nAlign" => "가운데\n맞춤",
"Bottom\nAlign" => "아래쪽\n맞춤",
"Left" => "왼쪽",
"Right" => "오른쪽",
"Center" => "가운데",
"Horizontal" => "가로",
"Vertical Text" => "세로 텍스트",
"Decrease Indent" => "들여쓰기 줄이기",
"Increase Indent" => "들여쓰기 늘리기",
"Orientation" => "방향",
"Angle Counterclockwise" => "시계 반대 방향 각도",
"Angle Clockwise" => "시계 방향 각도",
"Rotate Text Up" => "텍스트 위쪽으로 회전",
"Rotate Text Down" => "텍스트 아래쪽으로 회전",
"Accounting Number Format" => "회계 숫자 서식",
"Percent Style" => "백분율 스타일",
"Comma Style" => "쉼표 스타일",
"Increase Decimal" => "소수 자릿수 늘리기",
"Decrease Decimal" => "소수 자릿수 줄이기",
"More Accounting Formats…" => "회계 형식 더 보기…",
"Conditional\nFormatting" => "조건부\n서식",
"Conditional Formatting" => "조건부 서식",
"Format\nas Table" => "표 서식\n지정",
"Cell\nStyles" => "셀\n스타일",
"Format Cells…" => "셀 서식…",
"Format Cell Alignment…" => "셀 맞춤 서식…",
"Format\nPane" => "서식\n창",
"Format" => "서식",
"Insert" => "삽입",
"Insert Cells…" => "셀 삽입…",
"Delete" => "삭제",
"Delete Cells…" => "셀 삭제…",
"Insert Sheet Rows" => "시트 행 삽입",
"Insert Sheet Columns" => "시트 열 삽입",
"Delete Sheet Rows" => "시트 행 삭제",
"Delete Sheet Columns" => "시트 열 삭제",
"Row Height…" => "행 높이…",
"AutoFit Row Height" => "행 높이 자동 맞춤",
"Column Width…" => "열 너비…",
"AutoFit Column Width" => "열 너비 자동 맞춤",
"Default Width…" => "기본 너비…",
"Hide Rows" => "행 숨기기",
"Unhide Rows" => "행 숨기기 취소",
"Hide Columns" => "열 숨기기",
"Unhide Columns" => "열 숨기기 취소",
"Lock Cell" => "셀 잠금",
"Clear" => "지우기",
"Clear All" => "모두 지우기",
"Clear Formats" => "서식 지우기",
"Clear Contents" => "내용 지우기",
"Clear Comments and Notes" => "댓글 및 메모 지우기",
"Clear Hyperlinks" => "하이퍼링크 지우기",
"AutoSum" => "자동 합계",
"AutoSum (⌘⇧T)" => "자동 합계 (⌘⇧T)",
"Flash\nFill" => "빠른\n채우기",
"Flash Fill" => "빠른 채우기",
"Fill" => "채우기",
"Clear Print Area" => "인쇄 영역 지우기",
"Find &\nSelect" => "찾기 및\n선택",
"Find…" => "찾기…",
"Replace…" => "바꾸기…",
"Go To…" => "이동…",
"Go To Special…" => "선택하여 이동…",
"PivotTable" => "피벗 테이블",
"Recommended\nCharts" => "추천\n차트",
"Table" => "표",
"Pictures" => "그림",
"Shapes" => "도형",
"Icons" => "아이콘",
"Text\nBox" => "텍스트\n상자",
"Comment" => "댓글",
"New\nComment" => "새 댓글",
"New Note" => "새 메모",
"Show/Hide Note" => "메모 표시/숨기기",
"Header &\nFooter" => "머리글 및\n바닥글",
"Text to\nColumns" => "텍스트\n나누기",
"Link" => "링크",
"Symbol" => "기호",
"Sparklines" => "스파크라인",
"Header & Footer" => "머리글 및 바닥글",
"Draw with ink" => "잉크로 그리기",
"Eraser" => "지우개",
"Ink to\nShape" => "잉크를\n도형으로",
"Select\nObjects" => "개체\n선택",
"Themes" => "테마",
"Margins" => "여백",
"Size" => "용지 크기",
"Print\nArea" => "인쇄\n영역",
"Set Print Area" => "인쇄 영역 설정",
"Breaks" => "나누기",
"Insert Page Break" => "페이지 나누기 삽입",
"Remove Page Break" => "페이지 나누기 제거",
"Reset All Page Breaks" => "모든 페이지 나누기 다시 설정",
"Print\nTitles" => "인쇄\n제목",
"Narrow" => "좁게",
"Wide" => "넓게",
"Normal" => "보통",
"Portrait" => "세로",
"Landscape" => "가로",
"Custom Margins…" => "사용자 지정 여백…",
"Insert\nFunction" => "함수\n삽입",
"Name\nManager" => "이름\n관리자",
"Define Name" => "이름 정의",
"Use in Formula" => "수식에서 사용",
"Create from Selection" => "선택 영역에서 만들기",
"Trace Precedents" => "선행 셀 추적",
"Trace Dependents" => "종속 셀 추적",
"Remove Arrows" => "화살표 제거",
"Show Formulas" => "수식 표시",
"Error Checking" => "오류 검사",
"Evaluate Formula" => "수식 계산",
"Watch Window" => "감시 창",
"Calculation\nOptions" => "계산\n옵션",
"Calculate Now" => "지금 계산",
"Calculate Sheet" => "시트 계산",
"Automatic" => "자동",
"Automatic Except for Data Tables" => "데이터 표를 제외한 자동",
"Manual" => "수동",
"Sort &\nFilter" => "정렬 및\n필터",
"Sort" => "정렬",
"Sort A to Z" => "오름차순 정렬",
"Sort Z to A" => "내림차순 정렬",
"Custom Sort…" => "사용자 지정 정렬…",
"Filter" => "필터",
"Reapply" => "다시 적용",
"Get Data\n(Text/CSV)" => "데이터 가져오기\n(텍스트/CSV)",
"Remove\nDuplicates" => "중복\n항목 제거",
"Data\nValidation" => "데이터\n유효성 검사",
"Data Validation" => "데이터 유효성 검사",
"Data Validation…" => "데이터 유효성 검사…",
"Circle Invalid Data" => "잘못된 데이터 표시",
"Clear Validation" => "유효성 검사 지우기",
"Group" => "그룹",
"Ungroup" => "그룹 해제",
"Subtotal" => "부분합",
"What-If\nAnalysis" => "가상 분석",
"Data Table…" => "데이터 표…",
"Scenario Manager…" => "시나리오 관리자…",
"Goal Seek…" => "목표값 찾기…",
"Spelling" => "맞춤법 검사",
"Check\nAccessibility" => "접근성\n검사",
"Threaded Comments" => "스레드 댓글",
"Show\nComments" => "댓글\n표시",
"Protect\nWorkbook" => "통합 문서\n보호",
"Unprotect\nSheet" => "시트 보호\n해제",
"Protect Sheet…" => "시트 보호…",
"Comments" => "댓글",
"Notes" => "메모",
"Page Break\nPreview" => "페이지 나누기\n미리 보기",
"Page\nLayout" => "페이지\n레이아웃",
"Formula Bar" => "수식 입력줄",
"Gridlines" => "눈금선",
"Headings" => "머리글",
"Zoom" => "확대/축소",
"100%" => "100%",
"Zoom to\nSelection" => "선택 영역에\n맞춤",
"Freeze\nPanes" => "틀\n고정",
"Freeze Panes" => "틀 고정",
"Freeze Top Row" => "첫 행 고정",
"Freeze First Column" => "첫 열 고정",
"Unfreeze Panes" => "틀 고정 취소",
"Dark Mode" => "다크 모드",
"Interface language" => "인터페이스 언어",
"Command\nPalette" => "명령\n팔레트",
"Agent\nControl" => "에이전트\n제어",
"Action\nJournal" => "작업\n기록",
"About\nGridCraft" => "GridCraft\n정보",
"Share" => "공유",
"Add Chart\nElement" => "차트\n요소 추가",
"Change\nChart Type" => "차트 종류\n변경",
"Switch\nRow/Column" => "행/열\n전환",
"Data Labels: None" => "데이터 레이블: 없음",
"Data Labels: Show" => "데이터 레이블: 표시",
"Legend: None" => "범례: 없음",
"Legend: Bottom" => "범례: 아래쪽",
"Legend: Top" => "범례: 위쪽",
"Legend: Right" => "범례: 오른쪽",
"Gridlines: None" => "눈금선: 없음",
"Gridlines: Show" => "눈금선: 표시",
"Chart\nTitle" => "차트\n제목",
"Delete\nChart" => "차트\n삭제",
"Convert\nto Range" => "범위로\n변환",
"Table\nStyles" => "표\n스타일",
"Top 10 Items" => "상위 10개 항목",
"Top 10%" => "상위 10%",
"Bottom 10 Items" => "하위 10개 항목",
"Bottom 10%" => "하위 10%",
"Above Average" => "평균 초과",
"Below Average" => "평균 미만",
"Clear Rules from Entire Sheet" => "시트 전체에서 규칙 지우기",
"Clear Rules from Selected Cells" => "선택한 셀에서 규칙 지우기",
"Constants" => "상수",
"Data Bars" => "데이터 막대",
"Color Scales" => "색조",
"Icon Sets" => "아이콘 집합",
"Home" => "홈",
"Draw" => "그리기",
"Page Layout" => "페이지 레이아웃",
"Formulas" => "수식",
"Data" => "데이터",
"Review" => "검토",
"View" => "보기",
"Automate" => "자동화",
"Table Design" => "표 디자인",
"Chart Design" => "차트 디자인",
"Ready" => "준비",
"Enter" => "입력",
"Edit" => "편집",
"Point" => "가리키기",
"Calculate" => "계산",
"Select destination and press Enter or choose Paste" => "대상을 선택하고 Enter 키를 누르거나 [붙여넣기]를 선택하세요",
"General" => "일반",
"Number" => "숫자",
"Currency" => "통화",
"Accounting" => "회계",
"Date" => "날짜",
"Time" => "시간",
"Percentage" => "백분율",
"Fraction" => "분수",
"Scientific" => "과학",
"Text" => "텍스트",
"Special" => "특수",
"Custom" => "사용자 지정",
"Short Date" => "간단한 날짜",
"Long Date" => "자세한 날짜",
"More Number Formats…" => "숫자 서식 더 보기…",
"Financial" => "재무",
"Logical" => "논리",
"Date &\nTime" => "날짜 및\n시간",
"Lookup &\nReference" => "조회 및\n참조",
"Math &\nTrig" => "수학 및\n삼각",
"More\nFunctions" => "기타\n함수",
"Screen" => "화면",
"Print" => "인쇄",
_ => return None,
})
}

View File

@ -0,0 +1,268 @@
//! Russian translations for the interface strings in i18n.rs.
pub(super) fn translate(text: &str) -> Option<&'static str> {
Some(match text {
"Paste" => "Вставить",
"Paste Values" => "Вставить значения",
"Paste Formulas" => "Вставить формулы",
"Paste Formatting" => "Вставить форматирование",
"Transpose" => "Транспонировать",
"Paste Link" => "Вставить связь",
"Paste Special…" => "Специальная вставка…",
"Cut (⌘X)" => "Вырезать (⌘X)",
"Copy (⌘C)" => "Копировать (⌘C)",
"Format Painter (double-click to keep it on)" => "Формат по образцу (дважды щёлкните, чтобы закрепить)",
"Bold (⌘B)" => "Полужирный (⌘B)",
"Italic (⌘I)" => "Курсив (⌘I)",
"Underline (⌘U)" => "Подчёркивание (⌘U)",
"Double Underline" => "Двойное подчёркивание",
"Strikethrough" => "Зачёркивание",
"Underline" => "Подчёркивание",
"Increase Font Size" => "Увеличить размер шрифта",
"Decrease Font Size" => "Уменьшить размер шрифта",
"Wrap Text" => "Переносить текст",
"Merge & Center" => "Объединить и поместить в центре",
"Merge Cells" => "Объединить ячейки",
"Merge Across" => "Объединить по строкам",
"Unmerge Cells" => "Отменить объединение ячеек",
"Fill Color" => "Цвет заливки",
"Font Color" => "Цвет шрифта",
"Borders" => "Границы",
"Top\nAlign" => "По верхнему\nкраю",
"Middle\nAlign" => "По центру\nвертикали",
"Bottom\nAlign" => "По нижнему\nкраю",
"Left" => "Слева",
"Right" => "Справа",
"Center" => "По центру",
"Horizontal" => "По горизонтали",
"Vertical Text" => "Вертикальный текст",
"Decrease Indent" => "Уменьшить отступ",
"Increase Indent" => "Увеличить отступ",
"Orientation" => "Ориентация",
"Angle Counterclockwise" => "Повернуть против часовой стрелки",
"Angle Clockwise" => "Повернуть по часовой стрелке",
"Rotate Text Up" => "Повернуть текст вверх",
"Rotate Text Down" => "Повернуть текст вниз",
"Accounting Number Format" => "Финансовый числовой формат",
"Percent Style" => "Процентный формат",
"Comma Style" => "Разделитель разрядов",
"Increase Decimal" => "Увеличить разрядность",
"Decrease Decimal" => "Уменьшить разрядность",
"More Accounting Formats…" => "Другие финансовые форматы…",
"Conditional\nFormatting" => "Условное\nформатирование",
"Conditional Formatting" => "Условное форматирование",
"Format\nas Table" => "Форматировать\nкак таблицу",
"Cell\nStyles" => "Стили\nячеек",
"Format Cells…" => "Формат ячеек…",
"Format Cell Alignment…" => "Выравнивание ячеек…",
"Format\nPane" => "Область\nформатирования",
"Format" => "Формат",
"Insert" => "Вставить",
"Insert Cells…" => "Вставить ячейки…",
"Delete" => "Удалить",
"Delete Cells…" => "Удалить ячейки…",
"Insert Sheet Rows" => "Вставить строки листа",
"Insert Sheet Columns" => "Вставить столбцы листа",
"Delete Sheet Rows" => "Удалить строки листа",
"Delete Sheet Columns" => "Удалить столбцы листа",
"Row Height…" => "Высота строки…",
"AutoFit Row Height" => "Автоподбор высоты строки",
"Column Width…" => "Ширина столбца…",
"AutoFit Column Width" => "Автоподбор ширины столбца",
"Default Width…" => "Ширина по умолчанию…",
"Hide Rows" => "Скрыть строки",
"Unhide Rows" => "Показать строки",
"Hide Columns" => "Скрыть столбцы",
"Unhide Columns" => "Показать столбцы",
"Lock Cell" => "Заблокировать ячейку",
"Clear" => "Очистить",
"Clear All" => "Очистить всё",
"Clear Formats" => "Очистить форматы",
"Clear Contents" => "Очистить содержимое",
"Clear Comments and Notes" => "Удалить комментарии и заметки",
"Clear Hyperlinks" => "Удалить гиперссылки",
"AutoSum" => "Автосумма",
"AutoSum (⌘⇧T)" => "Автосумма (⌘⇧T)",
"Flash\nFill" => "Мгновенное\nзаполнение",
"Flash Fill" => "Мгновенное заполнение",
"Fill" => "Заполнить",
"Clear Print Area" => "Очистить область печати",
"Find &\nSelect" => "Найти и\nвыделить",
"Find…" => "Найти…",
"Replace…" => "Заменить…",
"Go To…" => "Перейти…",
"Go To Special…" => "Выделить группу ячеек…",
"PivotTable" => "Сводная таблица",
"Recommended\nCharts" => "Рекомендуемые\nдиаграммы",
"Table" => "Таблица",
"Pictures" => "Изображения",
"Shapes" => "Фигуры",
"Icons" => "Значки",
"Text\nBox" => "Текстовое\nполе",
"Comment" => "Комментарий",
"New\nComment" => "Новый\nкомментарий",
"New Note" => "Новая заметка",
"Show/Hide Note" => "Показать/скрыть заметку",
"Header &\nFooter" => "Верхний и\nнижний колонтитулы",
"Text to\nColumns" => "Текст\nпо столбцам",
"Link" => "Ссылка",
"Symbol" => "Символ",
"Sparklines" => "Спарклайны",
"Header & Footer" => "Верхний и нижний колонтитулы",
"Draw with ink" => "Рисование от руки",
"Eraser" => "Ластик",
"Ink to\nShape" => "Преобразовать рукописный\nввод в фигуру",
"Select\nObjects" => "Выбрать\nобъекты",
"Themes" => "Темы",
"Margins" => "Поля",
"Size" => "Размер",
"Print\nArea" => "Область\nпечати",
"Set Print Area" => "Задать область печати",
"Breaks" => "Разрывы",
"Insert Page Break" => "Вставить разрыв страницы",
"Remove Page Break" => "Удалить разрыв страницы",
"Reset All Page Breaks" => "Сбросить все разрывы страниц",
"Print\nTitles" => "Печатать\nзаголовки",
"Narrow" => "Узкие",
"Wide" => "Широкие",
"Normal" => "Обычные",
"Portrait" => "Книжная",
"Landscape" => "Альбомная",
"Custom Margins…" => "Настраиваемые поля…",
"Insert\nFunction" => "Вставить\nфункцию",
"Name\nManager" => "Диспетчер\nимён",
"Define Name" => "Присвоить имя",
"Use in Formula" => "Использовать в формуле",
"Create from Selection" => "Создать из выделенного",
"Trace Precedents" => "Влияющие ячейки",
"Trace Dependents" => "Зависимые ячейки",
"Remove Arrows" => "Убрать стрелки",
"Show Formulas" => "Показать формулы",
"Error Checking" => "Проверка ошибок",
"Evaluate Formula" => "Вычислить формулу",
"Watch Window" => "Окно контрольных значений",
"Calculation\nOptions" => "Параметры\nвычислений",
"Calculate Now" => "Вычислить сейчас",
"Calculate Sheet" => "Вычислить лист",
"Automatic" => "Автоматически",
"Automatic Except for Data Tables" => "Автоматически, кроме таблиц данных",
"Manual" => "Вручную",
"Sort &\nFilter" => "Сортировка и\nфильтр",
"Sort" => "Сортировка",
"Sort A to Z" => "Сортировка от А до Я",
"Sort Z to A" => "Сортировка от Я до А",
"Custom Sort…" => "Настраиваемая сортировка…",
"Filter" => "Фильтр",
"Reapply" => "Повторить",
"Get Data\n(Text/CSV)" => "Получить данные\n(текст/CSV)",
"Remove\nDuplicates" => "Удалить\nдубликаты",
"Data\nValidation" => "Проверка\nданных",
"Data Validation" => "Проверка данных",
"Data Validation…" => "Проверка данных…",
"Circle Invalid Data" => "Обвести неверные данные",
"Clear Validation" => "Очистить проверку данных",
"Group" => "Группировать",
"Ungroup" => "Разгруппировать",
"Subtotal" => "Промежуточный итог",
"What-If\nAnalysis" => "Анализ «что\nесли»",
"Data Table…" => "Таблица данных…",
"Scenario Manager…" => "Диспетчер сценариев…",
"Goal Seek…" => "Подбор параметра…",
"Spelling" => "Орфография",
"Check\nAccessibility" => "Проверка\nдоступности",
"Threaded Comments" => "Комментарии с ветками",
"Show\nComments" => "Показать\nкомментарии",
"Protect\nWorkbook" => "Защитить\nкнигу",
"Unprotect\nSheet" => "Снять защиту\nлиста",
"Protect Sheet…" => "Защитить лист…",
"Comments" => "Комментарии",
"Notes" => "Заметки",
"Page Break\nPreview" => "Предварительный просмотр\nразрывов страниц",
"Page\nLayout" => "Разметка\nстраницы",
"Formula Bar" => "Строка формул",
"Gridlines" => "Сетка",
"Headings" => "Заголовки",
"Zoom" => "Масштаб",
"100%" => "100%",
"Zoom to\nSelection" => "Масштаб по\nвыделению",
"Freeze\nPanes" => "Закрепить\nобласти",
"Freeze Panes" => "Закрепить области",
"Freeze Top Row" => "Закрепить верхнюю строку",
"Freeze First Column" => "Закрепить первый столбец",
"Unfreeze Panes" => "Снять закрепление областей",
"Dark Mode" => "Тёмный режим",
"Interface language" => "Язык интерфейса",
"Command\nPalette" => "Палитра\nкоманд",
"Agent\nControl" => "Управление\nагентом",
"Action\nJournal" => "Журнал\nдействий",
"About\nGridCraft" => "О\nGridCraft",
"Share" => "Поделиться",
"Add Chart\nElement" => "Добавить элемент\nдиаграммы",
"Change\nChart Type" => "Изменить тип\nдиаграммы",
"Switch\nRow/Column" => "Поменять\nстроки/столбцы",
"Data Labels: None" => "Подписи данных: нет",
"Data Labels: Show" => "Подписи данных: показать",
"Legend: None" => "Легенда: нет",
"Legend: Bottom" => "Легенда: снизу",
"Legend: Top" => "Легенда: сверху",
"Legend: Right" => "Легенда: справа",
"Gridlines: None" => "Линии сетки: нет",
"Gridlines: Show" => "Линии сетки: показать",
"Chart\nTitle" => "Название\nдиаграммы",
"Delete\nChart" => "Удалить\nдиаграмму",
"Convert\nto Range" => "Преобразовать\nв диапазон",
"Table\nStyles" => "Стили\nтаблиц",
"Top 10 Items" => "Первые 10 элементов",
"Top 10%" => "Верхние 10%",
"Bottom 10 Items" => "Последние 10 элементов",
"Bottom 10%" => "Нижние 10%",
"Above Average" => "Выше среднего",
"Below Average" => "Ниже среднего",
"Clear Rules from Entire Sheet" => "Очистить правила на всём листе",
"Clear Rules from Selected Cells" => "Очистить правила для выделенных ячеек",
"Constants" => "Константы",
"Data Bars" => "Гистограммы",
"Color Scales" => "Цветовые шкалы",
"Icon Sets" => "Наборы значков",
"Home" => "Главная",
"Draw" => "Рисование",
"Page Layout" => "Разметка страницы",
"Formulas" => "Формулы",
"Data" => "Данные",
"Review" => "Рецензирование",
"View" => "Вид",
"Automate" => "Автоматизация",
"Table Design" => "Конструктор таблиц",
"Chart Design" => "Конструктор диаграмм",
"Ready" => "Готово",
"Enter" => "Ввод",
"Edit" => "Изменение",
"Point" => "Указание",
"Calculate" => "Вычисление",
"Select destination and press Enter or choose Paste" => "Выберите место вставки и нажмите Enter или выберите «Вставить»",
"General" => "Общий",
"Number" => "Числовой",
"Currency" => "Денежный",
"Accounting" => "Финансовый",
"Date" => "Дата",
"Time" => "Время",
"Percentage" => "Процентный",
"Fraction" => "Дробный",
"Scientific" => "Экспоненциальный",
"Text" => "Текстовый",
"Special" => "Дополнительный",
"Custom" => "Пользовательский",
"Short Date" => "Краткая дата",
"Long Date" => "Длинная дата",
"More Number Formats…" => "Другие числовые форматы…",
"Financial" => "Финансовые",
"Logical" => "Логические",
"Date &\nTime" => "Дата и\nвремя",
"Lookup &\nReference" => "Поиск и\nссылки",
"Math &\nTrig" => "Математические и\nтригонометрические",
"More\nFunctions" => "Другие\nфункции",
"Screen" => "На экране",
"Print" => "Печать",
_ => return None,
})
}

View File

@ -0,0 +1,268 @@
//! Simplified Chinese translations for the interface strings in i18n.rs.
pub(super) fn translate(text: &str) -> Option<&'static str> {
Some(match text {
"Paste" => "粘贴",
"Paste Values" => "粘贴值",
"Paste Formulas" => "粘贴公式",
"Paste Formatting" => "粘贴格式",
"Transpose" => "转置",
"Paste Link" => "粘贴链接",
"Paste Special…" => "选择性粘贴…",
"Cut (⌘X)" => "剪切 (⌘X)",
"Copy (⌘C)" => "复制 (⌘C)",
"Format Painter (double-click to keep it on)" => "格式刷(双击可持续使用)",
"Bold (⌘B)" => "粗体 (⌘B)",
"Italic (⌘I)" => "斜体 (⌘I)",
"Underline (⌘U)" => "下划线 (⌘U)",
"Double Underline" => "双下划线",
"Strikethrough" => "删除线",
"Underline" => "下划线",
"Increase Font Size" => "增大字号",
"Decrease Font Size" => "减小字号",
"Wrap Text" => "自动换行",
"Merge & Center" => "合并后居中",
"Merge Cells" => "合并单元格",
"Merge Across" => "跨行合并",
"Unmerge Cells" => "取消合并单元格",
"Fill Color" => "填充颜色",
"Font Color" => "字体颜色",
"Borders" => "边框",
"Top\nAlign" => "顶端对齐",
"Middle\nAlign" => "垂直居中",
"Bottom\nAlign" => "底端对齐",
"Left" => "左对齐",
"Right" => "右对齐",
"Center" => "居中",
"Horizontal" => "水平",
"Vertical Text" => "竖排文字",
"Decrease Indent" => "减少缩进",
"Increase Indent" => "增加缩进",
"Orientation" => "文字方向",
"Angle Counterclockwise" => "逆时针旋转",
"Angle Clockwise" => "顺时针旋转",
"Rotate Text Up" => "文字向上旋转",
"Rotate Text Down" => "文字向下旋转",
"Accounting Number Format" => "会计数字格式",
"Percent Style" => "百分比样式",
"Comma Style" => "千位分隔样式",
"Increase Decimal" => "增加小数位数",
"Decrease Decimal" => "减少小数位数",
"More Accounting Formats…" => "更多会计格式…",
"Conditional\nFormatting" => "条件\n格式",
"Conditional Formatting" => "条件格式",
"Format\nas Table" => "套用表格\n格式",
"Cell\nStyles" => "单元格\n样式",
"Format Cells…" => "设置单元格格式…",
"Format Cell Alignment…" => "设置单元格对齐方式…",
"Format\nPane" => "格式\n窗格",
"Format" => "格式",
"Insert" => "插入",
"Insert Cells…" => "插入单元格…",
"Delete" => "删除",
"Delete Cells…" => "删除单元格…",
"Insert Sheet Rows" => "插入工作表行",
"Insert Sheet Columns" => "插入工作表列",
"Delete Sheet Rows" => "删除工作表行",
"Delete Sheet Columns" => "删除工作表列",
"Row Height…" => "行高…",
"AutoFit Row Height" => "自动调整行高",
"Column Width…" => "列宽…",
"AutoFit Column Width" => "自动调整列宽",
"Default Width…" => "默认列宽…",
"Hide Rows" => "隐藏行",
"Unhide Rows" => "取消隐藏行",
"Hide Columns" => "隐藏列",
"Unhide Columns" => "取消隐藏列",
"Lock Cell" => "锁定单元格",
"Clear" => "清除",
"Clear All" => "全部清除",
"Clear Formats" => "清除格式",
"Clear Contents" => "清除内容",
"Clear Comments and Notes" => "清除批注和备注",
"Clear Hyperlinks" => "清除超链接",
"AutoSum" => "自动求和",
"AutoSum (⌘⇧T)" => "自动求和 (⌘⇧T)",
"Flash\nFill" => "快速\n填充",
"Flash Fill" => "快速填充",
"Fill" => "填充",
"Clear Print Area" => "清除打印区域",
"Find &\nSelect" => "查找与\n选择",
"Find…" => "查找…",
"Replace…" => "替换…",
"Go To…" => "定位…",
"Go To Special…" => "定位条件…",
"PivotTable" => "数据透视表",
"Recommended\nCharts" => "推荐\n图表",
"Table" => "表格",
"Pictures" => "图片",
"Shapes" => "形状",
"Icons" => "图标",
"Text\nBox" => "文本\n框",
"Comment" => "批注",
"New\nComment" => "新建\n批注",
"New Note" => "新建备注",
"Show/Hide Note" => "显示/隐藏备注",
"Header &\nFooter" => "页眉和\n页脚",
"Text to\nColumns" => "分列",
"Link" => "链接",
"Symbol" => "符号",
"Sparklines" => "迷你图",
"Header & Footer" => "页眉和页脚",
"Draw with ink" => "墨迹绘制",
"Eraser" => "橡皮擦",
"Ink to\nShape" => "墨迹转\n形状",
"Select\nObjects" => "选择\n对象",
"Themes" => "主题",
"Margins" => "页边距",
"Size" => "纸张大小",
"Print\nArea" => "打印\n区域",
"Set Print Area" => "设置打印区域",
"Breaks" => "分页符",
"Insert Page Break" => "插入分页符",
"Remove Page Break" => "删除分页符",
"Reset All Page Breaks" => "重置所有分页符",
"Print\nTitles" => "打印\n标题",
"Narrow" => "窄",
"Wide" => "宽",
"Normal" => "普通",
"Portrait" => "纵向",
"Landscape" => "横向",
"Custom Margins…" => "自定义页边距…",
"Insert\nFunction" => "插入\n函数",
"Name\nManager" => "名称\n管理器",
"Define Name" => "定义名称",
"Use in Formula" => "在公式中使用",
"Create from Selection" => "根据所选内容创建",
"Trace Precedents" => "追踪引用单元格",
"Trace Dependents" => "追踪从属单元格",
"Remove Arrows" => "删除箭头",
"Show Formulas" => "显示公式",
"Error Checking" => "错误检查",
"Evaluate Formula" => "公式求值",
"Watch Window" => "监视窗口",
"Calculation\nOptions" => "计算\n选项",
"Calculate Now" => "立即计算",
"Calculate Sheet" => "计算工作表",
"Automatic" => "自动",
"Automatic Except for Data Tables" => "除数据表以外自动",
"Manual" => "手动",
"Sort &\nFilter" => "排序和\n筛选",
"Sort" => "排序",
"Sort A to Z" => "升序排序",
"Sort Z to A" => "降序排序",
"Custom Sort…" => "自定义排序…",
"Filter" => "筛选",
"Reapply" => "重新应用",
"Get Data\n(Text/CSV)" => "获取数据\n(文本/CSV)",
"Remove\nDuplicates" => "删除\n重复项",
"Data\nValidation" => "数据\n验证",
"Data Validation" => "数据验证",
"Data Validation…" => "数据验证…",
"Circle Invalid Data" => "圈出无效数据",
"Clear Validation" => "清除验证",
"Group" => "组合",
"Ungroup" => "取消组合",
"Subtotal" => "分类汇总",
"What-If\nAnalysis" => "假设分析",
"Data Table…" => "数据表…",
"Scenario Manager…" => "方案管理器…",
"Goal Seek…" => "单变量求解…",
"Spelling" => "拼写检查",
"Check\nAccessibility" => "检查\n辅助功能",
"Threaded Comments" => "线程式批注",
"Show\nComments" => "显示\n批注",
"Protect\nWorkbook" => "保护\n工作簿",
"Unprotect\nSheet" => "撤销\n工作表保护",
"Protect Sheet…" => "保护工作表…",
"Comments" => "批注",
"Notes" => "备注",
"Page Break\nPreview" => "分页符\n预览",
"Page\nLayout" => "页面\n布局",
"Formula Bar" => "编辑栏",
"Gridlines" => "网格线",
"Headings" => "标题",
"Zoom" => "缩放",
"100%" => "100%",
"Zoom to\nSelection" => "缩放到\n所选内容",
"Freeze\nPanes" => "冻结\n窗格",
"Freeze Panes" => "冻结窗格",
"Freeze Top Row" => "冻结首行",
"Freeze First Column" => "冻结首列",
"Unfreeze Panes" => "取消冻结窗格",
"Dark Mode" => "深色模式",
"Interface language" => "界面语言",
"Command\nPalette" => "命令\n面板",
"Agent\nControl" => "智能体\n控制",
"Action\nJournal" => "操作\n日志",
"About\nGridCraft" => "关于\nGridCraft",
"Share" => "分享",
"Add Chart\nElement" => "添加图表\n元素",
"Change\nChart Type" => "更改\n图表类型",
"Switch\nRow/Column" => "切换\n行/列",
"Data Labels: None" => "数据标签:无",
"Data Labels: Show" => "数据标签:显示",
"Legend: None" => "图例:无",
"Legend: Bottom" => "图例:底部",
"Legend: Top" => "图例:顶部",
"Legend: Right" => "图例:右侧",
"Gridlines: None" => "网格线:无",
"Gridlines: Show" => "网格线:显示",
"Chart\nTitle" => "图表\n标题",
"Delete\nChart" => "删除\n图表",
"Convert\nto Range" => "转换\n为区域",
"Table\nStyles" => "表格\n样式",
"Top 10 Items" => "前 10 项",
"Top 10%" => "前 10%",
"Bottom 10 Items" => "后 10 项",
"Bottom 10%" => "后 10%",
"Above Average" => "高于平均值",
"Below Average" => "低于平均值",
"Clear Rules from Entire Sheet" => "清除整张工作表的规则",
"Clear Rules from Selected Cells" => "清除所选单元格的规则",
"Constants" => "常量",
"Data Bars" => "数据条",
"Color Scales" => "色阶",
"Icon Sets" => "图标集",
"Home" => "开始",
"Draw" => "绘图",
"Page Layout" => "页面布局",
"Formulas" => "公式",
"Data" => "数据",
"Review" => "审阅",
"View" => "视图",
"Automate" => "自动化",
"Table Design" => "表格设计",
"Chart Design" => "图表设计",
"Ready" => "就绪",
"Enter" => "输入",
"Edit" => "编辑",
"Point" => "指向",
"Calculate" => "计算",
"Select destination and press Enter or choose Paste" => "选择目标位置并按 Enter,或选择“粘贴”",
"General" => "常规",
"Number" => "数值",
"Currency" => "货币",
"Accounting" => "会计专用",
"Date" => "日期",
"Time" => "时间",
"Percentage" => "百分比",
"Fraction" => "分数",
"Scientific" => "科学记数法",
"Text" => "文本",
"Special" => "特殊",
"Custom" => "自定义",
"Short Date" => "短日期",
"Long Date" => "长日期",
"More Number Formats…" => "更多数字格式…",
"Financial" => "财务",
"Logical" => "逻辑",
"Date &\nTime" => "日期和\n时间",
"Lookup &\nReference" => "查找和\n引用",
"Math &\nTrig" => "数学和\n三角函数",
"More\nFunctions" => "更多\n函数",
"Screen" => "屏幕",
"Print" => "打印",
_ => return None,
})
}

View File

@ -109,6 +109,7 @@ pub struct SheetApp {
pub perf: Perf,
pub fonts_ready: bool,
fonts_set: bool,
fonts_language: Option<i18n::Language>,
pub name_box: Option<String>,
pub(crate) shots: control::Shots,
/// Chart selected on the sheet (id).
@ -138,6 +139,7 @@ impl SheetApp {
perf: Perf::default(),
fonts_ready: false,
fonts_set: false,
fonts_language: None,
name_box: None,
shots: control::Shots::default(),
selected_chart: None,
@ -145,9 +147,14 @@ impl SheetApp {
}
}
/// One-time context setup: fonts and visuals.
/// One-time context setup: fonts and visuals, using the system's preferred language.
pub fn setup_context(ctx: &egui::Context, dark: bool) {
ctx.set_fonts(theme::font_definitions());
Self::setup_context_for_language(ctx, dark, i18n::Language::system());
}
/// Sets up fonts and visuals for a specific persisted interface language.
pub fn setup_context_for_language(ctx: &egui::Context, dark: bool, language: i18n::Language) {
ctx.set_fonts(theme::font_definitions_for_language(language));
theme::apply(ctx, dark);
}
@ -199,7 +206,10 @@ impl SheetApp {
self.ui.language = l;
Ok(json!({"language": l}))
}
None => Err(format!("unknown language {:?} (use \"en\" or \"ja\")", p.get("language").and_then(Json::as_str).unwrap_or(""))),
None => Err(format!(
"unknown language {:?} (use \"en\", \"zh\", \"ja\", \"ko\" or \"ru\")",
p.get("language").and_then(Json::as_str).unwrap_or("")
)),
}
}
"app.language.english" => {
@ -375,13 +385,18 @@ impl SheetApp {
/// Per-frame logic (control channel, screenshots). Call before `ui`.
pub fn logic(&mut self, ctx: &egui::Context) {
if self.fonts_language.is_some_and(|language| language != self.ui.language) {
self.fonts_ready = false;
self.fonts_set = false;
}
if !self.fonts_ready {
// New fonts apply from the next frame on: paint nothing until then.
if self.fonts_set {
self.fonts_ready = true;
} else {
SheetApp::setup_context(ctx, self.ui.dark);
SheetApp::setup_context_for_language(ctx, self.ui.dark, self.ui.language);
self.fonts_set = true;
self.fonts_language = Some(self.ui.language);
ctx.request_repaint();
}
}

View File

@ -1268,7 +1268,8 @@ fn page_layout(app: &mut SheetApp, ui: &mut Ui) {
.unwrap_or((true, true, false, false));
// These two checkboxes mean the screen view vs. the printed page, so they do not share the
// "View" tab label; pick per language rather than through the shared table.
let (screen, print) = if app.ui.language == crate::i18n::Language::Ja { ("画面", "印刷") } else { ("View", "Print") };
let screen = app.ui.language.tr("Screen");
let print = app.ui.language.tr("Print");
ui.vertical(|ui| {
ui.label(egui::RichText::new(app.ui.language.tr("Gridlines")).strong().small());
let mut v = sh.0;
@ -1588,8 +1589,7 @@ fn view(app: &mut SheetApp, ui: &mut Ui) {
|ui| {
for l in crate::i18n::Language::ALL {
if ui.selectable_label(l == lang, l.name()).clicked() {
let code = if l == crate::i18n::Language::Ja { "app.language.japanese" } else { "app.language.english" };
act(app, code, json!({}));
act(app, "app.language.set", json!({"language": l.code()}));
}
}
},

View File

@ -289,12 +289,18 @@ fn candidates(role: &str) -> Vec<(String, u32)> {
/// Builds the font set from system fonts (with egui's defaults as fallback).
pub fn font_definitions() -> FontDefinitions {
font_definitions_for_language(crate::i18n::Language::system())
}
/// Builds the font set with CJK glyph shapes ordered for the selected interface language.
pub fn font_definitions_for_language(language: crate::i18n::Language) -> FontDefinitions {
let mut fonts = FontDefinitions::default();
let base_prop: Vec<String> = fonts.families.get(&FontFamily::Proportional).cloned().unwrap_or_default();
let base_mono: Vec<String> = fonts.families.get(&FontFamily::Monospace).cloned().unwrap_or_default();
// CJK has to sit at the end of each chain: a Latin font owns Latin, and epaint picks the first
// face that has the glyph, so the CJK faces only ever catch Han/Kana/Hangul.
let cjk = load_cjk(&mut fonts);
// face that has the glyph, so the CJK faces only ever catch Han/Kana/Hangul. Keep both
// simplified-Chinese and Japanese Han faces: either font may lack a less common glyph.
let cjk = load_cjk(&mut fonts, language);
for role in [UI, UI_BOLD, CELL, CELL_BOLD, CELL_ITALIC, CELL_BOLD_ITALIC, SERIF, MONO] {
let paths = candidates(role);
let refs: Vec<(&str, u32)> = paths.iter().map(|(p, i)| (p.as_str(), *i)).collect();
@ -324,40 +330,64 @@ pub fn font_definitions() -> FontDefinitions {
/// family can carry them as fallback. egui's bundled fonts have no CJK coverage, so without this a
/// Japanese or Chinese workbook renders as tofu boxes even though the text was read correctly.
#[cfg(not(target_arch = "wasm32"))]
fn load_cjk(fonts: &mut FontDefinitions) -> Vec<String> {
let mut keys = Vec::new();
for (key, cands) in [("sys-cjk-han", cjk_han()), ("sys-cjk-hangul", cjk_hangul())] {
let refs: Vec<(&str, u32)> = cands.iter().map(|(p, i)| (p.as_str(), *i)).collect();
if let Some(fd) = try_load(&refs) {
fonts.font_data.insert(key.to_string(), Arc::new(fd));
keys.push(key.to_string());
}
}
keys
fn load_cjk(fonts: &mut FontDefinitions, language: crate::i18n::Language) -> Vec<String> {
let han_ja = load_cjk_face(fonts, "sys-cjk-han-ja", cjk_han_japanese());
let han_zh = load_cjk_face(fonts, "sys-cjk-han-zh", cjk_han_simplified());
let hangul = load_cjk_face(fonts, "sys-cjk-hangul", cjk_hangul());
let order = match language {
crate::i18n::Language::Ja => [han_ja, han_zh, hangul],
crate::i18n::Language::Ko => [hangul, han_zh, han_ja],
_ => [han_zh, han_ja, hangul],
};
order.into_iter().flatten().collect()
}
#[cfg(target_arch = "wasm32")]
fn load_cjk(_fonts: &mut FontDefinitions) -> Vec<String> {
fn load_cjk(_fonts: &mut FontDefinitions, _language: crate::i18n::Language) -> Vec<String> {
Vec::new()
}
/// Han + Kana faces, in fallback order. A Japanese face comes first (kana and JIS kanji render
/// with Japanese glyph shapes); a Chinese face follows to widen coverage. Every CJK face carries
/// kana, so any of them renders Japanese; the order only decides kanji glyph style.
#[cfg(not(target_arch = "wasm32"))]
fn cjk_han() -> Vec<(String, u32)> {
let mac = "/System/Library/Fonts";
fn load_cjk_face(fonts: &mut FontDefinitions, key: &str, candidates: Vec<(String, u32)>) -> Option<String> {
let refs: Vec<(&str, u32)> = candidates.iter().map(|(path, index)| (path.as_str(), *index)).collect();
let fd = try_load(&refs)?;
fonts.font_data.insert(key.to_string(), Arc::new(fd));
Some(key.to_string())
}
/// Japanese Han/Kana faces. A Japanese face goes first when the interface is Japanese so kanji
/// use Japanese glyph shapes; Chinese remains later in the chain for glyphs it does not contain.
#[cfg(not(target_arch = "wasm32"))]
fn cjk_han_japanese() -> Vec<(String, u32)> {
let win = std::env::var("WINDIR").map(|w| format!("{w}\\Fonts")).unwrap_or_else(|_| "C:\\Windows\\Fonts".into());
let lin = ["/usr/share/fonts/opentype/noto", "/usr/share/fonts/truetype/noto", "/usr/share/fonts/truetype", "/usr/share/fonts"];
let mut v: Vec<(String, u32)> = Vec::new();
v.push((format!("{win}\\YuGothR.ttc"), 0)); // Yu Gothic (Japanese)
v.push((format!("{win}\\msgothic.ttc"), 0)); // MS Gothic (Japanese)
v.push((format!("{win}\\msyh.ttc"), 0)); // Microsoft YaHei (Chinese)
v.push((format!("{mac}/Hiragino Sans GB.ttc"), 0));
v.push((format!("{mac}/PingFang.ttc"), 0));
v.push(("/System/Library/Fonts/Hiragino Sans W3.ttc".into(), 0));
v.push(("/System/Library/Fonts/ヒラギノ角ゴシック W3.ttc".into(), 0));
let lin = ["/usr/share/fonts/opentype/noto", "/usr/share/fonts/truetype/noto", "/usr/share/fonts/truetype", "/usr/share/fonts"];
for d in lin {
v.push((format!("{d}/NotoSansCJK-Regular.ttc"), 0));
v.push((format!("{d}/NotoSansCJKjp-Regular.otf"), 0));
v.push((format!("{d}/NotoSansCJK-Regular.ttc"), 0));
}
v
}
/// Simplified Chinese Han faces. These are loaded separately from Japanese fonts so a missing
/// glyph in the first face can continue through the family chain instead of becoming tofu.
#[cfg(not(target_arch = "wasm32"))]
fn cjk_han_simplified() -> Vec<(String, u32)> {
let win = std::env::var("WINDIR").map(|w| format!("{w}\\Fonts")).unwrap_or_else(|_| "C:\\Windows\\Fonts".into());
let mut v: Vec<(String, u32)> = Vec::new();
v.push((format!("{win}\\msyh.ttc"), 0)); // Microsoft YaHei
v.push((format!("{win}\\simsun.ttc"), 0)); // SimSun
v.push((format!("{win}\\simhei.ttf"), 0)); // SimHei
v.push(("/System/Library/Fonts/PingFang.ttc".into(), 0));
v.push(("/System/Library/Fonts/Hiragino Sans GB.ttc".into(), 0));
let lin = ["/usr/share/fonts/opentype/noto", "/usr/share/fonts/truetype/noto", "/usr/share/fonts/truetype", "/usr/share/fonts"];
for d in lin {
v.push((format!("{d}/NotoSansCJKsc-Regular.otf"), 0));
v.push((format!("{d}/NotoSansCJK-Regular.ttc"), 2)); // Simplified Chinese face in the Noto collection
v.push((format!("{d}/wqy-microhei.ttc"), 0));
}
v
@ -374,7 +404,7 @@ fn cjk_hangul() -> Vec<(String, u32)> {
v.push((format!("{mac}/AppleSDGothicNeo.ttc"), 0));
for d in lin {
v.push((format!("{d}/NotoSansCJKkr-Regular.otf"), 0));
v.push((format!("{d}/NotoSansCJK-Regular.ttc"), 0));
v.push((format!("{d}/NotoSansCJK-Regular.ttc"), 1)); // Korean face in the Noto collection
}
v
}
@ -423,32 +453,55 @@ mod tests {
fn cjk_fallback_is_appended_to_every_family() {
// Structural: whatever loaded, the CJK keys must sit at the end of each chain (after the
// Latin/default fonts) so Latin still wins for Latin text.
let fonts = font_definitions();
let cjk_keys: Vec<String> =
["sys-cjk-han", "sys-cjk-hangul"].into_iter().filter(|k| fonts.font_data.contains_key(*k)).map(str::to_string).collect();
let language = crate::i18n::Language::Zh;
let fonts = font_definitions_for_language(language);
let cjk_keys: Vec<String> = ["sys-cjk-han-zh", "sys-cjk-han-ja", "sys-cjk-hangul"]
.into_iter()
.filter(|key| fonts.font_data.contains_key(*key))
.map(str::to_string)
.collect();
if cjk_keys.is_empty() {
return; // no CJK face on this machine (minimal Linux)
}
for role in [UI, UI_BOLD, CELL, CELL_BOLD, CELL_ITALIC, CELL_BOLD_ITALIC, SERIF, MONO] {
let chain = fonts.families.get(&FontFamily::Name(role.into())).map(Vec::as_slice).unwrap_or_default();
assert!(chain.len() >= cjk_keys.len(), "{role} chain too short for its CJK fallback");
let tail = &chain[chain.len() - cjk_keys.len()..];
assert_eq!(tail, cjk_keys.as_slice(), "{role} must end its chain with the CJK faces");
let first_cjk = chain.iter().position(|key| key.starts_with("sys-cjk-")).unwrap_or(chain.len());
let tail = &chain[first_cjk..];
assert_eq!(tail, cjk_keys.as_slice(), "{role} must end its chain with CJK faces in the language order");
}
}
#[test]
fn cjk_fallback_covers_japanese() {
// Real render check: the proportional family (what cells and the UI use) must resolve
// Japanese kana/kanji to a glyph, not the tofu replacement char.
if cjk_han().iter().all(|(p, _)| std::fs::read(p).is_err()) {
return; // no CJK face on this machine (minimal Linux); nothing to assert
fn system_fallback_covers_supported_scripts() {
let cases = [
(crate::i18n::Language::Zh, cjk_han_simplified(), &["页面布局", "自动化", "粘贴格式"][..]),
(crate::i18n::Language::Ja, cjk_han_japanese(), &["カテゴリ", "貼り付け"][..]),
(crate::i18n::Language::Ko, cjk_hangul(), &["한국어"][..]),
];
for (language, paths, samples) in cases {
if paths.iter().all(|(path, _)| std::fs::read(path).is_err()) {
continue; // this machine has no system face for this script
}
let ctx = egui::Context::default();
ctx.set_fonts(font_definitions_for_language(language));
let mut out = ctx.run_ui(egui::RawInput::default(), |_ui| {});
out.textures_delta.clear(); // nothing consumes the atlas in a headless test
for sample in samples {
let covered = ctx.fonts_mut(|f| f.has_glyphs(&FontId::proportional(14.0), sample));
assert!(covered, "{sample:?} must render with a real glyph, not tofu");
}
if language == crate::i18n::Language::Zh {
let cell_font = FontId::new(14.0, FontFamily::Name(CELL.into()));
let covered = ctx.fonts_mut(|f| f.has_glyphs(&cell_font, "客户端配置 中文字体测试"));
assert!(covered, "Chinese workbook text must render with a real glyph, not tofu");
}
}
let ctx = egui::Context::default();
ctx.set_fonts(font_definitions());
ctx.set_fonts(font_definitions_for_language(crate::i18n::Language::Ru));
let mut out = ctx.run_ui(egui::RawInput::default(), |_ui| {});
out.textures_delta.clear(); // nothing consumes the atlas in a headless test
let covered = ctx.fonts_mut(|f| f.has_glyphs(&FontId::proportional(14.0), "カテゴリ"));
assert!(covered, "Japanese text must render with a real glyph, not tofu");
out.textures_delta.clear();
let cyrillic = ctx.fonts_mut(|f| f.has_glyphs(&ui_font(14.0), "Русский"));
assert!(cyrillic, "Russian interface text must render with a real glyph, not tofu");
}
}

View File

@ -102,15 +102,16 @@ fn renders_every_ribbon_tab_without_panicking() {
fn language_switch_translates_the_ribbon_and_persists() {
use gridcraft_ui_egui::i18n::Language;
let mut h = harness(blank());
// English by default on a machine whose system language is not Japanese.
assert_ne!(h.state().ui.language, Language::Ja);
h.state_mut().run("app.language.japanese", json!({})).unwrap();
h.run_steps(2); // the ribbon renders the Japanese tabs without panicking
assert_eq!(h.state().ui.language, Language::Ja);
// The preference round-trips through the saved UiState.
let restored: gridcraft_ui_egui::UiState = serde_json::from_str(&serde_json::to_string(&h.state().ui).unwrap()).unwrap();
assert_eq!(restored.language, Language::Ja);
h.state_mut().run("app.language.english", json!({})).unwrap();
assert_eq!(h.state().ui.language, Language::system());
for (language, expected) in [(Language::Zh, "数据"), (Language::Ja, "データ"), (Language::Ko, "데이터"), (Language::Ru, "Данные")] {
h.state_mut().run("app.language.set", json!({"language": language.code()})).unwrap();
h.run_steps(2); // every locale renders the ribbon without panicking
assert_eq!(h.state().ui.language, language);
assert_eq!(language.tr("Data"), expected);
let restored: gridcraft_ui_egui::UiState = serde_json::from_str(&serde_json::to_string(&h.state().ui).unwrap()).unwrap();
assert_eq!(restored.language, language);
}
h.state_mut().run("app.language.set", json!({"language": "en"})).unwrap();
assert_eq!(h.state().ui.language, Language::En);
}