i18n.tsx
14.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react";
import { KIOSK_EVENT } from "./kioskStorage";
export type AppLang = "zh-CN" | "en" | "bo";
const LS_LANG = "kiosk_lang_v1";
type Dict = Record<string, Record<AppLang, string>>;
const DICT: Dict = {
// Brand / header
"brand.title": {
"zh-CN": "稻城天文台信息查询",
en: "Daocheng Observatory Information Query",
bo: "འདབ་ཆུ་གནམ་གཟིགས་ལྟེ་གནས་ཀྱི་ཆ་འཕྲིན་འཚོལ་ཞིབ",
},
// Nav
"nav.home": { "zh-CN": "首页", en: "Home", bo: "མདུན་ངོས" },
"nav.tour": { "zh-CN": "共享天文台", en: "Shared Observatory", bo: "མཉམ་སྤྱོད་གནམ་གཟིགས་ལྟེ་གནས" },
"nav.planetarium": { "zh-CN": "天文馆导览", en: "Planetarium Guide", bo: "གནམ་གཟིགས་ཁང་སྟོན་ལམ" },
"nav.wiki": { "zh-CN": "天文知识库", en: "Astronomy Library", bo: "གནམ་གཟིགས་ཤེས་བྱའི་མཛོད" },
"nav.web": { "zh-CN": "天文馆官网", en: "Official Website", bo: "གཞུང་དྲ་ཚིགས" },
"nav.guide": { "zh-CN": "操作指南", en: "User Guide", bo: "སྤྱོད་སྟོན" },
"nav.maintain": { "zh-CN": "系统维护", en: "Maintenance", bo: "རྒྱུད་ཁོངས་སྲུང་སྐྱོང" },
// Search (knowledge library)
"search.title": { "zh-CN": "天文知识库", en: "Astronomy Library", bo: "གནམ་གཟིགས་ཤེས་བྱའི་མཛོད" },
"search.placeholder": { "zh-CN": "搜索天文知识...", en: "Search...", bo: "འཚོལ་ཞིབ..." },
"search.button": { "zh-CN": "搜索", en: "Search", bo: "འཚོལ" },
"search.foundPrefix": { "zh-CN": "找到", en: "Found", bo: "ཐོབ་པ" },
"search.foundSuffix": { "zh-CN": "条结果", en: "results", bo: "གྲངས" },
"search.noneTitle": { "zh-CN": "未找到匹配的结果", en: "No matching results", bo: "མ་ཐོབ" },
"search.noneHint": { "zh-CN": "请尝试其他搜索词或点击上方关键词", en: "Try other keywords.", bo: "ཚིག་གཞན་ཞིག་འཚོལ" },
"pager.label": { "zh-CN": "分页", en: "Pagination", bo: "ཤོག་གྲངས" },
"pager.prev": { "zh-CN": "上一页", en: "Prev", bo: "གོང་ཤོག" },
"pager.next": { "zh-CN": "下一页", en: "Next", bo: "འོག་ཤོག" },
"pager.page": { "zh-CN": "第", en: "Page", bo: "ཤོག" },
// Dialog
"dialog.loading": { "zh-CN": "正在加载官网…", en: "Loading…", bo: "འཇུག་བཞིན…" },
// Admin login / maintenance
"admin.login.title": { "zh-CN": "管理员登录", en: "Admin Login", bo: "དོ་དམ་པ་ནང་འཛུལ" },
"admin.login.desc": { "zh-CN": "请输入账号与密码后进入系统维护页面。", en: "Enter credentials to continue.", bo: "ཐོ་འགོད་ཆ་འཕྲིན་འཇུག" },
"admin.login.user": { "zh-CN": "账号", en: "Username", bo: "མིང" },
"admin.login.pass": { "zh-CN": "密码", en: "Password", bo: "གསང་ཨང" },
"admin.login.userPh": { "zh-CN": "请输入账号", en: "Enter username", bo: "མིང་འཇུག" },
"admin.login.passPh": { "zh-CN": "请输入密码", en: "Enter password", bo: "གསང་ཨང་འཇུག" },
"admin.login.btn": { "zh-CN": "登录", en: "Sign in", bo: "ནང་འཛུལ" },
"admin.logout": { "zh-CN": "退出登录", en: "Sign out", bo: "ཕྱིར་འབུད" },
"admin.langSwitch": { "zh-CN": "语言切换", en: "Language", bo: "སྐད་ཡིག" },
"admin.tab.background": { "zh-CN": "背景更换", en: "Background", bo: "རྒྱབ་ལྗོངས" },
"admin.tab.welcome": { "zh-CN": "欢迎词", en: "Welcome", bo: "དགའ་བསུ" },
"admin.tab.guide": { "zh-CN": "操作指南", en: "Guide", bo: "སྤྱོད་སྟོན" },
"admin.tab.query": { "zh-CN": "信息查询", en: "Query", bo: "འཚོལ་ཞིབ" },
"admin.tab.knowledge": { "zh-CN": "天文信息配对", en: "Knowledge Editor", bo: "ཤེས་བྱ་སྒྲིག" },
"admin.tab.stream": { "zh-CN": "视频源配置", en: "Video Source", bo: "བརྙན་མཐུད" },
"admin.knowledge.add": { "zh-CN": "添加到知识库", en: "Add to library", bo: "ཤེས་བྱའི་མཛོད་ལ་སྣོན" },
"admin.knowledge.saveEdit": { "zh-CN": "保存修改", en: "Save changes", bo: "བཅོས་པ་ཉར" },
"admin.knowledge.uploading": { "zh-CN": "正在上传到服务器…", en: "Uploading…", bo: "སྐྱེལ་བཞིན" },
"admin.knowledge.uploadFail": { "zh-CN": "上传到服务器失败", en: "Upload failed", bo: "སྐྱེལ་མ་ཐུབ" },
"admin.knowledge.saveFailTitle": { "zh-CN": "知识库保存失败", en: "Knowledge save failed", bo: "ཤེས་བྱ་ཉར་མ་ཐུབ" },
"admin.toast.saved": { "zh-CN": "保存成功", en: "Saved", bo: "ཉར་ཚར" },
"admin.toast.updated": { "zh-CN": "更新成功", en: "Updated", bo: "བཅོས་ཚར" },
"admin.toast.loginOk": { "zh-CN": "登录成功", en: "Signed in", bo: "ནང་འཛུལ་ཚར" },
"admin.toast.loginBad": { "zh-CN": "账号或密码错误", en: "Invalid username or password", bo: "མིང/གསང་ཨང་ནོར" },
"admin.toast.logoutOk": { "zh-CN": "已退出登录", en: "Signed out", bo: "ཕྱིར་འབུད་ཚར" },
"admin.toast.editMode": { "zh-CN": "已进入编辑模式", en: "Edit mode", bo: "བཟོ་བཅོས་རྣམ་པ" },
"admin.toast.videoSaved": { "zh-CN": "视频源已保存", en: "Video source saved", bo: "བརྙན་མཐུད་ཉར་ཚར" },
"admin.toast.videoReset": { "zh-CN": "已恢复默认(未配置)", en: "Reset to default", bo: "སྔོན་སྒྲིག་ལ་ལོག" },
"admin.syncFail": { "zh-CN": "同步服务器失败,请检查网络、后端日志与 ADMIN_API_TOKEN", en: "Server sync failed", bo: "མཉམ་སྒྲིག་མ་ཐུབ" },
"admin.alert.saveBgFail": { "zh-CN": "保存失败:图片数据可能过大,请减少张数或压缩图片后再试。", en: "Save failed: images too large.", bo: "ཉར་མ་ཐུབ། དཔེ་རིས་ཆེ་དྲགས" },
"admin.alert.saveMediaFail": { "zh-CN": "媒体处理失败(详见下方提示)", en: "Media operation failed", bo: "ཉར་མ་ཐུབ།" },
"admin.bg.help": { "zh-CN": "上传的图片按格子顺序轮播(先添加的先播放)。第一张同时用作全部内页底图。点击缩略图可更换该张;单张建议压缩后上传,避免超出浏览器存储上限。修改后请点「保存背景」。", en: "Uploaded images rotate in order. The first image is used as inner-page background. Click a thumbnail to replace it. Compress images to avoid storage limits, then click “Save background”.", bo: "འདྲ་པར་སྣོན་པའི་གོ་རིམ་ལྟར་འཁོར་སྐྱོད། མཐའ་མའི་དཔེ་རིས་དང་པོས་ནང་ངོས་རྒྱབ་ལྗོངས་བྱེད།" },
"admin.bg.title": { "zh-CN": "首页轮播背景", en: "Home backgrounds", bo: "མདུན་ངོས་རྒྱབ་ལྗོངས" },
"admin.bg.upload": { "zh-CN": "上传图片", en: "Upload image", bo: "དཔེ་རིས་སྣོན" },
"admin.bg.save": { "zh-CN": "保存背景", en: "Save background", bo: "རྒྱབ་ལྗོངས་ཉར" },
"admin.bg.noPrefix": { "zh-CN": "第", en: "No.", bo: "ཨང་" },
"admin.bg.noSuffix": { "zh-CN": "张", en: "", bo: "" },
"admin.bg.deletePrefix": { "zh-CN": "删除第", en: "Delete No.", bo: "ཨང་" },
"admin.bg.deleteSuffix": { "zh-CN": "张背景", en: "background", bo: "རྒྱབ་ལྗོངས་སུབ" },
"admin.bg.srPick": { "zh-CN": "选择首页背景图片文件", en: "Choose background image file", bo: "རྒྱབ་ལྗོངས་དཔེ་རིས་འདེམས" },
"admin.resetDefault": { "zh-CN": "恢复默认", en: "Reset", bo: "སྔོན་སྒྲིག་ལ་ལོག" },
"admin.common.clickReplace": { "zh-CN": "点击更换", en: "Click to replace", bo: "བསྣུན་ནས་བརྗེ" },
"admin.welcome.help": { "zh-CN": "修改首页轮播欢迎语(中 / 英 / 藏)。", en: "Edit rotating welcome messages (ZH/EN/BO).", bo: "དགའ་བསུ་ཚིག་བཟོ་བཅོས" },
"admin.welcome.save": { "zh-CN": "保存欢迎词", en: "Save welcome", bo: "དགའ་བསུ་ཉར" },
"admin.guide.help": { "zh-CN": "触屏说明 / 软件说明每条一行。保存后访客端「操作指南」页立即生效。", en: "One instruction per line. Changes apply immediately to the Guide page.", bo: "ཚན་པ་རེ་རེ་ཐིག་རེ། ཉར་རྗེས་འཕྲལ་དུ་ལས་སྐྱོང" },
"admin.guide.save": { "zh-CN": "保存操作指南", en: "Save guide", bo: "སྤྱོད་སྟོན་ཉར" },
"admin.query.placeholder": { "zh-CN": "标题或正文", en: "Title or content", bo: "མཚན/ནང་དོན" },
"admin.query.all": { "zh-CN": "全部", en: "All", bo: "ཚང་མ" },
"admin.query.keyword": { "zh-CN": "关键词", en: "Keyword", bo: "ཚིག་གནད" },
"admin.query.type": { "zh-CN": "类型", en: "Type", bo: "རིགས" },
"admin.query.tag": { "zh-CN": "标签", en: "Tag", bo: "རྟགས" },
"admin.query.help": { "zh-CN": "对当前知识库条目做关键词、类型、标签筛选(与前台「天文知识库」同一数据源)。", en: "Filter library entries by keyword, type, and tag (same data source as the Library page).", bo: "ཤེས་བྱ་མཛོད་ཀྱི་ཚན་པ་ལ་ཚིག་གནད/རིགས/རྟགས་བཤེར" },
"admin.query.totalPrefix": { "zh-CN": "共", en: "Total", bo: "བསྡོམས" },
"admin.query.itemsSuffix": { "zh-CN": "条", en: "items", bo: "རྣམ་པ" },
"admin.query.filteredPrefix": { "zh-CN": "筛选结果", en: "Filtered", bo: "བཤེར་འབྲས" },
"admin.type.text": { "zh-CN": "文字", en: "Text", bo: "ཡིག" },
"admin.type.image": { "zh-CN": "图片", en: "Image", bo: "པར" },
"admin.type.video": { "zh-CN": "视频", en: "Video", bo: "བརྙན" },
"admin.knowledge.help": { "zh-CN": "向知识库添加文字、图片或视频类条目;保存后可在「天文知识库」前台检索。封面与视频会先以 multipart 上传到服务器磁盘(单文件最大约 2GB,适合多数十分钟以内视频),数据库仅存路径;服务器需相应调大 PHP 与 Nginx 上传限制。", en: "Cover and video upload to the server first (up to ~2GB each); only paths are stored in the database. Raise PHP/Nginx limits on the server.", bo: "ཤེས་བྱ་མཛོད་ལ་ཡིག/པར/བརྙན་སྣོན།" },
"admin.knowledge.resetSample": { "zh-CN": "重置为示例数据", en: "Reset to sample data", bo: "དཔེ་མཚོན་ལ་ལོག" },
"admin.knowledge.srCover": { "zh-CN": "选择知识库条目封面图", en: "Choose cover image", bo: "ཁ་བྱང་དཔེ་རིས་འདེམས" },
"admin.knowledge.srVideo": { "zh-CN": "选择知识库条目视频文件", en: "Choose video file", bo: "བརྙན་ཡིག་ཆ་འདེམས" },
"admin.knowledge.title": { "zh-CN": "标题", en: "Title", bo: "མཚན" },
"admin.knowledge.type": { "zh-CN": "类型", en: "Type", bo: "རིགས" },
"admin.knowledge.date": { "zh-CN": "日期", en: "Date", bo: "ཟླ་ཚེས" },
"admin.knowledge.content": { "zh-CN": "正文 / 摘要", en: "Content / Summary", bo: "ནང་དོན" },
"admin.knowledge.tags": { "zh-CN": "标签(逗号分隔)", en: "Tags (comma separated)", bo: "རྟགས" },
"admin.knowledge.tagsPh": { "zh-CN": "例如:星云, 观测", en: "e.g. Nebula, Observation", bo: "དཔེར་ན..." },
"admin.knowledge.coverOpt": { "zh-CN": "封面图(可选)", en: "Cover (optional)", bo: "ཁ་བྱང (གདམ་ཁ)" },
"admin.knowledge.videoOpt": { "zh-CN": "视频文件(可选)", en: "Video (optional)", bo: "བརྙན (གདམ་ཁ)" },
"admin.knowledge.clearCover": { "zh-CN": "清除封面图", en: "Clear cover", bo: "ཁ་བྱང་སུབ" },
"admin.knowledge.clearVideo": { "zh-CN": "清除视频", en: "Clear video", bo: "བརྙན་སུབ" },
"admin.stream.save": { "zh-CN": "保存视频源", en: "Save video source", bo: "བརྙན་མཐུད་ཉར" },
"common.edit": { "zh-CN": "编辑", en: "Edit", bo: "བཟོ་བཅོས" },
"common.delete": { "zh-CN": "删除", en: "Delete", bo: "སུབ" },
"common.save": { "zh-CN": "保存", en: "Save", bo: "ཉར" },
"common.cancel": { "zh-CN": "取消", en: "Cancel", bo: "ཕྱིར་འཐེན" },
};
function loadLang(): AppLang {
try {
const raw = localStorage.getItem(LS_LANG);
if (raw === "zh-CN" || raw === "en" || raw === "bo") return raw;
} catch {
// ignore
}
return "zh-CN";
}
function saveLang(lang: AppLang) {
try {
localStorage.setItem(LS_LANG, lang);
} catch {
// ignore
}
try {
window.dispatchEvent(new Event(KIOSK_EVENT));
} catch {
// ignore
}
}
type Ctx = {
lang: AppLang;
setLang: (lang: AppLang) => void;
t: (key: string) => string;
};
const I18nContext = createContext<Ctx | null>(null);
export function I18nProvider({ children }: { children: React.ReactNode }) {
const [rev, setRev] = useState(0);
const [lang, setLangState] = useState<AppLang>(() => loadLang());
useEffect(() => {
const h = () => setRev((r) => r + 1);
window.addEventListener(KIOSK_EVENT, h);
window.addEventListener("storage", h);
return () => {
window.removeEventListener(KIOSK_EVENT, h);
window.removeEventListener("storage", h);
};
}, []);
useEffect(() => {
const next = loadLang();
setLangState(next);
}, [rev]);
useEffect(() => {
document.documentElement.lang = lang;
}, [lang]);
const setLang = useCallback((next: AppLang) => {
saveLang(next);
setLangState(next);
}, []);
const t = useCallback(
(key: string) => {
const row = DICT[key];
if (!row) return key;
return row[lang] ?? row["zh-CN"] ?? key;
},
[lang],
);
const value = useMemo(() => ({ lang, setLang, t }), [lang, setLang, t]);
return <I18nContext.Provider value={value}>{children}</I18nContext.Provider>;
}
export function useI18n() {
const ctx = useContext(I18nContext);
if (!ctx) throw new Error("useI18n must be used within I18nProvider");
return ctx;
}