feat: implement Pi Switch i18n UI and agent config switch
This commit is contained in:
1 parent
7d69aac08f
commit
af991a7bda
86 files changed
+4214
-235
No files matched your search
+111
@@ -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",
|
||||
};
|
||||
@@ -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
@@ -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;
|
||||
Reference in new issue
Block a user