feat: implement Pi Switch i18n UI and agent config switch

This commit is contained in:
KimiSwitch Dev committed 2026-07-08 08:53:26 +08:00
1 parent 7d69aac08f
commit af991a7bda
86 files changed
+4214 -235

No files matched your search

+111
View File
@@ -0,0 +1,111 @@
import type { TranslationKey } from "./zh";
export const enTranslations: Record<TranslationKey, string> = {
// App / global
appTitle: "Pi Switch",
loading: "Loading...",
loadTimeout: "Loading is taking a while. Try restarting the app if it stays stuck.",
loadFailed: "Failed to load:",
retry: "Retry",
saveConfig: "Save Config",
reloadConfig: "Reload Config",
openConfigDir: "Open Config Dir",
unsavedConfirm: "You have unsaved changes. Are you sure you want to reload?",
providerNotFound: "Provider not found",
backToList: "Back to list",
syncToOmp: "Sync to OMP",
syncedToOmp: "Synced to OMP: ",
ompOverwriteConfirm: "OMP already has provider \"{existing}\". Replace with \"{name}\"?",
deleteProviderConfirm: "Delete provider {name}?",
// Provider list
providers: "Providers",
addProvider: "Add Provider",
noProviders: "No providers yet",
addFirstProvider: "Add first provider",
noUrl: "No URL",
modelCount: "{count} models",
ompActive: "OMP Active",
defaultModel: "Default: {name}",
edit: "Edit",
delete: "Delete",
// Provider edit
editProvider: "Edit Provider",
back: "Back",
basicInfo: "Basic Info",
modelMapping: "Model Mapping",
configJson: "Config JSON",
providerName: "Provider Name",
note: "Note",
notePlaceholder: "e.g. company account",
officialUrl: "Official URL",
managedProvider: "Managed provider (no credentials needed)",
apiSettings: "API Settings",
apiFormat: "API Format",
authField: "Auth Field",
apiKey: "API Key",
show: "Show",
hide: "Hide",
requestUrl: "Request URL",
fullUrl: "Full URL",
baseUrlHint: "Enter a {type} API-compatible endpoint without a trailing slash.",
envPairs: "Env pairs",
addEnv: "+ Add",
modelMappingDesc: "Display name only affects the /model menu; 1M declares context capability for Kimi Code.",
oneClickSetup: "One-click setup",
fetchModels: "Fetch models",
fetchingModels: "Fetching...",
discoveredModels: "Discovered {count} models",
addSelected: "Add selected",
modelRole: "Model role",
displayName: "Display name",
actualModel: "Actual model",
contextSize: "Context size",
supports1M: "Supports 1M",
default: "Default",
operation: "Operation",
isDefault: "Default",
setDefault: "Set default",
deleteModelConfirm: "Delete model {alias}?",
noModelMappings: "No model mappings",
addModelMapping: "+ Add model mapping",
readOnlyPreview: "Editable JSON",
apply: "Apply",
invalidJson: "Invalid JSON: {message}",
// Agent / target
target: "Target",
targetKimi: "Kimi Code",
targetOmp: "OMP",
targetQwen: "Qwen Code",
agent: "Agent",
agentKimiCode: "Kimi Code",
agentPi: "Pi",
activate: "Activate",
activated: "Activated",
disabled: "Disabled",
unsavedChanges: "Unsaved changes",
qwenActivateNotImplemented: "Qwen Code activation will be implemented in a follow-up.",
importConfig: "Import current config",
importConfigConfirm: "Importing will overwrite unsaved changes. Continue?",
importConfigFailed: "Import failed: {message}",
capabilities: "Capabilities",
// Languages
language: "Language",
zh: "中文",
en: "English",
// Validation errors
emptyProviderName: "Provider name is empty",
duplicateProviderName: "Duplicate provider name: {0}",
missingCredentials: "Provider '{0}' is missing credentials (api_key or env key)",
ambiguousCredentials: "Provider '{0}' has both direct and env credentials; only one source is allowed",
missingVertexProject: "Vertex provider '{0}' is missing GOOGLE_CLOUD_PROJECT or GOOGLE_CLOUD_LOCATION",
emptyModelAlias: "Model alias is empty",
duplicateModelAlias: "Duplicate model alias: {0}",
unknownModelProvider: "Model '{0}' references unknown provider '{1}'",
emptyModelId: "Model '{0}' has an empty model ID",
invalidMaxContextSize: "Model '{0}' has an invalid max_context_size",
};
+82
View File
@@ -0,0 +1,82 @@
import { createContext, useContext, useEffect, useState, type ReactNode } from "react";
import { enTranslations } from "./en";
import { zhTranslations, type TranslationKey } from "./zh";
export type Language = "zh" | "en";
const translations = {
zh: zhTranslations,
en: enTranslations,
};
function interpolate(template: string, params?: Record<string, string | number>): string {
if (!params) return template;
return template.replace(/\{(\w+)\}/g, (_, key) => String(params[key] ?? `{${key}}`));
}
function interpolatePositional(template: string, args: string[]): string {
return template.replace(/\{(\d+)\}/g, (_, index) => args[Number(index)] ?? `{${index}}`);
}
interface I18nContextValue {
lang: Language;
setLang: (lang: Language) => void;
t: (key: TranslationKey, params?: Record<string, string | number>) => string;
tv: (code: string, args: string[]) => string;
}
const I18nContext = createContext<I18nContextValue | null>(null);
const STORAGE_KEY = "pi-switch-lang";
function getInitialLang(): Language {
try {
const stored = localStorage.getItem(STORAGE_KEY) as Language | null;
if (stored === "zh" || stored === "en") return stored;
} catch {
// ignore
}
const browser = navigator.language.toLowerCase();
return browser.startsWith("zh") ? "zh" : "en";
}
export function I18nProvider({ children }: { children: ReactNode }) {
const [lang, setLangState] = useState<Language>(getInitialLang);
const setLang = (next: Language) => {
setLangState(next);
try {
localStorage.setItem(STORAGE_KEY, next);
} catch {
// ignore
}
};
useEffect(() => {
document.documentElement.lang = lang === "zh" ? "zh-CN" : "en";
}, [lang]);
const t = (key: TranslationKey, params?: Record<string, string | number>) => {
const template = translations[lang][key];
return interpolate(template ?? key, params);
};
const tv = (code: string, args: string[]) => {
const template = translations[lang][code as TranslationKey];
return interpolatePositional(template ?? code, args);
};
return (
<I18nContext.Provider value={{ lang, setLang, t, tv }}>
{children}
</I18nContext.Provider>
);
}
export function useTranslation() {
const ctx = useContext(I18nContext);
if (!ctx) {
throw new Error("useTranslation must be used within I18nProvider");
}
return ctx;
}
+111
View File
@@ -0,0 +1,111 @@
export const zhTranslations = {
// App / global
appTitle: "Pi Switch",
loading: "加载中...",
loadTimeout: "加载时间较长,如果一直卡住,请尝试重新启动应用。",
loadFailed: "加载失败:",
retry: "重试",
saveConfig: "保存配置",
reloadConfig: "读取配置",
openConfigDir: "打开配置目录",
unsavedConfirm: "有未保存修改,确定要重新读取配置吗?",
providerNotFound: "供应商不存在",
backToList: "返回列表",
syncToOmp: "同步到 OMP",
syncedToOmp: "已同步到 OMP:",
ompOverwriteConfirm: "OMP 中已存在供应商「{existing}」,是否替换为「{name}」?",
deleteProviderConfirm: "确定删除供应商 {name}?",
// Provider list
providers: "供应商",
addProvider: "添加供应商",
noProviders: "暂无供应商",
addFirstProvider: "添加第一个供应商",
noUrl: "无 URL",
modelCount: "{count} 个模型",
ompActive: "OMP 活跃",
defaultModel: "默认: {name}",
edit: "编辑",
delete: "删除",
// Provider edit
editProvider: "编辑供应商",
back: "返回",
basicInfo: "基本信息",
modelMapping: "模型映射",
configJson: "配置 JSON",
providerName: "供应商名称",
note: "备注",
notePlaceholder: "例如:公司专用账号",
officialUrl: "官网链接",
managedProvider: "托管供应商(无需凭证)",
apiSettings: "API 设置",
apiFormat: "API 格式",
authField: "认证字段",
apiKey: "API Key",
show: "显示",
hide: "隐藏",
requestUrl: "请求地址",
fullUrl: "完整 URL",
baseUrlHint: "填写兼容 {type} API 的服务端点地址,不要以斜杠结尾",
envPairs: "Env 键值对",
addEnv: "+ 添加",
modelMappingDesc: "显示名称只影响 /model 菜单;1M 只是给 Kimi Code 的上下文能力声明。",
oneClickSetup: "一键设置",
fetchModels: "获取模型列表",
fetchingModels: "获取中...",
discoveredModels: "发现 {count} 个模型",
addSelected: "添加选中的",
modelRole: "模型角色",
displayName: "显示名称",
actualModel: "实际请求模型",
contextSize: "上下文长度",
supports1M: "声明支持 1M",
default: "默认",
operation: "操作",
isDefault: "已默认",
setDefault: "设为默认",
deleteModelConfirm: "确定删除模型 {alias}?",
noModelMappings: "暂无模型映射",
addModelMapping: "+ 添加模型映射",
readOnlyPreview: "可编辑 JSON",
apply: "应用",
invalidJson: "JSON 格式错误:{message}",
// Agent / target
target: "目标",
targetKimi: "Kimi Code",
targetOmp: "OMP",
targetQwen: "Qwen Code",
agent: "智能体",
agentKimiCode: "Kimi Code",
agentPi: "Pi",
activate: "启用",
activated: "已启用",
disabled: "已停用",
unsavedChanges: "未保存修改",
qwenActivateNotImplemented: "Qwen Code 启用功能将在后续实现。",
importConfig: "导入当前配置",
importConfigConfirm: "导入会覆盖当前未保存的修改,是否继续?",
importConfigFailed: "导入失败:{message}",
capabilities: "能力标签",
// Languages
language: "语言",
zh: "中文",
en: "English",
// Validation errors
emptyProviderName: "供应商名称为空",
duplicateProviderName: "重复的供应商名称:{0}",
missingCredentials: "供应商「{0}」缺少凭证(api_key 或环境变量键)",
ambiguousCredentials: "供应商「{0}」同时设置了直接凭证和 Env 凭证,只允许一种",
missingVertexProject: "Vertex 供应商「{0}」缺少 GOOGLE_CLOUD_PROJECT 或 GOOGLE_CLOUD_LOCATION",
emptyModelAlias: "模型别名为空",
duplicateModelAlias: "重复的模型别名:{0}",
unknownModelProvider: "模型「{0}」引用了未知供应商「{1}」",
emptyModelId: "模型「{0}」的模型 ID 为空",
invalidMaxContextSize: "模型「{0}」的 max_context_size 无效",
} as const;
export type TranslationKey = keyof typeof zhTranslations;