diff --git a/public/locales/ko/translation.json b/public/locales/ko/translation.json index fcfb6b4..7a6aa85 100644 --- a/public/locales/ko/translation.json +++ b/public/locales/ko/translation.json @@ -7,8 +7,8 @@ "chat-edit-cancel": "취소", "chat-edit-content": "내용", "chat-edit-delete": "삭제", - "chat-edit-insert-after": "", - "chat-edit-insert-before": "", + "chat-edit-insert-after": "이 메시지 뒤에 삽입", + "chat-edit-insert-before": "이 메시지 앞에 삽입", "chat-edit-is-breaking": "이 다음 메시지부터 스크린샷 나누기", "chat-edit-name-override": "이름 바꿔쓰기", "chat-edit-sender": "발신자", @@ -58,7 +58,7 @@ "topbar-show-help": "도움말", "topbar-show-info": "개요", "topbar-show-menu": "메뉴", - "yuzu-background-avatar": "", + "yuzu-background-avatar": "아바타 배경", "yuzu-kizuna-text": "{}의 인연 스토리로", "yuzu-kizuna-title": "인연 이벤트", "yuzu-reply-title": "답장하기", diff --git a/public/locales/zh-TW/translation.json b/public/locales/zh-TW/translation.json index 92e7a5f..63b76ea 100644 --- a/public/locales/zh-TW/translation.json +++ b/public/locales/zh-TW/translation.json @@ -1,67 +1,67 @@ { - "app-cfg-width": "", - "app-cfg-width-100vw": "", - "app-cfg-width-80vw": "", - "chat-edit-always-show-avatar": "", - "chat-edit-apply": "", - "chat-edit-cancel": "", - "chat-edit-content": "", - "chat-edit-delete": "", - "chat-edit-insert-after": "", - "chat-edit-insert-before": "", - "chat-edit-is-breaking": "", - "chat-edit-name-override": "", - "chat-edit-sender": "", - "chat-input-placeholder": "", - "clear-chat-confirm-cancel": "", - "clear-chat-confirm-text": "", - "clear-chat-confirm-title": "", - "clear-chat-confirm-yes": "", - "common-cfg-width": "", - "custom-char-create": "", - "custom-char-name": "", - "custom-char-title": "", - "data-version": "", - "item-type": "", - "item-type-choices": "", - "item-type-image": "", - "item-type-narration": "", - "item-type-relationship-story": "", - "item-type-section-title": "", - "item-type-selection": "", - "item-type-text": "", - "item-type-thoughts": "", - "latest-update": "", - "previous-updates": "", - "remove-custom-char-confirm-cancel": "", - "remove-custom-char-confirm-text": "", - "remove-custom-char-confirm-title": "", - "remove-custom-char-confirm-yes": "", - "save-code-dialog-cancel": "", - "save-code-dialog-no-save-custom-chars": "", - "save-code-dialog-save": "", - "save-code-dialog-save-active-chars": "", - "save-code-dialog-save-active-custom-chars": "", - "save-code-dialog-save-all-custom-chars": "", - "save-code-dialog-save-chat": "", - "save-code-dialog-save-used-custom-chars": "", - "save-code-dialog-title": "", - "search-label": "", - "source-enabled": "", - "topbar-clear-chat": "", - "topbar-config": "", - "topbar-feedback": "", - "topbar-load-code": "", - "topbar-save-code": "", - "topbar-save-image": "", - "topbar-show-charlist": "", - "topbar-show-help": "", - "topbar-show-info": "", - "topbar-show-menu": "", - "yuzu-background-avatar": "", + "app-cfg-width": "頁面寬度", + "app-cfg-width-100vw": "100% 視窗", + "app-cfg-width-80vw": "80% 視窗", + "chat-edit-always-show-avatar": "顯示頭像", + "chat-edit-apply": "確認", + "chat-edit-cancel": "取消", + "chat-edit-content": "內容", + "chat-edit-delete": "刪除", + "chat-edit-insert-after": "在此訊息之後插入", + "chat-edit-insert-before": "在此訊息之前插入", + "chat-edit-is-breaking": "截圖時在這裡分割圖片", + "chat-edit-name-override": "臨時改名", + "chat-edit-sender": "發送自", + "chat-input-placeholder": "聊天內容", + "clear-chat-confirm-cancel": "取消", + "clear-chat-confirm-text": "未儲存的聊天內容將無法找回!", + "clear-chat-confirm-title": "清空全部聊天?", + "clear-chat-confirm-yes": "清空", + "common-cfg-width": "聊天框寬度", + "custom-char-create": "上傳頭像", + "custom-char-name": "名字", + "custom-char-title": "建立自訂角色", + "data-version": "資料版本", + "item-type": "類型", + "item-type-choices": "選項", + "item-type-image": "圖片", + "item-type-narration": "旁白", + "item-type-relationship-story": "羈絆事件", + "item-type-section-title": "小標題", + "item-type-selection": "選擇", + "item-type-text": "文字", + "item-type-thoughts": "內心獨白", + "latest-update": "最近更新", + "previous-updates": "更新歷史", + "remove-custom-char-confirm-cancel": "取消", + "remove-custom-char-confirm-text": "如果刪除了角色,TA 的對話會在刷新後變成玩家說的話", + "remove-custom-char-confirm-title": "要刪除這個角色嗎?", + "remove-custom-char-confirm-yes": "刪除", + "save-code-dialog-cancel": "取消", + "save-code-dialog-no-save-custom-chars": "不儲存自訂角色", + "save-code-dialog-save": "下載", + "save-code-dialog-save-active-chars": "儲存右下角角色清單", + "save-code-dialog-save-active-custom-chars": "儲存右下角出現的自訂角色", + "save-code-dialog-save-all-custom-chars": "儲存所有自訂角色", + "save-code-dialog-save-chat": "儲存對話", + "save-code-dialog-save-used-custom-chars": "儲存對話中出現的自訂角色", + "save-code-dialog-title": "下載對話資料 JSON 檔案", + "search-label": "搜尋", + "source-enabled": "包含這些角色", + "topbar-clear-chat": "清空聊天", + "topbar-config": "渲染設定", + "topbar-feedback": "回饋", + "topbar-load-code": "匯入代碼", + "topbar-save-code": "儲存代碼", + "topbar-save-image": "儲存圖片", + "topbar-show-charlist": "角色", + "topbar-show-help": "幫助", + "topbar-show-info": "關於", + "topbar-show-menu": "選單", + "yuzu-background-avatar": "頭像背景", "yuzu-kizuna-text": "前往{}的羈絆劇情。", "yuzu-kizuna-title": "羈絆活動", "yuzu-reply-title": "回覆", - "yuzu-sensei": "", - "yuzu-theme": "" + "yuzu-sensei": "老師", + "yuzu-theme": "配色" } diff --git a/src/App.tsx b/src/App.tsx index f6cfd1e..58f4218 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,6 +2,7 @@ import { Box } from "@mui/system"; import { useEffect, useState } from "react"; import { useMediaQuery } from "react-responsive"; import "./App.css"; +import { getInitialLanguage } from "./i18n"; import AppConfig, { loadAppConfig, saveAppConfig } from "./model/AppConfig"; import { AppContext } from "./model/AppContext"; import AppData from "./model/AppData"; @@ -22,7 +23,7 @@ import TopBar from "./view/TopBar"; function App() { const [loaded, setLoaded] = useState(false); const [renderer, setRenderer] = useState((localStorage.getItem("renderer") || Renderers[1]) as RendererType); - const [lang, setLang] = useState(localStorage.getItem("lang") || "zh-cn"); + const [lang, setLang] = useState(() => getInitialLanguage()); const [activeChars, setActiveChars] = useState([]); const [characters, setCharacters] = useState(new Map()); const [stamps, setStamps] = useState([]); diff --git a/src/i18n.ts b/src/i18n.ts index c799d5e..bb4c7a8 100644 --- a/src/i18n.ts +++ b/src/i18n.ts @@ -2,11 +2,52 @@ import i18n from "i18next"; import { initReactI18next } from "react-i18next"; import Backend from "i18next-http-backend"; +const supportedLanguages = new Set(["en", "ja", "ko", "zh-cn", "zh-tw"]); + +const normalizeLanguage = (lang: string) => { + const lower = lang.toLowerCase(); + if (lower.startsWith("zh")) { + if (lower.includes("tw") || lower.includes("hant") || lower.includes("hk") || lower.includes("mo")) { + return "zh-tw"; + } + return "zh-cn"; + } + if (lower.startsWith("en")) { + return "en"; + } + if (lower.startsWith("ja")) { + return "ja"; + } + if (lower.startsWith("ko")) { + return "ko"; + } + return null; +}; + +const getBrowserLanguage = () => { + if (typeof navigator === "undefined") { + return null; + } + const candidates = [...(navigator.languages || []), navigator.language].filter(Boolean); + + return candidates.map(normalizeLanguage).find(lang => lang && supportedLanguages.has(lang)) ?? null; +}; + +export const getInitialLanguage = () => { + const stored = localStorage.getItem("lang"); + if (stored) { + return stored; + } + const initial = getBrowserLanguage() || "zh-cn"; + localStorage.setItem("lang", initial); + return initial; +}; + i18n .use(Backend) .use(initReactI18next) .init({ - lng: localStorage.getItem("lang") || "zh-cn", + lng: getInitialLanguage(), fallbackLng: "en", interpolation: { escapeValue: false,