fix(i18n): Translate interpolated UI text through a tx template tag (#98)

About 270 UI strings were JS template literals with interpolation passed to exact catalog lookup, so they could never be translated; about 25 more spliced lowercase English fragments into sentences.

`tx` now also works as a template tag (`` tx`Remove data source '${name}'?` ``): it looks up the entry by the literal parts (`{name}` placeholders), translates interpolated values that have entries, and remembers generated text so it re-translates after a language switch or when the catalog loads later. Call sites use the tag; spliced fragments are written as whole sentences per variant. Adds 280 Chinese translations and removes stale ones.

The catalog guard now also fails on untagged template literals reaching exact lookup (unless allowlisted with a reason), tagged text missing from the catalog, case-changed or ternary-suffix interpolations, and duplicate template patterns.

Claude-Session: https://claude.ai/code/session_01VUYsdXA2nkF5v6zsC93dpn
Цей коміт міститься в:
l0ng-ai authored and GitHub committed 2026-10-27 18:56:54 +08:00
1 джерело 2a8e5e791d
коміт 8568ff001b
142 files changed
+1031 -362

No files matched your search

+2 -1
Переглянути файл
@@ -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
Переглянути файл
@@ -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
Переглянути файл
@@ -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
Переглянути файл
@@ -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
Переглянути файл
@@ -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
Переглянути файл
@@ -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
Переглянути файл
@@ -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
Переглянути файл
@@ -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
Переглянути файл
@@ -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
Переглянути файл
@@ -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
Переглянути файл
@@ -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
Переглянути файл
@@ -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
Переглянути файл
@@ -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
Переглянути файл
@@ -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
Переглянути файл
@@ -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
Переглянути файл
@@ -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
Переглянути файл
@@ -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
Переглянути файл
@@ -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
Переглянути файл
@@ -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
Переглянути файл
@@ -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
Переглянути файл
@@ -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
Переглянути файл
@@ -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
Переглянути файл
@@ -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
Переглянути файл
@@ -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
Переглянути файл
@@ -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
Переглянути файл
@@ -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
Переглянути файл
@@ -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
Переглянути файл
@@ -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
Переглянути файл
@@ -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
Переглянути файл
@@ -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" />;
@@ -292,8 +293,8 @@ function BuildToolView({ tool }: { tool: BuildTool }) {
const config = isRunnable(node) && model ? nodeConfig(nodes.filter(isRunnable), model) : null;
if (config) {
items.push(
{ label: `Run '${config.name}'`, icon: "run", run: () => runConfig(config, "run") },
{ label: `Debug '${config.name}'`, icon: "debug", run: () => runConfig(config, "debug") },
{ label: tx`Run '${config.name}'`, icon: "run", run: () => runConfig(config, "run") },
{ label: tx`Debug '${config.name}'`, icon: "debug", run: () => runConfig(config, "debug") },
{ kind: "separator" },
);
if (node.kind === "runConfig")
@@ -318,7 +319,7 @@ function BuildToolView({ tool }: { tool: BuildTool }) {
const file = node.project.buildFile;
if (file)
items.push({
label: `Open ${file.split("/").pop()}`,
label: tx`Open ${file.split("/").pop()}`,
icon: "file-gradle",
run: () => void editorService().openFile(`${root}/${file}`),
});
+3 -2
Переглянути файл
@@ -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
Переглянути файл
@@ -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
Переглянути файл
@@ -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
Переглянути файл
@@ -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
Переглянути файл
@@ -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
Переглянути файл
@@ -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
Переглянути файл
@@ -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
Переглянути файл
@@ -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
Переглянути файл
@@ -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
Переглянути файл
@@ -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
Переглянути файл
@@ -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
Переглянути файл
@@ -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
Переглянути файл
@@ -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
Переглянути файл
@@ -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
Переглянути файл
@@ -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
Переглянути файл
@@ -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
Переглянути файл
@@ -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
Переглянути файл
@@ -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
Переглянути файл
@@ -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
Переглянути файл
@@ -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
Переглянути файл
@@ -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
Переглянути файл
@@ -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
Переглянути файл
@@ -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
Переглянути файл
@@ -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
Переглянути файл
@@ -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
Переглянути файл
@@ -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
Переглянути файл
@@ -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
Переглянути файл
@@ -21,7 +21,7 @@ import clsx from "clsx";
import { useEditorRegistry } from "@/editor/api";
import { executeAction } from "@/platform/actions";
import { setContext } from "@/platform/context";
import { useTxTemplate } from "@/platform/i18n";
import { tx, useTxTemplate } from "@/platform/i18n";
import { isTauri } from "@/platform/ipc";
import type { RootFolder } from "@/platform/languageSettings";
import { useEffectiveRoots } from "@/platform/projectRoots";
@@ -460,7 +460,7 @@ export function ProjectView() {
setDropTarget(valid ? target.key : null);
setDrag({
paths,
label: paths.length === 1 ? nameOf(paths[0]!) : `${paths.length} items`,
label: paths.length === 1 ? nameOf(paths[0]!) : tx`${paths.length} items`,
x: ev.clientX,
y: ev.clientY,
copy: ev.altKey,
+3 -2
Переглянути файл
@@ -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
Переглянути файл
@@ -42,6 +42,7 @@ import {
setViewOption,
useProjectTree,
} from "./store";
import { tx } from "@/platform/i18n";
const FOCUS = "projectViewFocus";
const OPEN = "projectOpen";
@@ -150,7 +151,7 @@ export function registerProjectActions(): () => void {
const toggles: Action[] = OPTION_LABELS.map(([key, label]) => ({
id: `project.option.${key}`,
title: `Project View: ${label}`,
title: tx`Project View: ${label}`,
category: "View",
keywords: ["project", "tree", "toggle"],
toggled: () => useProjectTree.getState().options[key],
@@ -159,7 +160,7 @@ export function registerProjectActions(): () => void {
const modes: Action[] = VIEW_MODES.map(([mode, label]) => ({
id: `project.viewMode.${mode}`,
title: `Project View: Show ${label}`,
title: tx`Project View: Show ${label}`,
category: "View",
when: OPEN,
keywords: ["project", "pane", "scope", "view as"],
@@ -335,7 +336,7 @@ export function registerProjectActions(): () => void {
copyPath("name", "project.copyFileName", "Copy File Name"),
{
id: "project.revealInFinder",
title: `Reveal in ${revealLabel()}`,
title: tx`Reveal in ${revealLabel()}`,
category: "Project View",
icon: "external-link",
when: OPEN,
+2 -1
Переглянути файл
@@ -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
Переглянути файл
@@ -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
Переглянути файл
@@ -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
Переглянути файл
@@ -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
Переглянути файл
@@ -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
Переглянути файл
@@ -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
Переглянути файл
@@ -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
Переглянути файл
@@ -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
Переглянути файл
@@ -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
Переглянути файл
@@ -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
Переглянути файл
@@ -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
Переглянути файл
@@ -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
Переглянути файл
@@ -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
Переглянути файл
@@ -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
Переглянути файл
@@ -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
Переглянути файл
@@ -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
Переглянути файл
@@ -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
Переглянути файл
@@ -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",
+6 -3
Переглянути файл
@@ -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
Переглянути файл
@@ -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
Переглянути файл
@@ -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
Переглянути файл
@@ -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
Переглянути файл
@@ -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
Переглянути файл
@@ -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
Переглянути файл
@@ -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)),
}))
+2 -1
Переглянути файл
@@ -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() },
+3 -2
Переглянути файл
@@ -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
Переглянути файл
@@ -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
Переглянути файл
@@ -27,7 +27,7 @@ import {
import { ACCENT_PRESETS, LIMITS, type ThemeId } from "../model/schema";
import type { PageSpec } from "./spec";
import styles from "./Behavior.module.css";
import { useT } from "@/platform/i18n";
import { tx, useT } from "@/platform/i18n";
import { BUILTIN_THEMES } from "@/platform/themes";
@@ -465,14 +465,14 @@ function NotificationGroupsTable() {
value={v.display}
options={DISPLAY_OPTIONS}
onChange={(display) => update(g.id, { display })}
aria-label={`${g.title} popup type`}
aria-label={tx`${g.title} popup type`}
/>
</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>
+2 -1
Переглянути файл
@@ -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
Переглянути файл
@@ -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>
);
+3 -2
Переглянути файл
@@ -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>
+13 -2
Переглянути файл
@@ -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
Переглянути файл
@@ -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
Переглянути файл
@@ -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);
+4 -3
Переглянути файл
@@ -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
Переглянути файл
@@ -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",
+1 -1
Переглянути файл
@@ -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>
+5 -4
Переглянути файл
@@ -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>
+2 -1
Переглянути файл
@@ -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>
Loaded 100 of 142 files, more files were not shown because too many files have changed in this diff. Show more