From 8568ff001bafcb9e1fd19916ec8c45d57a5ba981 Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Tue, 27 Oct 2026 18:56:54 +0800 Subject: [PATCH] fix(i18n): Translate interpolated UI text through a tx template tag (#98) About 270 UI strings were JS template literals with interpolation passed to exact catalog lookup, so they could never be translated; about 25 more spliced lowercase English fragments into sentences. `tx` now also works as a template tag (`` tx`Remove data source '${name}'?` ``): it looks up the entry by the literal parts (`{name}` placeholders), translates interpolated values that have entries, and remembers generated text so it re-translates after a language switch or when the catalog loads later. Call sites use the tag; spliced fragments are written as whole sentences per variant. Adds 280 Chinese translations and removes stale ones. The catalog guard now also fails on untagged template literals reaching exact lookup (unless allowlisted with a reason), tagged text missing from the catalog, case-changed or ternary-suffix interpolations, and duplicate template patterns. Claude-Session: https://claude.ai/code/session_01VUYsdXA2nkF5v6zsC93dpn --- src/editor/core/actions.ts | 3 +- src/editor/core/clipboardHistory.ts | 3 +- src/editor/core/cm/preview.ts | 3 +- src/editor/core/cm/setup.ts | 3 +- src/editor/core/cm/unwrap.ts | 3 +- src/editor/core/fileAttributes.ts | 5 +- src/editor/core/model/document.ts | 3 +- src/editor/core/view/EditorArea.tsx | 3 +- src/editor/lsp/codeLens.ts | 3 +- src/editor/lsp/fileReferences.ts | 3 +- src/editor/lsp/format.ts | 2 +- src/editor/lsp/inlay.ts | 3 +- src/editor/lsp/intentions.ts | 3 +- src/editor/lsp/navigation.ts | 3 +- src/editor/lsp/rename.ts | 11 +- src/editor/lsp/spelling.ts | 9 +- src/editor/lsp/syntaxFallback.ts | 3 +- src/editor/navigation/index.ts | 3 +- src/editor/refactor/introduce.ts | 5 +- src/editor/refactor/safeDelete.test.ts | 4 +- src/editor/refactor/safeDelete.ts | 25 +- src/editor/refactor/serverIntroduce.ts | 7 +- src/features/agents/AgentsView.tsx | 5 +- src/features/agents/Composer.tsx | 2 +- src/features/agents/composerModel.ts | 3 +- src/features/analysis/AnalysisDialogs.tsx | 3 +- src/features/analysis/cleanup.ts | 3 +- src/features/analysis/structural.ts | 3 +- src/features/app/projectActions.ts | 3 +- src/features/build-tools/BuildToolView.tsx | 7 +- src/features/code-insight/HierarchyView.tsx | 5 +- src/features/code-insight/ServerStatus.tsx | 7 +- src/features/code-insight/decorations.ts | 9 +- src/features/coverage/executor.ts | 3 +- src/features/database/DataDialogs.tsx | 7 +- src/features/database/DataView.tsx | 7 +- src/features/database/Dialogs.tsx | 5 +- src/features/database/ObjectDialogs.tsx | 3 +- src/features/database/SourceDialog.tsx | 26 +- src/features/database/TableEditor.tsx | 3 +- src/features/database/console.tsx | 3 +- src/features/database/store.ts | 7 +- src/features/database/treeActions.ts | 14 +- src/features/debugger/actions.ts | 3 +- src/features/debugger/hotswap.ts | 3 +- src/features/debugger/missingAdapter.ts | 14 +- src/features/debugger/ui/DebugToolWindow.tsx | 3 +- src/features/debugger/ui/Overlays.tsx | 5 +- src/features/github/PullRequestsView.tsx | 7 +- src/features/github/dialogs.tsx | 5 +- src/features/http-client/HttpClientView.tsx | 3 +- src/features/macros/MacrosDialog.tsx | 4 +- src/features/perf/MemoryStatus.tsx | 5 +- src/features/perf/actions.ts | 8 +- src/features/plugins/PluginsPage.tsx | 9 +- src/features/plugins/bridge.ts | 3 +- src/features/profiler/executor.ts | 3 +- src/features/project-tree/ProjectView.tsx | 4 +- .../project-tree/RefactoringPreview.tsx | 5 +- src/features/project-tree/actions.ts | 7 +- src/features/project-tree/contextMenu.ts | 3 +- src/features/project-tree/fileRefactor.ts | 5 +- src/features/project-tree/operations.ts | 3 +- src/features/run/ConfigChooser.tsx | 5 +- src/features/run/ConfigForm.tsx | 2 +- src/features/run/RunView.tsx | 7 +- src/features/run/RunWidget.tsx | 14 +- src/features/run/TestTreeView.tsx | 7 +- src/features/run/contextRun.ts | 7 +- src/features/run/dialogs.tsx | 12 +- src/features/run/index.ts | 3 +- src/features/run/navigate.ts | 3 +- src/features/run/producers.ts | 5 +- src/features/run/runGutter.ts | 5 +- src/features/run/services.tsx | 4 +- src/features/run/stackTrace.tsx | 3 +- src/features/run/store.ts | 5 +- src/features/run/types.ts | 13 +- .../search/bookmarks/BookmarksView.tsx | 9 +- src/features/search/index.ts | 5 +- src/features/services/docker.tsx | 6 +- src/features/services/ops.ts | 44 ++- src/features/services/runTypes.ts | 3 +- src/features/settings/components/Form.tsx | 2 +- src/features/settings/keymap/KeymapPage.tsx | 7 +- .../settings/keymap/RecordShortcutDialog.tsx | 3 +- .../languages/ProjectStructurePage.tsx | 5 +- src/features/settings/pages/FileTypes.tsx | 5 +- src/features/settings/pages/behavior.tsx | 6 +- .../settings/pages/editor/colorScheme.tsx | 3 +- src/features/settings/pages/editor/font.tsx | 3 +- .../settings/pages/editor/inspections.tsx | 5 +- .../settings/pages/editor/inspectionsModel.ts | 15 +- src/features/settings/pages/tools.tsx | 3 +- src/features/settings/pages/vcs.tsx | 3 +- .../templates/files/NewFileDialog.tsx | 7 +- src/features/templates/index.ts | 5 +- .../templates/settings/FileTemplatesPage.tsx | 2 +- .../templates/settings/LiveTemplatesPage.tsx | 9 +- .../templates/settings/PostfixPage.tsx | 3 +- src/features/todo/TodoSettingsPage.tsx | 5 +- src/features/todo/TodoView.tsx | 3 +- src/features/tools/LanguageServersPage.tsx | 7 +- src/features/tools/jdk.ts | 3 +- src/features/tools/missing.ts | 3 +- src/features/tools/store.ts | 11 +- src/features/vcs/BranchWidget.tsx | 13 +- src/features/vcs/CommitView.tsx | 2 +- src/features/vcs/CompareBranches.tsx | 7 +- src/features/vcs/GitLog.tsx | 3 +- src/features/vcs/LocalHistory.tsx | 13 +- src/features/vcs/MergeEditor.tsx | 7 +- src/features/vcs/actions.tsx | 3 +- src/features/vcs/branchMenu.ts | 21 +- src/features/vcs/branchOps.ts | 19 +- src/features/vcs/changeOps.tsx | 13 +- src/features/vcs/changelistsUi.tsx | 8 +- src/features/vcs/changes.ts | 9 +- src/features/vcs/commitChecks.tsx | 9 +- src/features/vcs/compare.tsx | 3 +- src/features/vcs/gitMenuDialogs.tsx | 13 +- src/features/vcs/index.ts | 11 +- src/features/vcs/model.ts | 3 +- src/features/vcs/operations.tsx | 28 +- src/features/vcs/patches.tsx | 3 +- src/features/vcs/rebase.tsx | 14 +- src/features/vcs/remoteUi.tsx | 4 +- src/features/vcs/shelf.tsx | 7 +- src/features/vim/VimSettingsPage.tsx | 3 +- src/features/web/index.ts | 3 +- src/features/welcome/ProjectsPage.tsx | 3 +- src/features/welcome/RecentList.tsx | 3 +- src/platform/i18n.test.ts | 20 ++ src/platform/i18n.ts | 83 ++++- src/platform/inspectionControls.ts | 3 +- src/platform/jdk.ts | 3 +- src/platform/locales/text/zh.json | 290 +++++++++++++++++- src/platform/locales/textCatalog.test.ts | 135 ++++++-- src/workbench/StatusBar.tsx | 5 +- src/workbench/tasks.ts | 3 +- src/workbench/toolwindows/ToolWindows.tsx | 4 +- src/workbench/toolwindows/actions.ts | 2 +- 142 files changed, 1031 insertions(+), 362 deletions(-) diff --git a/src/editor/core/actions.ts b/src/editor/core/actions.ts index be34d9d9f..cbfa16f6a 100644 --- a/src/editor/core/actions.ts +++ b/src/editor/core/actions.ts @@ -54,6 +54,7 @@ import { } from "./cm/structural"; import type { DocModel } from "./model/document"; import { structure } from "@/editor/structure"; +import { tx } from "@/platform/i18n"; /** Text-editing actions: the editor has focus, or a popup (Find Action) runs them on it. */ export const TEXT_WHEN = "editorTextFocus || textEditorActive && popupOpen"; @@ -293,7 +294,7 @@ function copyReference(view: EditorView, slot: EditorSlot) { }); recordClipboard(reference); void navigator.clipboard?.writeText(reference).catch(() => {}); - void executeAction("editor.showHint", { text: `Copied reference: ${reference}` }); + void executeAction("editor.showHint", { text: tx`Copied reference: ${reference}` }); } /** ⌃G; when nothing more is found, a hint first and the search from the top on the next press (IDEA). */ diff --git a/src/editor/core/clipboardHistory.ts b/src/editor/core/clipboardHistory.ts index 997d31112..640c0b53f 100644 --- a/src/editor/core/clipboardHistory.ts +++ b/src/editor/core/clipboardHistory.ts @@ -5,6 +5,7 @@ */ import { EditorView } from "@codemirror/view"; import type { EditorState } from "@codemirror/state"; +import { tx } from "@/platform/i18n"; export const MAX_CLIPBOARD_ENTRIES = 30; @@ -45,7 +46,7 @@ export function clipLabel(text: string, max = 80): { label: string; detail: stri const first = (lines.find((l) => l.trim()) ?? "").trim(); const label = first.length > max ? `${first.slice(0, max - 1)}…` : first || "(whitespace)"; const count = text.endsWith("\n") ? lines.length - 1 : lines.length; - return { label, detail: count > 1 ? `${count} lines` : undefined }; + return { label, detail: count > 1 ? tx`${count} lines` : undefined }; } export const clipboardHistoryExtension = EditorView.domEventHandlers({ diff --git a/src/editor/core/cm/preview.ts b/src/editor/core/cm/preview.ts index 0d20fd5b3..86fd2fb35 100644 --- a/src/editor/core/cm/preview.ts +++ b/src/editor/core/cm/preview.ts @@ -12,6 +12,7 @@ import { openideaHighlightStyle } from "./highlight"; import { openideaTheme } from "./theme"; import { findLanguageDescription, loadSupport, loadedSupport, uiModeForPath } from "./languages"; import { fontExtension } from "./setup"; +import { tx } from "@/platform/i18n"; export async function previewExtensions(path: string, settings: EditorSettings | undefined): Promise { const open = editorsFor(path)[0]?.ctx; @@ -25,7 +26,7 @@ export async function previewExtensions(path: string, settings: EditorSettings | syntaxHighlighting(openideaHighlightStyle), EditorState.readOnly.of(true), EditorView.editable.of(false), - EditorView.contentAttributes.of({ "aria-label": `Preview of ${path}` }), + EditorView.contentAttributes.of({ "aria-label": tx`Preview of ${path}` }), support ?? [], ]; } diff --git a/src/editor/core/cm/setup.ts b/src/editor/core/cm/setup.ts index b25a4184f..0fadf9218 100644 --- a/src/editor/core/cm/setup.ts +++ b/src/editor/core/cm/setup.ts @@ -51,6 +51,7 @@ import { gutterLineDrag } from "./gutterDrag"; import { overwriteField } from "./overwrite"; import { clipboardHistoryExtension } from "../clipboardHistory"; import type { LanguageInfo } from "../backend"; +import { tx } from "@/platform/i18n"; export const compartments = { language: new Compartment(), @@ -309,7 +310,7 @@ export function baseExtensions(o: SetupOptions): Extension { findExtension(), overviewLane, codeLensExtension, - EditorView.contentAttributes.of({ spellcheck: "false", autocorrect: "off", autocapitalize: "off", "aria-label": `Editor for ${o.ctx.path}` }), + EditorView.contentAttributes.of({ spellcheck: "false", autocorrect: "off", autocapitalize: "off", "aria-label": tx`Editor for ${o.ctx.path}` }), tabKeymap, keymap.of([...closeBracketsKeymap, ...editorBaseKeymap, indentWithTab]), compartments.language.of(o.language ?? []), diff --git a/src/editor/core/cm/unwrap.ts b/src/editor/core/cm/unwrap.ts index 197119178..9a221b18b 100644 --- a/src/editor/core/cm/unwrap.ts +++ b/src/editor/core/cm/unwrap.ts @@ -5,6 +5,7 @@ * every language with a grammar is covered. */ import type { Text } from "@codemirror/state"; +import { tx } from "@/platform/i18n"; export interface ChainNode { kind: string; @@ -133,7 +134,7 @@ export function unwrapOptions(doc: Text, chain: readonly ChainNode[]): UnwrapOpt const key = `${target.from}:${target.to}:${label}`; if (seen.has(key)) continue; seen.add(key); - out.push({ label: `Unwrap '${label}'`, target: { from: target.from, to: target.to }, ...replacement(doc, target, child) }); + out.push({ label: tx`Unwrap '${label}'`, target: { from: target.from, to: target.to }, ...replacement(doc, target, child) }); } return out; } diff --git a/src/editor/core/fileAttributes.ts b/src/editor/core/fileAttributes.ts index 2888373bd..35794ac45 100644 --- a/src/editor/core/fileAttributes.ts +++ b/src/editor/core/fileAttributes.ts @@ -8,11 +8,12 @@ import { basename } from "@/platform/project"; import { notifications } from "@/workbench/contributions"; import { confirm } from "@/workbench/messages"; import type { DocModel } from "./model/document"; +import { tx } from "@/platform/i18n"; export function readOnlyQuestion(name: string, readOnly: boolean) { return readOnly - ? { title: "Make File Writable", message: `'${name}' is read-only. Make it writable?`, confirm: "Make Writable" } - : { title: "Make File Read-only", message: `Make '${name}' read-only?`, confirm: "Make Read-only" }; + ? { title: "Make File Writable", message: tx`'${name}' is read-only. Make it writable?`, confirm: "Make Writable" } + : { title: "Make File Read-only", message: tx`Make '${name}' read-only?`, confirm: "Make Read-only" }; } /** Applies the toggle without asking. Returns the new read-only state, or null on failure. */ diff --git a/src/editor/core/model/document.ts b/src/editor/core/model/document.ts index af36f5e0f..2550294e5 100644 --- a/src/editor/core/model/document.ts +++ b/src/editor/core/model/document.ts @@ -44,6 +44,7 @@ import { } from "../cm/setup"; import { setMethodLines, softWrapActive } from "../cm/prefs"; import type { EditorSlot } from "./slot"; +import { tx } from "@/platform/i18n"; export type Banner = "binary" | "tooLarge" | "readOnly" | "library" | null; @@ -237,7 +238,7 @@ export class DocModel { manager.notify({ level: "warning", title: "Editor resynchronised", - body: `Some unsaved edits to ${this.path} could not be applied and were replaced by the file's current content.`, + body: tx`Some unsaved edits to ${this.path} could not be applied and were replaced by the file's current content.`, }), onError: (e) => console.warn(`document ${this.path}: sync error`, e), }, diff --git a/src/editor/core/view/EditorArea.tsx b/src/editor/core/view/EditorArea.tsx index 77c8f0cef..18a223169 100644 --- a/src/editor/core/view/EditorArea.tsx +++ b/src/editor/core/view/EditorArea.tsx @@ -44,6 +44,7 @@ import { FindBar } from "./FindBar"; import { EditorBanners, VirtualBanner } from "./Banner"; import { CodeBreadcrumbs } from "./StatusItems"; import styles from "./EditorArea.module.css"; +import { tx } from "@/platform/i18n"; export function EditorAreaView() { const layout = useTabs((s) => s.layout); @@ -343,7 +344,7 @@ function PaneContent({ paneId, tab }: { paneId: string; tab: Tab }) { ); } const kind = kinds.get(tab.input.kind); - if (!kind) return ; + if (!kind) return ; const Component = kind.component; return (
diff --git a/src/editor/lsp/codeLens.ts b/src/editor/lsp/codeLens.ts index 136ce82af..1b7772dda 100644 --- a/src/editor/lsp/codeLens.ts +++ b/src/editor/lsp/codeLens.ts @@ -16,6 +16,7 @@ import { posToOffset } from "./positions"; import { onLspRefresh, serverBackoff } from "./availability"; import { editorPrefs } from "@/platform/editorSettings"; import { authorLabel, authorSummary, authorTooltip, blameFor, onBlameInvalidated } from "./authorLens"; +import { tx } from "@/platform/i18n"; const LENS_KINDS = new Set([ "class", @@ -190,7 +191,7 @@ class UsagesLensPlugin { lenses.push({ pos: offset, title: usagesLabel(n), - tooltip: n ? `Show usages of ${t.name}` : undefined, + tooltip: n ? tx`Show usages of ${t.name}` : undefined, order: 10, run: n ? (v) => openUsages(v, Math.min(offset, v.state.doc.length)) : undefined, }); diff --git a/src/editor/lsp/fileReferences.ts b/src/editor/lsp/fileReferences.ts index a93123526..2ade17424 100644 --- a/src/editor/lsp/fileReferences.ts +++ b/src/editor/lsp/fileReferences.ts @@ -14,6 +14,7 @@ import { call, commands, isTauri, onIdeEvent, type PathCompletion } from "@/plat import { fileIconName, folderIconName } from "@/ui"; import { setFileDiagnostics } from "./diagnostics"; import { docOf, lspDoc } from "./lsp"; +import { tx } from "@/platform/i18n"; // --- completion --------------------------------------------------------------- @@ -118,7 +119,7 @@ export function unresolvedReferences(): Extension { refs.map((r) => ({ range: r.range, severity: "warning", - message: `Cannot resolve file '${r.value}'`, + message: tx`Cannot resolve file '${r.value}'`, source: doc.language === "html" || doc.language === "vue" || doc.language === "svelte" ? "HTML" : "CSS", code: null, })), diff --git a/src/editor/lsp/format.ts b/src/editor/lsp/format.ts index 44802b524..013e94e89 100644 --- a/src/editor/lsp/format.ts +++ b/src/editor/lsp/format.ts @@ -161,7 +161,7 @@ export async function reformatDialog( const name = ctx.path.slice(ctx.path.lastIndexOf("/") + 1); openPopup({ kind: "reformat", - title: `Reformat File: ${name}`, + title: tx`Reformat File: ${name}`, hasSelection: !sel.empty, canOptimizeImports: !contributedAt(view.state, CONTRIBUTED_FORMAT, 0), initial: lastOptions, diff --git a/src/editor/lsp/inlay.ts b/src/editor/lsp/inlay.ts index 2fc1cc403..605aa3012 100644 --- a/src/editor/lsp/inlay.ts +++ b/src/editor/lsp/inlay.ts @@ -17,6 +17,7 @@ import { debounce, docOf, isUnavailable, lspRequest } from "./lsp"; import { offsetsToRange, posToOffset } from "./positions"; import { onLspRefresh, serverBackoff } from "./availability"; import { gotoTargets } from "./navigation"; +import { tx } from "@/platform/i18n"; const KIND_LABEL: Record = { type: "Type hints", @@ -39,7 +40,7 @@ function typeHintOffset(view: EditorView, at: number): number | null { /** Right-click on a hint: turn its kind off (IDEA's "Disable '…' hints"), or open the settings. */ export function inlayMenuItems(kind: InlayHint["kind"]): MenuItem[] { return [ - { label: `Disable ${KIND_LABEL[kind]}`, run: () => void setEditorSetting(`inlay.${kind}`, false) }, + { label: tx`Disable ${KIND_LABEL[kind]}`, run: () => void setEditorSetting(`inlay.${kind}`, false) }, { kind: "separator" }, { label: "Inlay Hints Settings…", run: () => void executeAction("file.settings", "editor.inlayHints") }, ]; diff --git a/src/editor/lsp/intentions.ts b/src/editor/lsp/intentions.ts index fb121fe42..f459e77ce 100644 --- a/src/editor/lsp/intentions.ts +++ b/src/editor/lsp/intentions.ts @@ -21,6 +21,7 @@ import { spellingIntentions } from "./spelling"; import { inspectionsAt } from "./diagnostics"; import { SPELLING_INSPECTION } from "@/platform/inspections"; import { editInspectionSettings, intentionControls } from "@/platform/inspectionControls"; +import { tx } from "@/platform/i18n"; const isQuickFix = (a: CodeAction) => !!a.kind && (a.kind === "quickfix" || a.kind.startsWith("quickfix.")); @@ -195,7 +196,7 @@ export function withInspectionControls(view: EditorView, items: ListPopupItem[]) ...items, ...inspections.map((inspection) => ({ id: `inspection:${inspection.id}`, - label: `Inspection '${inspection.name}' options`, + label: tx`Inspection '${inspection.name}' options`, icon: "settings", run: () => editInspectionSettings(inspection.id), submenu: intentionControls(inspection), diff --git a/src/editor/lsp/navigation.ts b/src/editor/lsp/navigation.ts index ee774825a..973893e1e 100644 --- a/src/editor/lsp/navigation.ts +++ b/src/editor/lsp/navigation.ts @@ -52,6 +52,7 @@ import { isVirtualUri, virtualContainer } from "@/platform/virtualDocs"; import { shortcutLabel } from "@/platform/keymap"; import { isNavigationGesture, onNavigationModifier } from "@/platform/navigationModifier"; import { create } from "zustand"; +import { tx } from "@/platform/i18n"; export async function navigateTo(loc: Location) { await editorService().openFile(loc.path, { @@ -224,7 +225,7 @@ async function gotoWebReference(view: EditorView, doc: EditorDocContext, pos: Po openPopup({ kind: "list", anchor: caretAnchor(view, offset), - title: `Choose Declaration of '${label}' (${targets.length} found)`, + title: tx`Choose Declaration of '${label}' (${targets.length} found)`, items: await locationItems(targets), pin: pinToFind(label, targets), }); diff --git a/src/editor/lsp/rename.ts b/src/editor/lsp/rename.ts index 8ba26c19b..3f4afaefd 100644 --- a/src/editor/lsp/rename.ts +++ b/src/editor/lsp/rename.ts @@ -29,6 +29,7 @@ import { renameRisks, type LocalRenamePlan, } from "./syntaxFallback"; +import { tx } from "@/platform/i18n"; async function applyEdit(view: EditorView, edit: WorkspaceEdit) { try { @@ -83,7 +84,7 @@ export async function startRename(view: EditorView) { openPopup({ kind: "rename", anchor: caretAnchor(view, from), - title: `Rename '${oldName}'`, + title: tx`Rename '${oldName}'`, initial, onSubmit: (newName) => void finishRename(view, doc, pos, oldName, newName), }); @@ -99,7 +100,7 @@ export async function startRename(view: EditorView) { startInplaceRename(view, { primary: { from, to }, others, - hint: keys ? `Press ${keys} to show dialog with more options` : "Rename in place: ↵ to finish, Esc to cancel", + hint: keys ? tx`Press ${keys} to show dialog with more options` : "Rename in place: ↵ to finish, Esc to cancel", commit: (newName) => void commitInplace(view, doc, pos, oldName, newName), dialog, }); @@ -223,7 +224,7 @@ async function finishRename(view: EditorView, doc: EditorDocContext, pos: Pos, o } openPopup({ kind: "preview", - title: `Rename '${oldName}' to '${newName}'`, + title: tx`Rename '${oldName}' to '${newName}'`, summary, confirmLabel: "Do Refactor", onConfirm: () => void applyEdit(view, edit), @@ -265,7 +266,7 @@ async function startSyntaxRename(view: EditorView, doc: EditorDocContext, pos: P openPopup({ kind: "rename", anchor: caretAnchor(view, rangeToOffsets(view.state.doc, (at ?? plan.occurrences[0]!).range).from), - title: `Rename '${plan.symbol.name}'`, + title: tx`Rename '${plan.symbol.name}'`, initial: plan.symbol.name, note: `${SYNTAX_NOTE}: local occurrences only`, onSubmit: (newName) => void commitSyntaxRename(view, doc, plan, newName), @@ -315,7 +316,7 @@ export async function commitSyntaxRename(view: EditorView, doc: EditorDocContext }); openPopup({ kind: "preview", - title: `Rename '${oldName}' to '${newName}'`, + title: tx`Rename '${oldName}' to '${newName}'`, summary: { files: 1, occurrences: occurrences.length, diff --git a/src/editor/lsp/spelling.ts b/src/editor/lsp/spelling.ts index 80ba1ca80..11f2d74de 100644 --- a/src/editor/lsp/spelling.ts +++ b/src/editor/lsp/spelling.ts @@ -27,6 +27,7 @@ import { rangeToOffsets } from "./positions"; import { showHint } from "./hint"; import { caretAnchor, openPopup, type ListPopupItem } from "./popupStore"; import { renameNamePart } from "./rename"; +import { tx } from "@/platform/i18n"; const CHECK_DELAY = 800; @@ -124,7 +125,7 @@ async function changeTo(view: EditorView, typo: PlacedTypo) { openPopup({ kind: "list", anchor: caretAnchor(view, typo.from), - title: `${rename ? "Rename" : "Change"} '${typo.word}' to`, + title: rename ? tx`Rename '${typo.word}' to` : tx`Change '${typo.word}' to`, items: suggestions.map((s, i) => ({ id: `${i}`, label: s, run: () => applyCorrection(view, typo, s) })), }); } @@ -142,7 +143,7 @@ export function spellingIntentions(view: EditorView): ListPopupItem[] { ? [ { id: "spelling.saveProject", - label: `Save '${typo.word}' to project-level dictionary`, + label: tx`Save '${typo.word}' to project-level dictionary`, icon: "bulb-fix", run: () => saveToDictionary(typo.word, "project"), }, @@ -150,7 +151,7 @@ export function spellingIntentions(view: EditorView): ListPopupItem[] { : []), { id: "spelling.saveIde", - label: `Save '${typo.word}' to application-level dictionary`, + label: tx`Save '${typo.word}' to application-level dictionary`, icon: "bulb-fix", run: () => saveToDictionary(typo.word, "ide"), }, @@ -210,7 +211,7 @@ export function spellingExtension(): Extension { typos.map((t) => ({ range: t.range, severity: "info", - message: `Typo: In word '${t.word}'`, + message: tx`Typo: In word '${t.word}'`, source: "Spelling", code: null, })), diff --git a/src/editor/lsp/syntaxFallback.ts b/src/editor/lsp/syntaxFallback.ts index 0c54aa379..5a6ff2c56 100644 --- a/src/editor/lsp/syntaxFallback.ts +++ b/src/editor/lsp/syntaxFallback.ts @@ -9,6 +9,7 @@ import type { Location, Pos, Range, SymbolKind, SymbolMatch } from "@/bindings"; import { call, commands } from "@/platform/ipc"; import { structure, type Locals, type LocalSymbol, type NodeAt } from "@/editor/structure"; import { comparePos, rangeContains, rangeToOffsets } from "./positions"; +import { tx } from "@/platform/i18n"; /** Subtle marker for answers that did not come from the language server. */ export const SYNTAX_NOTE = "Based on syntax (language server not ready)"; @@ -237,7 +238,7 @@ export async function planLocalRename( if (locals.status === "notLocal") return { kind: "refused", - message: `Can't rename '${locals.name}' without the language server: only local variables can be renamed based on syntax`, + message: tx`Can't rename '${locals.name}' without the language server: only local variables can be renamed based on syntax`, }; const { symbol } = locals; const ranges = [symbol.definition, ...symbol.references].sort((a, b) => comparePos(a.start, b.start)); diff --git a/src/editor/navigation/index.ts b/src/editor/navigation/index.ts index c839f1ee5..8dccb8f16 100644 --- a/src/editor/navigation/index.ts +++ b/src/editor/navigation/index.ts @@ -20,6 +20,7 @@ import { editorService } from "@/workbench/contributions"; import { fileIconName } from "@/ui"; import { gotoTestOrSubject, type TestNavDeps } from "./gotoTest"; import { baseName, memberAt, methodStop, superMethods } from "./structureNav"; +import { tx } from "@/platform/i18n"; const EDITOR_WHEN = "textEditorActive && (editorFocus || popupOpen || !toolWindowFocus)"; @@ -97,7 +98,7 @@ async function superMethod(view: EditorView) { openPopup({ kind: "list", anchor: caretAnchor(view, head), - title: method ? `Choose Super Method of ${baseName(method.name)} (${locs.length} found)` : `Choose Super Class or Interface (${locs.length} found)`, + title: method ? tx`Choose Super Method of ${baseName(method.name)} (${locs.length} found)` : tx`Choose Super Class or Interface (${locs.length} found)`, items: await locationItems(locs), }); } diff --git a/src/editor/refactor/introduce.ts b/src/editor/refactor/introduce.ts index 5142bc172..b6d1d173a 100644 --- a/src/editor/refactor/introduce.ts +++ b/src/editor/refactor/introduce.ts @@ -35,6 +35,7 @@ import { type DeclResult, type Declaration, } from "./templates"; +import { tx } from "@/platform/i18n"; /** Why the language server could not do it (see `withFallback`). */ export type FallbackReason = "notReady" | "error" | "empty"; @@ -594,7 +595,7 @@ async function chooseOccurrences(p: Prepared, self: Occurrence, others: Occurren }, { id: "all", - label: `Replace all ${others.length + 1} occurrences`, + label: tx`Replace all ${others.length + 1} occurrences`, onActive: () => highlightRanges(view, [self, ...others]), run: () => apply(true), }, @@ -663,7 +664,7 @@ function applyAndName(p: Prepared, plan: Plan, names: string[]) { initial: name, suggestions: names, note: NOTES[p.reason], - hint: `↵ Apply · Esc Keep '${name}'`, + hint: tx`↵ Apply · Esc Keep '${name}'`, onSubmit: (chosen) => void rename(p, chosen, name, { original, applied, plan, ranges }), }); } diff --git a/src/editor/refactor/safeDelete.test.ts b/src/editor/refactor/safeDelete.test.ts index cfe0e53f4..ae9951a27 100644 --- a/src/editor/refactor/safeDelete.test.ts +++ b/src/editor/refactor/safeDelete.test.ts @@ -264,7 +264,7 @@ describe("safeDelete in the editor", () => { expect((await run("delete")).state.doc.toString()).toBe("fn unused() {\n unused();\n}\n\nfn main() {}\n"); }); - it("confirms first ('Delete function …?') and searches nothing when cancelled", async () => { + it("confirms first ('Delete Function …?') and searches nothing when cancelled", async () => { const v = editor(text, "unused() {"); outline = ok([item(text, "unused", "fn unused() {\n unused();\n}")]); confirmAnswer = null; @@ -272,7 +272,7 @@ describe("safeDelete in the editor", () => { expect(confirmChecked).toHaveBeenCalledTimes(1); const opts = confirmChecked.mock.calls[0]![0]; expect(opts.title).toBe("Safe Delete"); - expect(opts.message).toBe("Delete function 'unused'?"); + expect(opts.message).toBe("Delete Function 'unused'?"); expect(opts.checkbox).toBe("Search in comments and strings"); expect(lspReferences).not.toHaveBeenCalled(); expect(v.state.doc.toString()).toBe(text); diff --git a/src/editor/refactor/safeDelete.ts b/src/editor/refactor/safeDelete.ts index 81ceb1f5b..95858b212 100644 --- a/src/editor/refactor/safeDelete.ts +++ b/src/editor/refactor/safeDelete.ts @@ -25,6 +25,7 @@ import { ask, confirmChecked } from "@/workbench/messages"; import { openPopup, type PreviewOccurrence } from "@/editor/lsp/popupStore"; import { cannotPerform } from "./catalog"; import { cancelled, languageName } from "./query"; +import { tx } from "@/platform/i18n"; // --------------------------------------------------------------------------- // Deletion range (pure) @@ -269,11 +270,19 @@ export function usagesMessage( ): { message: string; detail: string } { const n = usages.length; const files = new Set(usages.map((u) => u.path)).size; - const noun = n === 1 ? "usage that is" : "usages that are"; - return { - message: `${n} ${n === 1 ? "usage" : "usages"} found`, - detail: `${subject} ${plural ? "have" : "has"} ${n} ${noun} not safe to delete${files > 1 ? ` (in ${files} files)` : ""}.`, - }; + const where = files > 1 ? tx` (in ${files} files)` : ""; + let detail: string; + if (plural) + detail = + n === 1 + ? tx`${subject} have ${n} usage that is not safe to delete${where}.` + : tx`${subject} have ${n} usages that are not safe to delete${where}.`; + else + detail = + n === 1 + ? tx`${subject} has ${n} usage that is not safe to delete${where}.` + : tx`${subject} has ${n} usages that are not safe to delete${where}.`; + return { message: n === 1 ? tx`${n} usage found` : tx`${n} usages found`, detail }; } /** IDEA's "Usages Detected" question; resolves "cancel" when dismissed. */ @@ -391,7 +400,7 @@ function previewTextOccurrences( }); openPopup({ kind: "preview", - title: `Safe Delete ${target.kind.toLowerCase()} '${target.name}'`, + title: tx`Safe Delete ${target.kind} '${target.name}'`, summary: { files: 1, occurrences: occurrences.length, @@ -436,7 +445,7 @@ export async function safeDelete(view: EditorView): Promise { // IDEA always confirms first; usages are searched after OK. const confirmed = await confirmChecked({ title: "Safe Delete", - message: `Delete ${target.kind.toLowerCase()} '${target.name}'?`, + message: tx`Delete ${target.kind} '${target.name}'?`, ok: "OK", checkbox: "Search in comments and strings", checked: searchInComments, @@ -453,7 +462,7 @@ export async function safeDelete(view: EditorView): Promise { ]); if (view.state.doc !== docBefore) return; if (usages.length) { - const answer = await askUsagesDetected(`${target.kind} '${target.name}'`, usages); + const answer = await askUsagesDetected(tx`${target.kind} '${target.name}'`, usages); if (answer === "view") viewUsages(target.name, usages); else if (answer === "delete") applyDeletion(view, docBefore, target, comments); else view.focus(); diff --git a/src/editor/refactor/serverIntroduce.ts b/src/editor/refactor/serverIntroduce.ts index d495fc0ad..17b1fd11c 100644 --- a/src/editor/refactor/serverIntroduce.ts +++ b/src/editor/refactor/serverIntroduce.ts @@ -27,6 +27,7 @@ import type { RefactoringId } from "./catalog"; import { expressionCandidates, expressionPreview } from "./introduce"; import { findTextOccurrences } from "./occurrences"; import { isIdentifier, isKeyword, suggestNames, uniqueNames } from "./names"; +import { tx } from "@/platform/i18n"; /** Refactorings that take an expression (chosen first when nothing is selected). */ export const EXPRESSION_REFACTORINGS: ReadonlySet = new Set([ @@ -135,7 +136,7 @@ export function chooseOccurrences(view: EditorView, actions: readonly CodeAction }, { id: "all", - label: n > 1 ? `Replace all ${n} occurrences` : "Replace all occurrences", + label: n > 1 ? tx`Replace all ${n} occurrences` : "Replace all occurrences", onActive: () => highlightRanges(view, n ? occurrences : [{ from: sel.from, to: sel.to }]), run: () => applyIntroduceAction(view, variants.all, id), }, @@ -291,13 +292,13 @@ export async function applyIntroduceAction(view: EditorView, action: CodeAction, primary, others, suggestions: names.length > 1 ? names : undefined, - hint: keys ? `Press ${keys} to show dialog with more options` : "↵ to finish, Esc to keep the name", + hint: keys ? tx`Press ${keys} to show dialog with more options` : "↵ to finish, Esc to keep the name", commit: (name) => renameIntroduced(view, [primary, ...others], found.name, name), dialog: (typed) => openPopup({ kind: "rename", anchor: caretAnchor(view, primary.from), - title: `Rename '${found.name}'`, + title: tx`Rename '${found.name}'`, initial: typed, suggestions: names, onSubmit: (name) => renameIntroduced(view, [primary, ...others], found.name, name), diff --git a/src/features/agents/AgentsView.tsx b/src/features/agents/AgentsView.tsx index fe0b8a013..c4bd39177 100644 --- a/src/features/agents/AgentsView.tsx +++ b/src/features/agents/AgentsView.tsx @@ -7,6 +7,7 @@ import { Icon, IconButton, Spinner, T, Tree, showContextMenu, type MenuItem } fr import { age, branchLabel, groupTasks, isBusy, type TaskGroup } from "./model"; import { archiveTask, deleteTask, openHome, openTask, useAgents } from "./store"; import styles from "./Agents.module.css"; +import { tx } from "@/platform/i18n"; export function AgentsHeaderActions() { return ; @@ -117,8 +118,8 @@ async function confirmDelete(task: AgentTask) { const r = await confirmChecked({ title: "Delete Agent Task", message: worktree - ? `Delete “${task.title}”? Its branch ${task.branch ?? ""} is kept.` - : `Delete “${task.title}”? Files it changed are kept.`, + ? tx`Delete “${task.title}”? Its branch ${task.branch ?? ""} is kept.` + : tx`Delete “${task.title}”? Files it changed are kept.`, ok: "Delete", icon: "warning", checkbox: worktree ? "Also remove its worktree folder" : undefined, diff --git a/src/features/agents/Composer.tsx b/src/features/agents/Composer.tsx index c090e83c8..710ab877e 100644 --- a/src/features/agents/Composer.tsx +++ b/src/features/agents/Composer.tsx @@ -157,7 +157,7 @@ export function Composer(props: ComposerProps) { for (const file of picked) { if (!file.type.startsWith("image/")) continue; if (file.size > MAX_IMAGE_BYTES) { - notifications().notify({ level: "warning", title: "Image too large", body: `${file.name} is over 5 MB.` }); + notifications().notify({ level: "warning", title: "Image too large", body: tx`${file.name} is over 5 MB.` }); continue; } const reader = new FileReader(); diff --git a/src/features/agents/composerModel.ts b/src/features/agents/composerModel.ts index ceaa2487c..ca21d89cd 100644 --- a/src/features/agents/composerModel.ts +++ b/src/features/agents/composerModel.ts @@ -1,5 +1,6 @@ /** Pure composer helpers: `@` / `/` completion, attachments, history recall, settings. */ import type { AgentCommand, AgentConfigOption, AgentImage, AgentPrompt, AgentUsage } from "@/bindings"; +import { tx } from "@/platform/i18n"; export interface FileRef { /** Absolute path (in the task's directory). */ @@ -114,7 +115,7 @@ export function usageLabel(u: AgentUsage): { short: string; detail: string } { const pct = Math.min(100, Math.round((u.used / u.size) * 100)); const k = (n: number) => (n >= 1000 ? `${Math.round(n / 1000)}k` : `${Math.round(n)}`); const cost = u.cost != null ? ` · ${u.currency === "USD" || !u.currency ? "$" : `${u.currency} `}${u.cost.toFixed(2)}` : ""; - return { short: `${pct}%`, detail: `${k(u.used)} / ${k(u.size)} tokens${cost}` }; + return { short: `${pct}%`, detail: tx`${k(u.used)} / ${k(u.size)} tokens${cost}` }; } /** `data:image/png;base64,AAA` → an image attachment. */ diff --git a/src/features/analysis/AnalysisDialogs.tsx b/src/features/analysis/AnalysisDialogs.tsx index 91dcac9a3..b5a481140 100644 --- a/src/features/analysis/AnalysisDialogs.tsx +++ b/src/features/analysis/AnalysisDialogs.tsx @@ -28,6 +28,7 @@ import { type StructuralForm, } from "./structural"; import styles from "./Analysis.module.css"; +import { tx } from "@/platform/i18n"; function InspectCodeDialog({ onClose }: { onClose: () => void }) { const [scope, setScope] = useState(() => defaultScope()); @@ -363,7 +364,7 @@ function ReplacePreviewDialog({ files, onClose }: { files: StructuralFile[]; onC toggle(key, on)} - aria-label={`Replace in ${f.relPath}:${m.range.start.line + 1}`} + aria-label={tx`Replace in ${f.relPath}:${m.range.start.line + 1}`} />
diff --git a/src/features/analysis/cleanup.ts b/src/features/analysis/cleanup.ts index 22e52d76c..2910023cf 100644 --- a/src/features/analysis/cleanup.ts +++ b/src/features/analysis/cleanup.ts @@ -11,6 +11,7 @@ import { maybeEditorService, notifications } from "@/workbench/contributions"; import { confirm } from "@/workbench/messages"; import { removeTask, upsertTask } from "@/workbench/tasks"; import { changedFiles, currentFile, type ScopeChoice } from "./scope"; +import { tx } from "@/platform/i18n"; export interface CleanupOptions { fixAll: boolean; @@ -97,7 +98,7 @@ export async function runCodeCleanup(choice: ScopeChoice, options: CleanupOption } if ( files.length > CONFIRM_ABOVE && - !(await confirm({ title: "Code Cleanup", message: `Clean up ${files.length} files?`, ok: "Clean Up" })) + !(await confirm({ title: "Code Cleanup", message: tx`Clean up ${files.length} files?`, ok: "Clean Up" })) ) return; const editors = maybeEditorService(); diff --git a/src/features/analysis/structural.ts b/src/features/analysis/structural.ts index 09370434f..5d35e0ea3 100644 --- a/src/features/analysis/structural.ts +++ b/src/features/analysis/structural.ts @@ -21,6 +21,7 @@ import { } from "@/platform/ipc"; import { notifications } from "@/workbench/contributions"; import { resolveScope, type ScopeChoice } from "./scope"; +import { tx } from "@/platform/i18n"; const STATE_KEY = "openidea.analysis.structural.v1"; @@ -152,7 +153,7 @@ export async function showStructuralResults( group: "analysis", level: "warning", title: "Search Structurally", - body: `Stopped after ${summary.matches} matches`, + body: tx`Stopped after ${summary.matches} matches`, }); } } diff --git a/src/features/app/projectActions.ts b/src/features/app/projectActions.ts index f53dc1dc1..288faa7b2 100644 --- a/src/features/app/projectActions.ts +++ b/src/features/app/projectActions.ts @@ -10,6 +10,7 @@ import { useProject } from "@/platform/project"; import { notifications, registerEditorBanner } from "@/workbench/contributions"; import { promptInput } from "@/workbench/inputDialogs"; import { showNewProject } from "./NewProject"; +import { tx } from "@/platform/i18n"; /** localStorage prefixes of IDE-level state exported with the settings (templates, macros, layouts). */ export const EXPORTED_STORAGE = [ @@ -93,7 +94,7 @@ async function renameProject() { initial: project.name, okLabel: "Rename", validate: () => null, - hint: `Leave empty to use the folder name (${folder}).`, + hint: tx`Leave empty to use the folder name (${folder}).`, }); if (name === null) return; try { diff --git a/src/features/build-tools/BuildToolView.tsx b/src/features/build-tools/BuildToolView.tsx index 8f6e98d1c..7060620a4 100644 --- a/src/features/build-tools/BuildToolView.tsx +++ b/src/features/build-tools/BuildToolView.tsx @@ -43,6 +43,7 @@ import { useBuild, } from "./store"; import styles from "./BuildTools.module.css"; +import { tx } from "@/platform/i18n"; export function MavenView() { return ; @@ -292,8 +293,8 @@ function BuildToolView({ tool }: { tool: BuildTool }) { const config = isRunnable(node) && model ? nodeConfig(nodes.filter(isRunnable), model) : null; if (config) { items.push( - { label: `Run '${config.name}'`, icon: "run", run: () => runConfig(config, "run") }, - { label: `Debug '${config.name}'`, icon: "debug", run: () => runConfig(config, "debug") }, + { label: tx`Run '${config.name}'`, icon: "run", run: () => runConfig(config, "run") }, + { label: tx`Debug '${config.name}'`, icon: "debug", run: () => runConfig(config, "debug") }, { kind: "separator" }, ); if (node.kind === "runConfig") @@ -318,7 +319,7 @@ function BuildToolView({ tool }: { tool: BuildTool }) { const file = node.project.buildFile; if (file) items.push({ - label: `Open ${file.split("/").pop()}`, + label: tx`Open ${file.split("/").pop()}`, icon: "file-gradle", run: () => void editorService().openFile(`${root}/${file}`), }); diff --git a/src/features/code-insight/HierarchyView.tsx b/src/features/code-insight/HierarchyView.tsx index 4ea71acd8..9323cfc3e 100644 --- a/src/features/code-insight/HierarchyView.tsx +++ b/src/features/code-insight/HierarchyView.tsx @@ -27,6 +27,7 @@ import { type HierarchyNode, } from "./hierarchy"; import styles from "./HierarchyView.module.css"; +import { tx } from "@/platform/i18n"; type Node = { kind: "root"; id: string } | { kind: "item"; id: string; node: HierarchyNode }; @@ -78,7 +79,7 @@ function HierarchyToolbar() { icon="arrow-up" size="small" iconSize={16} - title={`${directionLabel(mode, "incoming")} Hierarchy`} + title={tx`${directionLabel(mode, "incoming")} Hierarchy`} pressed={direction === "incoming"} onClick={() => setDirection("incoming")} /> @@ -86,7 +87,7 @@ function HierarchyToolbar() { icon="arrow-down" size="small" iconSize={16} - title={`${directionLabel(mode, "outgoing")} Hierarchy`} + title={tx`${directionLabel(mode, "outgoing")} Hierarchy`} pressed={direction === "outgoing"} onClick={() => setDirection("outgoing")} /> diff --git a/src/features/code-insight/ServerStatus.tsx b/src/features/code-insight/ServerStatus.tsx index 07115acbc..eb433ae5e 100644 --- a/src/features/code-insight/ServerStatus.tsx +++ b/src/features/code-insight/ServerStatus.tsx @@ -7,6 +7,7 @@ import { downloadJdkActions } from "@/platform/jdk"; import { relativePath, useProject } from "@/platform/project"; import { STATUS_LABEL, restartServer, serversSummary, useServers } from "./servers"; import styles from "./ServerStatus.module.css"; +import { tx } from "@/platform/i18n"; export function ServerStatus() { const map = useServers((s) => s.servers); @@ -23,7 +24,7 @@ export function ServerStatus() { setOpen((v) => !v)} > @@ -65,7 +66,7 @@ export function ServerStatus() { ) : s.status === "missing" && canInstall ? ( @@ -338,7 +339,7 @@ function InstalledDetails({ plugin }: { plugin: PluginInfo }) { variant="ghost" disabled={!live} onClick={() => void selectPluginTheme(plugin.id, t.id)} - aria-label={`Apply theme ${t.name}`} + aria-label={tx`Apply theme ${t.name}`} > Apply @@ -407,7 +408,7 @@ function InstalledDetails({ plugin }: { plugin: PluginInfo }) { variant="primary" disabled={busy} onClick={() => void uninstallPlugin(plugin.id)} - aria-label={`Confirm uninstall ${plugin.name}`} + aria-label={tx`Confirm uninstall ${plugin.name}`} > Uninstall diff --git a/src/features/plugins/bridge.ts b/src/features/plugins/bridge.ts index b7a70edc7..a5580d7eb 100644 --- a/src/features/plugins/bridge.ts +++ b/src/features/plugins/bridge.ts @@ -11,6 +11,7 @@ import { notifications } from "@/workbench/contributions"; import { applyThemeAttributes, currentThemeId, syncWindowBackground, windowBackgroundHex } from "@/workbench/theme"; import { pluginsBackend } from "./backend"; import { refreshPlugins, runPluginCommand, usePlugins } from "./store"; +import { tx } from "@/platform/i18n"; export const pluginActionId = (pluginId: string, commandId: string) => `plugin.${pluginId}.${commandId}`; @@ -25,7 +26,7 @@ export function pluginActions(plugins: readonly PluginInfo[]): Action[] { category: c.category ?? p.name, icon: "package", keywords: ["plugin", p.name, p.id], - description: `From plugin ${p.name}`, + description: tx`From plugin ${p.name}`, run: async ({ arg }) => { await runPluginCommand(p.id, c.id, typeof arg === "string" ? arg : null); }, diff --git a/src/features/profiler/executor.ts b/src/features/profiler/executor.ts index 6c0e7a41b..6d2e630d6 100644 --- a/src/features/profiler/executor.ts +++ b/src/features/profiler/executor.ts @@ -4,6 +4,7 @@ import { call, commands, isTauri, type ProfilerInstall, type RunConfig } from "@ import { notifications, toolWindowService, type ExecutorLaunch, type RunExecutorContribution } from "@/workbench/contributions"; import { profilerSupports } from "./model"; import { addSnapshot } from "./store"; +import { tx } from "@/platform/i18n"; const errorText = (e: unknown) => (e instanceof Error ? e.message : String(e)); @@ -35,7 +36,7 @@ function notifyMissing(tool: string, message: string, install: ProfilerInstall | level: "warning", key: `profiler-missing-${tool}`, title: `${tool} is not installed`, - body: line ? `${message} Install it with: ${line}` : homepage ? `${message} ${homepage}` : message, + body: line ? tx`${message} Install it with: ${line}` : homepage ? `${message} ${homepage}` : message, sticky: true, actions, }); diff --git a/src/features/project-tree/ProjectView.tsx b/src/features/project-tree/ProjectView.tsx index 9c48f5d36..3756fb245 100644 --- a/src/features/project-tree/ProjectView.tsx +++ b/src/features/project-tree/ProjectView.tsx @@ -21,7 +21,7 @@ import clsx from "clsx"; import { useEditorRegistry } from "@/editor/api"; import { executeAction } from "@/platform/actions"; import { setContext } from "@/platform/context"; -import { useTxTemplate } from "@/platform/i18n"; +import { tx, useTxTemplate } from "@/platform/i18n"; import { isTauri } from "@/platform/ipc"; import type { RootFolder } from "@/platform/languageSettings"; import { useEffectiveRoots } from "@/platform/projectRoots"; @@ -460,7 +460,7 @@ export function ProjectView() { setDropTarget(valid ? target.key : null); setDrag({ paths, - label: paths.length === 1 ? nameOf(paths[0]!) : `${paths.length} items`, + label: paths.length === 1 ? nameOf(paths[0]!) : tx`${paths.length} items`, x: ev.clientX, y: ev.clientY, copy: ev.altKey, diff --git a/src/features/project-tree/RefactoringPreview.tsx b/src/features/project-tree/RefactoringPreview.tsx index a5c81c69c..1fe2e8f27 100644 --- a/src/features/project-tree/RefactoringPreview.tsx +++ b/src/features/project-tree/RefactoringPreview.tsx @@ -12,6 +12,7 @@ import { Checkbox, Dialog, Icon, fileIconName } from "@/ui"; import { nameOf, relativeTo } from "./paths"; import { useProjectTree } from "./store"; import styles from "./FileRefactor.module.css"; +import { tx } from "@/platform/i18n"; type EditFile = Extract; @@ -67,7 +68,7 @@ function FileGroup({ file, root, included, onToggle }: { file: EditFile; root: s return (
  • - + {nameOf(file.path)} {relativeTo(root, file.path) || file.path} @@ -117,7 +118,7 @@ function PreviewDialog({ request }: { request: PreviewRequest }) { return ( request.resolve(null)} width={680} buttons={[ diff --git a/src/features/project-tree/actions.ts b/src/features/project-tree/actions.ts index 68b2a1e08..621d6805c 100644 --- a/src/features/project-tree/actions.ts +++ b/src/features/project-tree/actions.ts @@ -42,6 +42,7 @@ import { setViewOption, useProjectTree, } from "./store"; +import { tx } from "@/platform/i18n"; const FOCUS = "projectViewFocus"; const OPEN = "projectOpen"; @@ -150,7 +151,7 @@ export function registerProjectActions(): () => void { const toggles: Action[] = OPTION_LABELS.map(([key, label]) => ({ id: `project.option.${key}`, - title: `Project View: ${label}`, + title: tx`Project View: ${label}`, category: "View", keywords: ["project", "tree", "toggle"], toggled: () => useProjectTree.getState().options[key], @@ -159,7 +160,7 @@ export function registerProjectActions(): () => void { const modes: Action[] = VIEW_MODES.map(([mode, label]) => ({ id: `project.viewMode.${mode}`, - title: `Project View: Show ${label}`, + title: tx`Project View: Show ${label}`, category: "View", when: OPEN, keywords: ["project", "pane", "scope", "view as"], @@ -335,7 +336,7 @@ export function registerProjectActions(): () => void { copyPath("name", "project.copyFileName", "Copy File Name"), { id: "project.revealInFinder", - title: `Reveal in ${revealLabel()}`, + title: tx`Reveal in ${revealLabel()}`, category: "Project View", icon: "external-link", when: OPEN, diff --git a/src/features/project-tree/contextMenu.ts b/src/features/project-tree/contextMenu.ts index ba8961e6d..80ae286a8 100644 --- a/src/features/project-tree/contextMenu.ts +++ b/src/features/project-tree/contextMenu.ts @@ -27,6 +27,7 @@ import { vcsArg, } from "./operations"; import { isSameOrInside } from "./paths"; +import { tx } from "@/platform/i18n"; /** Keys handled by the project view itself (the default keymap has no entries for them). */ export const LOCAL_KEYS: Record = { @@ -352,7 +353,7 @@ export function contextMenuItems(nodes: ProjectNode[]): MenuItem[] { { id: "unmark", label: - current === "excluded" ? "Cancel Exclusion" : `Unmark as ${roles.find(([r]) => r === current)![1]}`, + current === "excluded" ? "Cancel Exclusion" : tx`Unmark as ${roles.find(([r]) => r === current)![1]}`, run: () => void markDirectory(single.path, null), }, ] diff --git a/src/features/project-tree/fileRefactor.ts b/src/features/project-tree/fileRefactor.ts index d8fdee596..79fb54835 100644 --- a/src/features/project-tree/fileRefactor.ts +++ b/src/features/project-tree/fileRefactor.ts @@ -12,6 +12,7 @@ import { projectFs } from "./fs"; import { isSameOrInside, nameOf, parentOf } from "./paths"; import { previewFileRefactoring } from "./RefactoringPreview"; import { useProjectTree } from "./store"; +import { tx } from "@/platform/i18n"; export interface FileRefactorOptions { /** Update references through the language servers. */ @@ -262,7 +263,7 @@ export async function runFileRefactoring( notifications().notify({ level: "info", title, - body: edited ? `Updated references in ${edited} ${edited === 1 ? "file" : "files"}` : undefined, + body: !edited ? undefined : edited === 1 ? tx`Updated references in ${edited} file` : tx`Updated references in ${edited} files`, actions: [{ label: "Undo", run: () => void undoFileRefactoring(id) }], key: "project.refactor.done", }); @@ -296,7 +297,7 @@ export async function undoFileRefactoring(id = lastMove?.id): Promise { level: outcome.conflicts.length ? "warning" : "info", title: "Refactoring undone", body: outcome.conflicts.length - ? `Changed since the refactoring, not reverted: ${outcome.conflicts.map(nameOf).join(", ")}` + ? tx`Changed since the refactoring, not reverted: ${outcome.conflicts.map(nameOf).join(", ")}` : undefined, key: "project.refactor.done", }); diff --git a/src/features/project-tree/operations.ts b/src/features/project-tree/operations.ts index 47793ff6e..1c8b54213 100644 --- a/src/features/project-tree/operations.ts +++ b/src/features/project-tree/operations.ts @@ -37,6 +37,7 @@ import { startEditing, useProjectTree, } from "./store"; +import { tx } from "@/platform/i18n"; /** Nodes that may be renamed, moved or deleted (not roots, groups or library sources). */ const mutable = (n: ProjectNode) => !n.root && !n.placeholder && !n.readOnly; @@ -367,7 +368,7 @@ export async function safeToDelete(files: string[]): Promise { if (user.choice) return user.choice === "skip"; if (result.status !== "ok" || !result.usages.length) return true; const name = files.length === 1 ? nameOf(files[0]!) : `${files.length} files`; - const subject = files.length === 1 ? `File '${name}'` : `The ${name}`; + const subject = files.length === 1 ? tx`File '${name}'` : tx`The ${files.length} files`; const answer = await askUsagesDetected(subject, result.usages, files.length > 1); if (answer === "view") viewUsages(name, result.usages); return answer === "delete"; diff --git a/src/features/run/ConfigChooser.tsx b/src/features/run/ConfigChooser.tsx index ce074f0ab..864b594af 100644 --- a/src/features/run/ConfigChooser.tsx +++ b/src/features/run/ConfigChooser.tsx @@ -7,6 +7,7 @@ import type { RunConfig } from "@/platform/ipc"; import { commandLine, configIcon, groupConfigs } from "./configs"; import { canDeleteConfig, CURRENT_FILE, deleteConfig, saveTemporary, selectConfig, startRun, useRun } from "./store"; import styles from "./Run.module.css"; +import { tx } from "@/platform/i18n"; type Row = | { type: "section"; key: string; label: string } @@ -281,7 +282,7 @@ function ConfigChooser() { icon="run" size="small" iconSize={11} - title={`Run '${c.name}'`} + title={tx`Run '${c.name}'`} tabIndex={-1} onClick={(e) => { e.stopPropagation(); @@ -292,7 +293,7 @@ function ConfigChooser() { icon="debug" size="small" iconSize={13} - title={`Debug '${c.name}'`} + title={tx`Debug '${c.name}'`} tabIndex={-1} onClick={(e) => { e.stopPropagation(); diff --git a/src/features/run/ConfigForm.tsx b/src/features/run/ConfigForm.tsx index 1b0d4698a..dcd55e56f 100644 --- a/src/features/run/ConfigForm.tsx +++ b/src/features/run/ConfigForm.tsx @@ -545,7 +545,7 @@ export function ConfigForm({ config, all, type, errors, template, onChange, modi icon="close" size="small" iconSize={10} - title={`Remove '${OPTION_LABELS[id]}'`} + title={tx`Remove '${OPTION_LABELS[id]}'`} onClick={() => update(optionPatch(id, false))} /> diff --git a/src/features/run/RunView.tsx b/src/features/run/RunView.tsx index c4187ed67..970a1be2b 100644 --- a/src/features/run/RunView.tsx +++ b/src/features/run/RunView.tsx @@ -28,6 +28,7 @@ import { import { TestTreeView, formatDuration } from "./TestTreeView"; import { StackTracePanel, closeStackTrace, useStackTraces, type StackTraceTab } from "./stackTrace"; import styles from "./Run.module.css"; +import { tx } from "@/platform/i18n"; function failed(s: RunSession): boolean { return s.status === "failed" || (s.status === "finished" && s.exitCode !== 0 && !s.stopped); @@ -81,7 +82,7 @@ function SessionPanel({ session }: { session: RunSession }) { cwd={cwd} onData={(data) => writeTerminal(session.id, data)} onResize={(cols, rows) => resizeTerminal(session.id, cols, rows)} - aria-label={`${session.config.name} terminal`} + aria-label={tx`${session.config.name} terminal`} /> ) : ( )}
    @@ -196,7 +197,7 @@ function sessionItems(sessions: readonly RunSession[]): TabItem[] { closable: true, tooltip: s.status === "finished" - ? `Finished in ${formatDuration((s.endedAt ?? s.startedAt) - s.startedAt)}, exit code ${s.exitCode ?? "?"}` + ? tx`Finished in ${formatDuration((s.endedAt ?? s.startedAt) - s.startedAt)}, exit code ${s.exitCode ?? "?"}` : undefined, })); } diff --git a/src/features/run/RunWidget.tsx b/src/features/run/RunWidget.tsx index d95cfde63..eb08ddfc1 100644 --- a/src/features/run/RunWidget.tsx +++ b/src/features/run/RunWidget.tsx @@ -85,7 +85,7 @@ export function RunWidget() { size="large" iconSize={16} className={clsx(styles.widgetButton, styles.widgetFilled, styles.widgetFilledRun)} - title={`Rerun '${compoundRunning.length && selected ? selected.name : target.config.name}'`} + title={tx`Rerun '${compoundRunning.length && selected ? selected.name : target.config.name}'`} shortcut={shortcutLabel("run.run")} onClick={() => (compoundRunning.length && selected ? void startRun(selected) : rerunSession(target.id))} /> @@ -96,10 +96,10 @@ export function RunWidget() { className={clsx(styles.widgetButton, styles.widgetFilled, styles.widgetFilledStop)} title={ compoundRunning.length && selected - ? `Stop '${selected.name}'` + ? tx`Stop '${selected.name}'` : running.length > 1 - ? `Stop (${running.length} running)` - : `Stop '${target.config.name}'` + ? tx`Stop (${running.length} running)` + : tx`Stop '${target.config.name}'` } shortcut={shortcutLabel("run.stop")} onClick={() => @@ -155,8 +155,8 @@ export function RunWidget() { title={ selected ? debugOnly - ? `'${selected.name}' can only be debugged` - : `Run '${selected.name}'` + ? tx`'${selected.name}' can only be debugged` + : tx`Run '${selected.name}'` : currentFile ? "Run Current File" : "Run" @@ -170,7 +170,7 @@ export function RunWidget() { size="large" iconSize={18} className={styles.widgetButton} - title={selected ? `Debug '${selected.name}'` : currentFile ? "Debug Current File" : "Debug"} + title={selected ? tx`Debug '${selected.name}'` : currentFile ? "Debug Current File" : "Debug"} shortcut={shortcutLabel("run.debug")} disabled={!selected && !currentFile} onClick={() => void executeAction("run.debug")} diff --git a/src/features/run/TestTreeView.tsx b/src/features/run/TestTreeView.tsx index 0e9ab2444..71aafa6ce 100644 --- a/src/features/run/TestTreeView.tsx +++ b/src/features/run/TestTreeView.tsx @@ -6,6 +6,7 @@ import { openTarget } from "./navigate"; import { nodeStatus, type TestNode, type TestResults, type TestStatus } from "./testing"; import { parseComparison } from "./testing/comparison"; import styles from "./Run.module.css"; +import { tx } from "@/platform/i18n"; /** Test tree filters, kept across runs and restarts (IDEA hides passed tests by default). */ const FILTERS_KEY = "openidea.run.testFilters"; @@ -39,7 +40,7 @@ export function showTestDiff(node: TestNode): boolean { if (!c) return false; void executeAction("diff.compareTexts", { path: `${node.path.join("/")}.txt`, - title: `Comparison Failure: ${node.name}`, + title: tx`Comparison Failure: ${node.name}`, left: { text: c.expected, label: c.labels?.expected ?? "Expected" }, right: { text: c.actual, label: c.labels?.actual ?? "Actual" }, }); @@ -136,8 +137,8 @@ export function TestTreeView({ results, version, cwd, selected, onSelect, runnin const items: MenuItem[] = []; if (onRunTests) { items.push( - { label: `Run '${name}'`, verbatim: true, icon: "run", run: () => onRunTests(ids, false) }, - { label: `Debug '${name}'`, verbatim: true, icon: "debug", run: () => onRunTests(ids, true) }, + { label: tx`Run '${name}'`, verbatim: true, icon: "run", run: () => onRunTests(ids, false) }, + { label: tx`Debug '${name}'`, verbatim: true, icon: "debug", run: () => onRunTests(ids, true) }, { kind: "separator" }, ); } diff --git a/src/features/run/contextRun.ts b/src/features/run/contextRun.ts index 5c3b70bb8..2603ad48b 100644 --- a/src/features/run/contextRun.ts +++ b/src/features/run/contextRun.ts @@ -32,6 +32,7 @@ import { runWithExecutor } from "./executors"; import { goModulePath, loadGoModules } from "./goModules"; import { toolEnv } from "./runtimes"; import { adoptTemporary, loadConfigs, modifyTemporary, startRun, useRun } from "./store"; +import { tx } from "@/platform/i18n"; const CONTENT_LIMIT = 400_000; @@ -180,7 +181,7 @@ export function choiceItems(choices: readonly Produced[], debug: boolean): MenuI { kind: "header", label: debug ? "Choose configuration to debug" : "Choose configuration to run" }, ...choices.map( (p): MenuItem => ({ - label: `${debug ? "Debug" : "Run"} '${p.name}'`, + label: debug ? tx`Debug '${p.name}'` : tx`Run '${p.name}'`, verbatim: true, icon: debug ? "debug" : "run", shortcut: p.detail, @@ -316,10 +317,10 @@ export function contextMenuItems(menu: ContextMenuId, ctx: MenuGroupContext): Me if (!p) return []; const arg: ContextArg = { path, line, dir, content: dir ? undefined : input.content }; return [ - { kind: "action", action: "run.runContext", arg, label: `Run '${p.name}'`, icon: "run" }, + { kind: "action", action: "run.runContext", arg, label: tx`Run '${p.name}'`, icon: "run" }, // Like IDEA, Debug only where a debugger can run it. ...(p.debug || p.config?.debugAdapter || p.missing - ? ([{ kind: "action", action: "run.debugContext", arg, label: `Debug '${p.name}'`, icon: "debug" }] as MenuItem[]) + ? ([{ kind: "action", action: "run.debugContext", arg, label: tx`Debug '${p.name}'`, icon: "debug" }] as MenuItem[]) : []), { label: "More Run/Debug", diff --git a/src/features/run/dialogs.tsx b/src/features/run/dialogs.tsx index 3bae27b36..edd3b5eaa 100644 --- a/src/features/run/dialogs.tsx +++ b/src/features/run/dialogs.tsx @@ -6,6 +6,7 @@ import { loadSettings } from "@/platform/project"; import { Dialog, Input } from "@/ui"; import { confirmChecked } from "@/workbench/messages"; import styles from "./Run.module.css"; +import { tx } from "@/platform/i18n"; interface Pending { title: string; @@ -20,8 +21,11 @@ const useRunDialog = create<{ pending: Pending | null }>(() => ({ pending: null */ export async function confirmRerun(name: string, instances = 1): Promise { const answer = await confirmChecked({ - title: `Process '${name}' Is Running`, - message: `'${name}' is not allowed to run in parallel.\nWould you like to stop ${instances > 1 ? `${instances} running instances` : "the running one"}?`, + title: tx`Process '${name}' Is Running`, + message: + instances > 1 + ? tx`'${name}' is not allowed to run in parallel.\nWould you like to stop ${instances} running instances?` + : tx`'${name}' is not allowed to run in parallel.\nWould you like to stop the running one?`, ok: "Stop and Rerun", checkbox: "Do not ask again", }); @@ -39,8 +43,8 @@ export async function confirmRerun(name: string, instances = 1): Promise { const answer = await confirmChecked({ - title: `Process '${name}' Is Running`, - message: `Do you want to terminate the process '${name}'?`, + title: tx`Process '${name}' Is Running`, + message: tx`Do you want to terminate the process '${name}'?`, ok: "Terminate", checkbox: "Do not ask again", }); diff --git a/src/features/run/index.ts b/src/features/run/index.ts index 7f8978c56..1355f5ca5 100644 --- a/src/features/run/index.ts +++ b/src/features/run/index.ts @@ -65,6 +65,7 @@ import { stopSession, useRun, } from "./store"; +import { tx } from "@/platform/i18n"; function hasProject() { return !!useProject.getState().project; @@ -117,7 +118,7 @@ async function debugConfig(config: RunConfig, chain: readonly string[] = []) { group: "run", level: "warning", title: "Debugger is not available", - body: `Running '${config.name}' without the debugger.`, + body: tx`Running '${config.name}' without the debugger.`, key: "run-no-debugger", }); await startRun(config, undefined, undefined, { prepared: true }); diff --git a/src/features/run/navigate.ts b/src/features/run/navigate.ts index 2d73e7ba4..77c18078d 100644 --- a/src/features/run/navigate.ts +++ b/src/features/run/navigate.ts @@ -3,6 +3,7 @@ import { call, commands, isTauri, nextRequestId } from "@/platform/ipc"; import { useProject } from "@/platform/project"; import { editorService, notifications } from "@/workbench/contributions"; import { joinPath, type FileTarget } from "./links"; +import { tx } from "@/platform/i18n"; async function exists(path: string): Promise { const slash = path.lastIndexOf("/"); @@ -99,7 +100,7 @@ export async function openTarget(target: FileTarget, cwd: string, focus = true): group: "run", level: "info", title: "No sources for this class", - body: `${target.className.split("$")[0]} is not part of the project, and no library sources are available for it.`, + body: tx`${target.className.split("$")[0]!} is not part of the project, and no library sources are available for it.`, key: "run-link", }); return; diff --git a/src/features/run/producers.ts b/src/features/run/producers.ts index a6fb12759..87d74641c 100644 --- a/src/features/run/producers.ts +++ b/src/features/run/producers.ts @@ -16,6 +16,7 @@ import { applicationConfig, jvmModuleDir } from "./application"; import { formatArgs } from "./configs"; import { contextConfig, fileRole, projectPython, testBasesFor, testConfigFor } from "./context"; import { detectRunTargets, targetConfig, targetLanguage, type RunTarget } from "./targets"; +import { tx } from "@/platform/i18n"; export interface LocatedTool { path: string | null; @@ -300,7 +301,7 @@ const LANGUAGE_NAMES: Partial> = { function missing(lang: ScriptLanguage, file: string, tools: string[]): Missing { const what = LANGUAGE_NAMES[lang] ?? tools[0] ?? lang; return { - title: `Cannot run '${file}'`, + title: tx`Cannot run '${file}'`, body: `${what} was not found (looked for ${tools.map((t) => `\`${t}\``).join(", ")}). ${INSTALL[lang] ?? ""}`.trim(), }; } @@ -421,7 +422,7 @@ function jsRuntime(lang: "js" | "ts", env: ToolEnv, s: FileSite): Produced { name, scope: "file", missing: { - title: `Cannot run '${name}'`, + title: tx`Cannot run '${name}'`, body: `Running TypeScript needs Node.js 22.6+ (type stripping), tsx or ts-node. ${found}` + "Upgrade Node.js (https://nodejs.org), or install tsx: `npm install -D tsx` (or `npm install -g tsx`).", diff --git a/src/features/run/runGutter.ts b/src/features/run/runGutter.ts index e42f206e4..ea69fa1b9 100644 --- a/src/features/run/runGutter.ts +++ b/src/features/run/runGutter.ts @@ -22,6 +22,7 @@ import { onTestStatesChanged, testState, testStatesVersion, type TestState } fro /** Languages with gutter run targets (C / C++ `main` included). */ const hasGutterTargets = (path: string) => !!targetLanguage(path) || /\.(c|cpp|cc|cxx)$/.test(path); import styles from "./Run.module.css"; +import { tx } from "@/platform/i18n"; interface PlacedTarget extends RunTarget { /** Line start offset in the current state. */ @@ -104,9 +105,9 @@ export function targetMenuItems(target: RunTarget, path: string, content: string // Like IDEA, Debug only where a debugger can run it. const debuggable = !p || !!(p.debug || p.config?.debugAdapter || p.missing); return [ - { label: `Run '${name}'`, icon: "run", shortcut: shortcutLabel("run.runContext"), ...pick(false) } as MenuItem, + { label: tx`Run '${name}'`, icon: "run", shortcut: shortcutLabel("run.runContext"), ...pick(false) } as MenuItem, ...(debuggable - ? [{ label: `Debug '${name}'`, icon: "debug", shortcut: shortcutLabel("run.debugContext"), ...pick(true) } as MenuItem] + ? [{ label: tx`Debug '${name}'`, icon: "debug", shortcut: shortcutLabel("run.debugContext"), ...pick(true) } as MenuItem] : []), ...runExecutors() .filter((e) => !!p?.config && e.supports(p.config)) diff --git a/src/features/run/services.tsx b/src/features/run/services.tsx index 597453d8d..348b0a16e 100644 --- a/src/features/run/services.tsx +++ b/src/features/run/services.tsx @@ -9,7 +9,7 @@ import { create } from "zustand"; import { executeAction } from "@/platform/actions"; import type { RunConfig } from "@/platform/ipc"; import { useProject } from "@/platform/project"; -import { useT, useTxTemplate } from "@/platform/i18n"; +import { tx, useT, useTxTemplate } from "@/platform/i18n"; import { Button, EmptyState, T } from "@/ui"; import type { MenuItem } from "@/ui"; import { @@ -120,7 +120,7 @@ export function runServiceItems( key: TYPE_PREFIX + kind, label: kindLabel(kind), icon: configIcon({ kind, isTest: false }), - detail: running ? `${running} running` : undefined, + detail: running ? tx`${running} running` : undefined, children, }; }); diff --git a/src/features/run/stackTrace.tsx b/src/features/run/stackTrace.tsx index 991326735..485d66ebb 100644 --- a/src/features/run/stackTrace.tsx +++ b/src/features/run/stackTrace.tsx @@ -15,6 +15,7 @@ import { ConsoleView } from "./ConsoleView"; import { findOutputLinks } from "./links"; import { useRun } from "./store"; import styles from "./StackTrace.module.css"; +import { tx } from "@/platform/i18n"; export interface StackTraceTab { id: string; @@ -142,7 +143,7 @@ export function StackTracePanel({ tab }: { tab: StackTraceTab }) { follow={false} onFollowChange={() => {}} cwd={root} - aria-label={`Stack trace ${tab.title}`} + aria-label={tx`Stack trace ${tab.title}`} /> ); } diff --git a/src/features/run/store.ts b/src/features/run/store.ts index 05270508f..e7c7b045e 100644 --- a/src/features/run/store.ts +++ b/src/features/run/store.ts @@ -21,6 +21,7 @@ import { failedTestPaths, leafTestPaths, rerunFailedConfig, testsConfig } from " import { compoundChildren } from "./configs"; import { TerminalOutput } from "./terminalOutput"; import { typeFor } from "./types"; +import { tx } from "@/platform/i18n"; /** The permanent "Current File" pseudo-configuration of the chooser (runs the active file's context). */ export const CURRENT_FILE = "current-file"; @@ -549,7 +550,7 @@ export async function startRun( } } catch (e) { const message = e instanceof Error ? e.message : String(e); - enqueue(sessionId, { kind: "stderr", text: `Error running '${config.name}': ${message}\n` }); + enqueue(sessionId, { kind: "stderr", text: tx`Error running '${config.name}': ${message}\n` }); flush(sessionId); patchSession(sessionId, { status: "failed", endedAt: Date.now() }); notifications().notify({ group: "run", level: "error", title: `Error running '${config.name}'`, body: message }); @@ -795,7 +796,7 @@ export async function sendStdin(sessionId: string, text: string): Promise try { await call(commands.runStdin(session.runId, text + "\n")); } catch (e) { - enqueue(sessionId, { kind: "system", text: `Cannot write to stdin: ${e instanceof Error ? e.message : String(e)}\n` }); + enqueue(sessionId, { kind: "system", text: tx`Cannot write to stdin: ${e instanceof Error ? e.message : String(e)}\n` }); } } diff --git a/src/features/run/types.ts b/src/features/run/types.ts index 9d30c8a11..f9a69453c 100644 --- a/src/features/run/types.ts +++ b/src/features/run/types.ts @@ -11,6 +11,7 @@ import { type RunConfigTypeContribution, } from "@/workbench/contributions"; import { formatArgs, parseArgs, uid, uniqueName } from "./configs"; +import { tx } from "@/platform/i18n"; export const COMPOUND = "compound"; @@ -203,21 +204,21 @@ function buildToolType( id: string, label: string, icon: string, - tasksLabel: string, + tasks: { label: string; description: string }, wrapper: { unix: string; windows: string; plain: string; label: string; byDefault: boolean }, ): RunConfigTypeContribution { return { id, label, icon, - description: `Run ${label} ${tasksLabel.toLowerCase()}`, + description: tasks.description, defaults: wrapper.byDefault ? { wrapper: "true" } : {}, ownsWorkingDirectory: true, fields: [ - { key: "projectDir", label: `${label} project`, mono: true, placeholder: "Project root" }, + { key: "projectDir", label: tx`${label} project`, mono: true, placeholder: "Project root" }, { key: "tasks", - label: tasksLabel, + label: tasks.label, mono: true, required: true, suggestions: (configs) => argsOf(configs, id, 0), @@ -244,7 +245,7 @@ function buildToolType( }; } -const gradle = buildToolType("gradle", "Gradle", "file-gradle", "Tasks", { +const gradle = buildToolType("gradle", "Gradle", "file-gradle", { label: "Tasks", description: "Run Gradle tasks" }, { unix: "gradlew", windows: "gradlew.bat", plain: "gradle", @@ -252,7 +253,7 @@ const gradle = buildToolType("gradle", "Gradle", "file-gradle", "Tasks", { byDefault: true, }); -const maven = buildToolType("maven", "Maven", "file-maven", "Goals", { +const maven = buildToolType("maven", "Maven", "file-maven", { label: "Goals", description: "Run Maven goals" }, { unix: "mvnw", windows: "mvnw.cmd", plain: "mvn", diff --git a/src/features/search/bookmarks/BookmarksView.tsx b/src/features/search/bookmarks/BookmarksView.tsx index 247ce263a..393e5856f 100644 --- a/src/features/search/bookmarks/BookmarksView.tsx +++ b/src/features/search/bookmarks/BookmarksView.tsx @@ -3,7 +3,7 @@ import clsx from "clsx"; import { executeAction } from "@/platform/actions"; import { shortcutLabel } from "@/platform/keymap"; import { confirm } from "@/workbench/messages"; -import { useT, useTx } from "@/platform/i18n"; +import { tx, useT, useTx } from "@/platform/i18n"; import { EmptyState, Icon, IconButton, Tree, fileIconName, showContextMenu, type MenuItem } from "@/ui"; import { basename, openLocation, relPath } from "../common/open"; import { @@ -118,7 +118,10 @@ async function removeList(list: BookmarkList, count: number) { count && !(await confirm({ title: "Delete Bookmark List", - message: `Delete the list '${list.name}' with its ${count} bookmark${count === 1 ? "" : "s"}?`, + message: + count === 1 + ? tx`Delete the list '${list.name}' with its ${count} bookmark?` + : tx`Delete the list '${list.name}' with its ${count} bookmarks?`, ok: "Delete", })) ) @@ -263,7 +266,7 @@ export function BookmarksView() { ...(bookmarks.length ? [{ label: "Move to Another List", submenu: moveMenu(bookmarks, lists) }] : []), { kind: "separator" as const }, { - label: targets.length > 1 ? `Delete ${targets.length} Bookmarks` : "Delete Bookmark", + label: targets.length > 1 ? tx`Delete ${targets.length} Bookmarks` : "Delete Bookmark", danger: true, run: () => removeNodes(targets), }, diff --git a/src/features/search/index.ts b/src/features/search/index.ts index c61173f46..f086032d4 100644 --- a/src/features/search/index.ts +++ b/src/features/search/index.ts @@ -43,6 +43,7 @@ import { RecentPopupsOverlay } from "./recent/RecentPopups"; import { cycleSwitcher, hideRecentPopup, showRecentPopup } from "./recent/store"; import { FileStructureOverlay, useFileStructurePopup } from "./structure/StructureView"; import { useOutline, useStructureOptions } from "./structure/store"; +import { tx } from "@/platform/i18n"; /** Single-line editor selection, used to prefill search fields. */ function editorSelectionText(): string | null { @@ -452,7 +453,7 @@ export function activate(): void | (() => void) { ...[..."0123456789"].flatMap((d) => [ { id: `navigate.toggleBookmark${d}`, - title: `Toggle Bookmark ${d}`, + title: tx`Toggle Bookmark ${d}`, category: "Bookmarks", when: "projectOpen && textEditorActive", run: () => { @@ -462,7 +463,7 @@ export function activate(): void | (() => void) { }, { id: `navigate.gotoBookmark${d}`, - title: `Go to Bookmark ${d}`, + title: tx`Go to Bookmark ${d}`, category: "Bookmarks", when: "projectOpen", run: () => { diff --git a/src/features/services/docker.tsx b/src/features/services/docker.tsx index 52442d216..55277aa17 100644 --- a/src/features/services/docker.tsx +++ b/src/features/services/docker.tsx @@ -14,7 +14,7 @@ import type { DockerObject, DockerRegistry, } from "@/platform/ipc"; -import { useTx, useTxTemplate } from "@/platform/i18n"; +import { tx, useTx, useTxTemplate } from "@/platform/i18n"; import { call, commands, isCancelled, nextRequestId } from "@/platform/ipc"; import { Button, EmptyState, IconButton, Spinner, Tabs, Toggle, T } from "@/ui"; import type { MenuItem } from "@/ui"; @@ -168,7 +168,7 @@ async function removeConnectionPrompt(c: DockerConnection) { if ( await confirm({ title: "Remove Docker Connection", - message: `Remove the connection '${c.name}' from Services?`, + message: tx`Remove the connection '${c.name}' from Services?`, ok: "Remove", }) ) @@ -179,7 +179,7 @@ async function removeRegistryPrompt(r: DockerRegistry) { if ( await confirm({ title: "Remove Docker Registry", - message: `Remove the registry '${r.name}' and its saved password?`, + message: tx`Remove the registry '${r.name}' and its saved password?`, ok: "Remove", }) ) diff --git a/src/features/services/ops.ts b/src/features/services/ops.ts index 4f358b416..62d0f6de0 100644 --- a/src/features/services/ops.ts +++ b/src/features/services/ops.ts @@ -1,5 +1,5 @@ /** What the Services toolbar, context menu and `docker.*` actions do with a node of a connection. */ -import { txTemplate } from "@/platform/i18n"; +import { tx, txTemplate } from "@/platform/i18n"; import type { PruneTarget } from "@/platform/ipc"; import { executeAction } from "@/platform/actions"; import type { MenuItem } from "@/ui"; @@ -47,6 +47,24 @@ function composeAction(conn: string, n: Extract undefined); } +function deleteQuestion(kind: "container" | "image" | "volume" | "network", label: string, running: boolean) { + switch (kind) { + case "container": + return { + title: "Delete Container", + message: running + ? tx`Delete container '${label}'? It is running and will be stopped.` + : tx`Delete container '${label}'?`, + }; + case "image": + return { title: "Delete Image", message: tx`Delete image '${label}'?` }; + case "volume": + return { title: "Delete Volume", message: tx`Delete volume '${label}'?` }; + case "network": + return { title: "Delete Network", message: tx`Delete network '${label}'?` }; + } +} + export async function remove(t: DockerTarget | null) { if (!t || !canRemove(t)) return; const n = t.node; @@ -54,7 +72,7 @@ export async function remove(t: DockerTarget | null) { if (n.kind === "compose") { const ok = await confirm({ title: "Compose Down", - message: `Stop and remove the containers and networks of '${label}'?`, + message: tx`Stop and remove the containers and networks of '${label}'?`, ok: "Down", }); if (ok) await composeAction(t.conn, n, "down"); @@ -64,8 +82,7 @@ export async function remove(t: DockerTarget | null) { const what = n.kind; const running = n.kind === "container" && isRunning(n.container); const answer = await confirmChecked({ - title: `Delete ${what[0]!.toUpperCase()}${what.slice(1)}`, - message: `Delete ${what} '${label}'?${running ? " It is running and will be stopped." : ""}`, + ...deleteQuestion(what, label, running), ok: "Delete", checkbox: n.kind === "image" ? "Force (also untag / remove when used by stopped containers)" : undefined, }); @@ -107,6 +124,23 @@ export async function prunePrompt(conn: string, target: PruneTarget) { if (ok) await prune(conn, target); } +function pruneLabel(target: PruneTarget): string { + switch (target) { + case "containers": + return tx("Prune Unused Containers…"); + case "images": + return tx("Prune Dangling Images…"); + case "allImages": + return tx("Prune Unused Images…"); + case "volumes": + return tx("Prune Unused Volumes…"); + case "networks": + return tx("Prune Unused Networks…"); + case "system": + return tx("Prune Everything Unused…"); + } +} + export function pruneMenu(conn: string): MenuItem[] { return PRUNE.map(([target, label]) => ({ label, run: () => void prunePrompt(conn, target) })); } @@ -155,7 +189,7 @@ export function nodeMenu(t: DockerTarget): MenuItem[] { if (n.kind === "group") { const target: PruneTarget = n.group === "containers" ? "containers" : n.group === "images" ? "images" : n.group === "volumes" ? "volumes" : "networks"; items.push({ - label: txTemplate(`Prune ${n.group === "images" ? "Dangling Images" : `Unused ${n.group[0]!.toUpperCase()}${n.group.slice(1)}`}…`), + label: pruneLabel(target), run: () => void prunePrompt(conn, target), }); } diff --git a/src/features/services/runTypes.ts b/src/features/services/runTypes.ts index 4f2ca0518..25413b5d5 100644 --- a/src/features/services/runTypes.ts +++ b/src/features/services/runTypes.ts @@ -11,6 +11,7 @@ import { call, commands, type RunConfig } from "@/platform/ipc"; import { registerRunConfigType, type RunConfigTypeContribution } from "@/workbench/contributions"; import { imageTag, targetTitle, type DockerTarget } from "./compose"; import { connections, endpointOf, loadSettings, useServices } from "./store"; +import { tx } from "@/platform/i18n"; export const DOCKERFILE = "docker-dockerfile"; export const DOCKER_IMAGE = "docker-image"; @@ -279,7 +280,7 @@ export function gutterActions( config: typedConfig(dockerfileType, `docker:dockerfile:${file}`, file, { ...params, runImage: "true" }), }, { - title: `Build Image '${params.tag}'`, + title: tx`Build Image '${params.tag}'`, config: typedConfig(dockerfileType, `docker:build:${file}`, `Build ${file}`, params), }, ]; diff --git a/src/features/settings/components/Form.tsx b/src/features/settings/components/Form.tsx index d5fa308dc..98c91225c 100644 --- a/src/features/settings/components/Form.tsx +++ b/src/features/settings/components/Form.tsx @@ -146,7 +146,7 @@ export function NumberInput({ id, value, onChange, min, max, step = 1, integer, aria-label={aria["aria-label"]} aria-valuemin={min} aria-valuemax={max} - title={invalid ? `Enter a value between ${min} and ${max}` : undefined} + title={invalid ? tx`Enter a value between ${min} and ${max}` : undefined} onChange={(e) => { setDraft(e.target.value); const n = parseNumberInput(e.target.value, { min, max, integer }); diff --git a/src/features/settings/keymap/KeymapPage.tsx b/src/features/settings/keymap/KeymapPage.tsx index 9fbf95632..c1be71848 100644 --- a/src/features/settings/keymap/KeymapPage.tsx +++ b/src/features/settings/keymap/KeymapPage.tsx @@ -52,6 +52,7 @@ import { KeymapNameDialog } from "./KeymapDialogs"; import { RecordShortcutDialog } from "./RecordShortcutDialog"; import { StrokeRecorder } from "./StrokeRecorder"; import styles from "./Keymap.module.css"; +import { tx } from "@/platform/i18n"; type KNode = | { kind: "group"; key: string; name: string; path: string[]; children: KNode[] } @@ -206,14 +207,14 @@ export function KeymapPage() { ...Object.keys(PRESETS).map((id) => ({ value: id, label: PRESET_TITLES[id] ?? id })), ...Object.entries(customs) .sort(([, a], [, b]) => a.name.localeCompare(b.name)) - .map(([id, k]) => ({ value: id, label: k.name, detail: `based on ${PRESET_TITLES[k.parent] ?? k.parent}` })), + .map(([id, k]) => ({ value: id, label: k.name, detail: tx`based on ${PRESET_TITLES[k.parent] ?? k.parent}` })), ]; const confirmDelete = async () => { if ( !(await confirm({ title: "Delete Keymap", - message: `Delete keymap '${keymapName(selectedId, customs)}'?`, + message: tx`Delete keymap '${keymapName(selectedId, customs)}'?`, ok: "Delete", })) ) @@ -252,7 +253,7 @@ export function KeymapPage() { { kind: "separator" }, ...(bindings.length ? bindings.map((b) => ({ - label: `Remove ${formatBinding(b)}`, + label: tx`Remove ${formatBinding(b)}`, icon: "minus", run: () => change(removeBinding(draft, action.id, b)), })) diff --git a/src/features/settings/keymap/RecordShortcutDialog.tsx b/src/features/settings/keymap/RecordShortcutDialog.tsx index f1e857899..ae7010832 100644 --- a/src/features/settings/keymap/RecordShortcutDialog.tsx +++ b/src/features/settings/keymap/RecordShortcutDialog.tsx @@ -12,6 +12,7 @@ import { findConflicts, resolveBindings, type Conflict, type KeymapDraft } from import { groupPath } from "./groups"; import { StrokeRecorder } from "./StrokeRecorder"; import styles from "./Keymap.module.css"; +import { tx } from "@/platform/i18n"; export interface RecordShortcutDialogProps { action: Pick; @@ -61,7 +62,7 @@ export function RecordShortcutDialog({ action, actions, draft, onCancel, onSubmi void ok() }, diff --git a/src/features/settings/languages/ProjectStructurePage.tsx b/src/features/settings/languages/ProjectStructurePage.tsx index dfdebf81f..58f1745b6 100644 --- a/src/features/settings/languages/ProjectStructurePage.tsx +++ b/src/features/settings/languages/ProjectStructurePage.tsx @@ -31,6 +31,7 @@ import { initProjectRoots, useDetectedRoots } from "@/platform/projectRoots"; import { choosePath } from "./api"; import { useProjectSetting } from "./components"; import styles from "./Sdk.module.css"; +import { tx } from "@/platform/i18n"; interface DirNode { path: string; @@ -136,7 +137,7 @@ function ContentRoots({ root }: { root: string }) { icon="close" size="small" iconSize={12} - title={`Remove content root ${r}`} + title={tx`Remove content root ${r}`} onClick={() => setRoots(list.filter((x) => x !== r))} />
  • @@ -305,7 +306,7 @@ export function ProjectStructurePage() { icon="close" size="small" iconSize={12} - title={`Unmark ${shown}`} + title={tx`Unmark ${shown}`} onClick={() => setPs(markFolder(ps, x.path, null, detected))} />
    diff --git a/src/features/settings/pages/FileTypes.tsx b/src/features/settings/pages/FileTypes.tsx index 531659cfa..ebf4bee05 100644 --- a/src/features/settings/pages/FileTypes.tsx +++ b/src/features/settings/pages/FileTypes.tsx @@ -19,6 +19,7 @@ import { import { GlobListEditor, Row } from "../components/Form"; import { useSetting } from "../components/context"; import styles from "./FileTypes.module.css"; +import { tx } from "@/platform/i18n"; function useLanguages(): LanguageInfo[] { const [langs, setLangs] = useState([]); @@ -108,7 +109,7 @@ export function RecognizedFileTypes() { value={patterns} onChange={(next) => setAssoc(applyPatternEdit(current, langs, selected, next))} placeholder="*.ext" - aria-label={`File name patterns of ${selected.name}`} + aria-label={tx`File name patterns of ${selected.name}`} /> setAssoc(setHashbangs(current, selected.id, next))} placeholder="interpreter" - aria-label={`Hashbang patterns of ${selected.name}`} + aria-label={tx`Hashbang patterns of ${selected.name}`} /> {selected.firstLine && ( diff --git a/src/features/settings/pages/behavior.tsx b/src/features/settings/pages/behavior.tsx index 8b26a387b..5afe55657 100644 --- a/src/features/settings/pages/behavior.tsx +++ b/src/features/settings/pages/behavior.tsx @@ -27,7 +27,7 @@ import { import { ACCENT_PRESETS, LIMITS, type ThemeId } from "../model/schema"; import type { PageSpec } from "./spec"; import styles from "./Behavior.module.css"; -import { useT } from "@/platform/i18n"; +import { tx, useT } from "@/platform/i18n"; import { BUILTIN_THEMES } from "@/platform/themes"; @@ -465,14 +465,14 @@ function NotificationGroupsTable() { value={v.display} options={DISPLAY_OPTIONS} onChange={(display) => update(g.id, { display })} - aria-label={`${g.title} popup type`} + aria-label={tx`${g.title} popup type`} /> update(g.id, { log })} - aria-label={`Show ${g.title} in tool window`} + aria-label={tx`Show ${g.title} in tool window`} /> diff --git a/src/features/settings/pages/editor/colorScheme.tsx b/src/features/settings/pages/editor/colorScheme.tsx index 128ab419a..b6f10a663 100644 --- a/src/features/settings/pages/editor/colorScheme.tsx +++ b/src/features/settings/pages/editor/colorScheme.tsx @@ -21,6 +21,7 @@ import { Row, Select } from "../../components/Form"; import { useSetting } from "../../components/context"; import { editorPage } from "./kit"; import styles from "./Editor.module.css"; +import { tx } from "@/platform/i18n"; type Overrides = Record>; @@ -238,7 +239,7 @@ function ColorInput({ { diff --git a/src/features/settings/pages/editor/font.tsx b/src/features/settings/pages/editor/font.tsx index 25e0eb205..73391f4a3 100644 --- a/src/features/settings/pages/editor/font.tsx +++ b/src/features/settings/pages/editor/font.tsx @@ -11,6 +11,7 @@ import { FONT_WEIGHTS, fontWeightOf, fontWeightVars } from "@/platform/editorSet import { LIMITS } from "../../model/schema"; import { editorPage } from "./kit"; import styles from "./Editor.module.css"; +import { tx } from "@/platform/i18n"; const CANDIDATES = [ "JetBrains Mono", @@ -156,7 +157,7 @@ function WeightRow({ label, value, onChange, defaultWeight, hint }: { label: str aria-label={label} /> {custom && ( - onChange(fontWeightOf(n, defaultWeight))} min={100} max={900} step={50} integer aria-label={`${label} custom value`} /> + onChange(fontWeightOf(n, defaultWeight))} min={100} max={900} step={50} integer aria-label={tx`${label} custom value`} /> )} ); diff --git a/src/features/settings/pages/editor/inspections.tsx b/src/features/settings/pages/editor/inspections.tsx index f57b53cbf..69a0103f5 100644 --- a/src/features/settings/pages/editor/inspections.tsx +++ b/src/features/settings/pages/editor/inspections.tsx @@ -64,6 +64,7 @@ import { type LanguageServers, } from "./inspectionsModel"; import styles from "./Inspections.module.css"; +import { tx } from "@/platform/i18n"; let languagesCache: LanguageServers[] | null = null; @@ -175,7 +176,7 @@ function InspectionsPage() { const isCopy = location.key !== null; const confirmDelete = async () => { - if (!(await confirm({ title: "Delete Profile", message: `Delete inspection profile '${label}'?`, ok: "Delete" }))) + if (!(await confirm({ title: "Delete Profile", message: tx`Delete inspection profile '${label}'?`, ok: "Delete" }))) return; apply(deleteProfile(settings, profileId)); }; @@ -418,7 +419,7 @@ function InspectionRow({ checked={state === "on"} indeterminate={state === "mixed"} onChange={() => onToggle(state !== "on")} - aria-label={`Enable ${node.name}`} + aria-label={tx`Enable ${node.name}`} /> diff --git a/src/features/settings/pages/editor/inspectionsModel.ts b/src/features/settings/pages/editor/inspectionsModel.ts index 358a20dd3..9f85990cb 100644 --- a/src/features/settings/pages/editor/inspectionsModel.ts +++ b/src/features/settings/pages/editor/inspectionsModel.ts @@ -21,6 +21,7 @@ import { type ProfileOverrides, type SeenCode, } from "@/platform/inspections"; +import { tx } from "@/platform/i18n"; export interface InspectionNode { /** Unique in the tree (a server serving several languages appears under each). */ @@ -45,14 +46,16 @@ function serverNode(group: string, server: string, codes: readonly SeenCode[], l key: `${group}/${id}`, id, name: server, - description: `Problems the ${server} language server reports${languages ? ` in ${languages} files` : ""}. Severity "As reported" keeps the level the server assigns; a code below can override it.`, + description: languages + ? tx`Problems the ${server} language server reports in ${languages} files. Severity "As reported" keeps the level the server assigns; a code below can override it.` + : tx`Problems the ${server} language server reports. Severity "As reported" keeps the level the server assigns; a code below can override it.`, children: [...codes] .sort((a, b) => a.code.localeCompare(b.code, undefined, { numeric: true })) .map((c) => ({ key: `${group}/${codeInspectionId(id, c.code)}`, id: codeInspectionId(id, c.code), name: c.code, - description: `${server} problems with code ${c.code}${c.source && c.source !== server ? ` (${c.source})` : ""}.${c.sample ? `\n\nExample: ${c.sample}` : ""}`, + description: codeDescription(server, c), children: [], })), }; @@ -270,3 +273,11 @@ export function deleteProfile(s: InspectionSettingsValid, id: ProfileId): Inspec const profile = s.profile === id ? (level === "project" ? "project" : "default") : s.profile; return { ...s, [field]: rest, profile }; } + +function codeDescription(server: string, { code, source, sample }: SeenCode): string { + const what = + source && source !== server + ? tx`${server} problems with code ${code} (${source}).` + : tx`${server} problems with code ${code}.`; + return sample ? tx`${what}\n\nExample: ${sample}` : what; +} diff --git a/src/features/settings/pages/tools.tsx b/src/features/settings/pages/tools.tsx index 0750d581d..24c923abd 100644 --- a/src/features/settings/pages/tools.tsx +++ b/src/features/settings/pages/tools.tsx @@ -19,6 +19,7 @@ import { selectPage, setQuery } from "../model/staging"; import type { PageSpec } from "./spec"; import { BoundToggle, Grid, Indented, PageLink, TableEditor } from "./workflowControls"; import styles from "./Workflow.module.css"; +import { tx } from "@/platform/i18n"; export const TOOLS_GROUP = "Tools"; @@ -468,7 +469,7 @@ function ExternalToolsEditor() { onMouseDown={() => setSelected(t.id)} > setTools(tools.map((x) => (x.id === t.id ? { ...x, enabled: on } : x)))} /> diff --git a/src/features/settings/pages/vcs.tsx b/src/features/settings/pages/vcs.tsx index bde64e8d3..dec8219f4 100644 --- a/src/features/settings/pages/vcs.tsx +++ b/src/features/settings/pages/vcs.tsx @@ -13,6 +13,7 @@ import { useSetting } from "../components/context"; import type { PageSpec } from "./spec"; import { BoundToggle, Grid, Indented, RadioRow, TableEditor, ToggleNumber } from "./workflowControls"; import styles from "./Workflow.module.css"; +import { tx } from "@/platform/i18n"; const L = WORKFLOW_LIMITS; @@ -110,7 +111,7 @@ function GitExecutableRow() { mono className={styles.wide} value={path} - placeholder={detected ? `Auto-detected: ${detected}` : "Auto-detected"} + placeholder={detected ? tx`Auto-detected: ${detected}` : "Auto-detected"} onChange={(e) => { setPath(e.target.value); setResult(null); diff --git a/src/features/templates/files/NewFileDialog.tsx b/src/features/templates/files/NewFileDialog.tsx index 97c42c323..1e1e08636 100644 --- a/src/features/templates/files/NewFileDialog.tsx +++ b/src/features/templates/files/NewFileDialog.tsx @@ -9,6 +9,7 @@ import { createFromTemplate, packageFor } from "./create"; import { planFile, withKind, type FileTemplate } from "./model"; import { useFileTemplates } from "./store"; import styles from "./NewFileDialog.module.css"; +import { tx } from "@/platform/i18n"; interface Request { directory: string; @@ -185,17 +186,17 @@ function NameStep({
    { setName(e.target.value); diff --git a/src/features/templates/index.ts b/src/features/templates/index.ts index 7de5f4ef4..440a4ab11 100644 --- a/src/features/templates/index.ts +++ b/src/features/templates/index.ts @@ -22,6 +22,7 @@ import type { FileTemplate } from "./files/model"; import { syncTemplatesFromStorage } from "./sync"; import { tagAtCaret, updateTag } from "./emmet/editor"; import { promptAbbreviation, UpdateTagDialogHost } from "./emmet/UpdateTagDialog"; +import { tx } from "@/platform/i18n"; export { EXTRA_COMPLETION_KEY } from "./editor"; @@ -42,7 +43,7 @@ async function updateTagWithEmmet() { notifications().notify({ level: "warning", title: "Cannot update the tag", - body: `'${abbreviation}' does not describe a single element.`, + body: tx`'${abbreviation}' does not describe a single element.`, key: "emmet-update-tag", }); } @@ -78,7 +79,7 @@ function directoryOf(arg: unknown): string | null { function newFileAction(t: FileTemplate): Action { return { id: `templates.new.${t.id}`, - title: t.id === "file" ? "New Empty File…" : `New ${t.name}…`, + title: t.id === "file" ? "New Empty File…" : tx`New ${t.name}…`, category: "File", icon: t.icon, when: "projectOpen", diff --git a/src/features/templates/settings/FileTemplatesPage.tsx b/src/features/templates/settings/FileTemplatesPage.tsx index 5dcf838bd..e67fc0567 100644 --- a/src/features/templates/settings/FileTemplatesPage.tsx +++ b/src/features/templates/settings/FileTemplatesPage.tsx @@ -136,7 +136,7 @@ function FilesTab() { saveFileTemplate({ ...t, enabled: v })} - aria-label={`Enable ${t.name}`} + aria-label={tx`Enable ${t.name}`} /> {t.name} diff --git a/src/features/templates/settings/LiveTemplatesPage.tsx b/src/features/templates/settings/LiveTemplatesPage.tsx index 0ee676cc0..dbe32ea2b 100644 --- a/src/features/templates/settings/LiveTemplatesPage.tsx +++ b/src/features/templates/settings/LiveTemplatesPage.tsx @@ -15,6 +15,7 @@ import { } from "../live/store"; import { BUILTIN_IDS } from "../live/builtins"; import styles from "./Settings.module.css"; +import { tx } from "@/platform/i18n"; export function groupTemplates(templates: readonly LiveTemplate[], filter: string): Array<[string, LiveTemplate[]]> { const q = filter.trim().toLowerCase(); @@ -106,7 +107,7 @@ export function LiveTemplatesPage() { saveTemplate({ ...t, enabled: v })} - aria-label={`Enable ${t.abbrev}`} + aria-label={tx`Enable ${t.abbrev}`} /> {t.abbrev} @@ -243,7 +244,7 @@ function TemplateEditor({ template: t }: { template: LiveTemplate }) { value={v.expression ?? ""} placeholder="className()" onChange={(e) => setVar(v.name, { expression: e.target.value || undefined })} - aria-label={`${v.name} expression`} + aria-label={tx`${v.name} expression`} /> @@ -252,14 +253,14 @@ function TemplateEditor({ template: t }: { template: LiveTemplate }) { value={v.defaultValue ?? ""} placeholder='"value"' onChange={(e) => setVar(v.name, { defaultValue: e.target.value || undefined })} - aria-label={`${v.name} default value`} + aria-label={tx`${v.name} default value`} /> setVar(v.name, { skipIfDefined: c })} - aria-label={`Skip ${v.name} if defined`} + aria-label={tx`Skip ${v.name} if defined`} /> diff --git a/src/features/templates/settings/PostfixPage.tsx b/src/features/templates/settings/PostfixPage.tsx index c8f097f33..fcb7aff90 100644 --- a/src/features/templates/settings/PostfixPage.tsx +++ b/src/features/templates/settings/PostfixPage.tsx @@ -5,6 +5,7 @@ import { Checkbox, T } from "@/ui"; import { POSTFIX_GROUPS, POSTFIX_TEMPLATES } from "../postfix/model"; import { setPostfixTemplateEnabled, updatePostfixSettings, usePostfixSettings } from "../postfix/store"; import styles from "./Settings.module.css"; +import { tx } from "@/platform/i18n"; export function PostfixPage() { const settings = usePostfixSettings(); @@ -59,7 +60,7 @@ export function PostfixPage() { checked={!disabled.has(t.id)} disabled={!settings.enabled} onChange={(v) => setPostfixTemplateEnabled(t.id, v)} - aria-label={`Enable .${t.key}`} + aria-label={tx`Enable .${t.key}`} /> .{t.key} {t.description} diff --git a/src/features/todo/TodoSettingsPage.tsx b/src/features/todo/TodoSettingsPage.tsx index beb6e0f95..f7a34e3fc 100644 --- a/src/features/todo/TodoSettingsPage.tsx +++ b/src/features/todo/TodoSettingsPage.tsx @@ -3,6 +3,7 @@ import { Button, Checkbox, IconButton, Input, Select, T } from "@/ui"; import { DEFAULT_PATTERNS, type TodoPattern } from "./model"; import { updateTodoSettings, useTodoSettings } from "./store"; import styles from "./TodoSettingsPage.module.css"; +import { tx } from "@/platform/i18n"; const COLORS = [ { value: "var(--info)", label: "Blue" }, @@ -55,7 +56,7 @@ export function TodoSettingsPage() { patch(p.id, { enabled: v })} - aria-label={`Enable ${p.keyword}`} + aria-label={tx`Enable ${p.keyword}`} /> patch(p.id, { color: v })} - aria-label={`${p.keyword} colour`} + aria-label={tx`${p.keyword} colour`} /> ({ value: l.id, label: l.name, detail: `${l.files.length} file${l.files.length === 1 ? "" : "s"}` }))} + options={lists.map((l) => ({ value: l.id, label: l.name, detail: l.files.length === 1 ? tx`${l.files.length} file` : tx`${l.files.length} files` }))} onChange={(id) => useTodoView.setState({ changelist: id })} />
    diff --git a/src/features/tools/LanguageServersPage.tsx b/src/features/tools/LanguageServersPage.tsx index a22cef798..549edcc66 100644 --- a/src/features/tools/LanguageServersPage.tsx +++ b/src/features/tools/LanguageServersPage.tsx @@ -19,6 +19,7 @@ import { type InstallJob, } from "./store"; import styles from "./LanguageServersPage.module.css"; +import { tx } from "@/platform/i18n"; type Filter = "all" | "missing" | "installed"; @@ -48,7 +49,7 @@ export function filterTools(tools: readonly ToolInfo[], query: string, filter: F function JobRow({ name, job }: { name: string; job: InstallJob }) { return (
    - +
    {job.message ?? "Starting…"} @@ -178,7 +179,9 @@ function Details({ tool }: { tool: ToolInfo }) { variant={tool.status === "missing" ? "primary" : "secondary"} disabled={busy} onClick={() => void installTool(tool.name)} - aria-label={`${tool.status === "installed" ? "Reinstall" : "Install"} ${tool.displayName}`} + aria-label={ + tool.status === "installed" ? tx`Reinstall ${tool.displayName}` : tx`Install ${tool.displayName}` + } > {tool.status === "installed" ? "Reinstall" : "Install"} diff --git a/src/features/tools/jdk.ts b/src/features/tools/jdk.ts index f0479d89c..70f9c24dc 100644 --- a/src/features/tools/jdk.ts +++ b/src/features/tools/jdk.ts @@ -21,6 +21,7 @@ import { import { useProject } from "@/platform/project"; import { notifications } from "@/workbench/contributions"; import { upsertTask } from "@/workbench/tasks"; +import { tx } from "@/platform/i18n"; const JAVA_SETTINGS = "languages.java"; const running = new Map>(); @@ -59,7 +60,7 @@ export function downloadJdkFor(args: DownloadJdkArgs): Promise { title: `${sdk.name} downloaded`, body: args.languageServer ? "The Java language server restarts on it." - : `Installed in ${sdk.path}; it is used for projects targeting Java ${args.version}.`, + : tx`Installed in ${sdk.path}; it is used for projects targeting Java ${args.version}.`, }); return sdk; } catch (e) { diff --git a/src/features/tools/missing.ts b/src/features/tools/missing.ts index f76afb163..f6b823d4e 100644 --- a/src/features/tools/missing.ts +++ b/src/features/tools/missing.ts @@ -8,6 +8,7 @@ import { executeAction } from "@/platform/actions"; import { onIdeEvent } from "@/platform/ipc"; import { notifications } from "@/workbench/contributions"; import { installTool, refreshTools, SETTINGS_PAGE_ID, toolByName, useTools } from "./store"; +import { tx } from "@/platform/i18n"; export const missingKey = (name: string) => `lsp-missing-${name}`; @@ -26,7 +27,7 @@ export async function promptInstall(name: string): Promise { group: "tools", level: "warning", title: `${tool.displayName} is not installed`, - body: `Code insight for ${langs} needs ${tool.command}. Install it via ${tool.installMethod}?`, + body: tx`Code insight for ${langs} needs ${tool.command}. Install it via ${tool.installMethod}?`, key, sticky: true, actions: [ diff --git a/src/features/tools/store.ts b/src/features/tools/store.ts index d9785097a..76baa23b1 100644 --- a/src/features/tools/store.ts +++ b/src/features/tools/store.ts @@ -6,6 +6,7 @@ import { isCancelled, nextRequestId } from "@/platform/ipc"; import { notifications } from "@/workbench/contributions"; import { upsertTask } from "@/workbench/tasks"; import { toolsBackend } from "./backend"; +import { tx } from "@/platform/i18n"; export const SETTINGS_PAGE_ID = "languages.servers"; const MAX_LOG_LINES = 400; @@ -99,19 +100,19 @@ export function installTool(name: string, opts: { notificationKey?: string } = { const taskId = `tools:${name}`; const key = opts.notificationKey ?? `tools-install-${name}`; useTools.setState((s) => ({ jobs: { ...s.jobs, [name]: { request, lines: [], fraction: null, message: null } } })); - upsertTask({ id: taskId, title: `Installing ${display}`, message: null, fraction: null, done: false }); + upsertTask({ id: taskId, title: tx`Installing ${display}`, message: null, fraction: null, done: false }); const p = (async () => { try { const info = await toolsBackend().install(name, request, (e) => { if (e.type === "line") { patchJob(name, (j) => ({ ...j, lines: [...j.lines, e.text].slice(-MAX_LOG_LINES), message: e.text })); - upsertTask({ id: taskId, title: `Installing ${display}`, message: e.text, fraction: null, done: false }); + upsertTask({ id: taskId, title: tx`Installing ${display}`, message: e.text, fraction: null, done: false }); } else { patchJob(name, (j) => ({ ...j, fraction: e.fraction, message: e.message })); upsertTask({ id: taskId, - title: `Installing ${display}`, + title: tx`Installing ${display}`, message: e.message, fraction: e.fraction, done: false, @@ -127,7 +128,7 @@ export function installTool(name: string, opts: { notificationKey?: string } = { group: "tools", level: "info", title: `${info.displayName} installed`, - body: info.version ? `Version ${info.version}` : undefined, + body: info.version ? tx`Version ${info.version}` : undefined, key, }); // Shared packages (e.g. vscode-langservers-extracted) change sibling rows too. @@ -150,7 +151,7 @@ export function installTool(name: string, opts: { notificationKey?: string } = { }); return null; } finally { - upsertTask({ id: taskId, title: `Installing ${display}`, message: null, fraction: 1, done: true }); + upsertTask({ id: taskId, title: tx`Installing ${display}`, message: null, fraction: 1, done: true }); running.delete(name); } })(); diff --git a/src/features/vcs/BranchWidget.tsx b/src/features/vcs/BranchWidget.tsx index b7302b14b..bed98625b 100644 --- a/src/features/vcs/BranchWidget.tsx +++ b/src/features/vcs/BranchWidget.tsx @@ -36,6 +36,7 @@ import { } from "./repoBranches"; import { currentRepo, focusRepo, loadBranches, repoName, useVcs } from "./store"; import styles from "./BranchWidget.module.css"; +import { tx } from "@/platform/i18n"; export const useBranchesPopup = create<{ open: boolean }>(() => ({ open: false })); export const showBranchesPopup = () => useBranchesPopup.setState({ open: true }); @@ -263,8 +264,8 @@ function BranchesPanel({ onClose, actions: baseActions, root, fallback, onBack } if (!el) return; setMenuFor( row.type === "branch" - ? { items: scoped(branchMenu(row.branch, current, closeAll)), label: `Branch ${row.branch.name}`, el } - : { items: scoped(tagMenu(row.tag, closeAll)), label: `Tag ${row.tag}`, el }, + ? { items: scoped(branchMenu(row.branch, current, closeAll)), label: tx`Branch ${row.branch.name}`, el } + : { items: scoped(tagMenu(row.tag, closeAll)), label: tx`Tag ${row.tag}`, el }, ); }; @@ -499,12 +500,12 @@ function MultiRepoPanel({ onClose }: { onClose: () => void }) { } setRepoMenu(null); if (row.type === "common") { - setMenuFor({ items: commonBranchMenu(row.branch, roots, closeAll), label: `Branch ${row.branch.name}`, el }); + setMenuFor({ items: commonBranchMenu(row.branch, roots, closeAll), label: tx`Branch ${row.branch.name}`, el }); } else if (row.type === "branch") { const items = inRepo(row.repo.root, branchMenu(row.branch, row.repo.head, closeAll)); - setMenuFor({ items, label: `Branch ${row.branch.name} of ${row.repo.name}`, el }); + setMenuFor({ items, label: tx`Branch ${row.branch.name} of ${row.repo.name}`, el }); } else if (row.type === "tag") { - setMenuFor({ items: inRepo(row.repo.root, tagMenu(row.tag, closeAll)), label: `Tag ${row.tag}`, el }); + setMenuFor({ items: inRepo(row.repo.root, tagMenu(row.tag, closeAll)), label: tx`Tag ${row.tag}`, el }); } }; const setOpen = (key: string, open: boolean) => @@ -625,7 +626,7 @@ function MultiRepoPanel({ onClose }: { onClose: () => void }) { placement="right-start" offset={2} autoFocus={repoMenu.focus} - aria-label={`Branches of ${repoName(repoMenu.root)}`} + aria-label={tx`Branches of ${repoName(repoMenu.root)}`} onClose={closeRepoMenu} className={styles.popup} > diff --git a/src/features/vcs/CommitView.tsx b/src/features/vcs/CommitView.tsx index b5f70fbd1..e0187afb6 100644 --- a/src/features/vcs/CommitView.tsx +++ b/src/features/vcs/CommitView.tsx @@ -464,7 +464,7 @@ function ChangesPanel() { toggle(selected.has(n.key) && selected.size > 1 ? selectedNodes() : [n])} /> {n.type === "group" ? null : ( diff --git a/src/features/vcs/CompareBranches.tsx b/src/features/vcs/CompareBranches.tsx index 76cc27dcd..868ee8cf1 100644 --- a/src/features/vcs/CompareBranches.tsx +++ b/src/features/vcs/CompareBranches.tsx @@ -8,6 +8,7 @@ import { openDialog } from "./dialogs"; import { formatTime } from "./model"; import { currentRepo, useVcs } from "./store"; import styles from "./common.module.css"; +import { tx } from "@/platform/i18n"; const LIMIT = 2000; @@ -123,7 +124,7 @@ function CompareDialog({ branch, current, onClose }: { branch: string; current: return ( { - const ok = await confirmDialog({ title: "Revert", message: `Revert ${what}?`, okLabel: "Revert" }); + const restore = async (targets: HistoryEntry[], question: string) => { + const ok = await confirmDialog({ title: "Revert", message: question, okLabel: "Revert" }); if (!ok) return; await runOp( - `Revert ${what} from Local History`, + "Revert from Local History", async () => { for (const e of targets) await call(commands.historyRestore(e.path, e.id)); }, @@ -113,7 +114,7 @@ export function LocalHistoryEditor({ input }: { input: EditorInput }) { if (!isLabel(current)) { void restore( [current], - `${baseName(current.path)} to the revision from ${formatTime(current.timestampMs)} (${current.label})`, + tx`Revert ${baseName(current.path)} to the revision from ${formatTime(current.timestampMs)} (${current.label})?`, ); return; } @@ -128,7 +129,9 @@ export function LocalHistoryEditor({ input }: { input: EditorInput }) { } void restore( targets, - `${view.folder ? `${targets.length} files under ${baseName(path)}` : baseName(path)} to label '${current.label}'`, + view.folder + ? tx`Revert ${targets.length} files under ${baseName(path)} to label '${current.label}'?` + : tx`Revert ${baseName(path)} to label '${current.label}'?`, ); }; diff --git a/src/features/vcs/MergeEditor.tsx b/src/features/vcs/MergeEditor.tsx index ef5bdbf26..d760b5e6d 100644 --- a/src/features/vcs/MergeEditor.tsx +++ b/src/features/vcs/MergeEditor.tsx @@ -8,7 +8,7 @@ import { keymap } from "@codemirror/view"; import { unifiedMergeView } from "@codemirror/merge"; import type { MergeSides } from "@/bindings"; import { embeddedEditorExtensions } from "@/editor/api"; -import { useTxTemplate } from "@/platform/i18n"; +import { tx, useTxTemplate } from "@/platform/i18n"; import { call, commands } from "@/platform/ipc"; import { Button, IconButton, Spinner, T } from "@/ui"; import { editorService, type EditorInput } from "@/workbench/contributions"; @@ -258,7 +258,10 @@ export function MergeEditor({ input }: { input: EditorInput }) { remaining > 0 && !(await confirmDialog({ title: "Unresolved Conflicts", - message: `${remaining} conflict${remaining === 1 ? " is" : "s are"} unresolved and keep the base text. Save and mark the file as resolved anyway?`, + message: + remaining === 1 + ? tx`${remaining} conflict is unresolved and keeps the base text. Save and mark the file as resolved anyway?` + : tx`${remaining} conflicts are unresolved and keep the base text. Save and mark the file as resolved anyway?`, okLabel: "Mark Resolved", })) ) diff --git a/src/features/vcs/actions.tsx b/src/features/vcs/actions.tsx index 68b27a466..19571405a 100644 --- a/src/features/vcs/actions.tsx +++ b/src/features/vcs/actions.tsx @@ -37,6 +37,7 @@ import { showWorktrees, } from "./gitMenuDialogs"; import { refreshNow, repoName } from "./store"; +import { tx } from "@/platform/i18n"; const VCS = "vcsAvailable"; const IN_COMMIT_WINDOW = "vcsAvailable && activeToolWindow == commit"; @@ -287,7 +288,7 @@ export function registerVcsActions(): () => void { group: "vcs", level: "info", title: "Submodule changes cannot be rolled back here", - body: `${repoName(submodule)} points to another commit. Check out the recorded commit in the submodule (git submodule update) to revert it.`, + body: tx`${repoName(submodule)} points to another commit. Check out the recorded commit in the submodule (git submodule update) to revert it.`, }); return showRollbackDialog(candidates); } diff --git a/src/features/vcs/branchMenu.ts b/src/features/vcs/branchMenu.ts index 3b546a531..e665b9d81 100644 --- a/src/features/vcs/branchMenu.ts +++ b/src/features/vcs/branchMenu.ts @@ -27,6 +27,7 @@ import { showCompareBranches } from "./CompareBranches"; import { showDiffWithWorkingTree } from "./operations"; import type { CommonBranch } from "./repoBranches"; import { focusRepo } from "./store"; +import { tx } from "@/platform/i18n"; export function branchMenu(b: Branch, current: string | null, done: () => void = () => {}): MenuItem[] { const run = (fn: () => unknown) => () => { @@ -48,7 +49,7 @@ export function branchMenu(b: Branch, current: string | null, done: () => void = }; if (b.current) { return [ - { label: `New Branch from '${b.name}'…`, icon: "branch", run: run(() => newBranch()) }, + { label: tx`New Branch from '${b.name}'…`, icon: "branch", run: run(() => newBranch()) }, { kind: "separator" }, { label: "Update", icon: "git-pull", run: run(() => executeAction("vcs.update")) }, { label: "Push…", icon: "git-push", run: run(() => executeAction("vcs.push")) }, @@ -60,15 +61,15 @@ export function branchMenu(b: Branch, current: string | null, done: () => void = } const items: MenuItem[] = [ { label: "Checkout", run: run(() => checkoutBranch(b.name)) }, - { label: `New Branch from '${b.name}'…`, icon: "branch", run: run(() => newBranch(b.name)) }, - ...(current ? [{ label: `Checkout and Rebase onto '${cur}'`, run: run(() => checkoutAndRebase(b.name)) }] : []), + { label: tx`New Branch from '${b.name}'…`, icon: "branch", run: run(() => newBranch(b.name)) }, + ...(current ? [{ label: tx`Checkout and Rebase onto '${cur}'`, run: run(() => checkoutAndRebase(b.name)) }] : []), { kind: "separator" }, - { label: `Compare with '${cur}'`, icon: "diff", run: run(() => showCompareBranches(b.name)) }, + { label: tx`Compare with '${cur}'`, icon: "diff", run: run(() => showCompareBranches(b.name)) }, { label: "Show Diff with Working Tree", run: run(() => showDiffWithWorkingTree(b.name)) }, history, { kind: "separator" }, - { label: `Rebase '${cur}' onto '${b.name}'`, run: run(() => rebaseOnto(b.name)) }, - { label: `Merge '${b.name}' into '${cur}'`, run: run(() => mergeIntoCurrent(b.name)) }, + { label: tx`Rebase '${cur}' onto '${b.name}'`, run: run(() => rebaseOnto(b.name)) }, + { label: tx`Merge '${b.name}' into '${cur}'`, run: run(() => mergeIntoCurrent(b.name)) }, ]; if (!b.remote) items.push( @@ -98,7 +99,7 @@ export function tagMenu(tag: string, done: () => void = () => {}): MenuItem[] { }; return [ { label: "Checkout", run: run(() => checkoutDetached(tag, tag)) }, - { label: `New Branch from '${tag}'…`, icon: "branch", run: run(() => newBranch(tag)) }, + { label: tx`New Branch from '${tag}'…`, icon: "branch", run: run(() => newBranch(tag)) }, { kind: "separator" }, { label: "Compare with Current", icon: "diff", run: run(() => showCompareBranches(tag)) }, { label: "Show Diff with Working Tree", run: run(() => showDiffWithWorkingTree(tag)) }, @@ -113,7 +114,7 @@ export function commonBranchMenu(b: CommonBranch, roots: readonly string[], done void fn(); }; const newFrom: MenuItem = { - label: `New Branch from '${b.name}'…`, + label: tx`New Branch from '${b.name}'…`, icon: "branch", run: run(() => newBranchInRepos(roots, b.name)), }; @@ -128,8 +129,8 @@ export function commonBranchMenu(b: CommonBranch, roots: readonly string[], done { label: "Checkout", run: run(() => checkoutInRepos(b.name, roots)) }, newFrom, { kind: "separator" }, - { label: `Rebase Current onto '${b.name}'`, run: run(() => rebaseInRepos(b.name, roots)) }, - { label: `Merge '${b.name}' into Current`, run: run(() => mergeInRepos(b.name, roots)) }, + { label: tx`Rebase Current onto '${b.name}'`, run: run(() => rebaseInRepos(b.name, roots)) }, + { label: tx`Merge '${b.name}' into Current`, run: run(() => mergeInRepos(b.name, roots)) }, ]; if (!b.remote) items.push( diff --git a/src/features/vcs/branchOps.ts b/src/features/vcs/branchOps.ts index d066ec2ae..79ddee418 100644 --- a/src/features/vcs/branchOps.ts +++ b/src/features/vcs/branchOps.ts @@ -10,6 +10,7 @@ import { showCommitRange } from "./CompareBranches"; import { confirmDialog, promptText } from "./dialogs"; import { runOp, runRemote, saveAll } from "./ops"; import { conflictedFiles, currentRepo, refreshNow, repoName, useVcs } from "./store"; +import { tx } from "@/platform/i18n"; const recentKey = () => `openidea.vcs.recentBranches:${useVcs.getState().root ?? ""}`; @@ -50,7 +51,7 @@ async function askSmartCheckout(files: readonly string[]): Promise 20 ? "\n…" : ""); const r = await ask({ title: "Git Checkout Problem", - body: `Your local changes to the following files would be overwritten by checkout:\n${list}\n\nSmart Checkout can stash the changes, checkout and unstash them after that.`, + body: tx`Your local changes to the following files would be overwritten by checkout:\n${list}\n\nSmart Checkout can stash the changes, checkout and unstash them after that.`, icon: "warning", width: 520, buttons: [ @@ -105,8 +106,8 @@ async function performCheckout( case "localBranchAhead": { const { local, remote } = outcome; const r = await ask({ - title: `Checkout ${remote}`, - body: `Local branch '${local}' has commits that do not exist in '${remote}'. Rebase '${local}' onto '${remote}', or drop local commits?`, + title: tx`Checkout ${remote}`, + body: tx`Local branch '${local}' has commits that do not exist in '${remote}'. Rebase '${local}' onto '${remote}', or drop local commits?`, buttons: [ { id: "rebase", label: "Rebase onto Remote", primary: true }, { id: "reset", label: "Drop Local Commits" }, @@ -145,7 +146,7 @@ export async function checkoutBranch(name: string) { export async function newBranch(from?: string) { const suggestion = from ? (from.includes("/") ? shortRemoteName(from) : `${from}-copy`) : ""; const r = await promptText({ - title: from ? `Create New Branch from ${from}` : "Create New Branch", + title: from ? tx`Create New Branch from ${from}` : "Create New Branch", label: "New branch name:", initial: from && from.includes("/") && !validateBranchName(suggestion) ? suggestion : "", mono: true, @@ -218,7 +219,7 @@ export async function rebaseOnto(branch: string) { export async function renameBranch(name: string) { const r = await promptText({ - title: `Rename Branch ${name}`, + title: tx`Rename Branch ${name}`, label: "New branch name:", initial: name, mono: true, @@ -290,7 +291,7 @@ export async function deleteBranch(name: string) { export async function deleteRemoteBranch(name: string) { const ok = await confirmDialog({ title: "Delete Remote Branch", - message: `Delete branch '${shortRemoteName(name)}' on the remote? The remote-tracking branch '${name}' is removed as well.`, + message: tx`Delete branch '${shortRemoteName(name)}' on the remote? The remote-tracking branch '${name}' is removed as well.`, okLabel: "Delete", danger: true, }); @@ -354,8 +355,8 @@ export async function checkoutInRepos(name: string, roots: readonly string[]) { /** New Branch in every repository of `roots` (from HEAD or `from`), checked out by default. */ export async function newBranchInRepos(roots: readonly string[], from?: string) { const r = await promptText({ - title: from ? `Create New Branch from ${from}` : "Create New Branch", - label: `New branch name (in ${roots.length} repositories):`, + title: from ? tx`Create New Branch from ${from}` : "Create New Branch", + label: tx`New branch name (in ${roots.length} repositories):`, initial: from?.includes("/") ? shortRemoteName(from) : "", mono: true, okLabel: "Create", @@ -409,7 +410,7 @@ export async function rebaseInRepos(branch: string, roots: readonly string[]) { export async function deleteInRepos(name: string, roots: readonly string[]) { const ok = await confirmDialog({ title: "Delete Branch", - message: `Delete branch '${name}' in ${roots.length} repositories? Unmerged commits are discarded.`, + message: tx`Delete branch '${name}' in ${roots.length} repositories? Unmerged commits are discarded.`, okLabel: "Delete", danger: true, }); diff --git a/src/features/vcs/changeOps.tsx b/src/features/vcs/changeOps.tsx index 8b86eba83..31433341e 100644 --- a/src/features/vcs/changeOps.tsx +++ b/src/features/vcs/changeOps.tsx @@ -14,6 +14,7 @@ import { KIND_COLOR, baseName, dirName, effectiveKind, relTo, rootOf, stripSlash import { logConsole, runOp, saveAll } from "./ops"; import { currentRepo, loadStashes, refreshNow, useVcs } from "./store"; import styles from "./common.module.css"; +import { tx } from "@/platform/i18n"; export function firstLine(message: string): string { return ( @@ -230,7 +231,7 @@ function RollbackDialog({ files, onClose }: { files: FileStatus[]; onClose: () = return (
    setSelected((s) => { @@ -366,10 +367,10 @@ export interface UnstashOptions { export function unstashTitles(o: UnstashOptions): { title: string; success: string } { const ref = `stash@{${o.index}}`; if (o.branch) - return { title: `Unstash ${ref} as branch ${o.branch}`, success: `Unstashed ${ref} on branch ${o.branch}` }; + return { title: tx`Unstash ${ref} as branch ${o.branch}`, success: `Unstashed ${ref} on branch ${o.branch}` }; return o.pop - ? { title: `Pop ${ref}`, success: `Unstashed ${ref}` } - : { title: `Apply ${ref}`, success: `Applied ${ref}` }; + ? { title: tx`Pop ${ref}`, success: `Unstashed ${ref}` } + : { title: tx`Apply ${ref}`, success: `Applied ${ref}` }; } export async function unstash(o: UnstashOptions) { @@ -455,7 +456,9 @@ function UnstashDialog({ initial, onClose }: { initial: number; onClose: () => v export async function dropStash(index: number, message: string) { const ok = await confirmDialog({ title: "Drop Stash", - message: `Drop stash@{${index}}${message ? ` “${message}”` : ""}? This cannot be undone.`, + message: message + ? tx`Drop ${`stash@{${index}}`} “${message}”? This cannot be undone.` + : tx`Drop ${`stash@{${index}}`}? This cannot be undone.`, okLabel: "Drop", danger: true, }); diff --git a/src/features/vcs/changelistsUi.tsx b/src/features/vcs/changelistsUi.tsx index b9277f486..6ac014aba 100644 --- a/src/features/vcs/changelistsUi.tsx +++ b/src/features/vcs/changelistsUi.tsx @@ -19,6 +19,7 @@ import { useCommit } from "./commitStore"; import { confirmDialog, openDialog } from "./dialogs"; import { useVcs } from "./store"; import styles from "./common.module.css"; +import { tx } from "@/platform/i18n"; interface FormValue { name: string; @@ -159,7 +160,10 @@ export async function deleteChangelist(id: string) { if (count) { const ok = await confirmDialog({ title: "Delete Changelist", - message: `Changelist '${list.name}' contains ${count} change${count === 1 ? "" : "s"}. They will be moved to '${activeList(state).name}'. Delete the changelist?`, + message: + count === 1 + ? tx`Changelist '${list.name}' contains ${count} change. It will be moved to '${activeList(state).name}'. Delete the changelist?` + : tx`Changelist '${list.name}' contains ${count} changes. They will be moved to '${activeList(state).name}'. Delete the changelist?`, okLabel: "Delete", danger: true, }); @@ -247,7 +251,7 @@ function MoveDialog({ paths, onClose }: { paths: readonly string[]; onClose: () disabled={mode !== "existing"} aria-label="Existing changelist" onChange={setTarget} - options={others.map((l) => ({ value: l.id, label: l.id === state.active ? `${l.name} (active)` : l.name }))} + options={others.map((l) => ({ value: l.id, label: l.id === state.active ? tx`${l.name} (active)` : l.name }))} />