mirror of
https://github.com/storytold/gridcraft.git
synced 2026-10-29 05:06:51 -06:00
Merge pull request #104 from neko233-com/feature/i18n-zh-ko-ru
i18n: add Simplified Chinese, Korean and Russian UI
This commit is contained in:
Generated
+1
@@ -1545,6 +1545,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"sys-locale",
|
||||
"toml",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -37,8 +37,17 @@ pub fn start() {
|
||||
} else if q.contains("sample") {
|
||||
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);
|
||||
let mut app = SheetApp::new(session, services(inbox.clone(), cc.egui_ctx.clone()));
|
||||
// Follow the browser's language, except a CJK one: the web build has no system
|
||||
// CJK fonts (none are bundled), so that interface would render as tofu.
|
||||
let browser = gridcraft_ui_egui::i18n::Language::system();
|
||||
if !matches!(
|
||||
browser,
|
||||
gridcraft_ui_egui::i18n::Language::Zh | gridcraft_ui_egui::i18n::Language::Ja | gridcraft_ui_egui::i18n::Language::Ko
|
||||
) {
|
||||
app.ui.language = browser;
|
||||
}
|
||||
SheetApp::setup_context_for_language(&cc.egui_ctx, false, app.ui.language);
|
||||
Ok(Box::new(WebShell { app, inbox }))
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -262,7 +262,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),
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -163,7 +163,7 @@ pub fn handle(app: &mut SheetApp, ctx: &egui::Context, method: &str, p: &Json) -
|
||||
("view.theme", "Display Theme", ""),
|
||||
("view.zoom100", "100%", ""),
|
||||
("ui.dialog", "Open Dialog", ""),
|
||||
("app.language.set", "Interface Language", "{language: \"en\"|\"ja\"} (alias: code)"),
|
||||
("app.language.set", "Interface Language", "{language: \"en\"|\"zh\"|\"ja\"|\"ko\"|\"ru\"} (alias: code)"),
|
||||
("app.language.english", "Interface Language: English", ""),
|
||||
("app.language.japanese", "Interface Language: Japanese", ""),
|
||||
] {
|
||||
|
||||
+69
-29
@@ -4,51 +4,64 @@
|
||||
//! 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 => "Русский",
|
||||
}
|
||||
}
|
||||
|
||||
/// The language's code, as saved in `ui.json` and taken by `app.language.set`.
|
||||
pub fn code(self) -> &'static str {
|
||||
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
|
||||
@@ -60,7 +73,7 @@ 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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,12 +82,14 @@ impl Language {
|
||||
/// A label that needs two translations (Home's text "Orientation" vs Page Layout's print
|
||||
/// "Orientation") is keyed `context|label`: English shows only the part after the `|`.
|
||||
pub fn tr(self, text: &str) -> &str {
|
||||
if self == Self::Ja
|
||||
&& let Some((_, japanese)) = JAPANESE.iter().find(|(english, _)| *english == text)
|
||||
{
|
||||
return japanese;
|
||||
}
|
||||
text.rsplit_once('|').map_or(text, |(_, label)| label)
|
||||
let translated = match self {
|
||||
Self::En => None,
|
||||
Self::Ja => JAPANESE.iter().find(|(english, _)| *english == text).map(|(_, translated)| *translated),
|
||||
Self::Zh => zh::translate(text),
|
||||
Self::Ko => ko::translate(text),
|
||||
Self::Ru => ru::translate(text),
|
||||
};
|
||||
translated.unwrap_or_else(|| text.rsplit_once('|').map_or(text, |(_, label)| label))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -384,6 +399,8 @@ const JAPANESE: &[(&str, &str)] = &[
|
||||
("Lookup &\nReference", "検索/行列"),
|
||||
("Math &\nTrig", "数学/三角"),
|
||||
("More\nFunctions", "その他の関数"),
|
||||
("Sheet Options|View", "画面"),
|
||||
("Print", "印刷"),
|
||||
];
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -397,8 +414,19 @@ mod tests {
|
||||
assert!(JAPANESE.iter().take(i).all(|(other, _)| en != other));
|
||||
assert_eq!(Language::En.tr(en), en.rsplit_once('|').map_or(*en, |(_, label)| label));
|
||||
}
|
||||
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");
|
||||
// A `context|label` key shows only its label in English and splits the translation.
|
||||
assert_eq!(Language::En.tr("Page Layout|Orientation"), "Orientation");
|
||||
assert_eq!(Language::Ja.tr("Page Layout|Orientation"), "印刷の向き");
|
||||
@@ -425,23 +453,35 @@ 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_is_english_until_the_app_applies_the_system_one() {
|
||||
// Deserializing never reads the host locale (tests stay deterministic); the desktop app
|
||||
// applies `Language::system()` itself when `ui.json` names no usable language.
|
||||
let restored: crate::UiState = serde_json::from_str("{}").unwrap();
|
||||
assert_eq!(restored.language, Language::En);
|
||||
assert_eq!(saved_language(&serde_json::json!({"language": "zh"})), Some(Language::Zh));
|
||||
}
|
||||
|
||||
#[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);
|
||||
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);
|
||||
}
|
||||
app.run("app.language.set", serde_json::json!({"code": "ja"})).unwrap();
|
||||
assert_eq!(app.ui.language, Language::Ja, "`code` is an alias for `language`");
|
||||
assert!(app.run("app.language.set", serde_json::json!({})).is_err(), "no language is an error, not a dialog");
|
||||
app.run("app.language.english", serde_json::json!({})).unwrap();
|
||||
assert_eq!(app.ui.language, Language::En);
|
||||
app.run("app.language.set", serde_json::json!({"language": "ja"})).unwrap();
|
||||
assert_eq!(app.ui.language, Language::Ja);
|
||||
app.run("app.language.set", serde_json::json!({"code": "en"})).unwrap();
|
||||
assert_eq!(app.ui.language, Language::En);
|
||||
assert!(app.run("app.language.set", serde_json::json!({})).is_err(), "no language is an error, not a dialog");
|
||||
for l in Language::ALL {
|
||||
assert_eq!(Language::parse(l.code()), Some(l));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,273 @@
|
||||
//! 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함수",
|
||||
"Page Layout|Orientation" => "용지 방향",
|
||||
"Display theme" => "표시 테마",
|
||||
"System" => "시스템",
|
||||
"Light" => "밝게",
|
||||
"Dark" => "어둡게",
|
||||
"Sheet Options|View" => "화면",
|
||||
"Print" => "인쇄",
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
//! 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функции",
|
||||
"Page Layout|Orientation" => "Ориентация",
|
||||
"Display theme" => "Тема оформления",
|
||||
"System" => "Системная",
|
||||
"Light" => "Светлая",
|
||||
"Dark" => "Тёмная",
|
||||
"Sheet Options|View" => "На экране",
|
||||
"Print" => "Печать",
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
//! 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函数",
|
||||
"Page Layout|Orientation" => "纸张方向",
|
||||
"Display theme" => "显示主题",
|
||||
"System" => "系统",
|
||||
"Light" => "浅色",
|
||||
"Dark" => "深色",
|
||||
"Sheet Options|View" => "屏幕",
|
||||
"Print" => "打印",
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
@@ -144,6 +144,9 @@ pub struct SheetApp {
|
||||
pub perf: Perf,
|
||||
pub fonts_ready: bool,
|
||||
fonts_set: bool,
|
||||
/// Han face order the installed fonts were built with; a language switch rebuilds the fonts
|
||||
/// only when it changes this (see `theme::HanOrder`).
|
||||
fonts_han: Option<theme::HanOrder>,
|
||||
effective_dark: bool,
|
||||
pub name_box: Option<String>,
|
||||
/// Transient ribbon keyboard navigation; never saved with UI preferences.
|
||||
@@ -177,6 +180,7 @@ impl SheetApp {
|
||||
perf: Perf::default(),
|
||||
fonts_ready: false,
|
||||
fonts_set: false,
|
||||
fonts_han: None,
|
||||
effective_dark: false,
|
||||
name_box: None,
|
||||
keytips: keytips::KeyTips::default(),
|
||||
@@ -186,9 +190,14 @@ impl SheetApp {
|
||||
}
|
||||
}
|
||||
|
||||
/// One-time context setup: fonts and visuals.
|
||||
/// One-time context setup: fonts and visuals (default Han order; never reads the host locale).
|
||||
pub fn setup_context(ctx: &egui::Context, dark: bool) {
|
||||
ctx.set_fonts(theme::font_definitions());
|
||||
Self::setup_context_for_language(ctx, dark, i18n::Language::En);
|
||||
}
|
||||
|
||||
/// 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);
|
||||
}
|
||||
|
||||
@@ -294,7 +303,7 @@ impl SheetApp {
|
||||
self.ui.language = l;
|
||||
Ok(json!({"language": l}))
|
||||
}
|
||||
None => Err(format!("unknown language {code:?} (use \"en\" or \"ja\")")),
|
||||
None => Err(format!("unknown language {code:?} (use \"en\", \"zh\", \"ja\", \"ko\" or \"ru\")")),
|
||||
}
|
||||
}
|
||||
"app.language.english" => {
|
||||
@@ -478,13 +487,18 @@ impl SheetApp {
|
||||
|
||||
/// Per-frame logic (control channel, screenshots). Call before `ui`.
|
||||
pub fn logic(&mut self, ctx: &egui::Context) {
|
||||
if self.fonts_han.is_some_and(|han| han != theme::HanOrder::of(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_han = Some(theme::HanOrder::of(self.ui.language));
|
||||
ctx.request_repaint();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1358,9 +1358,10 @@ fn page_layout(app: &mut SheetApp, ui: &mut Ui) {
|
||||
.active()
|
||||
.and_then(|d| d.wb.active().map(|s| (s.show_gridlines, s.show_headings, s.print.gridlines, s.print.headings)))
|
||||
.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") };
|
||||
// These two checkboxes mean the screen view vs. the printed page. English shows "View" (as in
|
||||
// Excel), but the key is distinct from the "View" tab so languages can word them apart.
|
||||
let screen = app.ui.language.tr("Sheet Options|View");
|
||||
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;
|
||||
|
||||
+129
-57
@@ -287,14 +287,37 @@ fn candidates(role: &str) -> Vec<(String, u32)> {
|
||||
v
|
||||
}
|
||||
|
||||
/// Builds the font set from system fonts (with egui's defaults as fallback).
|
||||
/// Builds the font set from system fonts (with egui's defaults as fallback), with the default
|
||||
/// (non-Japanese) Han order. Never reads the host locale, so tests and snapshots are stable.
|
||||
pub fn font_definitions() -> FontDefinitions {
|
||||
font_definitions_for_language(crate::i18n::Language::En)
|
||||
}
|
||||
|
||||
/// Which Han face leads the fallback chain. Both Han faces cover kana and most kanji, so this only
|
||||
/// decides glyph shapes (and which face catches a glyph the other lacks); switching between two
|
||||
/// languages with the same order needs no font rebuild.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum HanOrder {
|
||||
/// Japanese glyph shapes first (the interface is Japanese).
|
||||
Japanese,
|
||||
/// Simplified Chinese first: the widest Han coverage, for every other interface language.
|
||||
Chinese,
|
||||
}
|
||||
|
||||
impl HanOrder {
|
||||
pub fn of(language: crate::i18n::Language) -> Self {
|
||||
if language == crate::i18n::Language::Ja { Self::Japanese } else { Self::Chinese }
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds the font set with the Han fallback faces ordered for the 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();
|
||||
// The non-Latin fallback faces have to sit at the end of each chain: a Latin font owns Latin,
|
||||
// and epaint picks the first face that has the glyph, so they only catch their own scripts.
|
||||
let fallback = load_fallback(&mut fonts);
|
||||
let fallback = load_fallback(&mut fonts, HanOrder::of(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();
|
||||
@@ -326,13 +349,13 @@ pub fn font_definitions() -> FontDefinitions {
|
||||
/// was read correctly. With a face installed those scripts get real glyphs instead of tofu; nothing
|
||||
/// is bundled, and a machine without the face keeps the old behaviour.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn load_fallback(fonts: &mut FontDefinitions) -> Vec<String> {
|
||||
fn load_fallback(fonts: &mut FontDefinitions, han: HanOrder) -> Vec<String> {
|
||||
let mut keys = Vec::new();
|
||||
// Files already loaded as a fallback face. A file serves at most one key: the region-merged
|
||||
// Noto Sans CJK .ttc found for Han also carries Hangul (and Tahoma carries both Thai and
|
||||
// Hebrew), so loading it again for a later script would only duplicate tens of MB in memory.
|
||||
let mut loaded: Vec<String> = Vec::new();
|
||||
for (key, cands) in fallback_faces() {
|
||||
for (key, cands) in fallback_faces(han) {
|
||||
for (path, index) in &cands {
|
||||
if loaded.contains(path) {
|
||||
break; // a face loaded for an earlier script already covers this one
|
||||
@@ -349,16 +372,23 @@ fn load_fallback(fonts: &mut FontDefinitions) -> Vec<String> {
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn load_fallback(_fonts: &mut FontDefinitions) -> Vec<String> {
|
||||
fn load_fallback(_fonts: &mut FontDefinitions, _han: HanOrder) -> Vec<String> {
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
/// Every fallback face we look for, in chain order: `(key, candidate files)`. One file per script
|
||||
/// family is enough — epaint picks the first face with the glyph, and a script's glyphs live in one
|
||||
/// file. Order only matters between faces that both cover a glyph (e.g. Japanese vs Chinese kanji).
|
||||
/// file. Order only matters between faces that both cover a glyph (Japanese vs Chinese kanji), so
|
||||
/// the interface language reorders the two Han entries and nothing else.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn fallback_faces() -> Vec<(&'static str, Vec<(String, u32)>)> {
|
||||
vec![("sys-cjk-han", cjk_han()), ("sys-cjk-hangul", cjk_hangul()), ("sys-thai", thai()), ("sys-hebrew", hebrew())]
|
||||
fn fallback_faces(han: HanOrder) -> Vec<(&'static str, Vec<(String, u32)>)> {
|
||||
let ja = ("sys-cjk-han-ja", cjk_han_japanese());
|
||||
let zh = ("sys-cjk-han-zh", cjk_han_simplified());
|
||||
let (first, second) = match han {
|
||||
HanOrder::Japanese => (ja, zh),
|
||||
HanOrder::Chinese => (zh, ja),
|
||||
};
|
||||
vec![first, second, ("sys-cjk-hangul", cjk_hangul()), ("sys-thai", thai()), ("sys-hebrew", hebrew())]
|
||||
}
|
||||
|
||||
/// Linux directories that hold CJK fonts across distributions: Debian/Ubuntu (`opentype/noto`,
|
||||
@@ -378,28 +408,41 @@ const LINUX_CJK_DIRS: [&str; 10] = [
|
||||
"/usr/local/share/fonts/noto-cjk",
|
||||
];
|
||||
|
||||
/// 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.
|
||||
/// Japanese Han + Kana faces (Japanese kanji glyph shapes).
|
||||
///
|
||||
/// The region-merged `NotoSansCJK-Regular.ttc` holds one face per locale (JP 0, KR 1, SC 2, TC 3,
|
||||
/// HK 4) with the same glyph coverage; index 0 picks the Japanese glyph shapes.
|
||||
/// HK 4) with the same glyph coverage, so the index only picks glyph shapes. Whichever Han list
|
||||
/// comes first loads it; [`load_fallback`] does not load the same file again for the other.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn cjk_han() -> Vec<(String, u32)> {
|
||||
fn cjk_han_japanese() -> Vec<(String, u32)> {
|
||||
let mac = "/System/Library/Fonts";
|
||||
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}\\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!("{win}\\simsun.ttc"), 0)); // SimSun (Chinese, older installs)
|
||||
v.push((format!("{mac}/ヒラギノ角ゴシック W3.ttc"), 0)); // Hiragino Sans (Japanese)
|
||||
v.push((format!("{win}\\YuGothR.ttc"), 0)); // Yu Gothic
|
||||
v.push((format!("{win}\\msgothic.ttc"), 0)); // MS Gothic
|
||||
v.push((format!("{mac}/ヒラギノ角ゴシック W3.ttc"), 0)); // Hiragino Sans
|
||||
for d in LINUX_CJK_DIRS {
|
||||
v.push((format!("{d}/NotoSansCJKjp-Regular.otf"), 0));
|
||||
v.push((format!("{d}/NotoSansCJK-Regular.ttc"), 0)); // JP face
|
||||
}
|
||||
v
|
||||
}
|
||||
|
||||
/// Simplified Chinese Han faces. Loaded apart from the Japanese ones so a glyph one family lacks
|
||||
/// (simplified-only hanzi in a Japanese font) still falls through to a real glyph.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn cjk_han_simplified() -> Vec<(String, u32)> {
|
||||
let mac = "/System/Library/Fonts";
|
||||
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((format!("{mac}/Hiragino Sans GB.ttc"), 0)); // ships on every recent macOS
|
||||
v.push((format!("{mac}/PingFang.ttc"), 0)); // older macOS only; now a downloadable asset
|
||||
for d in LINUX_CJK_DIRS {
|
||||
v.push((format!("{d}/NotoSansCJK-Regular.ttc"), 0));
|
||||
v.push((format!("{d}/NotoSansCJKjp-Regular.otf"), 0));
|
||||
v.push((format!("{d}/NotoSansCJKsc-Regular.otf"), 0));
|
||||
v.push((format!("{d}/NotoSansCJK-Regular.ttc"), 2)); // SC face
|
||||
}
|
||||
for d in LINUX_CJK_DIRS {
|
||||
v.push((format!("{d}/wqy/wqy-microhei.ttc"), 0));
|
||||
@@ -515,33 +558,52 @@ pub fn color32(rgb: [u8; 3]) -> Color32 {
|
||||
#[cfg(all(test, not(target_arch = "wasm32")))]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::i18n::Language;
|
||||
|
||||
#[test]
|
||||
fn fallback_faces_are_appended_to_every_family() {
|
||||
// Structural: whatever loaded, the non-Latin fallback 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 fb_keys: Vec<String> = fallback_faces().into_iter().map(|(k, _)| k.to_string()).filter(|k| fonts.font_data.contains_key(k)).collect();
|
||||
if fb_keys.is_empty() {
|
||||
return; // no fallback face on this machine (minimal Linux)
|
||||
// (after the Latin/default fonts) so Latin still wins for Latin text, in the language's order.
|
||||
for han in [HanOrder::Chinese, HanOrder::Japanese] {
|
||||
let language = if han == HanOrder::Japanese { Language::Ja } else { Language::Zh };
|
||||
let fonts = font_definitions_for_language(language);
|
||||
let fb_keys: Vec<String> =
|
||||
fallback_faces(han).into_iter().map(|(k, _)| k.to_string()).filter(|k| fonts.font_data.contains_key(k)).collect();
|
||||
if fb_keys.is_empty() {
|
||||
return; // no fallback 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() >= fb_keys.len(), "{role} chain too short for its fallback");
|
||||
let tail = &chain[chain.len() - fb_keys.len()..];
|
||||
assert_eq!(tail, fb_keys.as_slice(), "{role} must end its chain with the fallback faces");
|
||||
}
|
||||
}
|
||||
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() >= fb_keys.len(), "{role} chain too short for its fallback");
|
||||
let tail = &chain[chain.len() - fb_keys.len()..];
|
||||
assert_eq!(tail, fb_keys.as_slice(), "{role} must end its chain with the fallback faces");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn language_reorders_only_the_han_faces() {
|
||||
let keys = |han| fallback_faces(han).into_iter().map(|(k, _)| k).collect::<Vec<_>>();
|
||||
assert_eq!(keys(HanOrder::Japanese), ["sys-cjk-han-ja", "sys-cjk-han-zh", "sys-cjk-hangul", "sys-thai", "sys-hebrew"]);
|
||||
assert_eq!(keys(HanOrder::Chinese), ["sys-cjk-han-zh", "sys-cjk-han-ja", "sys-cjk-hangul", "sys-thai", "sys-hebrew"]);
|
||||
assert_eq!(HanOrder::of(Language::Ja), HanOrder::Japanese);
|
||||
for l in [Language::En, Language::Zh, Language::Ko, Language::Ru] {
|
||||
assert_eq!(HanOrder::of(l), HanOrder::Chinese, "{l:?} shares the default order (no font rebuild)");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fallback_faces_load_each_file_once() {
|
||||
// The region-merged Noto Sans CJK .ttc serves Han and Hangul; it must be loaded once, not
|
||||
// once per script (tens of MB each).
|
||||
let fonts = font_definitions();
|
||||
let present: Vec<&Arc<FontData>> = fallback_faces().into_iter().filter_map(|(k, _)| fonts.font_data.get(k)).collect();
|
||||
for (i, a) in present.iter().enumerate() {
|
||||
for b in present.iter().skip(i + 1) {
|
||||
assert!(a.font != b.font, "the same font file is loaded for two fallback scripts");
|
||||
// The region-merged Noto Sans CJK .ttc serves Japanese, Chinese and Hangul; it must be loaded
|
||||
// once, not once per script (tens of MB each).
|
||||
for language in [Language::En, Language::Ja] {
|
||||
let fonts = font_definitions_for_language(language);
|
||||
let present: Vec<&Arc<FontData>> =
|
||||
fallback_faces(HanOrder::of(language)).into_iter().filter_map(|(k, _)| fonts.font_data.get(k)).collect();
|
||||
for (i, a) in present.iter().enumerate() {
|
||||
for b in present.iter().skip(i + 1) {
|
||||
assert!(a.font != b.font, "the same font file is loaded for two fallback scripts");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -551,31 +613,41 @@ mod tests {
|
||||
// Real render check, per script: the proportional family (what cells and the UI use) must
|
||||
// resolve a sample string to a real glyph, not the tofu replacement char. Skips a script
|
||||
// whose face is absent (a minimal machine) and fails if a face is present but not wired in.
|
||||
let cases: [(&str, Vec<(String, u32)>, &str); 4] = [
|
||||
("sys-cjk-han", cjk_han(), "カテゴリ"),
|
||||
let cases: [(&str, Vec<(String, u32)>, &str); 6] = [
|
||||
("sys-cjk-han-ja", cjk_han_japanese(), "カテゴリ"),
|
||||
("sys-cjk-han-zh", cjk_han_simplified(), "页面布局 客户端配置"),
|
||||
("sys-cjk-hangul", cjk_hangul(), "한글"),
|
||||
("sys-thai", thai(), "ทดสอบ"),
|
||||
("sys-hebrew", hebrew(), "עברית"),
|
||||
("ui", candidates(UI), "Русский"),
|
||||
];
|
||||
let ctx = egui::Context::default();
|
||||
ctx.set_fonts(font_definitions());
|
||||
let mut out = ctx.run_ui(egui::RawInput::default(), |_ui| {});
|
||||
out.textures_delta.clear(); // nothing consumes the atlas in a headless test
|
||||
// `has_glyphs` is a false negative when the face that owns the script is also the face that
|
||||
// draws the replacement glyph (DejaVu Sans as the UI font owns Hebrew), so compare the laid
|
||||
// out glyphs with the tofu glyph itself instead.
|
||||
let font = FontId::proportional(14.0);
|
||||
let glyphs = |text: &str| {
|
||||
let galley = ctx.fonts_mut(|f| f.layout_no_wrap(text.to_owned(), font.clone(), Color32::BLACK));
|
||||
galley.rows.iter().flat_map(|r| r.glyphs.iter().map(|g| g.uv_rect)).collect::<Vec<_>>()
|
||||
};
|
||||
let tofu = glyphs("\u{25FB}");
|
||||
for (key, cands, sample) in cases {
|
||||
if !cands.iter().any(|(p, _)| std::path::Path::new(p).exists()) {
|
||||
continue; // no face for this script on this machine; nothing to assert
|
||||
for language in [Language::En, Language::Ja] {
|
||||
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
|
||||
// `has_glyphs` is a false negative when the face that owns the script is also the face
|
||||
// that draws the replacement glyph (DejaVu Sans as the UI font owns Hebrew), so compare
|
||||
// the laid out glyphs with the tofu glyph itself instead.
|
||||
for family in [FontFamily::Proportional, FontFamily::Name(CELL.into())] {
|
||||
let font = FontId::new(14.0, family);
|
||||
let glyphs = |text: &str| {
|
||||
let galley = ctx.fonts_mut(|f| f.layout_no_wrap(text.to_owned(), font.clone(), Color32::BLACK));
|
||||
galley.rows.iter().flat_map(|r| r.glyphs.iter().filter(|g| g.chr != ' ').map(|g| g.uv_rect)).collect::<Vec<_>>()
|
||||
};
|
||||
let tofu = glyphs("\u{25FB}");
|
||||
for (key, cands, sample) in &cases {
|
||||
if !cands.iter().any(|(p, _)| std::path::Path::new(p).exists()) {
|
||||
continue; // no face for this script on this machine; nothing to assert
|
||||
}
|
||||
let drawn = glyphs(sample);
|
||||
assert!(
|
||||
!drawn.is_empty() && drawn.iter().all(|g| !tofu.contains(g)),
|
||||
"{key} is present but {sample:?} still renders as tofu ({language:?}, {:?})",
|
||||
font.family
|
||||
);
|
||||
}
|
||||
}
|
||||
let drawn = glyphs(sample);
|
||||
assert!(!drawn.is_empty() && drawn.iter().all(|g| !tofu.contains(g)), "{key} is present but {sample:?} still renders as tofu");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,13 +199,15 @@ fn language_switch_translates_the_ribbon_and_persists() {
|
||||
for id in ["app.language.set", "app.language.english", "app.language.japanese"] {
|
||||
assert!(ids.contains(&id), "engine.commands lists {id}");
|
||||
}
|
||||
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();
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user