дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-08 19:02:17 +02:00
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]>
Цей коміт міститься в:
1 джерело
3d8a6f07b4
коміт
5b8e0b9659
9 files changed
+197
-38
No files matched your search
@@ -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"}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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());
|
||||
});
|
||||
|
||||
@@ -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>();
|
||||
|
||||
@@ -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:": "作者:",
|
||||
|
||||
@@ -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>}
|
||||
|
||||
@@ -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)}
|
||||
/>
|
||||
))
|
||||
|
||||
Reference in new issue
Заблокувати користувача