feat: integrate Pi backend and Pi Web

This commit is contained in:
luckyyzh
2026-07-30 19:37:53 +08:00
commit 7392ab9dd7
1390 changed files with 337197 additions and 0 deletions
+19
View File
@@ -0,0 +1,19 @@
import assert from "node:assert/strict";
import test from "node:test";
import { formatRelativeTime, interpolateMessage, translateMessage } from "./format.ts";
test("interpolates string and numeric parameters", () => {
assert.equal(interpolateMessage("Hello, {name} ({count})", { name: "Pi", count: 2 }), "Hello, Pi (2)");
});
test("falls back to English and returns the key when both are missing", () => {
assert.equal(translateMessage("zh-CN", "common.ok", { en: { "common.ok": "OK" }, "zh-CN": {} }), "OK");
assert.equal(translateMessage("zh-CN", "missing.key", { en: {}, "zh-CN": {} }), "missing.key");
});
test("formats relative time using the selected locale", () => {
const now = new Date("2026-01-01T00:00:00.000Z");
assert.equal(formatRelativeTime(new Date("2026-01-01T00:05:00.000Z"), "en", now), "in 5 minutes");
assert.equal(formatRelativeTime(new Date("2025-12-31T23:00:00.000Z"), "zh-CN", now), "1小时前");
});
+60
View File
@@ -0,0 +1,60 @@
import type { Locale, TranslationParams } from "./types";
type MessagesByLocale = Record<string, Record<string, string>>;
/**
* 替换翻译消息中的简单插值占位符。
* @param message 原始翻译消息
* @param params 插值参数
* @returns 完成参数替换后的消息
*/
export function interpolateMessage(message: string, params: TranslationParams = {}): string {
return message.replace(/\{([\w.-]+)\}/g, (token, name: string) => {
const value = params[name];
return value === undefined ? token : String(value);
});
}
/**
* 从当前语言和英语语言包中解析消息。
* @param locale 当前语言
* @param key 翻译 key
* @param messages 各语言的消息字典
* @param params 可选的插值参数
* @returns 翻译结果,缺失时返回 key
*/
export function translateMessage(
locale: Locale,
key: string,
messages: MessagesByLocale,
params: TranslationParams = {},
): string {
const message = messages[locale]?.[key] ?? messages.en?.[key];
if (message === undefined) {
if (process.env.NODE_ENV !== "production") console.warn(`[i18n] Missing translation: ${key}`);
return key;
}
return interpolateMessage(message, params);
}
/**
* 按当前语言格式化相对时间。
* @param date 要格式化的时间
* @param locale 当前语言
* @param now 用于测试或特殊场景的当前时间
* @returns locale-aware 的相对时间文本
*/
export function formatRelativeTime(date: Date | string, locale: Locale, now = new Date()): string {
const target = date instanceof Date ? date : new Date(date);
const diffMs = target.getTime() - now.getTime();
const absMs = Math.abs(diffMs);
const [unit, divisor] = absMs < 60_000
? ["second", 1_000]
: absMs < 3_600_000
? ["minute", 60_000]
: absMs < 86_400_000
? ["hour", 3_600_000]
: ["day", 86_400_000];
const value = Math.round(diffMs / divisor);
return new Intl.RelativeTimeFormat(locale, { numeric: "always" }).format(value, unit as Intl.RelativeTimeFormatUnit);
}
+430
View File
@@ -0,0 +1,430 @@
import type { LocalePlugin } from "../types";
/** Pi Web 内置英语语言包。 */
export const enLocale: LocalePlugin = {
id: "en",
label: "English",
messages: {
"common.ok": "OK",
"common.language": "Language",
"common.models": "Models",
"common.skills": "Skills",
"common.plugins": "Plugins",
"sidebar.hide": "Hide sidebar",
"sidebar.show": "Show sidebar",
"theme.light": "Switch to light mode",
"theme.dark": "Switch to dark mode",
"history.full": "Full history",
"history.unsaved": "Full history is available after the session is saved",
"history.label": "Full history",
"title.generating": "Generating...",
"title.updated": "Title updated",
"title.failed": "Generation failed",
"title.generate": "Generate title",
"title.unsaved": "Title generation is available after the session is saved",
"title.noMessages": "Send a message before naming this session",
"title.generateSession": "Generate a session title",
"system.prompt": "System prompt",
"system.empty": "System prompt is empty (tools are disabled)",
"system.load": "Send a message to load the system prompt",
"system.label": "System",
"session.info": "Session info",
"session.name": "Name",
"session.file": "File",
"session.inMemory": "In-memory",
"session.id": "ID",
"session.user": "User",
"session.assistant": "Assistant",
"session.toolCalls": "Tool Calls",
"session.toolResults": "Tool Results",
"session.total": "Total",
"session.input": "Input",
"session.output": "Output",
"session.cacheRead": "Cache Read",
"session.cacheWrite": "Cache Write",
"session.cost": "Cost",
"session.context": "Context",
"session.copy": "Copy {value}",
"session.copied": "Copied",
"session.title": "Session info",
"session.load": "Send a message or run /session to load session info",
"session.infoSection": "Session Info",
"session.messages": "Messages",
"session.tokens": "Tokens",
"session.copyFile": "Copy file path",
"session.copyId": "Copy session ID",
"workspace.opening": "Opening workspace...",
"workspace.unable": "Unable to open workspace",
"workspace.selectSession": "Select a session from the sidebar",
"workspace.getStarted": "Get Started",
"workspace.selectProject": "Select a project directory from the sidebar",
"workspace.addModels": "Add models via the Models button at the bottom",
"workspace.stepProject": "Select a project directory from the sidebar",
"workspace.stepModels": "Add models via the Models button at the bottom",
"files.hidePanel": "Hide file panel",
"files.showPanel": "Show file panel",
"files.noneOpen": "No file open",
"sidebar.new": "New",
"sidebar.newSessionTitle": "New session in {path}",
"sidebar.refresh": "Refresh",
"sidebar.selectProject": "Select project…",
"sidebar.filterProjects": "Filter projects…",
"sidebar.noMatchingProjects": "No matching projects",
"sidebar.useDefaultDirectory": "Use default directory",
"sidebar.customPath": "Custom path…",
"directoryPicker.selectDirectory": "Select directory",
"directoryPicker.goToParent": "Go to parent directory",
"directoryPicker.directoryPath": "Directory path",
"directoryPicker.goToDirectory": "Go to directory",
"directoryPicker.go": "Go",
"directoryPicker.loadingDirectories": "Loading directories…",
"directoryPicker.noSubdirectories": "No subdirectories",
"directoryPicker.openBeforeSelecting": "Open this path before selecting it",
"directoryPicker.selectCurrentDirectory": "Select current directory",
"directoryPicker.selectThisFolder": "Select this folder",
"sidebar.checking": "Checking…",
"sidebar.open": "Open",
"sidebar.cancel": "Cancel",
"sidebar.openRepoRoot": "Open repo root",
"sidebar.openRepoRootTitle": "Open the repository root to manage worktrees.",
"sidebar.gitRepoRootOnly": "Git repo root only",
"sidebar.gitRepoRootOnlyTitle": "Worktrees are available in Git repository roots.",
"sidebar.worktrees": "Worktrees...",
"sidebar.checkingWorktrees": "Checking worktrees for this directory.",
"sidebar.filterWorktrees": "Filter worktrees…",
"sidebar.noMatchingWorktrees": "No matching worktrees",
"sidebar.switchWorktree": "Switch worktree",
"sidebar.switchWorktreeTitle": "Switch worktree: {path}",
"sidebar.main": "main",
"sidebar.forceRemoveCheckout": "Uncommitted changes. Force remove checkout?",
"sidebar.force": "Force",
"sidebar.createWorktreeTitle": "Create a worktree checkout for a branch",
"sidebar.newWorktree": "New worktree…",
"sidebar.branchName": "branch name",
"sidebar.creating": "Creating…",
"sidebar.create": "Create",
"sidebar.uploadFilesTitle": "Upload files to project root",
"sidebar.uploadFiles": "Upload files",
"sidebar.refreshExplorer": "Refresh explorer",
"sidebar.changedFiles": "{count} changed files",
"sidebar.loading": "Loading...",
"sidebar.noSessions": "No sessions found",
"sidebar.agentRunning": "Agent running…",
"sidebar.newActivity": "New activity",
"sidebar.newSessionActivity": "New session activity",
"sidebar.deleteSession": "Delete {title}?",
"sidebar.rename": "Rename",
"sidebar.delete": "Delete",
"sidebar.deleteWithShiftClick": "Delete (Shift+click to delete without confirmation)",
"sidebar.expandForks": "Expand forks",
"sidebar.collapseForks": "Collapse forks",
"sidebar.messagesCount": "{count} msgs",
"sidebar.worktreePath": "Worktree: {path}",
"sidebar.removeWorktreeTitle": "Remove worktree checkout {path}; the branch is kept",
"files.modified": "Modified",
"files.added": "Added",
"files.deleted": "Deleted",
"files.renamed": "Renamed",
"files.untracked": "Untracked",
"files.conflict": "Conflict",
"files.newlyUploaded": "Newly uploaded",
"files.containsChangedFiles": "Contains changed files",
"files.insertPath": "Insert path into chat",
"files.mention": "mention",
"files.download": "Download file",
"files.dismissError": "Dismiss error",
"files.addUploadedFile": "Add uploaded file to chat",
"files.addAllUploadedFiles": "Add all uploaded files to chat",
"files.dismissUploadResults": "Dismiss upload results",
"files.loading": "Loading files...",
"files.noFiles": "No files found",
"files.explorer": "Explorer",
"files.changedCount": "{count} files",
"files.changeStats": "{count} changed files, {additions} lines added, {deletions} lines deleted",
"files.conflictSummary": "{count} file{countSuffix} already exist: {files}",
"files.cannotReplace": "Cannot replace: {files}",
"files.replace": "Replace",
"files.skipExisting": "Skip existing",
"files.cancel": "Cancel",
"files.checking": "Checking files",
"files.uploading": "Uploading, {progress}%",
"chat.loadingSession": "Loading session...",
"chat.runningTool": "Running tool...",
"chat.runningNamedTool": "Running {name}...",
"chat.runningTools": "Running {names}...",
"chat.runningToolsMore": "Running {names} (+{count})...",
"chat.waitingModel": "Waiting for model...",
"chat.runningCommand": "Running command...",
"chat.thinking": "Thinking...",
"chat.processDetails": "Process details",
"chat.message": "message",
"chat.messages": "messages",
"chat.toolCall": "tool call",
"chat.toolCalls": "tool calls",
"chat.collapseProcess": "Collapse process details",
"chat.expandProcess": "Expand process details",
"chat.loadEarlier": "Scroll up to load earlier messages ({count} hidden)",
"chat.extensionRequest": "extension request",
"chat.cancel": "Cancel",
"chat.confirm": "Confirm",
"chat.submit": "Submit",
"chat.extensionInput": "Extension terminal input",
"chat.extensionPanel": "Extension panel",
"chat.close": "Close",
"chat.queued": "Queued · {count}",
"chat.recall": "Recall to input",
"chat.recallTitle": "Remove all queued messages and put them back into the input box for editing",
"chat.retrying": "Retrying ({attempt}/{max})…",
"chat.loadingCommands": "Loading commands...",
"chat.slashCommands": "Slash commands · {label}",
"chat.tabEnter": "Tab / Enter",
"chat.noCommands": "No extension, prompt, or skill commands found",
"chat.match": "1 match",
"chat.matches": "{count} matches",
"chat.command": "1 command",
"chat.commands": "{count} commands",
"chat.loadingFiles": "Loading files...",
"chat.files": "Files · {label}{hint}",
"chat.searching": "Searching…",
"chat.noMatchingFiles": "No matching files",
"chat.searchingAll": " · searching all files…",
"chat.indexTruncated": " · index truncated",
"chat.steerPlaceholder": "Steer now / queue follow-up...",
"chat.agentPlaceholder": "Agent is running…",
"chat.messagePlaceholder": "Message… Type / for commands, @ for files",
"chat.steer": "Steer",
"chat.followUp": "Follow-up",
"chat.send": "Send",
"chat.attachImage": "Attach image",
"chat.filterModels": "Filter models…",
"chat.noMatchingModels": "No matching models",
"chat.moreControls": "More controls",
"chat.collapseControls": "Collapse controls",
"chat.shell": "Shell",
"chat.outputLocal": "output stays local",
"chat.outputModel": "output sent to model",
"chat.noTools": "No tools, read-only",
"chat.builtInTools": "{count} built-in tools",
"chat.allBuiltInTools": "All built-in tools",
"chat.changeReasoning": "Change reasoning level: {level}",
"chat.changeReasoningLabel": "Change reasoning level",
"chat.changeToolPreset": "Change tool preset",
"chat.stopCompaction": "Stop compaction",
"chat.compactContext": "Compact context",
"chat.compacting": "Compacting…",
"chat.compact": "Compact",
"chat.stopAgent": "Stop agent",
"chat.stop": "Stop",
"chat.disableSound": "Disable completion sound",
"chat.enableSound": "Enable completion sound",
"chat.thinkingUseDefault": "Use pi default",
"chat.thinkingOff": "Reasoning off",
"chat.thinkingMinimal": "Minimal reasoning",
"chat.thinkingLow": "Low reasoning",
"chat.thinkingMedium": "Medium reasoning",
"chat.thinkingHigh": "High reasoning",
"chat.thinkingXhigh": "Extra-high reasoning",
"chat.thinkingMax": "Max reasoning",
"chat.builtIn": "Built-in",
"chat.extensions": "Extensions",
"chat.prompts": "Prompts",
"chat.skills": "Skills",
"chat.commandCompact": "Compress context, optionally with instructions",
"chat.commandReload": "Reload extensions, skills, prompts, and tools",
"chat.commandName": "Set the session display name",
"chat.commandSession": "Show session message, token, and cost stats",
"chat.commandCopy": "Copy the last assistant message",
"chat.compacted": "Compacted",
"chat.tokensSaved": "{saved} saved",
"i18n.close": "Close",
"i18n.copy": "Copy",
"i18n.copied": "Copied",
"i18n.cancel": "Cancel",
"i18n.save": "Save",
"i18n.saving": "Saving…",
"i18n.saved": "Saved",
"i18n.remove": "Remove",
"i18n.removing": "Removing…",
"i18n.delete": "Delete",
"i18n.loading": "Loading...",
"i18n.search": "Search",
"i18n.refresh": "Refresh",
"i18n.update": "Update",
"i18n.updating": "Updating...",
"i18n.check": "Check",
"i18n.checking": "Checking...",
"i18n.install": "Install",
"i18n.installing": "Installing...",
"i18n.noResults": "No results",
"i18n.searchProviders": "Search providers…",
"i18n.noProviders": "No providers match",
"i18n.newModel": "new model",
"i18n.addProvider": "Add provider",
"i18n.selectProviderModel": "Select a provider or model",
"i18n.visibleInPrompt": "Visible in model prompt — click to disable",
"i18n.hiddenFromPrompt": "Hidden from model prompt — click to enable",
"i18n.skillSearchPlaceholder": "e.g. react, testing, deploy",
"i18n.searching": "Searching…",
"i18n.noSkills": "No skills found",
"i18n.checkUpdates": "Check updates",
"i18n.updates": "updates",
"i18n.noResolvedResources": "No resolved resources",
"i18n.enablePackage": "Enable package",
"i18n.disablePackage": "Disable package",
"i18n.filtered": "filtered",
"i18n.openSessionToReload": "Open a session to reload",
"i18n.notFound": "Not found",
"i18n.noPlugins": "No plugins configured",
"trust.resourcesNotLoaded": "Restricted mode",
"trust.trustProject": "Trust project",
"trust.trusting": "Trusting...",
"trust.cancel": "Cancel",
"trust.dialogTitle": "Trust this project?",
"trust.dialogBody": "Project resources can run local code. Trust only projects whose contents you know.",
"trust.skillsNotLoaded": "Project skills are not loaded because this project is not trusted.",
"trust.pluginsNotLoaded": "Project plugins are not loaded because this project is not trusted.",
"trust.projectScopeUnavailable": "Project installs are unavailable while project resources are not loaded.",
"i18n.creatingSession": "Creating new session…",
"i18n.creating": "Creating…",
"i18n.subscription": "Subscription",
"i18n.subscriptions": "Subscriptions",
"i18n.customEndpoint": "Custom endpoint format",
"i18n.noChanges": "No changes",
"i18n.branches": "Branches",
"i18n.noActiveSession": "No active session",
"i18n.noBranches": "This session has no branches",
"i18n.source": "Source",
"i18n.preview": "Preview",
"i18n.diff": "Diff",
"i18n.downloadFile": "Download file",
"i18n.liveSync": "Live sync active",
"i18n.notWatching": "Not watching",
"i18n.fileViewMode": "File view mode",
"i18n.compareHead": "Compare working tree with HEAD",
"i18n.mentionSelectedLines": "Mention selected lines",
"i18n.disableWrap": "Disable word wrap",
"i18n.enableWrap": "Enable word wrap",
"i18n.htmlPreview": "HTML preview",
"i18n.previewFile": "Preview {file}",
"i18n.invalidMermaid": "Invalid Mermaid diagram",
"i18n.renderingMermaid": "Rendering Mermaid diagram",
"i18n.previewAfterStreaming": "Preview available after streaming",
"i18n.showMermaidSource": "Show Mermaid source",
"i18n.previewMermaid": "Preview Mermaid diagram",
"i18n.openMermaidViewer": "Open diagram viewer",
"i18n.mermaidViewer": "Mermaid diagram viewer",
"i18n.mermaidDiagram": "Mermaid diagram",
"i18n.zoomOut": "Zoom out",
"i18n.zoomIn": "Zoom in",
"i18n.fitToWidth": "Fit to width",
"models.discoveryFetching": "Importing models…",
"models.discoveryFetch": "Import models…",
"models.discoveryFilterPlaceholder": "Filter {count} models…",
"models.discoveryFilter": "Filter upstream models",
"models.discoverySelectShown": "Select shown",
"models.discoveryNoMatches": "No matching models.",
"models.discoveryAdded": "added",
"models.discoveryShowing": "Showing first {shown} of {total}",
"models.discoveryFetched": "{count} models fetched",
"models.discoveryAddSelected": "Add selected",
"models.discoveryAddSelectedCount": "Add selected ({count})",
"models.catalogSource": "source: models.dev ↗",
"models.catalogFilling": "Filling details…",
"models.catalogFill": "Fill model details",
"models.catalogFilled": "{count} fields filled",
"models.catalogNoEmptyFields": "No empty fields to fill",
"models.catalogNoExactMatch": "No exact models.dev match",
"models.catalogPriceProvider": "Price matched Provider {provider}",
"models.catalogPriceBaseUrl": "Price matched Base URL to {provider}",
"models.catalogPriceConsensus": "Price agreed by {support}/{total} records",
"models.catalogPriceUnreliable": "No reliable price found",
"models.catalogUndo": "Undo",
"i18n.testingModel": "Testing model connection...",
"i18n.connected": "Connected",
"i18n.failed": "Failed",
"i18n.test": "Test",
"i18n.testConnection": "Test model connection",
"i18n.provider": "Provider",
"i18n.model": "Model",
"i18n.providerName": "Provider name",
"i18n.rename": "Rename",
"i18n.default": "Default",
"i18n.disabled": "Disabled",
"i18n.noResources": "No resources",
"i18n.unknown": "Unknown",
"i18n.installedVersion": "installed {version}",
"i18n.configuredVersion": "configured {version}",
"i18n.extensions": "Extensions",
"i18n.prompts": "Prompts",
"i18n.themes": "Themes",
"i18n.resourceCount": "{count} {label}",
"i18n.extensionShort": "ext",
"i18n.skillShort": "skills",
"i18n.promptShort": "prompts",
"i18n.themeShort": "themes",
"i18n.custom": "Custom",
"i18n.clearAll": "clear all",
"i18n.connectedSuccessfully": "Connected successfully.",
"i18n.notConnected": "not connected",
"i18n.configured": "configured",
"i18n.notConfigured": "not configured",
"i18n.openingBrowser": "Opening browser…",
"i18n.submit": "Submit",
"i18n.login": "Login",
"i18n.relogin": "Re-login",
"i18n.disconnect": "Disconnect",
"i18n.networkError": "Network error",
"i18n.continuing": "Continuing…",
"i18n.skills": "Skills",
"i18n.plugins": "Plugins",
"i18n.addSkill": "Add skill",
"i18n.addPlugin": "Add plugin",
"i18n.selectSkill": "Select a skill",
"i18n.selectPackage": "Select a package",
"i18n.updateAvailable": "Update available",
"i18n.upToDate": "Up to date",
"i18n.automaticChecksUnavailable": "Automatic checks unavailable",
"i18n.checkFailed": "Check failed",
"i18n.installed": "Installed",
"i18n.status": "Status",
"i18n.version": "Version",
"i18n.package": "Package",
"i18n.resources": "Resources",
"i18n.installedPath": "Installed path",
"i18n.cwd": "CWD",
"i18n.resolvedResources": "Resolved Resources",
"i18n.reloadSession": "Reload session",
"i18n.reloading": "Reloading...",
"i18n.packageRemoved": "Package removed.",
"i18n.packageInstalled": "Package installed.",
"i18n.packageUpdated": "Package updated.",
"i18n.packageDisabled": "Package disabled.",
"i18n.packageEnabled": "Package enabled.",
"i18n.sessionReloaded": "Session reloaded.",
"i18n.thinking": "Thinking",
"i18n.loadingThinking": "Loading thinking...",
"i18n.copyMessage": "Copy message",
"i18n.editFromHere": "Edit from here",
"i18n.editFromHereTitle": "Edit from here — branches within this session",
"i18n.newSession": "New session",
"i18n.newSessionTitle": "New session — creates an independent copy from here",
"i18n.estimatedTokens": "Estimated token count while streaming",
"i18n.noOutput": "(no output)",
"i18n.noSummary": "(no summary)",
"i18n.conversationCompacted": "Conversation compacted",
"i18n.compactionDescription": "The conversation history before this point was compacted into the following summary:",
"i18n.fileContext": "File context: {details}",
"i18n.readFiles": "Read files",
"i18n.modifiedFiles": "Modified files",
"i18n.hiddenExtensionMessage": "hidden extension message",
"i18n.noMessage": "(no message)",
"i18n.showExtensionMessage": "Show extension message",
"i18n.collapse": "Collapse",
"i18n.expand": "Expand",
"i18n.hideDetails": "Hide details",
"i18n.showDetails": "Show details",
"i18n.thinkingUnavailable": "Thinking content unavailable",
"i18n.before": "Before",
"i18n.after": "After",
},
};
+430
View File
@@ -0,0 +1,430 @@
import type { LocalePlugin } from "../types";
/** Pi Web 内置简体中文语言包。 */
export const zhCNLocale: LocalePlugin = {
id: "zh-CN",
label: "简体中文",
messages: {
"common.ok": "确定",
"common.language": "语言",
"common.models": "模型",
"common.skills": "技能",
"common.plugins": "插件",
"sidebar.hide": "隐藏侧边栏",
"sidebar.show": "显示侧边栏",
"theme.light": "切换到浅色模式",
"theme.dark": "切换到深色模式",
"history.full": "完整历史",
"history.unsaved": "会话保存后才能查看完整历史",
"history.label": "完整历史",
"title.generating": "正在生成...",
"title.updated": "标题已更新",
"title.failed": "生成失败",
"title.generate": "生成标题",
"title.unsaved": "会话保存后才能生成标题",
"title.noMessages": "请先发送消息,再为会话命名",
"title.generateSession": "生成会话标题",
"system.prompt": "系统提示词",
"system.empty": "系统提示词为空(工具已禁用)",
"system.load": "发送消息以加载系统提示词",
"system.label": "系统",
"session.info": "会话信息",
"session.name": "名称",
"session.file": "文件",
"session.inMemory": "内存中",
"session.id": "ID",
"session.user": "用户",
"session.assistant": "助手",
"session.toolCalls": "工具调用",
"session.toolResults": "工具结果",
"session.total": "总计",
"session.input": "输入",
"session.output": "输出",
"session.cacheRead": "缓存读取",
"session.cacheWrite": "缓存写入",
"session.cost": "费用",
"session.context": "上下文",
"session.copy": "复制{value}",
"session.copied": "已复制",
"session.title": "会话信息",
"session.load": "发送消息或运行 /session 以加载会话信息",
"session.infoSection": "会话信息",
"session.messages": "消息",
"session.tokens": "Token",
"session.copyFile": "复制文件路径",
"session.copyId": "复制会话 ID",
"workspace.opening": "正在打开工作区...",
"workspace.unable": "无法打开工作区",
"workspace.selectSession": "从侧边栏选择一个会话",
"workspace.getStarted": "开始使用",
"workspace.selectProject": "从侧边栏选择项目目录",
"workspace.addModels": "点击底部的“模型”按钮添加模型",
"workspace.stepProject": "从侧边栏选择项目目录",
"workspace.stepModels": "点击底部的“模型”按钮添加模型",
"files.hidePanel": "隐藏文件面板",
"files.showPanel": "显示文件面板",
"files.noneOpen": "没有打开的文件",
"sidebar.new": "新建",
"sidebar.newSessionTitle": "在 {path} 中新建会话",
"sidebar.refresh": "刷新",
"sidebar.selectProject": "选择项目…",
"sidebar.filterProjects": "筛选项目…",
"sidebar.noMatchingProjects": "没有匹配的项目",
"sidebar.useDefaultDirectory": "使用默认目录",
"sidebar.customPath": "自定义路径…",
"directoryPicker.selectDirectory": "选择目录",
"directoryPicker.goToParent": "转到上级目录",
"directoryPicker.directoryPath": "目录路径",
"directoryPicker.goToDirectory": "转到目录",
"directoryPicker.go": "转到",
"directoryPicker.loadingDirectories": "正在加载目录…",
"directoryPicker.noSubdirectories": "没有子目录",
"directoryPicker.openBeforeSelecting": "选择前请先打开此路径",
"directoryPicker.selectCurrentDirectory": "选择当前目录",
"directoryPicker.selectThisFolder": "选择此文件夹",
"sidebar.checking": "检查中…",
"sidebar.open": "打开",
"sidebar.cancel": "取消",
"sidebar.openRepoRoot": "打开仓库根目录",
"sidebar.openRepoRootTitle": "打开仓库根目录以管理 worktree。",
"sidebar.gitRepoRootOnly": "仅 Git 仓库根目录",
"sidebar.gitRepoRootOnlyTitle": "只能在 Git 仓库根目录使用 worktree。",
"sidebar.worktrees": "Worktrees...",
"sidebar.checkingWorktrees": "正在检查此目录的 worktree。",
"sidebar.filterWorktrees": "筛选 worktree…",
"sidebar.noMatchingWorktrees": "没有匹配的 worktree",
"sidebar.switchWorktree": "切换 worktree",
"sidebar.switchWorktreeTitle": "切换 worktree:{path}",
"sidebar.main": "主分支",
"sidebar.forceRemoveCheckout": "存在未提交的更改。强制移除 checkout?",
"sidebar.force": "强制移除",
"sidebar.createWorktreeTitle": "为分支创建 worktree checkout",
"sidebar.newWorktree": "新建 worktree…",
"sidebar.branchName": "分支名称",
"sidebar.creating": "创建中…",
"sidebar.create": "创建",
"sidebar.uploadFilesTitle": "将文件上传到项目根目录",
"sidebar.uploadFiles": "上传文件",
"sidebar.refreshExplorer": "刷新文件浏览器",
"sidebar.changedFiles": "{count} 个变更文件",
"sidebar.loading": "加载中...",
"sidebar.noSessions": "未找到会话",
"sidebar.agentRunning": "Agent 运行中…",
"sidebar.newActivity": "有新活动",
"sidebar.newSessionActivity": "会话有新活动",
"sidebar.deleteSession": "删除 {title}?",
"sidebar.rename": "重命名",
"sidebar.delete": "删除",
"sidebar.deleteWithShiftClick": "删除(按住 Shift 点击可跳过确认)",
"sidebar.expandForks": "展开分叉",
"sidebar.collapseForks": "折叠分叉",
"sidebar.messagesCount": "{count} 条消息",
"sidebar.worktreePath": "Worktree:{path}",
"sidebar.removeWorktreeTitle": "移除 worktree checkout {path};保留分支",
"files.modified": "已修改",
"files.added": "已添加",
"files.deleted": "已删除",
"files.renamed": "已重命名",
"files.untracked": "未跟踪",
"files.conflict": "冲突",
"files.newlyUploaded": "刚上传",
"files.containsChangedFiles": "包含已更改文件",
"files.insertPath": "将路径插入聊天",
"files.mention": "提及",
"files.download": "下载文件",
"files.dismissError": "关闭错误",
"files.addUploadedFile": "将上传的文件添加到聊天",
"files.addAllUploadedFiles": "将所有上传的文件添加到聊天",
"files.dismissUploadResults": "关闭上传结果",
"files.loading": "正在加载文件...",
"files.noFiles": "未找到文件",
"files.explorer": "文件浏览器",
"files.changedCount": "{count} 个文件",
"files.changeStats": "{count} 个变更文件,新增 {additions} 行,删除 {deletions} 行",
"files.conflictSummary": "已有 {count} 个文件:{files}",
"files.cannotReplace": "无法替换:{files}",
"files.replace": "替换",
"files.skipExisting": "跳过已有文件",
"files.cancel": "取消",
"files.checking": "正在检查文件",
"files.uploading": "正在上传,{progress}%",
"chat.loadingSession": "正在加载会话...",
"chat.runningTool": "正在运行工具...",
"chat.runningNamedTool": "正在运行 {name}...",
"chat.runningTools": "正在运行 {names}...",
"chat.runningToolsMore": "正在运行 {names}(另有 {count} 个)...",
"chat.waitingModel": "正在等待模型...",
"chat.runningCommand": "正在运行命令...",
"chat.thinking": "正在思考...",
"chat.processDetails": "处理详情",
"chat.message": "条消息",
"chat.messages": "条消息",
"chat.toolCall": "次工具调用",
"chat.toolCalls": "次工具调用",
"chat.collapseProcess": "收起处理详情",
"chat.expandProcess": "展开处理详情",
"chat.loadEarlier": "向上滚动以加载更早的消息(隐藏 {count} 条)",
"chat.extensionRequest": "扩展请求",
"chat.cancel": "取消",
"chat.confirm": "确认",
"chat.submit": "提交",
"chat.extensionInput": "扩展终端输入",
"chat.extensionPanel": "扩展面板",
"chat.close": "关闭",
"chat.queued": "已排队 · {count}",
"chat.recall": "移回输入框",
"chat.recallTitle": "移除所有排队消息并将其放回输入框编辑",
"chat.retrying": "正在重试({attempt}/{max})…",
"chat.loadingCommands": "正在加载命令...",
"chat.slashCommands": "斜杠命令 · {label}",
"chat.tabEnter": "Tab / Enter",
"chat.noCommands": "未找到扩展、提示词或技能命令",
"chat.match": "1 个匹配项",
"chat.matches": "{count} 个匹配项",
"chat.command": "1 个命令",
"chat.commands": "{count} 个命令",
"chat.loadingFiles": "正在加载文件...",
"chat.files": "文件 · {label}{hint}",
"chat.searching": "搜索中…",
"chat.noMatchingFiles": "未找到匹配的文件",
"chat.searchingAll": " · 正在搜索所有文件…",
"chat.indexTruncated": " · 索引不完整",
"chat.steerPlaceholder": "立即引导 / 排队后续消息...",
"chat.agentPlaceholder": "Agent 运行中…",
"chat.messagePlaceholder": "消息…输入 / 使用命令,输入 @ 查找文件",
"chat.steer": "引导",
"chat.followUp": "后续消息",
"chat.send": "发送",
"chat.attachImage": "附加图片",
"chat.filterModels": "筛选模型…",
"chat.noMatchingModels": "没有匹配的模型",
"chat.moreControls": "更多控件",
"chat.collapseControls": "收起控件",
"chat.shell": "Shell",
"chat.outputLocal": "输出保留在本地",
"chat.outputModel": "输出发送给模型",
"chat.noTools": "不使用工具,仅只读",
"chat.builtInTools": "{count} 个内置工具",
"chat.allBuiltInTools": "全部内置工具",
"chat.changeReasoning": "更改推理级别:{level}",
"chat.changeReasoningLabel": "更改推理级别",
"chat.changeToolPreset": "更改工具预设",
"chat.stopCompaction": "停止压缩",
"chat.compactContext": "压缩上下文",
"chat.compacting": "正在压缩…",
"chat.compact": "压缩",
"chat.stopAgent": "停止 Agent",
"chat.stop": "停止",
"chat.disableSound": "关闭完成提示音",
"chat.enableSound": "开启完成提示音",
"chat.thinkingUseDefault": "使用 pi 默认设置",
"chat.thinkingOff": "关闭推理",
"chat.thinkingMinimal": "最低限度推理",
"chat.thinkingLow": "低强度推理",
"chat.thinkingMedium": "中等强度推理",
"chat.thinkingHigh": "高强度推理",
"chat.thinkingXhigh": "超高强度推理",
"chat.thinkingMax": "最高强度推理",
"chat.builtIn": "内置",
"chat.extensions": "扩展",
"chat.prompts": "提示词",
"chat.skills": "技能",
"chat.commandCompact": "压缩上下文,可选附加说明",
"chat.commandReload": "重新加载扩展、技能、提示词和工具",
"chat.commandName": "设置会话显示名称",
"chat.commandSession": "显示会话消息、Token 和费用统计",
"chat.commandCopy": "复制最后一条助手消息",
"chat.compacted": "已压缩",
"chat.tokensSaved": "节省 {saved}",
"i18n.close": "关闭",
"i18n.copy": "复制",
"i18n.copied": "已复制",
"i18n.cancel": "取消",
"i18n.save": "保存",
"i18n.saving": "保存中…",
"i18n.saved": "已保存",
"i18n.remove": "移除",
"i18n.removing": "移除中…",
"i18n.delete": "删除",
"i18n.loading": "加载中...",
"i18n.search": "搜索",
"i18n.refresh": "刷新",
"i18n.update": "更新",
"i18n.updating": "更新中...",
"i18n.check": "检查",
"i18n.checking": "检查中...",
"i18n.install": "安装",
"i18n.installing": "安装中...",
"i18n.noResults": "没有结果",
"i18n.searchProviders": "搜索 Provider…",
"i18n.noProviders": "没有匹配的 Provider",
"i18n.newModel": "新模型",
"i18n.addProvider": "添加 Provider",
"i18n.selectProviderModel": "选择 Provider 或模型",
"i18n.visibleInPrompt": "在模型提示词中可见,点击以禁用",
"i18n.hiddenFromPrompt": "在模型提示词中隐藏,点击以启用",
"i18n.skillSearchPlaceholder": "例如 react、testing、deploy",
"i18n.searching": "搜索中…",
"i18n.noSkills": "未找到技能",
"i18n.checkUpdates": "检查更新",
"i18n.updates": "项更新",
"i18n.noResolvedResources": "没有已解析资源",
"i18n.enablePackage": "启用包",
"i18n.disablePackage": "禁用包",
"i18n.filtered": "已筛选",
"i18n.openSessionToReload": "打开会话后才能重新加载",
"i18n.notFound": "未找到",
"i18n.noPlugins": "未配置插件",
"trust.resourcesNotLoaded": "受限模式",
"trust.trustProject": "信任项目",
"trust.trusting": "正在信任...",
"trust.cancel": "取消",
"trust.dialogTitle": "信任此项目?",
"trust.dialogBody": "项目资源可以运行本地代码。请仅信任你了解其内容的项目。",
"trust.skillsNotLoaded": "此项目尚未受信任,项目技能未加载。",
"trust.pluginsNotLoaded": "此项目尚未受信任,项目插件未加载。",
"trust.projectScopeUnavailable": "项目资源未加载时,无法安装到项目范围。",
"i18n.creatingSession": "正在创建新会话…",
"i18n.creating": "创建中…",
"i18n.subscription": "订阅",
"i18n.subscriptions": "订阅服务",
"i18n.customEndpoint": "自定义端点格式",
"i18n.noChanges": "没有更改",
"i18n.branches": "分支",
"i18n.noActiveSession": "没有活动会话",
"i18n.noBranches": "此会话没有分支",
"i18n.source": "源代码",
"i18n.preview": "预览",
"i18n.diff": "Diff",
"i18n.downloadFile": "下载文件",
"i18n.liveSync": "实时同步已启用",
"i18n.notWatching": "未监视",
"i18n.fileViewMode": "文件视图模式",
"i18n.compareHead": "比较工作区与 HEAD",
"i18n.mentionSelectedLines": "提及所选行",
"i18n.disableWrap": "禁用自动换行",
"i18n.enableWrap": "启用自动换行",
"i18n.htmlPreview": "HTML 预览",
"i18n.previewFile": "预览 {file}",
"i18n.invalidMermaid": "Mermaid 图表无效",
"i18n.renderingMermaid": "正在渲染 Mermaid 图表",
"i18n.previewAfterStreaming": "流式输出完成后可预览",
"i18n.showMermaidSource": "显示 Mermaid 源码",
"i18n.previewMermaid": "预览 Mermaid 图表",
"i18n.openMermaidViewer": "打开图表查看器",
"i18n.mermaidViewer": "Mermaid 图表查看器",
"i18n.mermaidDiagram": "Mermaid 图表",
"i18n.zoomOut": "缩小",
"i18n.zoomIn": "放大",
"i18n.fitToWidth": "适应宽度",
"models.discoveryFetching": "正在导入模型…",
"models.discoveryFetch": "导入模型…",
"models.discoveryFilterPlaceholder": "筛选 {count} 个模型…",
"models.discoveryFilter": "筛选上游模型",
"models.discoverySelectShown": "选择当前结果",
"models.discoveryNoMatches": "没有匹配的模型。",
"models.discoveryAdded": "已添加",
"models.discoveryShowing": "显示前 {shown} 个,共 {total} 个",
"models.discoveryFetched": "已获取 {count} 个模型",
"models.discoveryAddSelected": "添加所选模型",
"models.discoveryAddSelectedCount": "添加所选模型({count})",
"models.catalogSource": "来源:models.dev ↗",
"models.catalogFilling": "正在填入模型信息…",
"models.catalogFill": "填入模型信息",
"models.catalogFilled": "已填入 {count} 个字段",
"models.catalogNoEmptyFields": "没有需要填入的空字段",
"models.catalogNoExactMatch": "models.dev 中没有精确匹配",
"models.catalogPriceProvider": "价格已匹配 Provider {provider}",
"models.catalogPriceBaseUrl": "价格已按 Base URL 匹配 {provider}",
"models.catalogPriceConsensus": "价格得到 {support}/{total} 条记录支持",
"models.catalogPriceUnreliable": "未找到可靠价格",
"models.catalogUndo": "撤销",
"i18n.testingModel": "正在测试模型连接...",
"i18n.connected": "已连接",
"i18n.failed": "失败",
"i18n.test": "测试",
"i18n.testConnection": "测试模型连接",
"i18n.provider": "Provider",
"i18n.model": "模型",
"i18n.providerName": "Provider 名称",
"i18n.rename": "重命名",
"i18n.default": "默认",
"i18n.disabled": "已禁用",
"i18n.noResources": "没有资源",
"i18n.unknown": "未知",
"i18n.installedVersion": "已安装 {version}",
"i18n.configuredVersion": "已配置 {version}",
"i18n.extensions": "扩展",
"i18n.prompts": "提示词",
"i18n.themes": "主题",
"i18n.resourceCount": "{count}{label}",
"i18n.extensionShort": "扩展",
"i18n.skillShort": "技能",
"i18n.promptShort": "提示词",
"i18n.themeShort": "主题",
"i18n.custom": "自定义",
"i18n.clearAll": "清除全部",
"i18n.connectedSuccessfully": "连接成功。",
"i18n.notConnected": "未连接",
"i18n.configured": "已配置",
"i18n.notConfigured": "未配置",
"i18n.openingBrowser": "正在打开浏览器…",
"i18n.submit": "提交",
"i18n.login": "登录",
"i18n.relogin": "重新登录",
"i18n.disconnect": "断开连接",
"i18n.networkError": "网络错误",
"i18n.continuing": "继续中…",
"i18n.skills": "技能",
"i18n.plugins": "插件",
"i18n.addSkill": "添加技能",
"i18n.addPlugin": "添加插件",
"i18n.selectSkill": "选择技能",
"i18n.selectPackage": "选择包",
"i18n.updateAvailable": "有可用更新",
"i18n.upToDate": "已是最新",
"i18n.automaticChecksUnavailable": "自动检查不可用",
"i18n.checkFailed": "检查失败",
"i18n.installed": "已安装",
"i18n.status": "状态",
"i18n.version": "版本",
"i18n.package": "包",
"i18n.resources": "资源",
"i18n.installedPath": "安装路径",
"i18n.cwd": "CWD",
"i18n.resolvedResources": "已解析资源",
"i18n.reloadSession": "重新加载会话",
"i18n.reloading": "重新加载中...",
"i18n.packageRemoved": "包已移除。",
"i18n.packageInstalled": "包已安装。",
"i18n.packageUpdated": "包已更新。",
"i18n.packageDisabled": "包已禁用。",
"i18n.packageEnabled": "包已启用。",
"i18n.sessionReloaded": "会话已重新加载。",
"i18n.thinking": "思考",
"i18n.loadingThinking": "正在加载思考内容...",
"i18n.copyMessage": "复制消息",
"i18n.editFromHere": "从此处编辑",
"i18n.editFromHereTitle": "从此处编辑,此会话内创建分支",
"i18n.newSession": "新会话",
"i18n.newSessionTitle": "新会话,从此处创建独立副本",
"i18n.estimatedTokens": "流式输出期间的估算 Token 数",
"i18n.noOutput": "(无输出)",
"i18n.noSummary": "(无摘要)",
"i18n.conversationCompacted": "会话已压缩",
"i18n.compactionDescription": "此处之前的会话历史已压缩为以下摘要:",
"i18n.fileContext": "文件上下文:{details}",
"i18n.readFiles": "读取的文件",
"i18n.modifiedFiles": "修改的文件",
"i18n.hiddenExtensionMessage": "隐藏的扩展消息",
"i18n.noMessage": "(无消息)",
"i18n.showExtensionMessage": "显示扩展消息",
"i18n.collapse": "收起",
"i18n.expand": "展开",
"i18n.hideDetails": "隐藏详情",
"i18n.showDetails": "显示详情",
"i18n.thinkingUnavailable": "思考内容不可用",
"i18n.before": "之前",
"i18n.after": "之后",
},
};
+30
View File
@@ -0,0 +1,30 @@
import assert from "node:assert/strict";
import test from "node:test";
import {
getLocalePlugin,
getSupportedLocales,
registerLocale,
resolveBrowserLocale,
} from "./registry.ts";
test("uses the first supported browser language and falls back to English", () => {
assert.equal(resolveBrowserLocale(["zh-CN", "en-US"]), "zh-CN");
assert.equal(resolveBrowserLocale(["zh", "en-US"]), "zh-CN");
assert.equal(resolveBrowserLocale(["en-US", "zh-CN"]), "en");
assert.equal(resolveBrowserLocale(["fr-FR", "zh-CN"]), "zh-CN");
assert.equal(resolveBrowserLocale(["fr-FR"]), "en");
assert.equal(resolveBrowserLocale([]), "en");
});
test("returns only registered locales", () => {
assert.deepEqual(getSupportedLocales(), ["en", "zh-CN"]);
assert.equal(getLocalePlugin("en").id, "en");
assert.equal(getLocalePlugin("missing"), undefined);
});
test("allows a new locale plugin and rejects duplicate ids", () => {
registerLocale({ id: "test", label: "Test", messages: { "common.ok": "OK" } });
assert.equal(getLocalePlugin("test")?.label, "Test");
assert.throws(() => registerLocale({ id: "test", label: "Again", messages: {} }));
});
+43
View File
@@ -0,0 +1,43 @@
import { enLocale } from "./messages/en";
import { zhCNLocale } from "./messages/zh-CN";
import type { Locale, LocalePlugin } from "./types";
const localePlugins = new Map<string, LocalePlugin>();
/** 注册一个语言包;重复注册会抛出异常,避免静默覆盖翻译。 */
export function registerLocale(plugin: LocalePlugin): void {
if (!plugin.id.trim()) throw new Error("Locale id must not be empty");
if (localePlugins.has(plugin.id)) throw new Error(`Locale already registered: ${plugin.id}`);
localePlugins.set(plugin.id, plugin);
}
/**
* 根据标识获取已注册的语言包。
* @param id 要查询的语言标识
* @returns 已注册的语言包,不存在时返回 undefined
*/
export function getLocalePlugin(id: string): LocalePlugin | undefined {
return localePlugins.get(id);
}
/** 获取当前已注册语言的稳定顺序列表。 */
export function getSupportedLocales(): string[] {
return [...localePlugins.keys()];
}
/**
* 将浏览器语言列表解析为 Pi Web 内置语言。
* @param languages 浏览器按优先级排列的语言列表
* @returns 匹配的内置语言,无法匹配时返回英语
*/
export function resolveBrowserLocale(languages: readonly string[]): Locale {
for (const language of languages) {
const normalized = language.toLowerCase();
if (normalized === "en" || normalized.startsWith("en-")) return "en";
if (normalized === "zh" || normalized.startsWith("zh-")) return "zh-CN";
}
return "en";
}
registerLocale(enLocale);
registerLocale(zhCNLocale);
+15
View File
@@ -0,0 +1,15 @@
/** 内置的可用界面语言。 */
export type Locale = "en" | "zh-CN";
/** 翻译字符串使用的简单插值参数。 */
export type TranslationParams = Record<string, string | number>;
/** 可注册的语言包定义。 */
export interface LocalePlugin {
/** 语言包唯一标识。 */
id: string;
/** 用于语言选择菜单的显示名称。 */
label: string;
/** 以稳定 key 索引的翻译消息。 */
messages: Record<string, string>;
}