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>; 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(null); export function I18nProvider({ children }: { children: React.ReactNode }) { const [rev, setRev] = useState(0); const [lang, setLangState] = useState(() => 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 {children}; } export function useI18n() { const ctx = useContext(I18nContext); if (!ctx) throw new Error("useI18n must be used within I18nProvider"); return ctx; }