From 5b8e0b965978e048f4bbe9ab4ff8be69c7db1732 Mon Sep 17 00:00:00 2001 From: Richard <53105011+Richard2091@users.noreply.github.com> Date: Sat, 24 Oct 2026 14:14:43 +0800 Subject: [PATCH] =?UTF-8?q?feat(i18n):=20=E6=96=B0=E5=A2=9E=E7=AE=80?= =?UTF-8?q?=E4=BD=93=E4=B8=AD=E6=96=87=E7=95=8C=E9=9D=A2=E7=BF=BB=E8=AF=91?= =?UTF-8?q?=20(#15)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 <24760907+l0ng-ai@users.noreply.github.com> --- src/features/code-insight/UsagesView.tsx | 19 +++++-- src/features/run/RunWidget.tsx | 20 +++++-- .../search/everywhere/SearchEverywhere.tsx | 31 ++++++---- src/features/search/everywhere/model.ts | 34 +++++++---- src/platform/i18n.test.ts | 17 ++++++ src/platform/i18n.ts | 56 +++++++++++++++++++ src/platform/locales/text/zh.json | 42 ++++++++++++++ src/workbench/EditorArea.tsx | 5 +- src/workbench/Notifications.tsx | 11 ++-- 9 files changed, 197 insertions(+), 38 deletions(-) diff --git a/src/features/code-insight/UsagesView.tsx b/src/features/code-insight/UsagesView.tsx index 000c4bad1..4c71625f7 100644 --- a/src/features/code-insight/UsagesView.tsx +++ b/src/features/code-insight/UsagesView.tsx @@ -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 ( s.search); const root = useProject((s) => s.project?.root ?? ""); const [collapsed, setCollapsed] = useState<{ id: number | null; keys: ReadonlySet }>({ @@ -86,7 +89,7 @@ export function UsagesView() { Found usages - {total} {total === 1 ? "result" : "results"} + {total} {tx(total === 1 ? "result" : "results")} {search.note && {search.note}} @@ -99,7 +102,7 @@ export function UsagesView() { {basename(path)} {relativeDir(root, path)} - {n.group.usages.length} {n.group.usages.length === 1 ? "usage" : "usages"} + {n.group.usages.length} {tx(n.group.usages.length === 1 ? "usage" : "usages")} ); @@ -117,7 +120,7 @@ export function UsagesView() {
{search.loading ? ( <> - Searching for usages{search.symbol ? ` of '${search.symbol}'` : ""}… + {" "} + {search.symbol + ? formatMessage(tx("Searching for usages of '{symbol}'"), { symbol: search.symbol }) + : tx("Searching for usages")} + … ) : ( {search.error} @@ -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"} diff --git a/src/features/run/RunWidget.tsx b/src/features/run/RunWidget.tsx index 0aa385aff..95326de1c 100644 --- a/src/features/run/RunWidget.tsx +++ b/src/features/run/RunWidget.tsx @@ -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() { - {active && } + {active && ( + + )} )} - {selected?.name ?? (configs.length ? "Select Configuration" : "Add Configuration…")} + + {selected?.name ?? tx(configs.length ? "Select Configuration" : "Add Configuration…")} + @@ -156,7 +162,13 @@ export function RunWidget() { aria-expanded={moreOpen} onClick={() => setMoreOpen((v) => !v)} /> - setMoreOpen(false)} /> + setMoreOpen(false)} + />
); diff --git a/src/features/search/everywhere/SearchEverywhere.tsx b/src/features/search/everywhere/SearchEverywhere.tsx index b66eb76a5..9e1f6b5ef 100644 --- a/src/features/search/everywhere/SearchEverywhere.tsx +++ b/src/features/search/everywhere/SearchEverywhere.tsx @@ -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} >
-
+
{TABS.map((t) => ( ))}
@@ -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 && } - {hintFor(tab)} + {hintFor(tab)}
@@ -344,7 +346,9 @@ function SearchEverywhereDialog() { items={rows} getKey={(r) => r.key} isSection={isSectionRow} - renderSection={(r) => {r.kind === "section" ? r.title : ""}} + renderSection={(r) => ( + {r.kind === "section" ? {r.title} : ""} + )} 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) => ( )} @@ -360,7 +364,9 @@ function SearchEverywhereDialog() { )} {showEmpty && (
- {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", + )}
)} @@ -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 … more {SECTION_TITLES[row.section].toLowerCase()}; + return ( + + … {tx("more")} {tx(SECTION_TITLES[row.section]).toLowerCase()} + + ); case "file": return ; case "symbol": diff --git a/src/features/search/everywhere/model.ts b/src/features/search/everywhere/model.ts index a3e89e526..41fae597f 100644 --- a/src/features/search/everywhere/model.ts +++ b/src/features/search/everywhere/model.ts @@ -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, line: number | null = null, col: number | null = null): FileItem { +export function fileItem( + m: Pick, + 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, - opts: { headers: boolean }, -): SeRow[] { +export function buildRows(sections: ReadonlyMap, opts: { headers: boolean }): SeRow[] { const rows: SeRow[] = []; for (const id of SECTION_ORDER) { const s = sections.get(id); diff --git a/src/platform/i18n.test.ts b/src/platform/i18n.test.ts index 71356e702..6f8710e1f 100644 --- a/src/platform/i18n.test.ts +++ b/src/platform/i18n.test.ts @@ -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)) + expect(names(text), source).toEqual(names(source)); + }); + it("have the same keys", () => { expect(Object.keys(zh).sort()).toEqual(Object.keys(en).sort()); }); diff --git a/src/platform/i18n.ts b/src/platform/i18n.ts index 1205712d7..5476691c6 100644 --- a/src/platform/i18n.ts +++ b/src/platform/i18n.ts @@ -178,6 +178,62 @@ export function useTx(): typeof tx { }, [texts]); } +type Template = { pattern: RegExp; names: string[]; translation: string; literal: number }; + +const templateCache = new WeakMap(); + +/** 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(); diff --git a/src/platform/locales/text/zh.json b/src/platform/locales/text/zh.json index c3b59f6a1..13750585f 100644 --- a/src/platform/locales/text/zh.json +++ b/src/platform/locales/text/zh.json @@ -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:": "作者:", diff --git a/src/workbench/EditorArea.tsx b/src/workbench/EditorArea.tsx index a282ad738..64f980da2 100644 --- a/src/workbench/EditorArea.tsx +++ b/src/workbench/EditorArea.tsx @@ -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() {
{runnable ? ( ) : ( - h.label + {h.label} )}
{keys &&
{keys}
} diff --git a/src/workbench/Notifications.tsx b/src/workbench/Notifications.tsx index 08909dabb..f0abd2489 100644 --- a/src/workbench/Notifications.tsx +++ b/src/workbench/Notifications.tsx @@ -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 }) { ({ - 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)} /> ))