feat(i18n): 新增简体中文界面翻译 (#15)

* feat(i18n): 新增简体中文界面翻译

* test(i18n): 补充中文界面截图与端到端验证

* fix(i18n): Translate notifications and usages through catalog templates

Move the hard-coded Chinese notification patterns into source-text
templates (`{name} failed`) applied at render time, so text follows
locale switches and stays English under en. Use useTx in Search
Everywhere and add its missing zh strings.

* chore: Drop i18n screenshots

---------

Co-authored-by: l0ng-ai <[email protected]>
Цей коміт міститься в:
Richardтаl0ng-ai authored and GitHub committed 2026-10-24 14:14:43 +08:00
1 джерело 3d8a6f07b4
коміт 5b8e0b9659
9 files changed
+197 -38

No files matched your search

+13 -6
Переглянути файл
@@ -1,5 +1,6 @@
/** "usages" tool window: Find Usages results grouped by file with line previews. */
import { useCallback, useMemo, useState } from "react";
import { formatMessage, useTx } from "@/platform/i18n";
import { basename, relativeDir, useProject } from "@/platform/project";
import { EmptyState, Icon, IconButton, Spinner, Tabs, Tree, fileIconName, type TreeRowState, T } from "@/ui";
import { openLocation } from "./ipc";
@@ -24,9 +25,10 @@ export function UsagePreview({ usage }: { usage: Usage }) {
/** Tool window header: the current search as a content tab (`Usages of 'foo'`). */
export function UsagesTabs() {
const tx = useTx();
const search = useUsages((s) => s.search);
if (!search) return null;
const label = search.symbol ? `Usages of '${search.symbol}'` : "Usages";
const label = search.symbol ? formatMessage(tx("Usages of '{symbol}'"), { symbol: search.symbol }) : tx("Usages");
return (
<Tabs
variant="compact"
@@ -42,6 +44,7 @@ export function UsagesTabs() {
const SUMMARY_KEY = "\u0000summary";
export function UsagesView() {
const tx = useTx();
const search = useUsages((s) => s.search);
const root = useProject((s) => s.project?.root ?? "");
const [collapsed, setCollapsed] = useState<{ id: number | null; keys: ReadonlySet<string> }>({
@@ -86,7 +89,7 @@ export function UsagesView() {
<T>Found usages</T>
</span>
<span className={styles.count}>
{total} {total === 1 ? "result" : "results"}
{total} {tx(total === 1 ? "result" : "results")}
</span>
{search.note && <span className={styles.note}>{search.note}</span>}
</span>
@@ -99,7 +102,7 @@ export function UsagesView() {
<span className={styles.file}>{basename(path)}</span>
<span className={styles.muted}>{relativeDir(root, path)}</span>
<span className={styles.count}>
{n.group.usages.length} {n.group.usages.length === 1 ? "usage" : "usages"}
{n.group.usages.length} {tx(n.group.usages.length === 1 ? "usage" : "usages")}
</span>
</span>
);
@@ -117,7 +120,7 @@ export function UsagesView() {
<div className={styles.toolbar} role="toolbar" aria-orientation="vertical" aria-label="Usages actions">
<IconButton
icon="rerun"
title="Rerun Find Usages"
title={tx("Rerun Find Usages")}
size="small"
iconSize={16}
disabled={!search.rerun}
@@ -132,7 +135,11 @@ export function UsagesView() {
<div className={styles.summary} role="status" aria-live="polite">
{search.loading ? (
<>
<Spinner size={12} /> Searching for usages{search.symbol ? ` of '${search.symbol}'` : ""}…
<Spinner size={12} />{" "}
{search.symbol
? formatMessage(tx("Searching for usages of '{symbol}'"), { symbol: search.symbol })
: tx("Searching for usages")}
…
</>
) : (
<span className={styles.error}>{search.error}</span>
@@ -145,7 +152,7 @@ export function UsagesView() {
roots={roots}
getKey={(n) => n.key}
getLabel={(n) =>
n.kind === "summary" ? "Found usages" : n.kind === "file" ? basename(n.group.path) : n.usage.text.trim()
n.kind === "summary" ? tx("Found usages") : n.kind === "file" ? basename(n.group.path) : n.usage.text.trim()
}
getChildren={getChildren}
isExpandable={(n) => n.kind !== "usage"}
+16 -4
Переглянути файл
@@ -3,6 +3,7 @@ import clsx from "clsx";
import { BarButton, Icon, IconButton, Menu, type MenuItem } from "@/ui";
import { executeAction } from "@/platform/actions";
import { useContextKeys } from "@/platform/context";
import { useTx } from "@/platform/i18n";
import { shortcutLabel } from "@/platform/keymap";
import { runExecutors, useRunExecutors } from "@/workbench/contributions";
import { configIcon } from "./configs";
@@ -11,6 +12,7 @@ import styles from "./Run.module.css";
/** Title bar: configuration chooser, Run / Debug, and Rerun / Stop while running. */
export function RunWidget() {
const tx = useTx();
const configs = useRun((s) => s.configs);
const selectedId = useRun((s) => s.selectedId);
const sessions = useRun((s) => s.sessions);
@@ -48,7 +50,7 @@ export function RunWidget() {
<BarButton
variant="title"
className={styles.widgetChooser}
tooltip="Run/Debug Configurations"
tooltip={tx("Run/Debug Configurations")}
shortcut={shortcutLabel("run.chooseConfig")}
aria-haspopup="listbox"
aria-expanded={chooserOpen}
@@ -59,10 +61,14 @@ export function RunWidget() {
// IDEA: the configuration's type icon, with a status dot on its top-right corner while active.
<span className={styles.widgetConfigIcon}>
<Icon name={configIcon(selected)} size={16} />
{active && <span className={styles.widgetStatusDot} aria-label={debugging ? "Debugging" : "Running"} role="img" />}
{active && (
<span className={styles.widgetStatusDot} aria-label={debugging ? "Debugging" : "Running"} role="img" />
)}
</span>
)}
<span className={styles.widgetName}>{selected?.name ?? (configs.length ? "Select Configuration" : "Add Configuration…")}</span>
<span className={styles.widgetName}>
{selected?.name ?? tx(configs.length ? "Select Configuration" : "Add Configuration…")}
</span>
<Icon name="chevron-down" size={14} className={styles.widgetChevron} />
</BarButton>
<span className={styles.widgetButtons}>
@@ -156,7 +162,13 @@ export function RunWidget() {
aria-expanded={moreOpen}
onClick={() => setMoreOpen((v) => !v)}
/>
<Menu open={moreOpen} anchor={moreRef} items={moreItems} placement="bottom-end" onClose={() => setMoreOpen(false)} />
<Menu
open={moreOpen}
anchor={moreRef}
items={moreItems}
placement="bottom-end"
onClose={() => setMoreOpen(false)}
/>
</span>
</div>
);
+21 -10
Переглянути файл
@@ -2,6 +2,7 @@ import { useEffect, useMemo, useRef, useState, type KeyboardEvent, type MouseEve
import clsx from "clsx";
import { actionToggled, executeAction, getAction } from "@/platform/actions";
import type { ProjectFileSet } from "@/platform/ipc";
import { useTx } from "@/platform/i18n";
import { hasPrimaryModifier, shortcutLabel } from "@/platform/keymap";
import {
Checkbox,
@@ -62,6 +63,7 @@ export function SearchEverywhereOverlay() {
type Selection = { key: string | null; index: number; moved: boolean };
function SearchEverywhereDialog() {
const tx = useTx();
const tab = useSearchEverywhere((s) => s.tab);
const query = useSearchEverywhere((s) => s.query);
const includeNonProject = useSearchEverywhere((s) => s.includeNonProject);
@@ -251,11 +253,11 @@ function SearchEverywhereDialog() {
width={670}
position="top"
transparentBackdrop
aria-label="Search Everywhere"
aria-label={tx("Search Everywhere")}
className={styles.dialog}
>
<div className={styles.header}>
<div role="tablist" aria-label="Search scope" className={styles.tabs} onKeyDown={onTabKey}>
<div role="tablist" aria-label={tx("Search scope")} className={styles.tabs} onKeyDown={onTabKey}>
{TABS.map((t) => (
<button
key={t.id}
@@ -267,7 +269,7 @@ function SearchEverywhereDialog() {
onMouseDown={(e) => e.preventDefault()}
onClick={() => setTab(t.id)}
>
{t.label}
<T>{t.label}</T>
</button>
))}
</div>
@@ -300,7 +302,7 @@ function SearchEverywhereDialog() {
icon="external-link"
size="small"
className={styles.headerButton}
title="Open in Find Tool Window"
title={tx("Open in Find Tool Window")}
shortcut={tab === "text" ? "⌘↵" : undefined}
onClick={() => closeThen(() => void executeAction("find.inPath", { query: parsed.text }))}
/>
@@ -322,13 +324,13 @@ function SearchEverywhereDialog() {
aria-expanded
aria-controls="se-results"
aria-autocomplete="list"
aria-label="Search query"
aria-label={tx("Search query")}
onChange={(e) => useSearchEverywhere.setState({ query: e.target.value })}
onKeyDown={onKeyDown}
/>
{snap?.loading && !emptyQuery && <Spinner size={14} className={styles.spinner} />}
<span className={styles.hint} aria-hidden>
{hintFor(tab)}
<T>{hintFor(tab)}</T>
</span>
</div>
</div>
@@ -344,7 +346,9 @@ function SearchEverywhereDialog() {
items={rows}
getKey={(r) => r.key}
isSection={isSectionRow}
renderSection={(r) => <span className={styles.section}>{r.kind === "section" ? r.title : ""}</span>}
renderSection={(r) => (
<span className={styles.section}>{r.kind === "section" ? <T>{r.title}</T> : ""}</span>
)}
activeIndex={active}
onActiveChange={select}
onActivate={(r, _i, e) => activate(r, (e as MouseEvent).shiftKey)}
@@ -352,7 +356,7 @@ function SearchEverywhereDialog() {
rowHeight={ROW}
sectionHeight={ROW}
className={styles.list}
aria-label="Results"
aria-label={tx("Results")}
renderItem={(r, state) => (
<ResultRow row={r} active={state.active} toggleTick={toggleTick} onToggled={bumpToggles} />
)}
@@ -360,7 +364,9 @@ function SearchEverywhereDialog() {
)}
{showEmpty && (
<div className={styles.empty}>
{emptyQuery ? "Type to search" : includeNonProject ? "Nothing found" : "Nothing found in project files"}
{tx(
emptyQuery ? "Type to search" : includeNonProject ? "Nothing found" : "Nothing found in project files",
)}
</div>
)}
</div>
@@ -435,12 +441,17 @@ function ResultRow({
toggleTick: number;
onToggled: () => void;
}) {
const tx = useTx();
const tone = "strong";
switch (row.kind) {
case "section":
return null;
case "more":
return <span className={clsx(styles.name, styles.more)}>… more {SECTION_TITLES[row.section].toLowerCase()}</span>;
return (
<span className={clsx(styles.name, styles.more)}>
… {tx("more")} {tx(SECTION_TITLES[row.section]).toLowerCase()}
</span>
);
case "file":
return <FileRow path={row.path} name={row.name} dir={row.dir} indices={row.nameIndices} line={row.line} />;
case "symbol":
+22 -12
Переглянути файл
@@ -4,6 +4,7 @@
*/
import type { Action } from "@/platform/actions";
import type { FileMatch, LineMatch, SymbolMatch, WorkspaceSymbol } from "@/platform/ipc";
import { tx } from "@/platform/i18n";
import { basename, isTypeKind, relDir, symbolDisplayName } from "../common/open";
import { byScore, matchText } from "../common/rank";
@@ -131,7 +132,11 @@ export interface SectionState {
// ---------------------------------------------------------------------------
// Item builders
export function fileItem(m: Pick<FileMatch, "path" | "relPath" | "indices">, line: number | null = null, col: number | null = null): FileItem {
export function fileItem(
m: Pick<FileMatch, "path" | "relPath" | "indices">,
line: number | null = null,
col: number | null = null,
): FileItem {
const name = basename(m.relPath);
const nameStart = m.relPath.length - name.length;
return {
@@ -239,12 +244,19 @@ export function matchActions(
if (!enabled && opts.includeDisabled === false) continue;
let score: number;
let indices: number[] = [];
const title = matchText(a.title, q, { wordAligned: true });
const translated = tx(a.title);
const title = matchText(translated, q, { wordAligned: true });
// The English title still matches; its indices don't apply to the translated text.
const english = title || translated === a.title ? undefined : matchText(a.title, q, { wordAligned: true });
if (title) {
score = title.score;
indices = title.indices;
} else if (english) {
score = english.score;
} else {
const extra = [a.category, a.description, ...(a.keywords ?? [])].filter((s): s is string => !!s);
const extra = [a.category ? tx(a.category) : "", a.description, ...(a.keywords ?? [])].filter(
(s): s is string => !!s,
);
const best = Math.max(-Infinity, ...extra.map((s) => matchText(s, q, { wordAligned: true })?.score ?? -Infinity));
const idHit = a.id.toLowerCase().includes(q.toLowerCase()) ? 0 : -Infinity;
score = Math.max(best, idHit) - 400;
@@ -257,8 +269,8 @@ export function matchActions(
kind: "action",
key: `a:${a.id}`,
id: a.id,
title: a.title,
category: a.category,
title: translated,
category: a.category ? tx(a.category) : a.category,
icon: a.icon,
enabled,
indices,
@@ -273,8 +285,8 @@ export function actionItem(a: Action, enabled: boolean): ActionItem {
kind: "action",
key: `a:${a.id}`,
id: a.id,
title: a.title,
category: a.category,
title: tx(a.title),
category: a.category ? tx(a.category) : a.category,
icon: a.icon,
enabled,
indices: [],
@@ -288,7 +300,8 @@ export function textItems(path: string, rel: string, matches: readonly LineMatch
const lead = m.text.length - m.text.trimStart().length;
const text = m.text.slice(lead).trimEnd();
const indices: number[] = [];
for (const [s, e] of m.ranges) for (let i = Math.max(s, lead); i < e; i++) if (i - lead < text.length) indices.push(i - lead);
for (const [s, e] of m.ranges)
for (let i = Math.max(s, lead); i < e; i++) if (i - lead < text.length) indices.push(i - lead);
const first = m.ranges[0] ?? [0, 0];
out.push({
kind: "text",
@@ -308,10 +321,7 @@ export function textItems(path: string, rel: string, matches: readonly LineMatch
// ---------------------------------------------------------------------------
// Rows
export function buildRows(
sections: ReadonlyMap<SectionId, SectionState>,
opts: { headers: boolean },
): SeRow[] {
export function buildRows(sections: ReadonlyMap<SectionId, SectionState>, opts: { headers: boolean }): SeRow[] {
const rows: SeRow[] = [];
for (const id of SECTION_ORDER) {
const s = sections.get(id);
+17
Переглянути файл
@@ -9,6 +9,7 @@ import {
setLocale,
t,
tx,
txTemplate,
useI18n,
} from "./i18n";
import en from "./locales/en.json";
@@ -116,6 +117,16 @@ describe("t / locales", () => {
expect(tx(undefined)).toBeUndefined();
});
it("matches source-text templates, translating the substituted text", async () => {
expect(txTemplate("Push failed")).toBe("Push failed");
await setLocale("zh");
expect(txTemplate("ruff is not installed")).toBe("ruff 未安装");
expect(txTemplate("Language server 'gopls' is not installed")).toBe("语言服务器 'gopls' 未安装");
expect(txTemplate("Retry failed")).toBe(`${tx("Retry")}失败`);
expect(txTemplate("Sync with OS")).toBe("与操作系统同步");
expect(txTemplate("Something else entirely")).toBe("Something else entirely");
});
it("lists keys by prefix", () => {
const keys = keysWithPrefix("menu.");
expect(keys).toContain("menu.file.save");
@@ -133,6 +144,12 @@ describe("catalogs", () => {
}
});
it("keep the placeholders of source-text templates", () => {
const names = (m: string) => [...m.matchAll(/\{(\w+)\}/g)].map((x) => x[1]).sort();
for (const [source, text] of Object.entries(zhText as Record<string, string>))
expect(names(text), source).toEqual(names(source));
});
it("have the same keys", () => {
expect(Object.keys(zh).sort()).toEqual(Object.keys(en).sort());
});
+56
Переглянути файл
@@ -178,6 +178,62 @@ export function useTx(): typeof tx {
}, [texts]);
}
type Template = { pattern: RegExp; names: string[]; translation: string; literal: number };
const templateCache = new WeakMap<Catalog, Template[]>();
/** Source-text entries with `{name}` placeholders, e.g. `"{name} is not installed"`. */
function templatesOf(texts: Catalog): Template[] {
let templates = templateCache.get(texts);
if (templates) return templates;
templates = [];
for (const [source, translation] of Object.entries(texts)) {
const names: string[] = [];
const parts = source.split(/\{(\w+)\}/);
if (parts.length < 2) continue;
const pattern = parts
.map((part, i) => (i % 2 ? (names.push(part), "(.+?)") : part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")))
.join("");
templates.push({
pattern: new RegExp(`^${pattern}$`),
names,
translation,
literal: source.length - names.join("").length,
});
}
// Most specific first: `Language server '{name}' is not installed` before `{name} is not installed`.
templates.sort((a, b) => b.literal - a.literal);
templateCache.set(texts, templates);
return templates;
}
function translateText(texts: Catalog, text: string): string {
const exact = texts[text];
if (exact !== undefined) return exact;
for (const { pattern, names, translation } of templatesOf(texts)) {
const m = pattern.exec(text);
if (!m) continue;
const params: MessageParams = {};
names.forEach((name, i) => (params[name] = texts[m[i + 1]!] ?? m[i + 1]));
return formatMessage(translation, params);
}
return text;
}
/**
* `tx` for text built at runtime (notification titles): also matches source-text
* templates such as `"{name} failed"`, translating the substituted parts as text.
*/
export function txTemplate(text: string): string {
return translateText(useI18n.getState().texts, text);
}
/** `txTemplate` bound to the current locale; components re-render when it changes. */
export function useTxTemplate(): (text: string) => string {
const texts = useI18n((s) => s.texts);
return useCallback((text: string) => translateText(texts, text), [texts]);
}
/** All keys with `prefix`, across the active catalog and the English fallback. */
export function keysWithPrefix(prefix: string): string[] {
const keys = new Set<string>();
+42
Переглянути файл
@@ -30,6 +30,7 @@
"Accessibility": "辅助功能",
"Action id (bind it in Keymap)": "操作 ID(可在“按键映射”中绑定)",
"Actions": "操作",
"Add Configuration…": "添加配置…",
"Actions on Save": "保存时操作",
"Actions run when a file is saved with Save (⌘S) or Save All. Trailing spaces and the final line break are configured in": "使用“保存”(⌘S) 或“全部保存”保存文件时运行的操作。尾随空格和末尾换行符的配置位置:",
"Activate on Save": "保存时激活",
@@ -154,6 +155,7 @@
"Bookmarks": "书签",
"Bottom": "底部",
"Branch": "分支",
"Branch '{name}' has no tracked branch": "分支 '{name}' 没有跟踪分支",
"Branch, %": "分支,%",
"Branch, tag or commit hash": "分支、标签或提交哈希",
"Branches": "分支",
@@ -189,18 +191,23 @@
"Cannot annotate": "无法注解",
"Cannot change line separators": "无法更改行分隔符",
"Cannot commit": "无法提交",
"Cannot connect to {target}": "无法连接到 {target}",
"Cannot convert encoding": "无法转换编码",
"Cannot create scratch file": "无法创建临时文件",
"Cannot create the HTTP request file": "无法创建 HTTP 请求文件",
"Cannot duplicate the data source": "无法复制数据源",
"Cannot execute": "无法执行",
"Cannot execute on {target}": "无法在 {target} 上执行",
"Cannot export data": "无法导出数据",
"Cannot find usages of '{name}'": "无法查找 '{name}' 的用法",
"Cannot generate the DDL": "无法生成 DDL",
"Cannot get the DDL of {name}": "无法获取 {name} 的 DDL",
"Cannot open file": "无法打开文件",
"Cannot open library class": "无法打开库类",
"Cannot open link": "无法打开链接",
"Cannot open the query console": "无法打开查询控制台",
"Cannot open the response": "无法打开响应",
"Cannot profile '{name}'": "无法分析 '{name}'",
"Cannot put label": "无法添加标签",
"Cannot read data sources": "无法读取数据源",
"Cannot read the file:": "无法读取文件:",
@@ -211,6 +218,7 @@
"Cannot reload with encoding": "无法使用该编码重新加载",
"Cannot remove recent project": "无法移除最近的项目",
"Cannot roll back": "无法回滚",
"Cannot run '{name}' with coverage": "无法使用覆盖率运行 '{name}'",
"Cannot run the script": "无法运行脚本",
"Cannot save run configurations": "无法保存运行配置",
"Cannot save setting": "无法保存设置",
@@ -235,6 +243,7 @@
"Changes were not submitted": "更改未提交",
"Check TODO": "检查 TODO",
"Check out a branch to create a pull request from": "请先签出一个分支,再从它创建拉取请求",
"Checked out pull request #{number}": "已检出拉取请求 #{number}",
"Checking installed tools…": "正在检查已安装的工具…",
"Checking tools": "正在检查工具",
"Checkout": "签出",
@@ -666,6 +675,8 @@
"External tools": "外部工具",
"External tools actions": "外部工具操作",
"Extract Method…": "提取方法…",
"Failed to install {name}": "安装 {name} 失败",
"Failed to uninstall {name}": "卸载 {name} 失败",
"FIXME comment": "FIXME 注释",
"Failed": "失败",
"Failed to restart language server": "重启语言服务器失败",
@@ -853,6 +864,8 @@
"IDE Updates": "IDE 更新",
"IDE font": "IDE 字体",
"IDE font size": "IDE 字体大小",
"Installed {name}": "已安装 {name}",
"Installing {name}": "正在安装 {name}",
"IPC round trip": "IPC 往返",
"Idle seconds": "空闲秒数",
"Ignore files and folders": "忽略文件和文件夹",
@@ -962,6 +975,7 @@
"Language": "语言",
"Language Defaults": "语言默认值",
"Language Server": "语言服务器",
"Language server '{name}' is not installed": "语言服务器 '{name}' 未安装",
"Language Servers": "语言服务器",
"Language Servers…": "语言服务器…",
"Language Services": "语言服务",
@@ -1139,11 +1153,13 @@
"Next Occurrence": "下一个匹配项",
"Next Page": "下一页",
"Next error action goes to high priority problems only": "“下一个错误”操作仅转到高优先级问题",
"No coverage data for '{name}'": "没有 '{name}' 的覆盖率数据",
"No Environment": "无环境",
"No GitHub repository": "没有 GitHub 仓库",
"No Gradle projects": "没有 Gradle 项目",
"No HTTP request at the caret": "光标处没有 HTTP 请求",
"No Maven projects": "没有 Maven 项目",
"No profile for '{name}'": "没有 '{name}' 的分析数据",
"No Python found": "未找到 Python",
"No TODO items found": "未找到 TODO 项",
"No TODO items in this file": "此文件中没有 TODO 项",
@@ -1209,6 +1225,7 @@
"No surround templates for this language": "此语言没有包围模板",
"No tools match.": "没有匹配的工具。",
"No usages found": "未找到用法",
"No usages of '{name}' found": "未找到 '{name}' 的用法",
"Node interpreter": "Node 解释器",
"None": "无",
"None configured": "未配置",
@@ -1216,6 +1233,7 @@
"Not logged in to GitHub": "未登录 GitHub",
"Not null": "非空",
"Nothing found": "未找到任何内容",
"Nothing found in project files": "项目文件中未找到任何内容",
"Nothing here": "这里什么都没有",
"Nothing is running": "没有正在运行的内容",
"Nothing matches": "没有匹配项",
@@ -1466,6 +1484,7 @@
"Rebase the current branch on top of incoming changes": "将当前分支变基到传入的更改之上",
"Rebuild Last": "重新构建上一次",
"Rebuild Project": "重新构建项目",
"Recent Actions": "最近的操作",
"Recent Files": "最近的文件",
"Recent Locations": "最近的位置",
"Recent Projects": "最近的项目",
@@ -1610,6 +1629,8 @@
"Restore Size": "恢复大小",
"Restore detected roots": "恢复检测到的根",
"Result": "结果",
"result": "个结果",
"results": "个结果",
"Results": "结果",
"Resume Program": "恢复程序",
"Retry": "重试",
@@ -1625,6 +1646,7 @@
"Review comment": "审查评论",
"Review comments": "审查评论",
"Review requested": "请求审查",
"Review submitted on #{number}": "已在 #{number} 上提交审查",
"Review ▾": "审查 ▾",
"Reviewers": "审查者",
"Revision": "修订",
@@ -1643,6 +1665,7 @@
"Run Code › Analyze Code › Locate Duplicates… to find copied code": "运行“代码 › 分析代码 › 查找重复项…”以查找复制的代码",
"Run Code › Inspect Code… to analyse files": "运行“代码 › 检查代码…”以分析文件",
"Run Command in Terminal": "在终端中运行命令",
"Run `pnpm tauri dev` to use the file system.": "运行 `pnpm tauri dev` 以使用文件系统。",
"Run Configuration": "运行配置",
"Run Context Configuration": "运行上下文配置",
"Run Context Configuration with Coverage": "使用覆盖率运行上下文配置",
@@ -1684,6 +1707,7 @@
"Runs the language server's Organize Imports action.": "运行语言服务器的“整理导入”操作。",
"Runs the language server's formatter before an explicit save (Actions on Save).": "在显式保存前运行语言服务器的格式化程序(保存时操作)。",
"Runs this project": "运行此项目",
"Searching for usages of '{symbol}'": "正在搜索 '{symbol}' 的用法",
"SQL Inserts": "SQL Insert 语句",
"SQL Script Template": "SQL 脚本模板",
"SQL Scripts": "SQL 脚本",
@@ -1821,6 +1845,7 @@
"Show TODO Items in Current File": "显示当前文件中的 TODO 项",
"Show Tool Window Names": "显示工具窗口名称",
"Show Usage Results": "显示用法结果",
"Select Configuration": "选择配置",
"Show Usages": "显示用法",
"Show Warnings": "显示警告",
"Show all output": "显示所有输出",
@@ -1935,6 +1960,7 @@
"Stop Request": "停止请求",
"Stop and Rerun": "停止并重新运行",
"Stop to Edit": "停止以编辑",
"Stopped after {count} matches": "匹配 {count} 处后停止",
"Strikeout": "删除线",
"Struct": "结构体",
"Struct type": "结构体类型",
@@ -1966,6 +1992,7 @@
"System Settings": "系统设置",
"System default": "系统默认",
"System-Dependent": "取决于系统",
"Tests failed: {detail}": "测试失败:{detail}",
"TODO comment": "TODO 注释",
"TODO filters": "TODO 过滤器",
"TODO items": "TODO 项",
@@ -2082,11 +2109,13 @@
"Tx: Auto": "事务:自动",
"Tx: Manual": "事务:手动",
"Type Declaration": "类型声明",
"Type file name, path or file:42": "输入文件名、路径或 file:42",
"Type Hierarchy": "类型层次结构",
"Type an expression followed by": "输入表达式,后跟",
"Type hierarchy": "类型层次结构",
"Type to filter by file name or code": "输入以按文件名或代码过滤",
"Type to filter symbols": "输入以过滤符号",
"Type to search": "输入以搜索",
"TypeScript version": "TypeScript 版本",
"Types": "类型",
"Typography Settings": "排版设置",
@@ -2094,6 +2123,7 @@
"UI elements take up less screen space": "UI 元素占用更少的屏幕空间",
"UI font": "UI 字体",
"UI font size": "UI 字体大小",
"Uninstalled {name}": "已卸载 {name}",
"URL:": "URL:",
"Unchecked values are inherited from the scheme.": "未勾选的值继承自方案。",
"Uncommitted changes are stashed before the update and restored afterwards.": "更新前会储藏未提交的更改,并在之后恢复。",
@@ -2128,9 +2158,14 @@
"Upgrade": "升级",
"Uptime": "运行时间",
"Usage searches": "用法搜索",
"Usage search did not finish; nothing was deleted": "用法搜索未完成;未删除任何内容",
"usage": "处用法",
"usages": "处用法",
"more": "更多",
"Usages": "用法",
"Usages Detected": "检测到用法",
"Usages actions": "用法操作",
"Usages of '{symbol}'": "'{symbol}' 的用法",
"Usages of element at caret": "光标处元素的用法",
"Use \"CamelHumps\" words": "使用“驼峰”单词",
"Use 24-hour time format": "使用 24 小时制",
@@ -2297,10 +2332,17 @@
"with BOM under Windows, with no BOM otherwise": "在 Windows 下带 BOM,其他系统不带 BOM",
"with NO BOM": "不带 BOM",
"with statement": "with 语句",
"{count} rows exported": "已导出 {count} 行",
"{name} failed": "{name}失败",
"{name} has unsaved changes": "{name} 有未保存的更改",
"{name} installed": "{name} 已安装",
"{name} is not installed": "{name} 未安装",
"· Conflicts": "· 冲突",
"· read-only": "· 只读",
"… more": "… 更多",
"↵ Open · ⇧↵ Open to the side · ⌘↵ Open in Find Window": "↵ 打开 · ⇧↵ 在侧边打开 · ⌘↵ 在查找窗口中打开",
"⇥ to switch scope": "⇥ 切换范围",
"⌘↵ to open in Find tool window": "⌘↵ 在“查找”工具窗口中打开",
"⌥← / ⌥→ and ⌥⌫ stop at humps of camelCase names.": "⌥← / ⌥→ 和 ⌥⌫ 会在 camelCase 名称的驼峰处停止。",
"Specify commit message": "请填写提交信息",
"Author:": "作者:",
+3 -2
Переглянути файл
@@ -1,5 +1,6 @@
import { executeAction, useActions } from "@/platform/actions";
import { shortcutLabel } from "@/platform/keymap";
import { T } from "@/ui";
import { useWorkbenchSlots } from "./contributions";
import { ErrorBoundary, Slot } from "./ErrorBoundary";
import styles from "./EditorArea.module.css";
@@ -26,10 +27,10 @@ export function EmptyEditor() {
<dt>
{runnable ? (
<button type="button" className={styles.hintButton} onClick={() => void executeAction(h.action!)}>
{h.label}
<T>{h.label}</T>
</button>
) : (
h.label
<T>{h.label}</T>
)}
</dt>
{keys && <dd className={styles.keys}>{keys}</dd>}
+7 -4
Переглянути файл
@@ -1,4 +1,5 @@
import { useEffect, useState } from "react";
import { useTxTemplate } from "@/platform/i18n";
import { EmptyState, IconButton, Toast, T } from "@/ui";
import {
BALLOON_TIMEOUT,
@@ -21,6 +22,7 @@ function timeLabel(t: number) {
function Balloon({ n }: { n: IdeNotification }) {
const [hover, setHover] = useState(false);
const txt = useTxTemplate();
useEffect(() => {
const ms = BALLOON_TIMEOUT[n.level];
if (n.sticky || ms === null || hover) return;
@@ -31,10 +33,10 @@ function Balloon({ n }: { n: IdeNotification }) {
<Toast
className={styles.balloon}
level={n.level}
title={n.title}
title={txt(n.title)}
body={n.body}
actions={n.actions?.map((a) => ({
label: a.label,
label: txt(a.label),
run: () => {
dismissBalloon(n.id);
a.run();
@@ -73,6 +75,7 @@ function useListVisible() {
/** The Notifications tool window (right stripe, as in IDEA): history, newest first. */
export function NotificationsToolWindow() {
const txt = useTxTemplate();
const all = useNotifications((s) => s.items);
const items = all.filter((n) => n.logged);
const visible = useListVisible();
@@ -92,10 +95,10 @@ export function NotificationsToolWindow() {
key={n.id}
className={styles.entry}
level={n.level}
title={n.title}
title={txt(n.title)}
body={n.body}
meta={timeLabel(n.time)}
actions={n.actions}
actions={n.actions?.map((a) => ({ ...a, label: txt(a.label) }))}
onClose={() => removeNotification(n.id)}
/>
))