diff --git a/README_i18n.md b/README_i18n.md index eef75a77..662892f9 100644 --- a/README_i18n.md +++ b/README_i18n.md @@ -4,7 +4,7 @@ 1. **安装依赖**:添加了 `react-i18next` 和 `i18next` 包 2. **配置国际化**:在 `src/i18n/` 目录下创建了配置文件 -3. **翻译文件**:创建了英文和中文翻译文件 +3. **翻译文件**:创建了英文、中文、日文翻译文件 4. **组件更新**:替换了主要组件中的硬编码文案 5. **语言切换器**:添加了语言切换按钮 @@ -16,6 +16,7 @@ src/ │ ├── index.ts # 国际化配置文件 │ └── locales/ │ ├── en.json # 英文翻译 +│ ├── ja.json # 日文翻译 │ └── zh.json # 中文翻译 ├── components/ │ └── LanguageSwitcher.tsx # 语言切换组件 @@ -24,7 +25,7 @@ src/ ## 默认语言设置 -- **默认语言**:英文 (en) +- **默认语言**:中文 (zh)(无首选时根据浏览器/系统语言选择 zh/en/ja) - **回退语言**:英文 (en) ## 使用方式 @@ -57,7 +58,7 @@ src/ ## 测试功能 -应用已添加了语言切换按钮(地球图标),点击可以在中英文之间切换,验证国际化功能是否正常工作。 +应用已添加了语言切换按钮,支持中文、英文、日文三种语言切换,验证国际化功能是否正常工作。 ## 已更新的组件 diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index cd8cbc9f..fbfc96f9 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -69,6 +69,11 @@ impl TrayTexts { no_provider_hint: " (No providers yet, please add them from the main window)", quit: "Quit", }, + "ja" => Self { + show_main: "メインウィンドウを開く", + no_provider_hint: " (プロバイダーがまだありません。メイン画面から追加してください)", + quit: "終了", + }, _ => Self { show_main: "打开主界面", no_provider_hint: " (无供应商,请在主界面添加)", diff --git a/src-tauri/src/settings.rs b/src-tauri/src/settings.rs index b078d83a..bd239b5e 100644 --- a/src-tauri/src/settings.rs +++ b/src-tauri/src/settings.rs @@ -118,7 +118,7 @@ impl AppSettings { .language .as_ref() .map(|s| s.trim()) - .filter(|s| matches!(*s, "en" | "zh")) + .filter(|s| matches!(*s, "en" | "zh" | "ja")) .map(|s| s.to_string()); } diff --git a/src/components/settings/LanguageSettings.tsx b/src/components/settings/LanguageSettings.tsx index dd86400b..8ef2cb13 100644 --- a/src/components/settings/LanguageSettings.tsx +++ b/src/components/settings/LanguageSettings.tsx @@ -2,9 +2,11 @@ import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; import { useTranslation } from "react-i18next"; +type LanguageOption = "zh" | "en" | "ja"; + interface LanguageSettingsProps { - value: "zh" | "en"; - onChange: (value: "zh" | "en") => void; + value: LanguageOption; + onChange: (value: LanguageOption) => void; } export function LanguageSettings({ value, onChange }: LanguageSettingsProps) { @@ -25,6 +27,9 @@ export function LanguageSettings({ value, onChange }: LanguageSettingsProps) { onChange("en")}> {t("settings.languageOptionEnglish")} + onChange("ja")}> + {t("settings.languageOptionJapanese")} + ); diff --git a/src/hooks/useSettings.ts b/src/hooks/useSettings.ts index 280d5aec..d6dd7458 100644 --- a/src/hooks/useSettings.ts +++ b/src/hooks/useSettings.ts @@ -12,7 +12,7 @@ import { } from "./useDirectorySettings"; import { useSettingsMetadata } from "./useSettingsMetadata"; -type Language = "zh" | "en"; +type Language = "zh" | "en" | "ja"; interface SaveResult { requiresRestart: boolean; diff --git a/src/hooks/useSettingsForm.ts b/src/hooks/useSettingsForm.ts index 742e735d..c823992b 100644 --- a/src/hooks/useSettingsForm.ts +++ b/src/hooks/useSettingsForm.ts @@ -3,7 +3,7 @@ import { useTranslation } from "react-i18next"; import { useSettingsQuery } from "@/lib/query"; import type { Settings } from "@/types"; -type Language = "zh" | "en"; +type Language = "zh" | "en" | "ja"; export type SettingsFormState = Omit & { language: Language; @@ -11,7 +11,8 @@ export type SettingsFormState = Omit & { const normalizeLanguage = (lang?: string | null): Language => { if (!lang) return "zh"; - return lang === "en" ? "en" : "zh"; + const normalized = lang.toLowerCase(); + return normalized === "en" || normalized === "ja" ? normalized : "zh"; }; const sanitizeDir = (value?: string | null): string | undefined => { @@ -51,8 +52,8 @@ export function useSettingsForm(): UseSettingsFormResult { const readPersistedLanguage = useCallback((): Language => { if (typeof window !== "undefined") { const stored = window.localStorage.getItem("language"); - if (stored === "en" || stored === "zh") { - return stored; + if (stored === "en" || stored === "zh" || stored === "ja") { + return stored as Language; } } return normalizeLanguage(i18n.language); diff --git a/src/i18n/index.ts b/src/i18n/index.ts index 5002f0c3..81c55275 100644 --- a/src/i18n/index.ts +++ b/src/i18n/index.ts @@ -2,15 +2,18 @@ import i18n from "i18next"; import { initReactI18next } from "react-i18next"; import en from "./locales/en.json"; +import ja from "./locales/ja.json"; import zh from "./locales/zh.json"; -const DEFAULT_LANGUAGE: "zh" | "en" = "zh"; +type Language = "zh" | "en" | "ja"; -const getInitialLanguage = (): "zh" | "en" => { +const DEFAULT_LANGUAGE: Language = "zh"; + +const getInitialLanguage = (): Language => { if (typeof window !== "undefined") { try { const stored = window.localStorage.getItem("language"); - if (stored === "zh" || stored === "en") { + if (stored === "zh" || stored === "en" || stored === "ja") { return stored; } } catch (error) { @@ -28,6 +31,10 @@ const getInitialLanguage = (): "zh" | "en" => { return "zh"; } + if (navigatorLang?.startsWith("ja")) { + return "ja"; + } + if (navigatorLang?.startsWith("en")) { return "en"; } @@ -39,6 +46,9 @@ const resources = { en: { translation: en, }, + ja: { + translation: ja, + }, zh: { translation: zh, }, diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 0e9073ac..92d6b678 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -165,6 +165,7 @@ "autoReload": "Data will refresh automatically in 2 seconds...", "languageOptionChinese": "中文", "languageOptionEnglish": "English", + "languageOptionJapanese": "日本語", "windowBehavior": "Window Behavior", "windowBehaviorHint": "Configure window minimize and Claude plugin integration policies.", "launchOnStartup": "Launch on Startup", diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json new file mode 100644 index 00000000..ed099ff5 --- /dev/null +++ b/src/i18n/locales/ja.json @@ -0,0 +1,837 @@ +{ + "app": { + "title": "CC Switch", + "description": "Claude Code・Codex・Gemini CLI のためのオールインワンアシスタント" + }, + "common": { + "add": "追加", + "edit": "編集", + "delete": "削除", + "save": "保存", + "saving": "保存中...", + "cancel": "キャンセル", + "confirm": "確認", + "close": "閉じる", + "done": "完了", + "settings": "設定", + "about": "バージョン情報", + "version": "バージョン", + "loading": "読み込み中...", + "success": "成功", + "error": "エラー", + "unknown": "不明", + "enterValidValue": "有効な値を入力してください", + "clear": "クリア", + "toggleTheme": "テーマを切り替え", + "format": "フォーマット", + "formatSuccess": "整形しました", + "formatError": "整形に失敗しました: {{error}}", + "copy": "コピー", + "view": "表示", + "back": "戻る" + }, + "apiKeyInput": { + "placeholder": "API Key を入力", + "show": "API Key を表示", + "hide": "API Key を隠す" + }, + "jsonEditor": { + "mustBeObject": "設定はオブジェクト形式の JSON で入力してください(配列や他の型は不可)", + "invalidJson": "JSON 形式が正しくありません" + }, + "claudeConfig": { + "configLabel": "Claude Code settings.json (JSON) *", + "writeCommonConfig": "共通設定を書き込む", + "editCommonConfig": "共通設定を編集", + "editCommonConfigTitle": "共通設定スニペットを編集", + "commonConfigHint": "「共通設定を書き込む」がオンのとき settings.json にマージされます", + "fullSettingsHint": "Claude Code の settings.json 全文" + }, + "header": { + "viewOnGithub": "GitHub で見る", + "toggleDarkMode": "ダークモードに切り替え", + "toggleLightMode": "ライトモードに切り替え", + "addProvider": "プロバイダーを追加", + "switchToChinese": "中国語に切り替え", + "switchToEnglish": "英語に切り替え", + "enterEditMode": "編集モードに入る", + "exitEditMode": "編集モードを終了" + }, + "provider": { + "noProviders": "まだプロバイダーがありません", + "noProvidersDescription": "右上の「プロバイダーを追加」を押して最初の API プロバイダーを登録してください", + "currentlyUsing": "現在使用中", + "enable": "有効化", + "inUse": "使用中", + "editProvider": "プロバイダーを編集", + "editProviderHint": "保存すると現在のプロバイダーにすぐ反映されます。", + "deleteProvider": "プロバイダーを削除", + "addNewProvider": "新しいプロバイダーを追加", + "addClaudeProvider": "Claude Code プロバイダーを追加", + "addCodexProvider": "Codex プロバイダーを追加", + "addGeminiProvider": "Gemini プロバイダーを追加", + "addProviderHint": "一覧にすばやく切り替えられるよう、ここに情報を入力してください。", + "editClaudeProvider": "Claude Code プロバイダーを編集", + "editCodexProvider": "Codex プロバイダーを編集", + "configError": "設定エラー", + "notConfigured": "公式サイト用に未設定", + "applyToClaudePlugin": "Claude プラグインに適用", + "removeFromClaudePlugin": "Claude プラグインから解除", + "dragToReorder": "ドラッグで並べ替え", + "dragHandle": "ドラッグで並べ替え", + "duplicate": "複製", + "sortUpdateFailed": "並び順の更新に失敗しました", + "configureUsage": "利用状況を設定", + "name": "プロバイダー名", + "namePlaceholder": "例: Claude Official", + "websiteUrl": "Web サイト URL", + "notes": "メモ", + "notesPlaceholder": "例: 会社用アカウント", + "configJson": "Config JSON", + "writeCommonConfig": "共通設定を書き込む", + "editCommonConfigButton": "共通設定を編集", + "configJsonHint": "Claude Code の設定をすべて入力してください", + "editCommonConfigTitle": "共通設定スニペットを編集", + "editCommonConfigHint": "共通設定スニペットは、この機能をオンにしたすべてのプロバイダーへマージされます", + "addProvider": "プロバイダーを追加", + "sortUpdated": "並び順を更新しました", + "usageSaved": "利用状況の設定を保存しました", + "usageSaveFailed": "利用状況設定の保存に失敗しました", + "geminiConfig": "Gemini 設定", + "geminiConfigHint": ".env 形式で Gemini を設定してください", + "form": { + "gemini": { + "model": "モデル", + "oauthTitle": "OAuth 認証モード", + "oauthHint": "Google 公式は OAuth 個人認証を使用するため API Key は不要です。初回利用時にブラウザが開きます。", + "apiKeyPlaceholder": "Gemini API Key を入力" + } + } + }, + "notifications": { + "providerAdded": "プロバイダーを追加しました", + "providerSaved": "プロバイダー設定を保存しました", + "providerDeleted": "プロバイダーを削除しました", + "switchSuccess": "切り替え成功! {{appName}} ターミナルを再起動すると反映されます", + "switchFailedTitle": "切り替えに失敗しました", + "switchFailed": "切り替えに失敗しました: {{error}}", + "autoImported": "既存設定からデフォルトプロバイダーを自動作成しました", + "addFailed": "プロバイダーの追加に失敗しました: {{error}}", + "saveFailed": "保存に失敗しました: {{error}}", + "saveFailedGeneric": "保存に失敗しました。もう一度お試しください", + "appliedToClaudePlugin": "Claude プラグインに適用しました", + "removedFromClaudePlugin": "Claude プラグインから削除しました", + "syncClaudePluginFailed": "Claude プラグインとの同期に失敗しました", + "updateSuccess": "プロバイダーを更新しました", + "updateFailed": "プロバイダーの更新に失敗しました: {{error}}", + "deleteSuccess": "プロバイダーを削除しました", + "deleteFailed": "プロバイダーの削除に失敗しました: {{error}}", + "settingsSaved": "設定を保存しました", + "settingsSaveFailed": "設定の保存に失敗しました: {{error}}" + }, + "confirm": { + "deleteProvider": "プロバイダーを削除", + "deleteProviderMessage": "プロバイダー「{{name}}」を削除してもよろしいですか?この操作は元に戻せません。" + }, + "settings": { + "title": "設定", + "general": "一般", + "tabGeneral": "一般", + "tabAdvanced": "詳細", + "language": "言語", + "languageHint": "切り替えるとすぐにプレビューされ、保存後に永続化されます。", + "theme": "テーマ", + "themeHint": "アプリのテーマを選択します。すぐに反映されます。", + "themeLight": "ライト", + "themeDark": "ダーク", + "themeSystem": "システム", + "importExport": "SQL インポート/エクスポート", + "importExportHint": "移行や復元用にデータベースの SQL バックアップをインポート/エクスポートします。", + "exportConfig": "SQL バックアップをエクスポート", + "selectConfigFile": "SQL ファイルを選択", + "noFileSelected": "ファイルが選択されていません。", + "import": "インポート", + "importing": "インポート中...", + "importSuccess": "インポート成功!", + "importFailed": "インポート失敗", + "syncLiveFailed": "インポートしましたが、現在のプロバイダーへの同期に失敗しました。手動で再選択してください。", + "importPartialSuccess": "設定はインポートされましたが、現在のプロバイダーへの同期に失敗しました。", + "importPartialHint": "ライブ設定を更新するため、もう一度プロバイダーを選択してください。", + "configExported": "設定をエクスポートしました:", + "exportFailed": "エクスポートに失敗しました", + "selectFileFailed": "有効な SQL バックアップファイルを選択してください", + "configCorrupted": "SQL ファイルが壊れているか形式が無効な可能性があります", + "backupId": "バックアップ ID", + "autoReload": "2 秒後に自動で再読み込みします...", + "languageOptionChinese": "中文", + "languageOptionEnglish": "English", + "languageOptionJapanese": "日本語", + "windowBehavior": "ウィンドウ動作", + "windowBehaviorHint": "最小化動作や Claude プラグイン連携を設定します。", + "launchOnStartup": "起動時に自動実行", + "launchOnStartupDescription": "システム起動時に CC Switch を自動起動します", + "autoLaunchFailed": "自動起動の設定に失敗しました", + "minimizeToTray": "閉じるときトレイへ最小化", + "minimizeToTrayDescription": "チェックすると閉じるボタンでトレイに隠し、オフならアプリを終了します。", + "enableClaudePluginIntegration": "Claude Code 拡張に適用", + "enableClaudePluginIntegrationDescription": "オンにすると VS Code の Claude Code 拡張のプロバイダーも同期します", + "configDirectoryOverride": "設定ディレクトリの上書き(詳細)", + "configDirectoryDescription": "WSL などで Claude Code や Codex を使う場合、ここで設定ディレクトリを WSL 側に合わせるとデータを揃えられます。", + "appConfigDir": "CC Switch 設定ディレクトリ", + "appConfigDirDescription": "CC Switch の保存場所をカスタマイズします(クラウド同期フォルダを指定すると設定を同期できます)", + "browsePlaceholderApp": "例: C:\\\\Users\\\\Administrator\\\\.cc-switch", + "claudeConfigDir": "Claude Code 設定ディレクトリ", + "claudeConfigDirDescription": "Claude の設定ディレクトリ(settings.json)を上書きし、claude.json(MCP)も同じ場所に置きます。", + "codexConfigDir": "Codex 設定ディレクトリ", + "codexConfigDirDescription": "Codex の設定ディレクトリを上書きします。", + "geminiConfigDir": "Gemini 設定ディレクトリ", + "geminiConfigDirDescription": "Gemini の設定ディレクトリ(.env)を上書きします。", + "browsePlaceholderClaude": "例: /home//.claude", + "browsePlaceholderCodex": "例: /home//.codex", + "browsePlaceholderGemini": "例: /home//.gemini", + "browseDirectory": "ディレクトリを選択", + "resetDefault": "デフォルトに戻す(保存後に反映)", + "checkForUpdates": "アップデートを確認", + "updateTo": "v{{version}} に更新", + "updating": "更新中...", + "checking": "確認中...", + "upToDate": "最新バージョンです", + "aboutHint": "バージョン情報と更新状況を表示します。", + "portableMode": "ポータブルモード: 更新は手動ダウンロードが必要です。", + "updateAvailable": "新しいバージョンがあります: {{version}}", + "updateFailed": "更新のインストールに失敗しました。ダウンロードページを開こうとしました。", + "checkUpdateFailed": "更新の確認に失敗しました。時間をおいて再試行してください。", + "openReleaseNotesFailed": "リリースノートの表示に失敗しました", + "releaseNotes": "リリースノート", + "viewReleaseNotes": "このバージョンのリリースノートを見る", + "viewCurrentReleaseNotes": "現在のバージョンのリリースノートを見る", + "importFailedError": "設定のインポートに失敗しました: {{message}}", + "exportFailedError": "設定のエクスポートに失敗しました:", + "restartRequired": "再起動が必要です", + "restartRequiredMessage": "CC Switch の設定ディレクトリを変更すると再起動が必要です。今すぐ再起動しますか?", + "restartNow": "今すぐ再起動", + "restartLater": "後で再起動", + "restartFailed": "アプリの再起動に失敗しました。手動で閉じて再度開いてください。", + "devModeRestartHint": "開発モードでは自動再起動をサポートしていません。手動で再起動してください。", + "saving": "保存中..." + }, + "apps": { + "claude": "Claude Code", + "codex": "Codex", + "gemini": "Gemini" + }, + "console": { + "providerSwitchReceived": "プロバイダー切り替えイベントを受信:", + "setupListenerFailed": "プロバイダー切り替えリスナーの設定に失敗:", + "updateProviderFailed": "プロバイダー更新に失敗:", + "autoImportFailed": "デフォルト設定の自動インポートに失敗:", + "openLinkFailed": "リンクを開けませんでした:", + "getVersionFailed": "バージョン情報の取得に失敗:", + "loadSettingsFailed": "設定の読み込みに失敗:", + "getConfigPathFailed": "設定パスの取得に失敗:", + "getConfigDirFailed": "設定ディレクトリの取得に失敗:", + "detectPortableFailed": "ポータブルモードの検出に失敗:", + "saveSettingsFailed": "設定の保存に失敗:", + "updateFailed": "更新に失敗:", + "checkUpdateFailed": "更新確認に失敗:", + "openConfigFolderFailed": "設定フォルダを開けませんでした:", + "selectConfigDirFailed": "設定ディレクトリの選択に失敗:", + "getDefaultConfigDirFailed": "デフォルト設定ディレクトリの取得に失敗:", + "openReleaseNotesFailed": "リリースノートを開けませんでした:" + }, + "providerForm": { + "supplierName": "プロバイダー名", + "supplierNameRequired": "プロバイダー名 *", + "supplierNamePlaceholder": "例: Anthropic Official", + "websiteUrl": "Web サイト URL", + "websiteUrlPlaceholder": "https://example.com(任意)", + "apiEndpoint": "API エンドポイント", + "apiEndpointPlaceholder": "https://your-api-endpoint.com", + "codexApiEndpointPlaceholder": "https://your-api-endpoint.com/v1", + "manageAndTest": "管理・テスト", + "configContent": "設定内容", + "officialNoApiKey": "公式ログインは API Key 不要です。そのまま保存できます", + "codexOfficialNoApiKey": "公式は API Key 不要です。そのまま保存してください", + "codexApiKeyAutoFill": "ここに入力すれば auth.json も自動で埋まります", + "apiKeyAutoFill": "ここに入力すれば下の設定も自動で埋まります", + "cnOfficialApiKeyHint": "💡 API Key のみ入力すれば OK。エンドポイントはプリセット済みです", + "aggregatorApiKeyHint": "💡 API Key のみ入力すれば OK。エンドポイントはプリセット済みです", + "thirdPartyApiKeyHint": "💡 API Key のみ入力すれば OK。エンドポイントはプリセット済みです", + "customApiKeyHint": "💡 カスタム設定では必要な項目をすべて手動で入力してください", + "officialHint": "💡 公式プロバイダーはブラウザログインで、API Key は不要です", + "getApiKey": "API Key を取得", + "partnerPromotion": { + "zhipu": "Zhipu GLM は CC Switch の公式パートナーです。リンク経由でチャージすると 10% 割引", + "packycode": "PackyCode は CC Switch の公式パートナーです。登録後チャージ時に \"cc-switch\" を入力すると 10% オフ", + "minimax": "MiniMax Coding Plan Black Friday、Starter が月額 $2(80% OFF)" + }, + "parameterConfig": "パラメーター設定 - {{name}} *", + "mainModel": "メインモデル(任意)", + "mainModelPlaceholder": "例: GLM-4.6", + "fastModel": "高速モデル(任意)", + "fastModelPlaceholder": "例: GLM-4.5-Air", + "modelHint": "💡 空欄ならプロバイダーのデフォルトモデルを使用します", + "apiHint": "💡 Claude API 互換サービスのエンドポイントを入力してください", + "codexApiHint": "💡 OpenAI Response 互換のサービスエンドポイントを入力してください", + "fillSupplierName": "プロバイダー名を入力してください", + "fillConfigContent": "設定内容を入力してください", + "fillParameter": "{{label}} を入力してください", + "fillTemplateValue": "{{label}} を入力してください", + "endpointRequired": "公式以外は API エンドポイントが必須です", + "apiKeyRequired": "公式以外は API Key が必須です", + "configJsonError": "Config JSON の形式が正しくありません。構文を確認してください", + "authJsonRequired": "auth.json は JSON オブジェクトで入力してください", + "authJsonError": "auth.json の形式が正しくありません。JSON を確認してください", + "fillAuthJson": "auth.json の設定を入力してください", + "fillApiKey": "OPENAI_API_KEY を入力してください", + "visitWebsite": "{{url}} を開く", + "anthropicModel": "メインモデル", + "anthropicSmallFastModel": "高速モデル", + "anthropicDefaultHaikuModel": "既定 Haiku モデル", + "anthropicDefaultSonnetModel": "既定 Sonnet モデル", + "anthropicDefaultOpusModel": "既定 Opus モデル", + "modelPlaceholder": "", + "smallModelPlaceholder": "", + "haikuModelPlaceholder": "", + "modelHelper": "任意: 既定で使いたい Claude モデルを指定。空欄ならシステム既定を使用します。", + "categoryOfficial": "公式", + "categoryCnOfficial": "オープンソース公式", + "categoryAggregation": "アグリゲーター", + "categoryThirdParty": "サードパーティ" + }, + "endpointTest": { + "title": "API エンドポイント管理", + "endpoints": "エンドポイント", + "autoSelect": "自動選択", + "testSpeed": "テスト", + "testing": "テスト中", + "addEndpointPlaceholder": "https://api.example.com", + "done": "完了", + "noEndpoints": "エンドポイントがありません", + "failed": "失敗", + "enterValidUrl": "有効な URL を入力してください", + "invalidUrlFormat": "URL 形式が正しくありません", + "onlyHttps": "HTTP/HTTPS のみサポートします", + "urlExists": "この URL はすでに存在します", + "saveFailed": "保存に失敗しました。もう一度お試しください", + "loadEndpointsFailed": "カスタムエンドポイントの読み込みに失敗:", + "addEndpointFailed": "カスタムエンドポイントの追加に失敗:", + "removeEndpointFailed": "カスタムエンドポイントの削除に失敗:", + "removeFailed": "削除に失敗しました: {{error}}", + "updateLastUsedFailed": "エンドポイントの最終使用時間の更新に失敗しました", + "pleaseAddEndpoint": "まずエンドポイントを追加してください", + "testUnavailable": "速度テストを実行できません", + "noResult": "結果がありません", + "testFailed": "速度テストに失敗しました: {{error}}", + "status": "ステータス: {{code}}" + }, + "codexConfig": { + "authJson": "auth.json (JSON) *", + "authJsonPlaceholder": "{\n \"OPENAI_API_KEY\": \"sk-your-api-key-here\"\n}", + "authJsonHint": "Codex の auth.json 設定内容", + "configToml": "config.toml (TOML)", + "configTomlHint": "Codex の config.toml 設定内容", + "writeCommonConfig": "共通設定を書き込む", + "editCommonConfig": "共通設定を編集", + "editCommonConfigTitle": "Codex 共通設定スニペットを編集", + "commonConfigHint": "「共通設定を書き込む」がオンの場合、config.toml の末尾に追記されます", + "apiUrlLabel": "API リクエスト URL" + }, + "geminiConfig": { + "envFile": "環境変数 (.env)", + "envFileHint": ".env 形式で Gemini の環境変数を設定", + "configJson": "設定ファイル (config.json)", + "configJsonHint": "Gemini 拡張パラメーターを JSON 形式で設定(任意)", + "writeCommonConfig": "共通設定を書き込む", + "editCommonConfig": "共通設定を編集", + "editCommonConfigTitle": "Gemini 共通設定スニペットを編集", + "commonConfigHint": "共通設定スニペットは、この機能をオンにしたすべての Gemini プロバイダーへマージされます" + }, + "providerPreset": { + "label": "プロバイダータイプ", + "custom": "カスタム設定", + "other": "その他", + "hint": "プリセットを選んだ後でも、下のフィールドで調整できます。" + }, + "usage": { + "queryFailed": "照会に失敗しました", + "refreshUsage": "利用状況を更新", + "planUsage": "プラン利用状況", + "invalid": "期限切れ", + "total": "合計:", + "used": "使用:", + "remaining": "残り:", + "justNow": "たった今", + "minutesAgo": "{{count}} 分前", + "hoursAgo": "{{count}} 時間前", + "daysAgo": "{{count}} 日前" + }, + "usageScript": { + "title": "利用状況を設定", + "enableUsageQuery": "利用状況照会を有効にする", + "presetTemplate": "プリセットテンプレート", + "requestUrl": "リクエスト URL", + "requestUrlPlaceholder": "例: https://api.example.com", + "method": "HTTP メソッド", + "templateCustom": "カスタム", + "templateGeneral": "General", + "templateNewAPI": "NewAPI", + "credentialsConfig": "認証情報", + "baseUrl": "Base URL", + "accessToken": "Access Token", + "accessTokenPlaceholder": "「Security Settings」で生成", + "userId": "ユーザー ID", + "userIdPlaceholder": "例: 114514", + "defaultPlan": "デフォルトプラン", + "queryFailedMessage": "照会に失敗しました", + "queryScript": "照会スクリプト (JavaScript)", + "timeoutSeconds": "タイムアウト(秒)", + "headers": "ヘッダー", + "body": "ボディ", + "timeoutHint": "範囲: 2〜30 秒", + "timeoutMustBeInteger": "タイムアウトは整数で入力してください(小数は切り捨て)", + "timeoutCannotBeNegative": "タイムアウトは負の値にできません", + "autoIntervalMinutes": "自動照会間隔(分、0 で無効)", + "autoQueryInterval": "自動照会間隔(分)", + "autoQueryIntervalHint": "0 で無効。推奨 5〜60 分", + "intervalMustBeInteger": "間隔は整数で入力してください(小数は切り捨て)", + "intervalCannotBeNegative": "間隔は負の値にできません", + "intervalAdjusted": "間隔を {{value}} 分に調整しました", + "scriptHelp": "スクリプトの書き方:", + "configFormat": "設定の形式:", + "commentOptional": "任意", + "commentResponseIsJson": "response は API から返る JSON データです", + "extractorFormat": "抽出関数の返却形式(すべて任意):", + "tips": "💡 ヒント:", + "testing": "テスト中...", + "testScript": "スクリプトをテスト", + "format": "整形", + "saveConfig": "設定を保存", + "scriptEmpty": "スクリプト設定は空にできません", + "mustHaveReturn": "スクリプトには return 文が必要です", + "testSuccess": "テスト成功!", + "testFailed": "テストに失敗しました", + "formatSuccess": "整形に成功しました", + "formatFailed": "整形に失敗しました", + "variablesHint": "使用可能な変数: {{apiKey}}, {{baseUrl}} | extractor 関数には API 応答の JSON オブジェクトが渡されます", + "scriptConfig": "リクエスト設定", + "extractorCode": "抽出コード", + "extractorHint": "戻り値のオブジェクトに残り枠の項目を含めてください", + "fieldIsValid": "• isValid: Boolean。プランが有効かどうか", + "fieldInvalidMessage": "• invalidMessage: String。無効時の理由(isValid が false のとき表示)", + "fieldRemaining": "• remaining: Number。残り枠", + "fieldUnit": "• unit: String。単位(例: \"USD\")", + "fieldPlanName": "• planName: String。プラン名", + "fieldTotal": "• total: Number。総枠", + "fieldUsed": "• used: Number。使用量", + "fieldExtra": "• extra: String。自由記述の追加テキスト", + "tip1": "• 変数 {{apiKey}} と {{baseUrl}} は自動で置換されます", + "tip2": "• 抽出関数はサンドボックスで実行され、ES2020+ の構文を使えます", + "tip3": "• 全体を () で囲み、オブジェクトリテラル式にしてください" + }, + "errors": { + "usage_query_failed": "利用状況の取得に失敗しました" + }, + "presetSelector": { + "title": "設定タイプを選択", + "custom": "カスタム", + "customDescription": "手動で設定。完全な構成が必要", + "officialDescription": "公式ログイン。API Key 不要", + "presetDescription": "プリセットを使用。API Key だけ入力すれば OK" + }, + "mcp": { + "title": "MCP 管理", + "claudeTitle": "Claude Code MCP 管理", + "codexTitle": "Codex MCP 管理", + "geminiTitle": "Gemini MCP 管理", + "unifiedPanel": { + "title": "MCP サーバー管理", + "addServer": "サーバーを追加", + "editServer": "サーバーを編集", + "deleteServer": "サーバーを削除", + "deleteConfirm": "サーバー「{{id}}」を削除しますか?この操作は元に戻せません。", + "noServers": "まだサーバーがありません", + "enabledApps": "有効なアプリ", + "apps": { + "claude": "Claude", + "codex": "Codex", + "gemini": "Gemini" + } + }, + "userLevelPath": "ユーザーレベルの MCP パス", + "serverList": "サーバー一覧", + "loading": "読み込み中...", + "empty": "MCP サーバーがありません", + "emptyDescription": "右上のボタンから最初の MCP サーバーを追加してください", + "add": "MCP を追加", + "addServer": "MCP を追加", + "editServer": "MCP を編集", + "addClaudeServer": "Claude Code MCP を追加", + "editClaudeServer": "Claude Code MCP を編集", + "addCodexServer": "Codex MCP を追加", + "editCodexServer": "Codex MCP を編集", + "configPath": "設定パス", + "serverCount": "MCP サーバー: {{count}} 件", + "enabledCount": "{{count}} 件が有効", + "template": { + "fetch": "クイックテンプレート: mcp-fetch" + }, + "form": { + "title": "MCP ID(ユニーク)", + "titlePlaceholder": "my-mcp-server", + "name": "表示名", + "namePlaceholder": "例: @modelcontextprotocol/server-time", + "enabledApps": "適用するアプリ", + "noAppsWarning": "少なくとも 1 つ選択してください", + "description": "説明", + "descriptionPlaceholder": "任意の説明", + "tags": "タグ(カンマ区切り)", + "tagsPlaceholder": "stdio, time, utility", + "homepage": "ホームページ", + "homepagePlaceholder": "https://example.com", + "docs": "ドキュメント", + "docsPlaceholder": "https://example.com/docs", + "additionalInfo": "追加情報", + "jsonConfig": "JSON 全設定", + "jsonConfigOrPrefix": "JSON 全設定、または", + "tomlConfigOrPrefix": "TOML 全設定、または", + "jsonPlaceholder": "{\n \"type\": \"stdio\",\n \"command\": \"uvx\",\n \"args\": [\"mcp-server-fetch\"]\n}", + "tomlConfig": "TOML 全設定", + "tomlPlaceholder": "type = \"stdio\"\ncommand = \"uvx\"\nargs = [\"mcp-server-fetch\"]", + "useWizard": "設定ウィザード", + "syncOtherSide": "{{target}} にも反映", + "syncOtherSideHint": "{{target}} に同じ設定を書き込みます。既存の同一 ID は上書きされます。", + "willOverwriteWarning": "{{target}} の既存設定を上書きします" + }, + "wizard": { + "title": "MCP 設定ウィザード", + "hint": "MCP サーバーを素早く設定し JSON を自動生成します", + "type": "タイプ", + "typeStdio": "stdio", + "typeHttp": "http", + "typeSse": "sse", + "command": "コマンド", + "commandPlaceholder": "npx または uvx", + "args": "引数", + "argsPlaceholder": "arg1\narg2", + "env": "環境変数", + "envPlaceholder": "KEY1=value1\nKEY2=value2", + "url": "URL", + "urlPlaceholder": "https://api.example.com/mcp", + "urlRequired": "URL を入力してください", + "headers": "ヘッダー(任意)", + "headersPlaceholder": "Authorization: Bearer your_token_here\nContent-Type: application/json", + "preview": "設定プレビュー", + "apply": "設定を反映" + }, + "id": "識別子(ユニーク)", + "type": "タイプ", + "command": "コマンド", + "validateCommand": "コマンドを検証", + "args": "引数", + "argsPlaceholder": "例: mcp-server-fetch --help", + "env": "環境変数(1 行に 1 件、KEY=VALUE)", + "envPlaceholder": "FOO=bar\nHELLO=world", + "reset": "リセット", + "notice": { + "restartClaude": "書き込みました。Claude を再起動すると反映されます。" + }, + "msg": { + "saved": "保存しました", + "deleted": "削除しました", + "enabled": "有効化しました", + "disabled": "無効化しました", + "templateAdded": "テンプレートを追加しました" + }, + "error": { + "idRequired": "識別子を入力してください", + "idExists": "この識別子は既に存在します。別のものを選んでください。", + "jsonInvalid": "JSON 形式が無効です", + "tomlInvalid": "TOML 形式が無効です", + "commandRequired": "コマンドを入力してください", + "singleServerObjectRequired": "単一の MCP サーバーオブジェクトを貼り付けてください(トップレベルの mcpServers は不要)", + "saveFailed": "保存に失敗しました", + "deleteFailed": "削除に失敗しました" + }, + "validation": { + "ok": "コマンドが見つかりました", + "fail": "コマンドが見つかりません" + }, + "confirm": { + "deleteTitle": "MCP サーバーを削除", + "deleteMessage": "MCP サーバー「{{id}}」を削除してもよろしいですか?この操作は元に戻せません。" + }, + "presets": { + "title": "MCP タイプを選択", + "enable": "有効化", + "enabled": "有効", + "installed": "インストール済み", + "docs": "ドキュメント", + "requiresEnv": "環境変数が必要", + "fetch": { + "name": "mcp-server-fetch", + "description": "汎用 HTTP リクエストツール。GET/POST などに対応し、API テストや Web データ取得に最適です" + }, + "time": { + "name": "@modelcontextprotocol/server-time", + "description": "現在時刻、タイムゾーン変換、日付計算を提供する時間クエリツール" + }, + "memory": { + "name": "@modelcontextprotocol/server-memory", + "description": "エンティティ・関係・観測を扱うナレッジグラフ型メモリ。会話の重要情報を AI に記憶させます" + }, + "sequential-thinking": { + "name": "@modelcontextprotocol/server-sequential-thinking", + "description": "複雑な問題をステップに分解して深く考えるためのシーケンシャル思考ツール" + }, + "context7": { + "name": "@upstash/context7-mcp", + "description": "最新のライブラリドキュメントとコード例を提供する Context7 ドキュメント検索ツール。キー設定で上限が拡張されます" + } + } + }, + "prompts": { + "manage": "プロンプト", + "title": "{{appName}} プロンプト管理", + "claudeTitle": "Claude プロンプト管理", + "codexTitle": "Codex プロンプト管理", + "add": "プロンプトを追加", + "edit": "プロンプトを編集", + "addTitle": "{{appName}} プロンプトを追加", + "editTitle": "{{appName}} プロンプトを編集", + "import": "既存をインポート", + "count": "{{count}} 件のプロンプト", + "enabled": "有効", + "enable": "有効化", + "enabledName": "有効: {{name}}", + "noneEnabled": "有効なプロンプトがありません", + "currentFile": "現在の {{filename}} の内容", + "empty": "まだプロンプトがありません", + "emptyDescription": "上のボタンからプロンプトを追加またはインポートしてください", + "loading": "読み込み中...", + "name": "名前", + "namePlaceholder": "例: デフォルトプロジェクトプロンプト", + "description": "説明", + "descriptionPlaceholder": "任意の説明", + "content": "内容", + "contentPlaceholder": "# {{filename}}\n\nここにプロンプト内容を入力...", + "loadFailed": "プロンプトの読み込みに失敗しました", + "saveSuccess": "保存しました", + "saveFailed": "保存に失敗しました", + "deleteSuccess": "削除しました", + "deleteFailed": "削除に失敗しました", + "enableSuccess": "有効化しました", + "enableFailed": "有効化に失敗しました", + "disableSuccess": "無効化しました", + "disableFailed": "無効化に失敗しました", + "importSuccess": "インポートしました", + "importFailed": "インポートに失敗しました", + "confirm": { + "deleteTitle": "削除の確認", + "deleteMessage": "プロンプト「{{name}}」を削除してもよろしいですか?" + } + }, + "env": { + "warning": { + "title": "競合する環境変数を検出しました", + "description": "設定を上書きする可能性のある環境変数を {{count}} 件見つけました" + }, + "actions": { + "expand": "詳細を表示", + "collapse": "折りたたむ", + "selectAll": "すべて選択", + "clearSelection": "選択を解除", + "deleteSelected": "選択 {{count}} 件を削除", + "deleting": "削除中..." + }, + "field": { + "value": "値", + "source": "ソース" + }, + "source": { + "userRegistry": "ユーザー環境変数(レジストリ)", + "systemRegistry": "システム環境変数(レジストリ)", + "systemEnv": "システム環境変数" + }, + "delete": { + "success": "環境変数を削除しました", + "error": "環境変数の削除に失敗しました" + }, + "backup": { + "location": "バックアップ場所: {{path}}" + }, + "confirm": { + "title": "環境変数を削除しますか?", + "message": "{{count}} 件の環境変数を削除してもよろしいですか?", + "backupNotice": "削除前に自動バックアップを作成します。後で復元できます。再起動またはターミナル再起動後に反映されます。", + "confirm": "削除を確認" + }, + "error": { + "noSelection": "削除する環境変数を選択してください" + } + }, + "skills": { + "manage": "Skills", + "title": "Claude スキル管理", + "description": "人気リポジトリから Claude Skills を探してインストールし、Claude Code/Codex を拡張", + "refresh": "更新", + "refreshing": "更新中...", + "repoManager": "リポジトリ管理", + "count": "{{count}} 個のスキル", + "empty": "スキルがありません", + "emptyDescription": "スキルリポジトリを追加して探索してください", + "addRepo": "スキルリポジトリを追加", + "loading": "読み込み中...", + "installed": "インストール済み", + "install": "インストール", + "installing": "インストール中...", + "uninstall": "アンインストール", + "uninstalling": "アンインストール中...", + "view": "表示", + "noDescription": "説明なし", + "loadFailed": "読み込みに失敗しました", + "installSuccess": "スキル {{name}} をインストールしました", + "installFailed": "インストールに失敗しました", + "uninstallSuccess": "スキル {{name}} をアンインストールしました", + "uninstallFailed": "アンインストールに失敗しました", + "error": { + "skillNotFound": "スキルが見つかりません: {{directory}}", + "missingRepoInfo": "リポジトリ情報(owner または name)が不足しています", + "downloadTimeout": "リポジトリ {{owner}}/{{name}} のダウンロードがタイムアウトしました({{timeout}} 秒)", + "downloadTimeoutHint": "ネットワークを確認するか、時間をおいて再試行してください", + "skillPathNotFound": "リポジトリ {{owner}}/{{name}} にスキルパス '{{path}}' がありません", + "skillDirNotFound": "スキルディレクトリが見つかりません: {{path}}", + "emptyArchive": "ダウンロードしたアーカイブが空です", + "downloadFailed": "ダウンロードに失敗しました: HTTP {{status}}", + "allBranchesFailed": "すべてのブランチで失敗しました。試行: {{branches}}", + "httpError": "HTTP エラー {{status}}", + "http403": "GitHub へのアクセスが制限されています(レート制限の可能性)", + "http404": "リポジトリまたはブランチが見つかりません。URL を確認してください", + "http429": "リクエストが多すぎます。時間をおいて再試行してください", + "parseMetadataFailed": "スキルメタデータの解析に失敗しました", + "getHomeDirFailed": "ユーザーのホームディレクトリを取得できません", + "networkError": "ネットワークエラー", + "fsError": "ファイルシステムエラー", + "unknownError": "不明なエラー", + "suggestion": { + "checkNetwork": "ネットワーク接続を確認してください", + "checkProxy": "HTTP プロキシの設定を検討してください", + "retryLater": "時間をおいて再試行してください", + "checkRepoUrl": "リポジトリ URL とブランチ名を確認してください", + "checkDiskSpace": "ディスク容量を確認してください", + "checkPermission": "ディレクトリの権限を確認してください" + } + }, + "repo": { + "title": "スキルリポジトリを管理", + "description": "GitHub のスキルリポジトリソースを追加または削除します", + "url": "リポジトリ URL", + "urlPlaceholder": "owner/name または https://github.com/owner/name", + "branch": "ブランチ", + "branchPlaceholder": "main", + "path": "スキルパス", + "pathPlaceholder": "skills(任意。空欄はルート)", + "add": "リポジトリを追加", + "list": "追加済みリポジトリ", + "empty": "リポジトリがありません", + "invalidUrl": "リポジトリ URL の形式が無効です", + "addSuccess": "リポジトリ {{owner}}/{{name}} を追加しました。検出スキル: {{count}} 件", + "addFailed": "追加に失敗しました", + "removeSuccess": "リポジトリ {{owner}}/{{name}} を削除しました", + "removeFailed": "削除に失敗しました", + "skillCount": "{{count}} 件のスキルを検出" + }, + "search": "スキルを検索", + "searchPlaceholder": "スキル名または説明で検索...", + "filter": { + "placeholder": "状態で絞り込み", + "all": "すべて", + "installed": "インストール済み", + "uninstalled": "未インストール" + }, + "noResults": "一致するスキルが見つかりませんでした" + }, + "deeplink": { + "confirmImport": "プロバイダーのインポートを確認", + "confirmImportDescription": "次の設定をディープリンクから CC Switch へインポートします", + "importPrompt": "プロンプトをインポート", + "importPromptDescription": "このシステムプロンプトをインポートするか確認してください", + "importMcp": "MCP サーバーをインポート", + "importMcpDescription": "これらの MCP サーバーをインポートするか確認してください", + "importSkill": "スキルリポジトリを追加", + "importSkillDescription": "このスキルリポジトリを追加するか確認してください", + "promptImportSuccess": "プロンプトをインポートしました", + "promptImportSuccessDescription": "インポートされたプロンプト: {{name}}", + "mcpImportSuccess": "MCP サーバーをインポートしました", + "mcpImportSuccessDescription": "{{count}} 件のサーバーをインポートしました", + "mcpPartialSuccess": "一部のみインポート成功", + "mcpPartialSuccessDescription": "成功: {{success}}、失敗: {{failed}}", + "skillImportSuccess": "スキルリポジトリを追加しました", + "skillImportSuccessDescription": "追加したリポジトリ: {{repo}}", + "app": "アプリ種別", + "providerName": "プロバイダー名", + "homepage": "ホームページ", + "endpoint": "API エンドポイント", + "apiKey": "API Key", + "icon": "アイコン", + "model": "モデル", + "haikuModel": "Haiku モデル", + "sonnetModel": "Sonnet モデル", + "opusModel": "Opus モデル", + "multiModel": "マルチモーダルモデル", + "notes": "メモ", + "import": "インポート", + "importing": "インポート中...", + "warning": "インポート前に内容を確認してください。後から一覧で編集・削除できます。", + "parseError": "ディープリンクの解析に失敗しました", + "importSuccess": "インポート成功", + "importSuccessDescription": "プロバイダー「{{name}}」をインポートしました", + "importError": "インポートに失敗しました", + "configSource": "設定ソース", + "configEmbedded": "埋め込み設定", + "configRemote": "リモート設定", + "configDetails": "設定の詳細", + "configUrl": "設定ファイル URL", + "configMergeError": "設定ファイルのマージに失敗しました", + "mcp": { + "title": "MCP サーバーを一括インポート", + "targetApps": "ターゲットアプリ", + "serverCount": "MCP サーバー({{count}} 件)", + "enabledWarning": "インポート後、指定したすべてのアプリに即座に書き込まれます" + }, + "prompt": { + "title": "システムプロンプトをインポート", + "app": "アプリ", + "name": "名前", + "description": "説明", + "contentPreview": "内容プレビュー", + "enabledWarning": "インポート後すぐにこのプロンプトが有効になり、他は無効になります" + }, + "skill": { + "title": "Claude スキルリポジトリを追加", + "repo": "GitHub リポジトリ", + "directory": "対象ディレクトリ", + "branch": "ブランチ", + "skillsPath": "スキルパス", + "hint": "この操作でスキルリポジトリが一覧に追加されます。", + "hintDetail": "追加後、スキル管理ページから個別のスキルをインストールできます。" + } + }, + "iconPicker": { + "search": "アイコンを検索", + "searchPlaceholder": "アイコン名を入力...", + "noResults": "一致するアイコンが見つかりません", + "category": { + "aiProvider": "AI プロバイダー", + "cloud": "クラウドプラットフォーム", + "tool": "開発ツール", + "other": "その他" + } + }, + "providerIcon": { + "label": "アイコン", + "colorLabel": "アイコンカラー", + "selectIcon": "アイコンを選択", + "preview": "プレビュー" + } +} diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index d00d5bc5..e379bfcc 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -165,6 +165,7 @@ "autoReload": "数据将在2秒后自动刷新...", "languageOptionChinese": "中文", "languageOptionEnglish": "English", + "languageOptionJapanese": "日本語", "windowBehavior": "窗口行为", "windowBehaviorHint": "配置窗口最小化与 Claude 插件联动策略。", "launchOnStartup": "开机自启", diff --git a/src/lib/schemas/settings.ts b/src/lib/schemas/settings.ts index e754e34e..61e6b9da 100644 --- a/src/lib/schemas/settings.ts +++ b/src/lib/schemas/settings.ts @@ -13,7 +13,7 @@ export const settingsSchema = z.object({ minimizeToTrayOnClose: z.boolean(), enableClaudePluginIntegration: z.boolean().optional(), launchOnStartup: z.boolean().optional(), - language: z.enum(["en", "zh"]).optional(), + language: z.enum(["en", "zh", "ja"]).optional(), // 设备级目录覆盖 claudeConfigDir: directorySchema.nullable().optional(), diff --git a/src/types.ts b/src/types.ts index ad8e59d0..bdf46fcf 100644 --- a/src/types.ts +++ b/src/types.ts @@ -109,7 +109,7 @@ export interface Settings { // 是否开机自启 launchOnStartup?: boolean; // 首选语言(可选,默认中文) - language?: "en" | "zh"; + language?: "en" | "zh" | "ja"; // ===== 设备级目录覆盖 ===== // 覆盖 Claude Code 配置目录(可选) diff --git a/tests/hooks/useSettingsForm.test.tsx b/tests/hooks/useSettingsForm.test.tsx index 4a84c9eb..f6dbd96b 100644 --- a/tests/hooks/useSettingsForm.test.tsx +++ b/tests/hooks/useSettingsForm.test.tsx @@ -58,6 +58,29 @@ describe("useSettingsForm Hook", () => { expect(changeLanguageSpy).toHaveBeenCalledWith("en"); }); + it("should support japanese language preference from server data", async () => { + useSettingsQueryMock.mockReturnValue({ + data: { + showInTray: true, + minimizeToTrayOnClose: true, + enableClaudePluginIntegration: false, + claudeConfigDir: "/Users/demo", + codexConfigDir: null, + language: "ja", + }, + isLoading: false, + }); + + const { result } = renderHook(() => useSettingsForm()); + + await waitFor(() => { + expect(result.current.settings?.language).toBe("ja"); + }); + + expect(result.current.initialLanguage).toBe("ja"); + expect(changeLanguageSpy).toHaveBeenCalledWith("ja"); + }); + it("should prioritize reading language from local storage in readPersistedLanguage", () => { useSettingsQueryMock.mockReturnValue({ data: null,