diff --git a/src/editor/core/actions.ts b/src/editor/core/actions.ts index be34d9d9..cbfa16f6 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 997d3111..640c0b53 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 0d20fd5b..86fd2fb3 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 b25a4184..0fadf921 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 19711917..9a221b18 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 2888373b..35794ac4 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 af36f5e0..2550294e 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 77c8f0ce..18a22316 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 136ce82a..1b7772dd 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 a9312352..2ade1742 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 44802b52..013e94e8 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 2fc1cc40..605aa301 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 fb121fe4..f459e77c 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 ee774825..973893e1 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 8ba26c19..3f4afaef 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 80ba1ca8..11f2d74d 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 0c54aa37..5a6ff2c5 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 c839f1ee..8dccb8f1 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 5142bc17..b6d1d173 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 cfe0e53f..ae9951a2 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 81ceb1f5..95858b21 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 d495fc0a..17b1fd11 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 fe0b8a01..c4bd3917 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 c090e83c..710ab877 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 ceaa2487..ca21d89c 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 91dcac9a..b5a48114 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 22e52d76..2910023c 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 09370434..5d35e0ea 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 f53dc1dc..288faa7b 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 f4b64d6d..0b503c2f 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 ; @@ -291,8 +292,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") @@ -314,7 +315,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 4ea71acd..9323cfc3 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 07115acb..eb433ae5 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 b7a70edc..a5580d7e 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 6c0e7a41..6d2e630d 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 04cca951..b1cfcdce 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"; @@ -442,7 +442,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 a5c81c69..1fe2e8f2 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 ed85f921..e4d2f7eb 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"], run: () => setViewOption(key, !useProjectTree.getState().options[key]), @@ -158,7 +159,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"], @@ -334,7 +335,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 ba8961e6..80ae286a 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 d8fdee59..79fb5483 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 47793ff6..1c8b5421 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 ce074f0a..864b594a 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 1b0d4698..dcd55e56 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 c4187ed6..970a1be2 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 d95cfde6..eb08ddfc 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 0e9ab244..71aafa6c 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 5c3b70bb..2603ad48 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 3bae27b3..edd3b5ea 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 7f8978c5..1355f5ca 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 2d73e7ba..77c18078 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 a6fb1275..87d74641 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 e42f206e..ea69fa1b 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 597453d8..348b0a16 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 99132673..485d66eb 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 05270508..e7c7b045 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 9d30c8a1..f9a69453 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 247ce263..393e5856 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 c61173f4..f086032d 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 52442d21..55277aa1 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 4f358b41..62d0f6de 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 4f2ca051..25413b5d 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 d5fa308d..98c91225 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 9fbf9563..c1be7184 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 f1e85789..ae701083 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 dfdebf81..58f1745b 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 531659cf..ebf4bee0 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 cf40f187..1d6939d6 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"; export const APPEARANCE_GROUP = "Appearance & Behavior"; @@ -467,14 +467,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 128ab419..b6f10a66 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 25e0eb20..73391f4a 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 f57b53cb..69a0103f 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 358a20dd..9f85990c 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 0750d581..24c923ab 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 bde64e8d..dec8219f 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 97c42c32..1e1e0863 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 7de5f4ef..440a4ab1 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 5dcf838b..e67fc056 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 0ee676cc..dbe32ea2 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 c8f097f3..fcb7aff9 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 beb6e0f9..f7a34e3f 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 a22cef79..549edcc6 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 f0479d89..70f9c24d 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 f76afb16..f6b823d4 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 d9785097..76baa23b 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 b7302b14..bed98625 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 b5f70fbd..e0187afb 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 76cc27dc..868ee8cf 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 ef5bdbf2..d760b5e6 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 68b27a46..19571405 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 3b546a53..e665b9d8 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 d066ec2a..79ddee41 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 8b86eba8..31433341 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 b9277f48..6ac014ab 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 }))} />