fix(i18n): Translate UI text built from template literals

Template literals such as `Remove data source '${name}'?` reached props and
calls that look text up exactly (dialog messages, labels, aria-labels, menu
items, action titles), so they always showed in English. Some notification
titles also spliced lowercase English words into a sentence.

- `tx` also works as a tag: tx`Remove data source '${name}'?` looks up
  "Remove data source '{name}'?" by its literal parts, fills in the values
  (translated when the catalog has them) and remembers the result, so the
  primitives that pass strings through `tx` translate it again after a locale
  change or once the catalog has loaded (actions registered at startup).
- 269 template literals at exact-lookup sites now use the tag; text that
  spliced words or plural suffixes is written as one sentence per variant
  (safe delete, Docker delete/prune, rebase, conflicts, VCS "No … in
  progress", database copy errors, inspection descriptions, ...).
- zh: 280 new entries; stale and duplicate-shape entries removed.
- textCatalog guard: untagged template literals at exact-lookup sites fail
  (with an allowlist for code-like text), tagged text is checked against the
  catalog by shape, case-changing values and `c ? "a" : "b"` suffixes inside
  translated templates fail, and a catalog may hold one entry per template shape.

Claude-Session: https://claude.ai/code/session_01VUYsdXA2nkF5v6zsC93dpn
This commit is contained in:
l0ng-ai
2026-10-27 18:43:53 +08:00
parent 2616e3fe6d
commit b03983847c
142 changed files with 1032 additions and 363 deletions
+2 -1
View File
@@ -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). */
+2 -1
View File
@@ -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({
+2 -1
View File
@@ -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<Extension> {
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 ?? [],
];
}
+2 -1
View File
@@ -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 ?? []),
+2 -1
View File
@@ -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;
}
+3 -2
View File
@@ -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. */
+2 -1
View File
@@ -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),
},
+2 -1
View File
@@ -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 <EmptyState icon="file" title={`No editor for “${tab.input.kind}”`} />;
if (!kind) return <EmptyState icon="file" title={tx`No editor for “${tab.input.kind}”`} />;
const Component = kind.component;
return (
<div className={styles.kind} data-editor-focus="" tabIndex={-1} key={tab.id}>
+2 -1
View File
@@ -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<SymbolKind>([
"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,
});
+2 -1
View File
@@ -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,
})),
+1 -1
View File
@@ -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,
+2 -1
View File
@@ -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<InlayHint["kind"], string> = {
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") },
];
+2 -1
View File
@@ -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),
+2 -1
View File
@@ -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),
});
+6 -5
View File
@@ -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,
+5 -4
View File
@@ -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,
})),
+2 -1
View File
@@ -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));
+2 -1
View File
@@ -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),
});
}
+3 -2
View File
@@ -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 }),
});
}
+2 -2
View File
@@ -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);
+17 -8
View File
@@ -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<void> {
// 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<void> {
]);
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();
+4 -3
View File
@@ -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<RefactoringId> = 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),
+3 -2
View File
@@ -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 <IconButton icon="plus" action="agents.newTask" size="small" />;
@@ -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,
+1 -1
View File
@@ -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();
+2 -1
View File
@@ -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. */
+2 -1
View File
@@ -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<ScopeChoice>(() => defaultScope());
@@ -363,7 +364,7 @@ function ReplacePreviewDialog({ files, onClose }: { files: StructuralFile[]; onC
<Checkbox
checked={!excluded.has(key)}
onChange={(on) => 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}`}
/>
<div className={styles.previewBody}>
<span className={styles.previewWhere}>
+2 -1
View File
@@ -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();
+2 -1
View File
@@ -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`,
});
}
}
+2 -1
View File
@@ -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 {
+4 -3
View File
@@ -43,6 +43,7 @@ import {
useBuild,
} from "./store";
import styles from "./BuildTools.module.css";
import { tx } from "@/platform/i18n";
export function MavenView() {
return <BuildToolView tool="maven" />;
@@ -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}`),
});
+3 -2
View File
@@ -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")}
/>
+4 -3
View File
@@ -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() {
<BarButton
ref={ref}
aria-expanded={open}
aria-label={`Language servers: ${summary.text}`}
aria-label={tx`Language servers: ${summary.text}`}
tooltip="Language servers"
onClick={() => setOpen((v) => !v)}
>
@@ -65,7 +66,7 @@ export function ServerStatus() {
) : s.status === "missing" && canInstall ? (
<Button
size="small"
aria-label={`Install ${s.name}`}
aria-label={tx`Install ${s.name}`}
onClick={() => {
setOpen(false);
void executeAction("tools.install", s.name);
@@ -77,7 +78,7 @@ export function ServerStatus() {
<IconButton
icon="refresh"
size="small"
title={`Restart ${s.name}`}
title={tx`Restart ${s.name}`}
disabled={s.status === "missing" || s.status === "starting"}
onClick={() => void restartServer(s.id)}
/>
+4 -5
View File
@@ -1,17 +1,16 @@
/** "problems" file decorations: files with errors (and their folders) get IDEA's red underline. */
import { setFileDecorations, type FileDecoration } from "@/workbench/contributions";
import { countSeverities, fileDiagnostics, useProblems, type ProblemsMap } from "./problemsStore";
import { tx } from "@/platform/i18n";
export function problemDecorations(map: ProblemsMap): Map<string, FileDecoration> {
const out = new Map<string, FileDecoration>();
for (const path of map.keys()) {
const c = countSeverities(fileDiagnostics(map, path));
if (!c.error) continue;
out.set(path, {
error: true,
propagate: true,
tooltip: `${c.error} ${c.error === 1 ? "error" : "errors"}${c.warning ? `, ${c.warning} ${c.warning === 1 ? "warning" : "warnings"}` : ""}`,
});
const errors = c.error === 1 ? tx`${c.error} error` : tx`${c.error} errors`;
const warnings = !c.warning ? "" : c.warning === 1 ? tx`${c.warning} warning` : tx`${c.warning} warnings`;
out.set(path, { error: true, propagate: true, tooltip: warnings ? `${errors}, ${warnings}` : errors });
}
return out;
}
+2 -1
View File
@@ -7,6 +7,7 @@ import { call, commands, isTauri, type InstallCommand, type RunConfig } from "@/
import { notifications, toolWindowService, type ExecutorLaunch, type RunExecutorContribution } from "@/workbench/contributions";
import { coverageSupports, formatPercent } from "./model";
import { addSuite } from "./store";
import { tx } from "@/platform/i18n";
const errorText = (e: unknown) => (e instanceof Error ? e.message : String(e));
@@ -47,7 +48,7 @@ export function notifyMissingTool(
level: "warning",
key: `${group}-missing-${tool}`,
title: `${tool} is not installed`,
body: line ? `${missing.message} Install it with: ${line}` : missing.homepage ? `${missing.message} ${missing.homepage}` : missing.message,
body: line ? tx`${missing.message} Install it with: ${line}` : missing.homepage ? `${missing.message} ${missing.homepage}` : missing.message,
sticky: true,
actions,
});
+4 -3
View File
@@ -8,6 +8,7 @@ import { CodePreview } from "./CodePreview";
import { guessSeparator, importEdits, parseDelimited } from "./model";
import { closeDialog, detailsKey, loadDetails, openConsoleWith, targetLabel, useDatabase } from "./store";
import styles from "./Database.module.css";
import { tx } from "@/platform/i18n";
function when(t: number): string {
const d = new Date(t);
@@ -60,7 +61,7 @@ export function HistoryDialog({ id, path }: { id: string; path: string | null })
<Dialog
open
onClose={closeDialog}
title={`${targetLabel(id)} Query History`}
title={tx`${targetLabel(id)} Query History`}
width={820}
footerStart={
<IconButton icon="trash" title="Clear History" disabled={!entries?.length} onClick={() => void remove(null)} />
@@ -192,12 +193,12 @@ export function ImportDialog({ id, schema, table }: { id: string; schema: string
<Dialog
open
onClose={closeDialog}
title={`Import Data into ${schema}.${table}`}
title={tx`Import Data into ${schema}.${table}`}
width={720}
buttons={[
{ label: "Cancel", role: "cancel" },
{
label: `Import ${edits.length} Rows`,
label: tx`Import ${edits.length} Rows`,
role: "default",
disabled: !edits.length || !mapped.length || busy,
onClick: () => void run(),
+4 -3
View File
@@ -21,6 +21,7 @@ import {
} from "./resultView";
import { openDialog } from "./store";
import styles from "./Database.module.css";
import { tx } from "@/platform/i18n";
export type DataPanel = "record" | "value" | "aggregates";
@@ -30,7 +31,7 @@ export function exportMenu(data: ExportData, source: string, format?: ExportForm
{ label: "Export Data…", icon: "export", run: () => openDialog({ kind: "export", data, source, format }) },
{ kind: "separator" },
...EXPORT_FORMATS.map((f): MenuItem => ({
label: `Copy as ${f.label}`,
label: tx`Copy as ${f.label}`,
icon: "copy",
run: () => void navigator.clipboard?.writeText(exportData(f.id, data)),
})),
@@ -207,14 +208,14 @@ function FilterPopup(props: {
anchor={props.anchor}
placement="bottom-start"
onClose={props.onClose}
aria-label={`Local filter of ${props.column}`}
aria-label={tx`Local filter of ${props.column}`}
className={styles.filterPopup}
trapFocus
>
<Input
autoFocus
aria-label="Search values"
placeholder={`Values of ${props.column}`}
placeholder={tx`Values of ${props.column}`}
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
+3 -2
View File
@@ -7,6 +7,7 @@ import { SourceDialog } from "./SourceDialog";
import { closeDialog, useDatabase } from "./store";
import styles from "./Database.module.css";
import { isImeKey } from "@/platform/ime";
import { tx } from "@/platform/i18n";
const ObjectDialogs = lazyComponent(() => import("./ObjectDialogs").then((m) => m.ObjectDialogs));
const ExportDialog = lazyComponent(() => import("./ExportDialog").then((m) => m.ExportDialog));
@@ -28,7 +29,7 @@ function PasswordDialog({ source, resolve }: { source: DataSource; resolve: (ok:
<Dialog
open
onClose={() => done(false)}
title={`Connect to ${source.name}`}
title={tx`Connect to ${source.name}`}
width={400}
buttons={[
{ label: "Cancel", role: "cancel" },
@@ -82,7 +83,7 @@ function SshSecretDialog({
<Dialog
open
onClose={() => done(false)}
title={`SSH: ${sshLabel(source) || source.name}`}
title={tx`SSH: ${sshLabel(source) || source.name}`}
width={440}
buttons={[
{ label: "Cancel", role: "cancel" },
+2 -1
View File
@@ -38,6 +38,7 @@ import {
} from "./store";
import { openTableData } from "./treeActions";
import styles from "./Database.module.css";
import { tx } from "@/platform/i18n";
/** Read-only SQL with the editor's syntax colours (IDEA's dialog Preview pane). */
function SqlPreview({ sql, label = "Preview" }: { sql: string; label?: string }) {
@@ -287,7 +288,7 @@ function ModifyTableDialog(props: ModifyProps) {
<Dialog
open
onClose={closeDialog}
title={`Modify Table ${table}`}
title={tx`Modify Table ${table}`}
width={440}
buttons={[{ label: "Cancel", role: "cancel" }]}
>
+16 -10
View File
@@ -17,7 +17,7 @@ import {
type DbSslMode,
type DbTxMode,
} from "@/platform/ipc";
import { useTx, useTxTemplate } from "@/platform/i18n";
import { tx, useTx, useTxTemplate } from "@/platform/i18n";
import { Button, Checkbox, Dialog, IconButton, Input, Select, Tabs, T, type SelectOption } from "@/ui";
import { colorVar, connectRemedy, fileSourceName, newSourceId, parseSourceUrl, sshLabel } from "./model";
import { closeDialog, loadDatabases, loadSchemas, saveSource, useDatabase } from "./store";
@@ -224,7 +224,7 @@ function SecondsOption({
<Input
className={styles.secondsInput}
inputMode="numeric"
aria-label={`${label} (seconds)`}
aria-label={tx`${label} (seconds)`}
disabled={!on}
value={draft}
onChange={(e) => {
@@ -243,14 +243,18 @@ function SecondsOption({
/** Schemas tab: "All …" or a choice of names and `*` / `?` patterns. */
function NamePicker({
title,
allLabel,
addLabel,
all,
chosen,
known,
onAll,
onChosen,
}: {
title: string;
/** "All databases" */
allLabel: string;
/** Accessible name of the pattern input. */
addLabel: string;
all: boolean;
chosen: string[];
known: string[];
@@ -266,7 +270,7 @@ function NamePicker({
};
return (
<div className={styles.namePicker}>
<Checkbox checked={all} onChange={onAll} label={`All ${title}`} />
<Checkbox checked={all} onChange={onAll} label={allLabel} />
{!all && (
<div className={styles.nameList}>
{names.map((n) => (
@@ -282,7 +286,7 @@ function NamePicker({
className={styles.fill}
value={pattern}
placeholder="Name or pattern, e.g. app_*"
aria-label={`Add ${title} pattern`}
aria-label={addLabel}
onChange={(e) => setPattern(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter" && !isImeKey(e.nativeEvent)) {
@@ -401,7 +405,7 @@ export function SourceDialog({
setTest(null);
try {
const v = await call(commands.dbTest(effective(), password ?? null, sshOn ? (sshSecret ?? null) : null));
setTest({ ok: true, text: `Succeeded: ${v}` });
setTest({ ok: true, text: tx`Succeeded: ${v}` });
} catch (e) {
const text = e instanceof Error ? e.message : String(e);
const trust = e instanceof IpcError && connectRemedy(e.kind, e.message) === "trustHostKey";
@@ -802,7 +806,8 @@ export function SourceDialog({
<div className={styles.formWide}>
{postgres && (
<NamePicker
title="databases"
allLabel={tx("All databases")}
addLabel={tx("Add databases pattern")}
all={intro.allDatabases !== false}
chosen={intro.databases ?? []}
known={knownDbs}
@@ -811,7 +816,8 @@ export function SourceDialog({
/>
)}
<NamePicker
title={s.driver === "mysql" ? "schemas (databases)" : "schemas"}
allLabel={s.driver === "mysql" ? tx("All schemas (databases)") : tx("All schemas")}
addLabel={s.driver === "mysql" ? tx("Add schemas (databases) pattern") : tx("Add schemas pattern")}
all={intro.allSchemas !== false}
chosen={intro.schemas ?? []}
known={knownSchemas}
@@ -921,7 +927,7 @@ export function SourceDialog({
<Dialog
open
onClose={closeDialog}
title={initial ? `Data Source: ${initial.name}` : "New Data Source"}
title={initial ? tx`Data Source: ${initial.name}` : "New Data Source"}
width={640}
footerStart={
<Button onClick={() => void runTest()} disabled={busy || !valid}>
+2 -1
View File
@@ -29,6 +29,7 @@ import {
import { openDialog, sourceById, withConnection } from "./store";
import type { TableRef } from "./tableInput";
import styles from "./Database.module.css";
import { tx } from "@/platform/i18n";
const PAGE_SIZES = ["100", "500", "1000", "5000"];
@@ -306,7 +307,7 @@ export function TableEditor({ input }: { input: EditorInput }) {
) : (
<div className={styles.fill}>
<DataView
aria-label={`${ref.table} data`}
aria-label={tx`${ref.table} data`}
columns={gridColumns}
rowCount={rowCount}
fixedRows={loaded}
+2 -1
View File
@@ -58,6 +58,7 @@ import {
useDatabase,
} from "./store";
import styles from "./Database.module.css";
import { tx } from "@/platform/i18n";
/** `templates` feature's language-data key for finer template contexts. */
const TEMPLATE_CONTEXT_KEY = "openideaTemplateContexts";
@@ -287,7 +288,7 @@ function ConsoleBar({ path, view }: { path: string; view: EditorView }) {
<Select
aria-label="Row limit"
value={String(maxRows)}
options={ROW_LIMITS.map((v) => ({ value: v, label: `${Number(v).toLocaleString("en")} rows` }))}
options={ROW_LIMITS.map((v) => ({ value: v, label: tx`${Number(v).toLocaleString("en")} rows` }))}
onChange={(v) => useDatabase.setState({ maxRows: Number(v) })}
/>
<span className={styles.toolbarSeparator} />
+4 -3
View File
@@ -38,6 +38,7 @@ import type { ExportData, ExportFormat } from "./export";
import type { DbObjectRef } from "./objects";
import { formatted } from "./sql/format";
import { bindParams, findParams } from "./sql/params";
import { tx } from "@/platform/i18n";
export const DB_TOOL_WINDOW = "database";
export const RESULTS_VIEW = "dbResults";
@@ -281,7 +282,7 @@ export async function renameSource(id: string, name: string): Promise<void> {
export async function removeSource(source: DataSource): Promise<void> {
const ok = await confirm({
title: "Remove Data Source",
message: `Remove data source '${source.name}'? Its query console file stays on disk.`,
message: tx`Remove data source '${source.name}'? Its query console file stays on disk.`,
ok: "Remove",
});
if (!ok) return;
@@ -788,7 +789,7 @@ export async function executeConsole(
.filter((o) => o.result?.columns.length)
.map((outcome) => {
const n = nextTab++;
return { id: n, title: `Result ${n}`, path, sourceId: id, outcome };
return { id: n, title: tx`Result ${n}`, path, sourceId: id, outcome };
});
const failed = outcomes.some((o) => o.error);
useDatabase.setState((s) => ({
@@ -957,7 +958,7 @@ export async function finishTransaction(id: string, commit: boolean): Promise<bo
async function askEndTransaction(source: DataSource): Promise<boolean | null> {
const { button } = await ask({
title: "Switch to Auto-Commit",
body: `${source.name} has uncommitted changes. Commit or roll them back before switching to auto-commit?`,
body: tx`${source.name} has uncommitted changes. Commit or roll them back before switching to auto-commit?`,
buttons: [
{ id: "rollback", label: "Roll Back" },
{ id: "commit", label: "Commit", primary: true },
+8 -6
View File
@@ -37,6 +37,7 @@ import {
removeSource,
} from "./store";
import { tableInput } from "./tableInput";
import { tx } from "@/platform/i18n";
/** Context key: the Database tree has focus. */
export const DB_VIEW_FOCUS = "databaseViewFocus";
@@ -57,11 +58,12 @@ function sourceOf(n: DbNode | null): DataSource | undefined {
return n ? sourceById(nodeTarget(n)) : undefined;
}
async function copy(text: string, what: string) {
/** Copies `text`; `failure` is the notification title when the clipboard refuses it. */
async function copy(text: string, failure: string) {
try {
await navigator.clipboard.writeText(text);
} catch {
notifications().notify({ level: "error", title: `Cannot copy the ${what}` });
notifications().notify({ level: "error", title: failure });
}
}
@@ -373,10 +375,10 @@ export function registerDatabaseTreeActions(): () => void {
const n = nodeFrom(arg);
if (!n) return;
const id = nodeTarget(n);
if (n.kind === "source") return void copy(n.source.name ?? "", "name");
if (n.kind === "database") return void copy(n.name, "name");
if (n.kind === "source") return void copy(n.source.name ?? "", tx("Cannot copy the name"));
if (n.kind === "database") return void copy(n.name, tx("Cannot copy the name"));
const o = objectOf(n);
if (id && o) void copy(reference(driverOf(id), o), "reference");
if (id && o) void copy(reference(driverOf(id), o), tx("Cannot copy the reference"));
},
}),
tree({
@@ -397,7 +399,7 @@ export function registerDatabaseTreeActions(): () => void {
run: async ({ arg }) => {
const n = nodeFrom(arg);
const ddl = n && (await ddlOf(n));
if (ddl) await copy(ddl.text, "DDL");
if (ddl) await copy(ddl.text, tx("Cannot copy the DDL"));
},
}),
tree({
+2 -1
View File
@@ -43,6 +43,7 @@ import {
openSmartStepChooser,
useOverlays,
} from "./ui/popoverStore";
import { tx } from "@/platform/i18n";
function isRunConfig(arg: unknown): arg is RunConfig {
return !!arg && typeof arg === "object" && "command" in arg && "args" in arg && "debugAdapter" in arg;
@@ -159,7 +160,7 @@ async function listenForJvm(name: string, host: string, port: number): Promise<n
key,
sticky: true,
title: `'${name}': listening for the remote JVM on port ${port}`,
body: `Start the remote JVM with -agentlib:jdwp=transport=dt_socket,server=n,address=${host}:${port},suspend=y`,
body: tx`Start the remote JVM with -agentlib:jdwp=transport=dt_socket,server=n,address=${host}:${port},suspend=y`,
actions: [{ label: "Cancel", run: () => void commands.cancelRequest(request) }],
});
try {
+2 -1
View File
@@ -8,6 +8,7 @@
import { call, commands, isTauri, type DebugEvent, type DebugSessionId } from "@/platform/ipc";
import { maybeEditorService, notifications } from "@/workbench/contributions";
import { isLive, useDebug, type DebugSession } from "./state";
import { tx } from "@/platform/i18n";
const KEY = "debug.hotswap";
/** `BUILD_COMPLETE` events of our own build are not worth a prompt. */
@@ -25,7 +26,7 @@ function plural(n: number, word: string) {
export function hotSwapMessage(changed: number, errors: string[]): { level: "info" | "error"; title: string; body?: string } {
if (errors.length) return { level: "error", title: "Hot Swap failed", body: errors.join("\n") };
if (changed === 0) return { level: "info", title: "Loaded classes are up to date. Nothing to reload." };
return { level: "info", title: `${plural(changed, "class")} reloaded` };
return { level: "info", title: tx`${plural(changed, "class")} reloaded` };
}
/** Compiles (unless `build` is false) and reloads changed classes into every Java debug session. */
+7 -7
View File
@@ -5,11 +5,11 @@
import { IpcError } from "@/platform/ipc";
/** Adapters the tools installer can provide (adapter id → tool name). */
const INSTALLABLE: Record<string, { tool: string; label: string }> = {
debugpy: { tool: "debugpy", label: "debugpy" },
"js-debug": { tool: "js-debug", label: "the JavaScript debugger (vscode-js-debug)" },
delve: { tool: "delve", label: "Delve" },
codelldb: { tool: "codelldb", label: "CodeLLDB" },
const INSTALLABLE: Record<string, { tool: string; title: string }> = {
debugpy: { tool: "debugpy", title: "debugpy is not installed" },
"js-debug": { tool: "js-debug", title: "The JavaScript debugger (vscode-js-debug) is not installed" },
delve: { tool: "delve", title: "Delve is not installed" },
codelldb: { tool: "codelldb", title: "CodeLLDB is not installed" },
};
export interface MissingAdapter {
@@ -25,14 +25,14 @@ export function missingAdapter(adapter: string, e: unknown): MissingAdapter | nu
if (adapter === "debugpy" && e.message.startsWith("debugpy is not installed")) {
return {
tool: entry.tool,
title: "debugpy is not installed",
title: entry.title,
body: `${e.message}. OpenIDEA can install a private copy that every Python interpreter uses for debugging.`,
};
}
if (!e.message.startsWith(`debug adapter \`${adapter}\` not found`)) return null;
return {
tool: entry.tool,
title: `${entry.label[0]!.toUpperCase()}${entry.label.slice(1)} is not installed`,
title: entry.title,
body: e.message,
};
}
+2 -1
View File
@@ -40,6 +40,7 @@ import {
} from "../state";
import { VariablesView, type VarRoot } from "./VariablesView";
import styles from "./DebugToolWindow.module.css";
import { tx } from "@/platform/i18n";
function useActionEnabled(id: string): boolean {
const ctx = useContextKeys();
@@ -114,7 +115,7 @@ export function DebugTabs() {
if (!list.length) return null;
const items: TabItem[] = list.map((s) => ({
id: String(s.id),
label: s.parent != null ? `${s.name} (child)` : s.name,
label: s.parent != null ? tx`${s.name} (child)` : s.name,
text: s.name,
icon: "debug",
italic: s.status === "terminated",
+3 -2
View File
@@ -34,6 +34,7 @@ import { VariablesView, type VarRoot } from "./VariablesView";
import { pushHistory, readHistory } from "./expressionHistory";
import styles from "./Overlays.module.css";
import { isImeKey } from "@/platform/ime";
import { tx } from "@/platform/i18n";
export function DebuggerOverlays() {
return (
@@ -447,7 +448,7 @@ function BreakpointsDialog() {
<Checkbox
checked={r.bp.enabled}
onChange={(enabled) => updateSpecial(r.bp.id, { enabled })}
aria-label={`Enable ${specialTitle(r.bp)}`}
aria-label={tx`Enable ${specialTitle(r.bp)}`}
/>
<Icon name={r.bp.kind === "method" ? "breakpoint-method" : "breakpoint-field"} size={14} />
<span className={styles.bpName}>{specialTitle(r.bp)}</span>
@@ -467,7 +468,7 @@ function BreakpointsDialog() {
<Checkbox
checked={r.bp.enabled}
onChange={(enabled) => updateBreakpoint(r.path, r.bp.id, { enabled })}
aria-label={`Enable breakpoint at ${basename(r.path)}:${r.bp.line + 1}`}
aria-label={tx`Enable breakpoint at ${basename(r.path)}:${r.bp.line + 1}`}
/>
<span
className={styles.bpDot}
+4 -3
View File
@@ -35,6 +35,7 @@ import {
import { addComment, checkoutPullRequest, openCommentLocation, openFileDiff, openUrl, submitReview } from "./ops";
import { cancelBusy, loadDetails, refreshAll, select, setQuery, useGithub, type DetailTab } from "./store";
import styles from "./Github.module.css";
import { tx } from "@/platform/i18n";
const STATES: Array<{ value: PrStateFilter; label: string }> = [
{ value: "open", label: "Open" },
@@ -515,9 +516,9 @@ function Details() {
aria-label="Pull request"
items={[
{ id: "overview", label: "Overview" },
{ id: "files", label: `Files ${pr.files.length}` },
{ id: "commits", label: `Commits ${pr.commits.length}` },
{ id: "conversation", label: `Conversation ${pr.timeline.length + comments.length}` },
{ id: "files", label: tx`Files ${pr.files.length}` },
{ id: "commits", label: tx`Commits ${pr.commits.length}` },
{ id: "conversation", label: tx`Conversation ${pr.timeline.length + comments.length}` },
]}
/>
{tab === "overview" && <Overview pr={pr} />}
+3 -2
View File
@@ -5,6 +5,7 @@ import type { ReviewEvent } from "@/platform/ipc";
import { call, commands } from "@/platform/ipc";
import { Checkbox, Dialog, Input, Select, T } from "@/ui";
import styles from "./Github.module.css";
import { tx } from "@/platform/i18n";
interface Entry {
id: number;
@@ -134,7 +135,7 @@ function CreateDialog(props: {
/>
<div className={styles.formRow}>
<Checkbox checked={draft} onChange={setDraft} label="Draft" />
<Checkbox checked={push} onChange={setPush} label={`Push '${props.head}' first`} />
<Checkbox checked={push} onChange={setPush} label={tx`Push '${props.head}' first`} />
</div>
</div>
</Dialog>
@@ -165,7 +166,7 @@ function ReviewDialog(props: {
return (
<Dialog
open
title={`Review #${props.number}`}
title={tx`Review #${props.number}`}
onClose={() => props.onDone(null)}
width={520}
buttons={[
+2 -1
View File
@@ -10,6 +10,7 @@ import { EmptyState, IconButton, SplitGroup, SplitHandle, SplitPanel, Spinner, T
import { bodyKind, formatMs, formatSize, prettyBody } from "./format";
import { cancelRunning, clearHistory, refreshHistory, selectEntry, useHttpClient } from "./store";
import styles from "./HttpClient.module.css";
import { tx } from "@/platform/i18n";
function statusTone(status: number | null | undefined): string {
if (status == null) return styles.statusError ?? "";
@@ -73,7 +74,7 @@ function ExchangeView({ ex }: { ex: HttpExchange }) {
onSelect={(id) => setTab(id as Tab)}
items={[
{ id: "body", label: "Response" },
{ id: "headers", label: `Headers${r ? ` (${r.headers.length})` : ""}` },
{ id: "headers", label: r ? tx`Headers (${r.headers.length})` : "Headers" },
{ id: "request", label: "Request" },
]}
trailing={
+2 -2
View File
@@ -2,7 +2,7 @@
import { useState } from "react";
import { create } from "zustand";
import { executeAction, getAction } from "@/platform/actions";
import { t } from "@/platform/i18n";
import { t, tx } from "@/platform/i18n";
import { BarButton, Button, Dialog, Icon, List, T } from "@/ui";
import { confirm } from "@/workbench/messages";
import { promptInput } from "@/workbench/inputDialogs";
@@ -60,7 +60,7 @@ function EditMacrosDialog({ onClose }: { onClose: () => void }) {
};
const remove = async () => {
if (!macro) return;
if (await confirm({ title: "Delete Macro", message: `Delete the macro '${macro.name}'?`, ok: "Delete", icon: "warning" }))
if (await confirm({ title: "Delete Macro", message: tx`Delete the macro '${macro.name}'?`, ok: "Delete", icon: "warning" }))
deleteMacro(macro.name);
};
+3 -2
View File
@@ -14,6 +14,7 @@ import { indicatorLabel, indicatorTooltip, memorySections, summarizeMemory } fro
import { startPolling } from "./poll";
import { METRICS_INTERVAL_MS, refreshMetrics, usePerf } from "./store";
import styles from "./perf.module.css";
import { tx } from "@/platform/i18n";
/** Status bar widget id (hidden by default, toggled from the status bar context menu). */
export const MEMORY_WIDGET = { id: "perf.memory", title: "Memory Indicator", defaultVisible: false } as const;
@@ -41,7 +42,7 @@ export function MemoryStatus() {
ref={ref}
className={styles.indicator}
aria-expanded={open}
aria-label={`Memory: ${tooltip}`}
aria-label={tx`Memory: ${tooltip}`}
tooltip={tooltip}
onClick={() => usePerf.setState({ popupOpen: !open })}
>
@@ -101,7 +102,7 @@ export function MemoryDetails({ metrics: m }: { metrics: AppMetrics }) {
label={r.label}
detail={r.detail}
value={r.value}
title={r.processes > 1 ? `${r.processes} processes` : undefined}
title={r.processes > 1 ? tx`${r.processes} processes` : undefined}
indented
/>
))}
+6 -2
View File
@@ -6,6 +6,7 @@ import { useProject } from "@/platform/project";
import { formatSize } from "./format";
import { MEMORY_WIDGET } from "./MemoryStatus";
import { clearCaches, refreshMetrics, usePerf } from "./store";
import { tx } from "@/platform/i18n";
function memoryIndicatorShown(): boolean {
const overrides = useProject.getState().settings?.appearance?.statusBarWidgets;
@@ -30,12 +31,15 @@ export function perfActions(): Action[] {
}
const memory =
r.rssBeforeBytes != null && r.rssAfterBytes != null
? ` Memory ${formatSize(r.rssBeforeBytes)} → ${formatSize(r.rssAfterBytes)}.`
? tx` Memory ${formatSize(r.rssBeforeBytes)} → ${formatSize(r.rssAfterBytes)}.`
: "";
notifications().notify({
level: "info",
title: "Caches cleared",
body: `${r.syntaxTrees} syntax tree${r.syntaxTrees === 1 ? "" : "s"} dropped, index compacted.${memory}`,
body:
r.syntaxTrees === 1
? tx`${r.syntaxTrees} syntax tree dropped, index compacted.${memory}`
: tx`${r.syntaxTrees} syntax trees dropped, index compacted.${memory}`,
key: "perf.clearCaches",
});
} catch (e) {
+5 -4
View File
@@ -39,6 +39,7 @@ import {
usePlugins,
} from "./store";
import styles from "./PluginsPage.module.css";
import { tx } from "@/platform/i18n";
const STATUS: Record<PluginStatus, { label: string; tone: "success" | "accent" | "neutral" | "danger" }> = {
active: { label: "Running", tone: "success" },
@@ -137,7 +138,7 @@ function InstalledRow({ plugin, selected, onSelect }: { plugin: PluginInfo; sele
checked={plugin.enabled}
disabled={busy}
onChange={(on) => void setPluginEnabled(plugin.id, on)}
aria-label={`${plugin.enabled ? "Disable" : "Enable"} ${plugin.name}`}
aria-label={plugin.enabled ? tx`Disable ${plugin.name}` : tx`Enable ${plugin.name}`}
/>
</li>
);
@@ -308,7 +309,7 @@ function InstalledDetails({ plugin }: { plugin: PluginInfo }) {
icon="run"
disabled={!running}
onClick={() => void runPluginCommand(plugin.id, c.id)}
aria-label={`Run ${c.title}`}
aria-label={tx`Run ${c.title}`}
>
<T>Run</T>
</Button>
@@ -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}`}
>
<T>Apply</T>
</Button>
@@ -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}`}
>
<T>Uninstall</T>
</Button>
+2 -1
View File
@@ -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);
},
+2 -1
View File
@@ -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,
});
+2 -2
View File
@@ -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,
@@ -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<WorkspaceEdit["files"][number], { kind: "edit" }>;
@@ -67,7 +68,7 @@ function FileGroup({ file, root, included, onToggle }: { file: EditFile; root: s
return (
<li className={styles.previewFile}>
<div className={styles.previewFileRow}>
<Checkbox checked={included} onChange={onToggle} aria-label={`Include ${nameOf(file.path)}`} />
<Checkbox checked={included} onChange={onToggle} aria-label={tx`Include ${nameOf(file.path)}`} />
<Icon name={fileIconName(nameOf(file.path))} size={14} />
<span className={styles.previewName}>{nameOf(file.path)}</span>
<span className={styles.previewPath}>{relativeTo(root, file.path) || file.path}</span>
@@ -117,7 +118,7 @@ function PreviewDialog({ request }: { request: PreviewRequest }) {
return (
<Dialog
open
title={`Refactoring Preview: ${request.title}`}
title={tx`Refactoring Preview: ${request.title}`}
onClose={() => request.resolve(null)}
width={680}
buttons={[
+4 -3
View File
@@ -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,
+2 -1
View File
@@ -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<string, string> = {
@@ -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),
},
]
+3 -2
View File
@@ -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<boolean> {
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",
});
+2 -1
View File
@@ -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<boolean> {
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";
+3 -2
View File
@@ -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();
+1 -1
View File
@@ -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))}
/>
</span>
+4 -3
View File
@@ -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`}
/>
) : (
<ConsoleView
@@ -102,7 +103,7 @@ function SessionPanel({ session }: { session: RunSession }) {
}
: undefined
}
aria-label={`${session.config.name} output`}
aria-label={tx`${session.config.name} output`}
/>
)}
</div>
@@ -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,
}));
}
+7 -7
View File
@@ -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")}
+4 -3
View File
@@ -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" },
);
}
+4 -3
View File
@@ -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",
+8 -4
View File
@@ -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<boolean> {
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<boolean
/** IDEA's question before closing a Run tab whose process still runs. Resolves true for Terminate. */
export async function confirmTerminate(name: string): Promise<boolean> {
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",
});
+2 -1
View File
@@ -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 });
+2 -1
View File
@@ -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<boolean> {
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;
+3 -2
View File
@@ -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<Record<ScriptLanguage, string>> = {
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`).",
+3 -2
View File
@@ -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))
+2 -2
View File
@@ -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,
};
});
+2 -1
View File
@@ -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}`}
/>
);
}
+3 -2
View File
@@ -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<void>
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` });
}
}
+7 -6
View File
@@ -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",
@@ -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),
},
+3 -2
View File
@@ -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: () => {
+3 -3
View File
@@ -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",
})
)
+39 -5
View File
@@ -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<ServiceNode, { kind: "compose" }
return composeOp(conn, n.project, first?.composeFiles ?? [], first?.composeWorkingDir ?? null, op).then(() => 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),
});
}
+2 -1
View File
@@ -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),
},
];
+1 -1
View File
@@ -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 });
+4 -3
View File
@@ -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)),
}))
@@ -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<Action, "id" | "title" | "category" | "when">;
@@ -61,7 +62,7 @@ export function RecordShortcutDialog({ action, actions, draft, onCancel, onSubmi
<Dialog
open
onClose={onCancel}
title={`Keyboard Shortcut: ${action.title}`}
title={tx`Keyboard Shortcut: ${action.title}`}
width={440}
buttons={[
{ label: "OK", role: "default", disabled: !binding || already, onClick: () => void ok() },
@@ -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))}
/>
</div>
@@ -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))}
/>
</div>
+3 -2
View File
@@ -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<LanguageInfo[]>([]);
@@ -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}`}
/>
</Row>
<Row
@@ -120,7 +121,7 @@ export function RecognizedFileTypes() {
value={hashbangs}
onChange={(next) => setAssoc(setHashbangs(current, selected.id, next))}
placeholder="interpreter"
aria-label={`Hashbang patterns of ${selected.name}`}
aria-label={tx`Hashbang patterns of ${selected.name}`}
/>
</Row>
{selected.firstLine && (
+3 -3
View File
@@ -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`}
/>
</td>
<td className={styles.center}>
<Checkbox
checked={v.log}
onChange={(log) => update(g.id, { log })}
aria-label={`Show ${g.title} in tool window`}
aria-label={tx`Show ${g.title} in tool window`}
/>
</td>
</tr>
@@ -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<string, Record<string, TextAttributesOverride>>;
@@ -238,7 +239,7 @@ function ColorInput({
<Input
mono
className={styles.swatchInput}
aria-label={`${label} hex`}
aria-label={tx`${label} hex`}
value={draft ?? value}
invalid={draft !== null && !HEX_COLOR.test(draft)}
onChange={(e) => {
+2 -1
View File
@@ -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 && (
<NumberInput value={weight} onChange={(n) => onChange(fontWeightOf(n, defaultWeight))} min={100} max={900} step={50} integer aria-label={`${label} custom value`} />
<NumberInput value={weight} onChange={(n) => onChange(fontWeightOf(n, defaultWeight))} min={100} max={900} step={50} integer aria-label={tx`${label} custom value`} />
)}
</Row>
);
@@ -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}`}
/>
</span>
</span>
@@ -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;
}
+2 -1
View File
@@ -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)}
>
<Checkbox
aria-label={`Enable ${t.name}`}
aria-label={tx`Enable ${t.name}`}
checked={t.enabled}
onChange={(on) => setTools(tools.map((x) => (x.id === t.id ? { ...x, enabled: on } : x)))}
/>
+2 -1
View File
@@ -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);
@@ -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({
<Dialog
open
onClose={close}
title={`New ${template.name}`}
title={tx`New ${template.name}`}
width={380}
position="top"
aria-label={`New ${template.name}`}
aria-label={tx`New ${template.name}`}
>
<div className={styles.form}>
<Input
data-autofocus
value={name}
invalid={!!error}
placeholder={effective.extension ? `Name (.${effective.extension})` : "File name with extension"}
placeholder={effective.extension ? tx`Name (.${effective.extension})` : "File name with extension"}
aria-label="Name"
onChange={(e) => {
setName(e.target.value);
+3 -2
View File
@@ -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",
@@ -136,7 +136,7 @@ function FilesTab() {
<Checkbox
checked={t.enabled !== false}
onChange={(v) => saveFileTemplate({ ...t, enabled: v })}
aria-label={`Enable ${t.name}`}
aria-label={tx`Enable ${t.name}`}
/>
<Icon name={t.icon} size={14} />
<span className={clsx(isModifiedFileTemplate(t.id) && styles.modified)}>{t.name}</span>
@@ -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() {
<Checkbox
checked={t.enabled !== false}
onChange={(v) => saveTemplate({ ...t, enabled: v })}
aria-label={`Enable ${t.abbrev}`}
aria-label={tx`Enable ${t.abbrev}`}
/>
<span className={clsx(styles.itemLabel, isModifiedBuiltin(t.id) && styles.modified)}>
{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`}
/>
</td>
<td>
@@ -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`}
/>
</td>
<td>
<Checkbox
checked={!!v.skipIfDefined}
onChange={(c) => setVar(v.name, { skipIfDefined: c })}
aria-label={`Skip ${v.name} if defined`}
aria-label={tx`Skip ${v.name} if defined`}
/>
</td>
</tr>
@@ -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}`}
/>
<span className={styles.mono}>.{t.key}</span>
<span className={styles.itemDetail}>{t.description}</span>

Some files were not shown because too many files have changed in this diff Show More