fix(i18n): Translate the remaining UI text (Git, hierarchy, pull requests, dialogs, aria labels) (#114)

Hard-coded UI text, titles and aria-labels in about 110 components (Git panels, hierarchy, pull requests, dialogs, settings, welcome) now go through tx; spliced fragments became whole sentences (fileCount, noneFoundLabel, hierarchyTitle) and pull request states use keyed messages. 225 zh entries added, stale fragment entries removed.

Also: tx used as a tag without interpolations (tx`No checks`) now looks the text up exactly instead of as a template, so it is translated like tx("No checks"); the text-catalog test checks such tags as plain literals. Check and test counts reuse the existing "{count} passed" / "{failed} failing" entries.

Claude-Session: https://claude.ai/code/session_01VUYsdXA2nkF5v6zsC93dpn
Co-authored-by: jaw52 <53564781+jaw52@users.noreply.github.com>
This commit is contained in:
Jaw
2026-10-28 12:18:46 +08:00
committed by GitHub
parent faf666d125
commit 70f10e741c
121 changed files with 854 additions and 457 deletions
+4
View File
@@ -6,6 +6,10 @@ contributors as part of the IntelliJ Platform (https://github.com/JetBrains/inte
https://github.com/JetBrains/intellij-plugins), licensed under the Apache License 2.0.
Copyright 2000-2026 JetBrains s.r.o. and contributors.
Some Simplified Chinese UI terms follow the Visual Studio Code Simplified Chinese
language pack (https://github.com/microsoft/vscode-loc), Copyright (c) Microsoft
Corporation, licensed under the MIT License.
OpenIDEA is not affiliated with, endorsed by or sponsored by JetBrains s.r.o.
IntelliJ and IntelliJ IDEA are trademarks of JetBrains s.r.o.
+39
View File
@@ -250,3 +250,42 @@ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
```
## Chinese UI terminology from Visual Studio Code
Some Simplified Chinese terms in `src/platform/locales/text/zh.json` follow the
wording of the Visual Studio Code Simplified Chinese language pack
([microsoft/vscode-loc](https://github.com/microsoft/vscode-loc), MIT), e.g.
"未跟踪" (untracked), "合并冲突" (merge conflicts), "当前" (current), "已修改"
(modified), "已过时" (outdated), "中止" (abort), "正在卸载" (uninstalling),
"深色" / "浅色" (dark / light) and "只读" (read-only). The remaining translations
are written for this project.
```
Visual Studio Code Localization Packs
Copyright (c) Microsoft Corporation
All rights reserved.
MIT License
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE
```
+2 -1
View File
@@ -3,6 +3,7 @@ import { Icon, T } from "@/ui";
import { useEditorBanners, type EditorBannerContribution } from "@/workbench/contributions";
import { useDocMeta } from "../model/document";
import styles from "./EditorArea.module.css";
import { tx } from "@/platform/i18n";
const TEXT = {
binary: "This file appears to be binary and is shown read-only.",
@@ -30,7 +31,7 @@ export function VirtualBanner({ decompiled, description }: { decompiled: boolean
<div className={`${styles.banner} ${styles.libraryBanner}`} role="status">
<Icon name={decompiled ? "info" : "lock"} size={14} className={styles.bannerIcon} />
<span className={styles.bannerText}>
{decompiled ? "Decompiled .class file: sources are not available." : "Library source (read-only)."}
{decompiled ? tx("Decompiled .class file: sources are not available.") : tx("Library source (read-only).")}
</span>
{description && (
<span className={styles.bannerDetail} title={description}>
+1 -1
View File
@@ -283,7 +283,7 @@ function PaneView({ paneId }: { paneId: string }) {
tabPrefs.placement === "right" && styles.tabsRight,
)}
data-pane-id={paneId}
aria-label="Editor"
aria-label={tx("Editor")}
onFocus={onFocus}
onBlur={onBlur}
>
+2 -1
View File
@@ -3,6 +3,7 @@ import { useState, type FormEvent } from "react";
import { Dialog, Input } from "@/ui";
import { editorController, useEditorStatus, useGotoLine } from "../controller";
import styles from "./EditorArea.module.css";
import { tx } from "@/platform/i18n";
export function parseLineCol(text: string): { line: number; col: number } | null {
const m = /^\s*(\d+)\s*(?:[:,]\s*(\d+))?\s*$/.exec(text);
@@ -53,7 +54,7 @@ function GotoLineInner() {
placeholder="line[:column]"
/>
<span className={styles.gotoHint}>
{status ? `Current position ${status.line + 1}:${status.col + 1}` : "No active editor"}
{status ? tx`Current position ${status.line + 1}:${status.col + 1}` : tx("No active editor")}
</span>
</form>
</Dialog>
+3 -2
View File
@@ -23,6 +23,7 @@ import {
type NavSegment,
} from "./navBar";
import styles from "./NavBarPopup.module.css";
import { tx } from "@/platform/i18n";
export const useNavBarPopup = create<{ open: boolean; seq: number }>(() => ({ open: false, seq: 0 }));
@@ -155,7 +156,7 @@ function NavBar({ path, line, col }: { path: string; line: number; col: number }
data-autofocus
onKeyDown={onKeyDown}
role="toolbar"
aria-label="Navigation bar"
aria-label={tx("Navigation bar")}
>
<div className={styles.bar}>
{segments.map((s, i) => (
@@ -184,7 +185,7 @@ function NavBar({ path, line, col }: { path: string; line: number; col: number }
className={styles.list}
style={{ marginLeft: list.left }}
role="listbox"
aria-label="Children"
aria-label={tx("Children")}
>
{list.items.length === 0 && (
<div className={styles.empty}>
+3 -2
View File
@@ -15,6 +15,7 @@ import { symbolChain } from "./outline";
import { NavChildrenPopup } from "./NavBarPopup";
import type { NavSegment } from "./navBar";
import styles from "./EditorArea.module.css";
import { tx } from "@/platform/i18n";
function useActiveMeta() {
const path = useEditorStatus((s) => s.status?.path ?? null);
@@ -95,7 +96,7 @@ export function Breadcrumbs() {
nav: { kind: "symbol", path, label: item.name, item },
});
return (
<nav className={styles.status} aria-label="Breadcrumbs">
<nav className={styles.status} aria-label={tx("Breadcrumbs")}>
{segments.map((s, i) => (
<Fragment key={i}>
{i > 0 && <Icon name="chevron-right" size={12} className={styles.statusSep} />}
@@ -152,7 +153,7 @@ export function CodeBreadcrumbs({
return (
<nav
className={clsx(styles.codeCrumbs, position === "top" ? styles.codeCrumbsTop : styles.codeCrumbsBottom)}
aria-label="Code breadcrumbs"
aria-label={tx("Code breadcrumbs")}
>
{chain.map((item, i) => (
<Fragment key={i}>
+5 -4
View File
@@ -42,6 +42,7 @@ import { renderCode, renderMarkup } from "./markdown";
import { describeSummary } from "./workspaceEdit";
import { useUsagesStatus } from "./navigation";
import styles from "./Popups.module.css";
import { tx } from "@/platform/i18n";
export function BulbIcon({ fix }: { fix?: boolean }) {
return <Icon name={fix ? "bulb-fix" : "bulb"} size={16} className={styles.bulb} />;
@@ -58,7 +59,7 @@ function ListPreviewPanel({ preview }: { preview: ListPreview | null | "loading"
if (preview === "loading") return <div className={styles.previewPanel} aria-busy="true" />;
if (!preview) return null;
return (
<div className={styles.previewPanel} aria-label="Preview">
<div className={styles.previewPanel} aria-label={tx("Preview")}>
{preview.kind === "text" ? (
<div className={styles.previewText}>{preview.text}</div>
) : (
@@ -316,7 +317,7 @@ function RenamePopup({ popup }: { popup: Extract<LspPopup, { kind: "rename" }> }
aria-label="New name"
/>
{suggestions.length > 1 && (
<ul className={styles.renameSuggestions} aria-label="Suggested names">
<ul className={styles.renameSuggestions} aria-label={tx("Suggested names")}>
{suggestions.map((s) => (
<li key={s}>
<button
@@ -556,7 +557,7 @@ function PreviewDialog({ popup }: { popup: Extract<LspPopup, { kind: "preview" }
))}
<div className={styles.previewSummary}>{describeSummary(summary)}</div>
{popup.occurrences ? (
<ul className={styles.previewList} aria-label="Occurrences">
<ul className={styles.previewList} aria-label={tx("Occurrences")}>
{popup.occurrences.map((o, i) => {
const lead = o.text.length - o.text.trimStart().length;
const text = o.text.trim();
@@ -575,7 +576,7 @@ function PreviewDialog({ popup }: { popup: Extract<LspPopup, { kind: "preview" }
})}
</ul>
) : (
<ul className={styles.previewList} aria-label="Changes">
<ul className={styles.previewList} aria-label={tx("Changes")}>
{summary.entries.map((e) => (
<li key={e.path} className={styles.previewRow}>
<Icon name={fileIconName(basename(e.path))} size={14} className={styles.itemIcon} />
+3 -3
View File
@@ -21,7 +21,7 @@ import {
selectableText,
showContextMenu,
} from "@/ui";
import { useTx } from "@/platform/i18n";
import { useTx, tx } from "@/platform/i18n";
import { TaskStatusIcon } from "./AgentsView";
import { Composer } from "./Composer";
import { canSend, emptyDraft, usageLabel, workTime } from "./composerModel";
@@ -669,7 +669,7 @@ function SessionComposer({ task, entries }: { task: AgentTask; entries: AgentEnt
function Queue({ task }: { task: AgentTask }) {
const fail = (e: unknown) => notifications().notify({ level: "error", title: "Couldn't update the queue", body: String(e) });
return (
<div className={styles.queue} aria-label="Queued messages">
<div className={styles.queue} aria-label={tx("Queued messages")}>
{task.queue.map((q) => (
<div key={q.id} className={styles.queued}>
<Icon name="pause" size={12} className={styles.muted} />
@@ -701,7 +701,7 @@ function ChangesPane({ task, changes, head }: { task: AgentTask; changes: AgentC
const added = changes?.reduce((n, c) => n + c.added, 0) ?? 0;
const removed = changes?.reduce((n, c) => n + c.removed, 0) ?? 0;
return (
<aside className={styles.changes} aria-label="Changes">
<aside className={styles.changes} aria-label={tx("Changes")}>
<div className={styles.changesHeader}>
<span className={styles.changesTitle}>
<T>Changes</T>
+4 -2
View File
@@ -171,7 +171,9 @@ function DuplicatesDialog({ onClose }: { onClose: () => void }) {
onChange={(e) => setMinText(e.target.value)}
aria-label="Minimum duplicate size in tokens"
/>
<span className={styles.hint}>tokens</span>
<span className={styles.hint}>
<T>tokens</T>
</span>
</label>
<Checkbox
checked={options.anonymizeIdentifiers}
@@ -355,7 +357,7 @@ function ReplacePreviewDialog({ files, onClose }: { files: StructuralFile[]; onC
{ label: "Cancel", role: "cancel" },
]}
>
<ul className={styles.previewList} aria-label="Replacements">
<ul className={styles.previewList} aria-label={tx("Replacements")}>
{files.flatMap((f) =>
f.matches.map((m, i) => {
const key = matchKey(f.path, i);
+4 -3
View File
@@ -6,6 +6,7 @@ import { editorService } from "@/workbench/contributions";
import { EmptyState, Icon, IconButton, Spinner, Tree, fileIconName } from "@/ui";
import { cancelDuplicates, fragmentLines, rerunDuplicates, useDuplicates } from "./duplicates";
import styles from "./Analysis.module.css";
import { tx } from "@/platform/i18n";
type Node =
| { kind: "group"; key: string; index: number; group: DuplicateGroup }
@@ -38,7 +39,7 @@ function FragmentPane({ fragment }: { fragment: Fragment }) {
type="button"
className={styles.fragmentHeader}
onClick={() => openFragment(fragment)}
title="Open in editor"
title={tx("Open in editor")}
>
<Icon name={fileIconName(basename(fragment.path))} size={16} className={styles.fileIcon} />
<span className={styles.strong}>{basename(fragment.path)}</span>
@@ -104,9 +105,9 @@ export function DuplicatesView() {
n.kind === "group" ? (
<span className={styles.row}>
<span className={styles.strong}>
{n.group.fragments.length} occurrences · {n.group.lines} lines
{tx`${n.group.fragments.length} occurrences · ${n.group.lines} lines`}
</span>
<span className={styles.count}>{n.group.tokens} tokens</span>
<span className={styles.count}>{tx`${n.group.tokens} tokens`}</span>
</span>
) : (
<span className={styles.row}>
@@ -6,7 +6,7 @@
import { useMemo, useState, type MouseEvent } from "react";
import clsx from "clsx";
import { call, commands, type MavenDependency, type MavenProject } from "@/platform/ipc";
import { useTx, useTxTemplate } from "@/platform/i18n";
import { useTx, useTxTemplate, tx } from "@/platform/i18n";
import { editorService, notifications, type EditorInput } from "@/workbench/contributions";
import {
Button,
@@ -207,7 +207,7 @@ function UsagesPane({
</span>
</div>
)}
<div className={styles.usageList} aria-label="Usages">
<div className={styles.usageList} aria-label={tx("Usages")}>
{usages.map((u, i) => (
<UsageBlock key={i} usage={u} module={module} pom={pom} />
))}
@@ -302,7 +302,7 @@ export function DependencyAnalyzer(_props: { input: EditorInput }) {
return (
<div className={styles.view}>
<div className={styles.toolbar} role="toolbar" aria-label="Dependency Analyzer">
<div className={styles.toolbar} role="toolbar" aria-label={tx("Dependency Analyzer")}>
<Select
aria-label="Module"
className={styles.module}
+3 -2
View File
@@ -36,6 +36,7 @@ import {
} from "./model";
import { navigateToMessage, rerunBuild, selectBuildNode, useBuildRun } from "./store";
import styles from "./Build.module.css";
import { tx } from "@/platform/i18n";
const SEVERITY_ICON: Record<Severity, string> = { error: "error", warning: "warning", info: "info", hint: "info" };
@@ -218,7 +219,7 @@ function BuildConsole() {
className={styles.console}
tabIndex={0}
role="log"
aria-label="Build output"
aria-label={tx("Build output")}
onScroll={(e) => {
const el = e.currentTarget;
stick.current = el.scrollHeight - el.scrollTop - el.clientHeight < LINE_HEIGHT * 2;
@@ -252,7 +253,7 @@ function MessageDetail({ message }: { message: BuildMessage }) {
}`
: null;
return (
<div className={styles.detail} tabIndex={0} role="document" aria-label="Build message">
<div className={styles.detail} tabIndex={0} role="document" aria-label={tx("Build message")}>
{location && (
<button type="button" className={styles.detailLocation} onClick={() => navigateToMessage(message, true)}>
{location}
+6 -4
View File
@@ -19,9 +19,11 @@ import { openLocation } from "./ipc";
import {
ROOT_ID,
directionLabel,
hierarchyTitle,
hierarchyTarget,
loadChildren,
refreshHierarchy,
noneFoundLabel,
setDirection,
useHierarchy,
type HierarchyNode,
@@ -56,7 +58,7 @@ export function HierarchyTabs() {
const direction = useHierarchy((s) => s.direction);
const root = useHierarchy((s) => s.root);
if (!root) return null;
const label = `${directionLabel(mode, direction)} of ${root.name}`;
const label = hierarchyTitle(mode, direction, root.name);
return (
<Tabs
variant="compact"
@@ -74,7 +76,7 @@ function HierarchyToolbar() {
const direction = useHierarchy((s) => s.direction);
const autoscroll = useHierarchyOptions((s) => s.autoscroll);
return (
<div className={styles.toolbar} role="toolbar" aria-label="Hierarchy actions">
<div className={styles.toolbar} role="toolbar" aria-label={tx("Hierarchy actions")}>
<IconButton
icon="arrow-up"
size="small"
@@ -165,7 +167,7 @@ export function HierarchyView() {
</span>
)}
{!loading && !failed.has(ROOT_ID) && children.get(ROOT_ID)?.length === 0 && (
<span className={styles.muted}>— no {directionLabel(mode, direction).toLowerCase()} found</span>
<span className={styles.muted}>{noneFoundLabel(mode, direction)}</span>
)}
</span>
);
@@ -180,7 +182,7 @@ export function HierarchyView() {
<span className={styles.muted}>
{basename(item.path)}:{item.selectionRange.start.line + 1}
</span>
{calls > 1 && <span className={styles.muted}>({calls} calls)</span>}
{calls > 1 && <span className={styles.muted}>{tx`(${calls} calls)`}</span>}
{failed.has(n.id) && (
<span className={styles.error}>
<T>failed to load</T>
+7 -5
View File
@@ -16,6 +16,7 @@ import { openLocation, readFileLines } from "./ipc";
import { showUsageResults, usageCount, useUsagesPopup, type Usage, type UsageGroup } from "./usages";
import { UsagePreview } from "./UsagesView";
import styles from "./UsagesPopup.module.css";
import { tx } from "@/platform/i18n";
const close = () => useUsagesPopup.setState({ open: false });
@@ -118,8 +119,8 @@ function UsagesPopupInner() {
<div className={styles.focus} tabIndex={0} data-autofocus onKeyDown={onKeyDown}>
<div className={styles.header}>
<span className={styles.title}>
Usages{symbol ? ` of ${symbol}` : ""}{" "}
<span className={styles.count}>{filter ? `${items.length} of ${total}` : total} found</span>
{symbol ? tx`Usages of ${symbol}` : tx("Usages")}{" "}
<span className={styles.count}>{filter ? tx`${items.length} of ${total} found` : tx`${total} found`}</span>
</span>
{filter && (
<span className={clsx(styles.filter, !items.length && styles.noMatch)} role="status">
@@ -135,7 +136,8 @@ function UsagesPopupInner() {
onClick={() => setUsagesPopupPreview(!preview)}
/>
<button type="button" className={styles.link} onClick={openInFindWindow}>
Open in Find Window{keys ? ` ${keys}` : ""}
{tx("Open in Find Window")}
{keys ? ` ${keys}` : ""}
</button>
</div>
{note && <div className={styles.note}>{note}</div>}
@@ -150,7 +152,7 @@ function UsagesPopupInner() {
rowHeight={ROW}
aria-label="Usages"
onActivate={pick}
empty={<div className={styles.empty}>Nothing matches '{filter}'</div>}
empty={<div className={styles.empty}>{tx`Nothing matches '${filter}'`}</div>}
renderItem={(u) => (
<div className={styles.row}>
<Icon name={fileIconName(basename(u.path))} size={14} className={styles.icon} />
@@ -234,7 +236,7 @@ function UsageCodePreview({ usage }: { usage: Usage | null }) {
}, [usage, loaded]);
return (
<div className={styles.preview} role="region" aria-label="Preview">
<div className={styles.preview} role="region" aria-label={tx("Preview")}>
<div ref={host} className={clsx(styles.previewEditor, loaded !== path && styles.previewHidden)} />
</div>
);
+15 -2
View File
@@ -5,6 +5,7 @@ import { IpcError, call, commands } from "@/platform/ipc";
import { toolWindowService } from "@/workbench/contributions";
import { basename } from "@/platform/project";
import { caretContext, hint, lspRequest, reportError, withDocument, type CaretContext } from "./ipc";
import { tx } from "@/platform/i18n";
export type HierarchyMode = "call" | "type";
@@ -56,9 +57,21 @@ export function resetHierarchy() {
}));
}
/** The empty-result line of the hierarchy tree. */
export function noneFoundLabel(mode: HierarchyMode, direction: HierarchyDirection): string {
if (mode === "call") return direction === "incoming" ? tx`— no callers found` : tx`— no callees found`;
return direction === "incoming" ? tx`— no supertypes found` : tx`— no subtypes found`;
}
export function directionLabel(mode: HierarchyMode, direction: HierarchyDirection): string {
if (mode === "call") return direction === "incoming" ? "Callers" : "Callees";
return direction === "incoming" ? "Supertypes" : "Subtypes";
if (mode === "call") return direction === "incoming" ? tx("Callers") : tx("Callees");
return direction === "incoming" ? tx("Supertypes") : tx("Subtypes");
}
/** The tab title of the hierarchy tree, e.g. "Callers of main". */
export function hierarchyTitle(mode: HierarchyMode, direction: HierarchyDirection, name: string): string {
if (mode === "call") return direction === "incoming" ? tx`Callers of ${name}` : tx`Callees of ${name}`;
return direction === "incoming" ? tx`Supertypes of ${name}` : tx`Subtypes of ${name}`;
}
function request(mode: HierarchyMode, doc: number, line: number, col: number, direction: HierarchyDirection) {
+1 -1
View File
@@ -225,7 +225,7 @@ export function ImportDialog({ id, schema, table }: { id: string; schema: string
<T>Columns:</T>
</span>
<span className={styles.formHint}>
{mapped.length ? mapped.join(", ") : "No column of the file matches the table"}
{mapped.length ? mapped.join(", ") : tx("No column of the file matches the table")}
</span>
</div>
{preview.length > 0 && (
+3 -3
View File
@@ -148,7 +148,7 @@ export function DataView(props: DataViewProps) {
/>
{filter.size > 0 && (
<div className={styles.filterPill} role="status">
{shown} of {rowCount} rows
{tx`${shown} of ${rowCount} rows`}
<button type="button" className={styles.filterPillClear} onClick={() => setFilter(new Map())}>
<T>Clear local filter</T>
</button>
@@ -219,7 +219,7 @@ function FilterPopup(props: {
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<div className={styles.filterValues} role="group" aria-label="Values">
<div className={styles.filterValues} role="group" aria-label={tx("Values")}>
{list.map(({ value: v, count }) => (
<div key={v ?? "\u0000null"} className={styles.filterValue}>
<Checkbox
@@ -270,7 +270,7 @@ function SidePanel(props: {
}) {
const { range, value, columns } = props;
return (
<aside className={styles.sidePanel} aria-label="Value panel">
<aside className={styles.sidePanel} aria-label={tx("Value panel")}>
<div className={styles.sidePanelHead}>
<Tabs
variant="compact"
+4 -3
View File
@@ -42,6 +42,7 @@ import {
import { DB_VIEW_FOCUS, openTableData } from "./treeActions";
import { newMenu, nodeMenu } from "./treeMenu";
import styles from "./Database.module.css";
import { tx } from "@/platform/i18n";
const GROUP_LABELS: Record<Group, string> = {
columns: "columns",
@@ -427,7 +428,7 @@ export function DatabaseView() {
<span className={styles.nodeIcon}>
<Icon name={i} size={16} />
{/* IDEA: a green dot on the data source icon while connected. */}
{live && <span className={styles.connectedDot} title="Connected" />}
{live && <span className={styles.connectedDot} title={tx("Connected")} />}
</span>
)}
<span className={labelClass}>{label(n)}</span>
@@ -490,7 +491,7 @@ function DatabaseToolbar() {
const active = !!target && [...connected].some((t) => splitTarget(t).id === splitTarget(target).id);
const button = { size: "small", iconSize: 16 } as const;
return (
<div className={styles.dbToolbar} role="toolbar" aria-label="Database actions">
<div className={styles.dbToolbar} role="toolbar" aria-label={tx("Database actions")}>
<IconButton
ref={plus}
{...button}
@@ -520,7 +521,7 @@ function DatabaseToolbar() {
<button
type="button"
className={styles.textTool}
aria-label="Go to DDL"
aria-label={tx("Go to DDL")}
disabled={!table}
onClick={() => void executeAction("db.goToDdl")}
{...tooltipAttrs("Go to DDL", shortcutLabel("db.goToDdl"))}
+2 -1
View File
@@ -9,6 +9,7 @@ import clsx from "clsx";
import { Icon, showContextMenu, type MenuItem } from "@/ui";
import { toCsv, toTsv, type Cell } from "./model";
import styles from "./Database.module.css";
import { tx } from "@/platform/i18n";
export interface GridColumn {
name: string;
@@ -291,7 +292,7 @@ export function Grid(props: GridProps) {
type="button"
className={clsx(styles.headerFilter, props.filtered?.(ci) && styles.headerFilterOn)}
aria-label={`Filter ${c.name}`}
title="Local filter"
title={tx("Local filter")}
onMouseDown={(e) => e.stopPropagation()}
onClick={(e) => {
e.stopPropagation();
+5 -4
View File
@@ -120,7 +120,7 @@ function RenameDialog({ id, object }: { id: string; object: DbObjectRef | null }
>
<div className={styles.dialogBody}>
<label className={styles.formLabelLeft} htmlFor="db-rename">
Rename {what} '{initial}' to:
{tx`Rename ${what} '${initial}' to:`}
</label>
<Input
id="db-rename"
@@ -156,7 +156,8 @@ function DropDialog({ id, object }: { id: string; object: DbObjectRef }) {
>
<div className={styles.dialogBody}>
<div className={styles.dialogLead}>
Drop {KIND_LABEL[object.kind]} <strong>{reference(driver, object)}</strong>? This cannot be undone.
{tx`Drop ${KIND_LABEL[object.kind]}`} <strong>{reference(driver, object)}</strong>
{tx`? This cannot be undone.`}
</div>
<Checkbox
checked={ifExists}
@@ -387,7 +388,7 @@ function ModifyTableForm({
onClick={remove}
/>
</div>
<div role="listbox" aria-label="Columns" className={styles.modifyList}>
<div role="listbox" aria-label={tx("Columns")} className={styles.modifyList}>
{columns.map((c) => (
<div
key={c.key}
@@ -521,7 +522,7 @@ function SourceUrlDialog() {
/>
<span className={styles.formHint}>
{url.trim() && !parsed
? "Not a URL of PostgreSQL, MySQL, SQLite or a compatible database"
? tx("Not a URL of PostgreSQL, MySQL, SQLite or a compatible database")
: "postgresql://, mysql://, mariadb://, redshift://, tidb://…, jdbc:…, sqlite:path"}
</span>
</div>
+5 -4
View File
@@ -24,6 +24,7 @@ import {
type ResultTab,
} from "./store";
import styles from "./Database.module.css";
import { tx } from "@/platform/i18n";
function time(t: number): string {
const d = new Date(t);
@@ -44,7 +45,7 @@ function Output() {
<span className={styles.spacer} />
<IconButton icon="trash" title="Clear All" disabled={!lines.length} onClick={clearOutput} />
</div>
<div ref={ref} className={styles.output} role="log" aria-label="Database output">
<div ref={ref} className={styles.output} role="log" aria-label={tx("Database output")}>
{lines.map((l: OutputLine) => (
<div key={l.id} className={clsx(styles.outputLine, l.kind === "error" && styles.outputError)}>
<span className={styles.outputTime}>[{time(l.time)}]</span>{" "}
@@ -115,8 +116,8 @@ function Result({ tab }: { tab: ResultTab }) {
aria-label={tab.title}
/>
<div className={styles.rowPill} role="status" title={outcomeSummary(tab.outcome)}>
{r.rows.length} row{r.rows.length === 1 ? "" : "s"}
{r.hasMore ? " (limit reached)" : ""}
{r.rows.length === 1 ? tx`${r.rows.length} row` : tx`${r.rows.length} rows`}
{r.hasMore ? ` ${tx("(limit reached)")}` : ""}
</div>
</div>
)}
@@ -208,7 +209,7 @@ function PlanResult({ tab }: { tab: ResultTab }) {
<EmptyState title="Empty plan" />
)}
</div>
<table className={styles.planProperties} aria-label="Plan node properties">
<table className={styles.planProperties} aria-label={tx("Plan node properties")}>
<thead>
<tr>
<th>
+5 -3
View File
@@ -534,7 +534,7 @@ export function SourceDialog({
{sshOn && (
<>
<span />
<span className={styles.formHint}>Reached from the SSH host {sshLabel({ ssh: sshUsed })}.</span>
<span className={styles.formHint}>{tx`Reached from the SSH host ${sshLabel({ ssh: sshUsed })}.`}</span>
</>
)}
<label className={styles.formLabel} htmlFor="db-user">
@@ -916,8 +916,10 @@ export function SourceDialog({
</Button>
<span className={styles.formHint}>
{initial?.id
? "The Database tree shows only the checked names and patterns (* and ?); the connected database always shows."
: "Save the data source to pick from the server's names; patterns (* and ?) work any time."}
? tx(
"The Database tree shows only the checked names and patterns (* and ?); the connected database always shows.",
)
: tx("Save the data source to pick from the server's names; patterns (* and ?) work any time.")}
</span>
</div>
</div>
+1 -1
View File
@@ -297,7 +297,7 @@ export function TableEditor({ input }: { input: EditorInput }) {
</div>
{page && !editable && (
<div className={styles.notice}>
{pk.length ? "Read-only data source." : "Read-only: the table has no primary key to identify rows."}
{pk.length ? tx("Read-only data source.") : tx("Read-only: the table has no primary key to identify rows.")}
</div>
)}
{error ? (
+11 -9
View File
@@ -152,8 +152,8 @@ export function DebugToolWindow() {
return (
<div className={styles.root}>
<div className={styles.toolbar} role="toolbar" aria-label="Debugger actions">
<div role="tablist" aria-label="Debug views" className={styles.tabs}>
<div className={styles.toolbar} role="toolbar" aria-label={tx("Debugger actions")}>
<div role="tablist" aria-label={tx("Debug views")} className={styles.tabs}>
{(
[
["variables", "Threads & Variables"],
@@ -262,7 +262,7 @@ function FramesPane({ session }: { session: DebugSession }) {
<div
className={styles.frameList}
role="listbox"
aria-label="Frames"
aria-label={tx("Frames")}
tabIndex={0}
onKeyDown={onKeyDown}
onFocus={() =>
@@ -276,7 +276,7 @@ function FramesPane({ session }: { session: DebugSession }) {
}
onBlur={() => blurDebugView("frames")}
>
{!paused && <div className={styles.note}>{session.status === "terminated" ? "Finished" : "Running…"}</div>}
{!paused && <div className={styles.note}>{session.status === "terminated" ? tx("Finished") : "Running…"}</div>}
{items.map((item) =>
item.kind === "hidden" ? (
<button
@@ -285,7 +285,7 @@ function FramesPane({ session }: { session: DebugSession }) {
className={clsx(styles.frame, styles.hiddenFrames)}
onClick={() => setShowAll(true)}
>
{item.count} library frames hidden
{tx`${item.count} library frames hidden`}
</button>
) : (
<button
@@ -496,7 +496,7 @@ function VariablesPane({ session }: { session: DebugSession }) {
}
}}
placeholder={frame != null ? "Evaluate expression (↩) or add a watch (⇧⌘↩)" : "Evaluate expression"}
aria-label="Evaluate expression"
aria-label={tx("Evaluate expression")}
spellCheck={false}
autoComplete="off"
disabled={frame == null}
@@ -516,7 +516,9 @@ function VariablesPane({ session }: { session: DebugSession }) {
</div>
{frame == null && !watches.length && !lastEval ? (
<div className={styles.note}>
{session.status === "terminated" ? "The session has finished." : "Variables are available while suspended."}
{session.status === "terminated"
? tx("The session has finished.")
: tx("Variables are available while suspended.")}
</div>
) : (
<VariablesView
@@ -589,7 +591,7 @@ function ConsoleView({ session }: { session: DebugSession }) {
showContextMenu(e, [{ label: "Clear All", icon: "trash", run: () => clearConsole(session.id) }]);
}}
role="log"
aria-label="Debug console"
aria-label={tx("Debug console")}
tabIndex={0}
>
{(lines ?? []).map((l) => (
@@ -609,7 +611,7 @@ function ConsoleView({ session }: { session: DebugSession }) {
onKeyDown={onKeyDown}
placeholder={isLive(session) ? "Evaluate in the debug console" : "Session finished"}
disabled={!isLive(session)}
aria-label="Debug console input"
aria-label={tx("Debug console input")}
spellCheck={false}
autoComplete="off"
/>
+21 -9
View File
@@ -458,7 +458,11 @@ function BreakpointsDialog() {
{r.bp.line != null ? `:${r.bp.line + 1}` : ""}
</span>
)}
{st && !st.verified && isLive(activeSession()) && <span className={styles.bpCond}>unverified</span>}
{st && !st.verified && isLive(activeSession()) && (
<span className={styles.bpCond}>
<T>unverified</T>
</span>
)}
</div>
);
}
@@ -486,7 +490,11 @@ function BreakpointsDialog() {
</span>
<span className={styles.bpDir}>{relativePath(root, r.path)}</span>
{isConditional(r.bp) && <span className={styles.bpCond}>{suspends(r.bp) ? "if" : "log"}</span>}
{r.bp.temporary && <span className={styles.bpCond}>temp</span>}
{r.bp.temporary && (
<span className={styles.bpCond}>
<T>temp</T>
</span>
)}
</div>
);
}}
@@ -514,7 +522,7 @@ function ExceptionBreakpoints({ open }: { open: boolean }) {
if (open) void refreshExceptionFilters();
}, [open]);
return (
<section className={styles.exceptions} aria-label="Exception breakpoints">
<section className={styles.exceptions} aria-label={tx("Exception breakpoints")}>
<div className={styles.exceptionsTitle}>
<T>Exception Breakpoints</T>
</div>
@@ -564,7 +572,7 @@ function BreakpointDetail({ row, onGo }: { row: BpRow; onGo: () => void }) {
<T>Remove</T>
</Button>
</div>
{st && <div className={styles.statusLine}>{st.verified ? "Verified" : (st.message ?? "Not verified")}</div>}
{st && <div className={styles.statusLine}>{st.verified ? tx("Verified") : (st.message ?? "Not verified")}</div>}
<BreakpointFields path={path} bp={bp} />
<Checkbox
checked={!!bp.temporary}
@@ -595,7 +603,7 @@ function SpecialDetail({ bp, onGo }: { bp: SpecialBreakpoint; onGo: () => void }
<T>Remove</T>
</Button>
</div>
{st && <div className={styles.statusLine}>{st.verified ? "Verified" : (st.message ?? "Not verified")}</div>}
{st && <div className={styles.statusLine}>{st.verified ? tx("Verified") : (st.message ?? "Not verified")}</div>}
<Checkbox checked={bp.enabled} onChange={(enabled) => updateSpecial(bp.id, { enabled })} label="Enabled" />
{bp.kind === "method" ? (
<label className={styles.field}>
@@ -614,7 +622,7 @@ function SpecialDetail({ bp, onGo }: { bp: SpecialBreakpoint; onGo: () => void }
</label>
) : (
<>
<div className={styles.statusLine}>Watching {bp.dataId}</div>
<div className={styles.statusLine}>{tx`Watching ${bp.dataId}`}</div>
<label className={styles.field}>
<span className={styles.label}>
<T>Suspend on</T>
@@ -776,7 +784,7 @@ function EvaluateDialogInner({ initial }: { initial: string }) {
spellCheck={false}
autoComplete="off"
data-autofocus=""
aria-label="Expression"
aria-label={tx("Expression")}
placeholder={paused ? "Expression to evaluate in the selected frame" : "Pause the program to evaluate"}
/>
<span ref={historyButton}>
@@ -805,7 +813,7 @@ function EvaluateDialogInner({ initial }: { initial: string }) {
<VariablesView roots={[result]} session={session?.id ?? null} aria-label="Evaluation result" />
) : (
<div className={styles.empty}>
{paused ? "Press Enter to evaluate" : "Evaluation is available while the program is suspended"}
{paused ? tx("Press Enter to evaluate") : tx("Evaluation is available while the program is suspended")}
</div>
)}
</div>
@@ -938,7 +946,11 @@ function AttachPopup() {
focusable={false}
aria-label="Processes"
className={styles.attachList}
empty={<div className={styles.empty}>No attachable processes{filter ? " match the filter" : ""}.</div>}
empty={
<div className={styles.empty}>
{filter ? tx`No attachable processes match the filter.` : tx`No attachable processes.`}
</div>
}
renderItem={(r) =>
r.kind === "process" ? (
<div className={styles.chooserRow} title={r.process.commandLine}>
+1 -1
View File
@@ -123,7 +123,7 @@ export function RemoteHostView() {
return (
<div className={styles.view}>
<div className={styles.toolbar} role="toolbar" aria-label="Remote host actions">
<div className={styles.toolbar} role="toolbar" aria-label={tx("Remote host actions")}>
<Select
className={styles.serverSelect}
aria-label="Deployment server"
+23 -17
View File
@@ -35,14 +35,9 @@ 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";
import { t, tx } from "@/platform/i18n";
const STATES: Array<{ value: PrStateFilter; label: string }> = [
{ value: "open", label: "Open" },
{ value: "merged", label: "Merged" },
{ value: "closed", label: "Closed" },
{ value: "all", label: "All" },
];
const STATES: PrStateFilter[] = ["open", "merged", "closed", "all"];
function Unavailable({ availability }: { availability: GhAvailability }) {
const retry = (
@@ -138,7 +133,10 @@ function Filters() {
className={styles.search}
/>
<div className={styles.filterRow}>
<Select value={query.state} options={STATES} onChange={(state) => setQuery({ state })} aria-label="State" />
<Select
value={query.state}
options={STATES.map((value) => ({ value, label: t(`github.prState.${value}`) }))}
onChange={(state) => setQuery({ state })} aria-label="State" />
<Checkbox checked={query.authorMe} onChange={(authorMe) => setQuery({ authorMe })} label="Mine" />
<Checkbox
checked={query.reviewRequested}
@@ -247,7 +245,7 @@ function Overview({ pr }: { pr: PullRequestDetails }) {
<div className={styles.scroll}>
<Markdown text={pr.body} />
<section className={styles.section}>
<h3 className={styles.sectionTitle}>Checks · {checksText(summary)}</h3>
<h3 className={styles.sectionTitle}>{tx`Checks · ${checksText(summary)}`}</h3>
{pr.checks.map((c, i) => (
<div key={i} className={styles.checkRow}>
<Icon name={CHECK_ICON[c.status]} size={14} className={styles[`check-${c.status}`]} />
@@ -303,7 +301,7 @@ function Overview({ pr }: { pr: PullRequestDetails }) {
))}
</div>
)}
{pr.assignees.length > 0 && <div className={styles.dim}>Assignees: {pr.assignees.join(", ")}</div>}
{pr.assignees.length > 0 && <div className={styles.dim}>{tx`Assignees: ${pr.assignees.join(", ")}`}</div>}
</section>
)}
</div>
@@ -318,7 +316,7 @@ function Files({ pr }: { pr: PullRequestDetails }) {
return m;
}, [comments]);
return (
<div className={styles.scroll} role="list" aria-label="Changed files">
<div className={styles.scroll} role="list" aria-label={tx("Changed files")}>
{pr.files.map((f) => {
const slash = f.path.lastIndexOf("/");
return (
@@ -334,7 +332,7 @@ function Files({ pr }: { pr: PullRequestDetails }) {
<span className={clsx(styles.fileName, styles[`change-${f.changeType}`])}>{f.path.slice(slash + 1)}</span>
<span className={clsx(styles.dim, styles.ellipsis)}>{slash > 0 ? f.path.slice(0, slash) : ""}</span>
<span className={styles.flex} />
{counts.get(f.path) ? <span className={styles.dim}>{counts.get(f.path)} comments</span> : null}
{counts.get(f.path) ? <span className={styles.dim}>{tx`${counts.get(f.path)} comments`}</span> : null}
<span className={styles.added}>+{f.additions}</span>
<span className={styles.deleted}>−{f.deletions}</span>
</button>
@@ -346,7 +344,7 @@ function Files({ pr }: { pr: PullRequestDetails }) {
function Commits({ pr }: { pr: PullRequestDetails }) {
return (
<div className={styles.scroll} role="list" aria-label="Commits">
<div className={styles.scroll} role="list" aria-label={tx("Commits")}>
{pr.commits.map((c) => (
<div
key={c.oid}
@@ -406,7 +404,11 @@ function Conversation({ pr }: { pr: PullRequestDetails }) {
<button type="button" className={styles.link} onClick={() => void openCommentLocation(pr, t.root.path)}>
{t.root.path}:{t.root.line ?? t.root.originalLine ?? "?"}
</button>
{t.root.line == null && <span className={styles.dim}>outdated</span>}
{t.root.line == null && (
<span className={styles.dim}>
<T>outdated</T>
</span>
)}
</header>
{[t.root, ...t.replies].map((c) => (
<div key={c.id} className={styles.threadItem}>
@@ -421,8 +423,8 @@ function Conversation({ pr }: { pr: PullRequestDetails }) {
))}
<div className={styles.newComment}>
<textarea
aria-label="New comment"
placeholder="Add a comment (Markdown)"
aria-label={tx("New comment")}
placeholder={tx("Add a comment (Markdown)")}
className={styles.textarea}
rows={4}
value={draft}
@@ -565,7 +567,11 @@ export function PullRequestsView() {
if (!useGithub.getState().availability) void refreshAll();
}, []);
if (!availability)
return <div className={styles.loading}>{checking && <Spinner size={14} />} Connecting to GitHub…</div>;
return (
<div className={styles.loading}>
{checking && <Spinner size={14} />} {tx("Connecting to GitHub…")}
</div>
);
if (availability.kind !== "ready")
return (
<div className={styles.view}>
+3 -3
View File
@@ -126,8 +126,8 @@ function CreateDialog(props: {
onChange={(e) => setTitle(e.target.value)}
/>
<textarea
aria-label="Description"
placeholder="Description (Markdown)"
aria-label={tx("Description")}
placeholder={tx("Description (Markdown)")}
className={styles.textarea}
rows={10}
value={body}
@@ -188,7 +188,7 @@ function ReviewDialog(props: {
/>
<textarea
data-autofocus
aria-label="Review comment"
aria-label={tx("Review comment")}
placeholder={needsBody ? "Leave a comment (required)" : "Leave a comment (optional)"}
className={styles.textarea}
rows={8}
+1 -1
View File
@@ -44,7 +44,7 @@ describe("pull request model", () => {
const c = (status: PrCheck["status"]): PrCheck => ({ name: status, workflow: null, status, url: null });
const s = checksSummary([c("failure"), c("success"), c("success"), c("pending"), c("skipped")]);
expect(s).toEqual({ failed: 1, pending: 1, passed: 2, other: 1 });
expect(checksText(s)).toBe("1 failing, 1 pending, 2 successful, 1 skipped");
expect(checksText(s)).toBe("1 failing, 1 pending, 2 passed, 1 skipped");
expect(checksText(checksSummary([]))).toBe("No checks");
});
+27 -25
View File
@@ -1,20 +1,22 @@
/** Pure helpers of the Pull Requests view (labels, check summaries, review threads, diff inputs). */
import type { ChangedFile, PrCheck, PrDiffRevisions, PrFile, PrReviewComment, PullRequestDetails, PullRequestSummary } from "@/platform/ipc";
import { t, tx } from "@/platform/i18n";
/** Keyed, not source text: "Open" as a state reads differently from the "Open" action in some locales. */
export function stateLabel(pr: PullRequestSummary): string {
if (pr.state === "merged") return "Merged";
if (pr.state === "closed") return "Closed";
return pr.isDraft ? "Draft" : "Open";
if (pr.state === "merged") return t("github.prState.merged");
if (pr.state === "closed") return t("github.prState.closed");
return pr.isDraft ? t("github.prState.draft") : t("github.prState.open");
}
export function reviewDecisionLabel(decision: string | null): string | null {
switch (decision) {
case "APPROVED":
return "Approved";
return tx("Approved");
case "CHANGES_REQUESTED":
return "Changes requested";
return tx("Changes requested");
case "REVIEW_REQUIRED":
return "Review required";
return tx("Review required");
default:
return null;
}
@@ -23,15 +25,15 @@ export function reviewDecisionLabel(decision: string | null): string | null {
export function reviewStateLabel(state: string): string {
switch (state) {
case "APPROVED":
return "approved";
return tx("approved");
case "CHANGES_REQUESTED":
return "requested changes";
return tx("requested changes");
case "COMMENTED":
return "reviewed";
return tx("reviewed");
case "DISMISSED":
return "review dismissed";
return tx("review dismissed");
case "REQUESTED":
return "review requested";
return tx("review requested");
default:
return state.toLowerCase().replace(/_/g, " ");
}
@@ -58,11 +60,11 @@ export function checksSummary(checks: readonly PrCheck[]): ChecksSummary {
export function checksText(s: ChecksSummary): string {
const parts: string[] = [];
if (s.failed) parts.push(`${s.failed} failing`);
if (s.pending) parts.push(`${s.pending} pending`);
if (s.passed) parts.push(`${s.passed} successful`);
if (s.other) parts.push(`${s.other} skipped`);
return parts.length ? parts.join(", ") : "No checks";
if (s.failed) parts.push(tx`${s.failed} failing`);
if (s.pending) parts.push(tx`${s.pending} pending`);
if (s.passed) parts.push(tx`${s.passed} passed`);
if (s.other) parts.push(tx`${s.other} skipped`);
return parts.length ? parts.join(", ") : tx`No checks`;
}
/** "3 minutes ago"-style text for an ISO date. */
@@ -70,18 +72,18 @@ export function relativeTime(iso: string, now = Date.now()): string {
const t = Date.parse(iso);
if (Number.isNaN(t)) return "";
const s = Math.max(0, Math.round((now - t) / 1000));
const units: Array<[number, string]> = [
[60 * 60 * 24 * 365, "year"],
[60 * 60 * 24 * 30, "month"],
[60 * 60 * 24, "day"],
[60 * 60, "hour"],
[60, "minute"],
const units: Array<[number, (n: number) => string]> = [
[60 * 60 * 24 * 365, (n) => (n === 1 ? tx`${n} year ago` : tx`${n} years ago`)],
[60 * 60 * 24 * 30, (n) => (n === 1 ? tx`${n} month ago` : tx`${n} months ago`)],
[60 * 60 * 24, (n) => (n === 1 ? tx`${n} day ago` : tx`${n} days ago`)],
[60 * 60, (n) => (n === 1 ? tx`${n} hour ago` : tx`${n} hours ago`)],
[60, (n) => (n === 1 ? tx`${n} minute ago` : tx`${n} minutes ago`)],
];
for (const [size, name] of units) {
for (const [size, text] of units) {
const n = Math.floor(s / size);
if (n >= 1) return `${n} ${name}${n === 1 ? "" : "s"} ago`;
if (n >= 1) return text(n);
}
return "just now";
return tx("just now");
}
export interface ReviewThread {
+4 -4
View File
@@ -94,10 +94,10 @@ function ExchangeView({ ex }: { ex: HttpExchange }) {
<pre className={styles.pre}>{ex.entry.error}</pre>
) : body == null ? (
<EmptyState title="Binary response">
{r.contentType ?? "unknown type"}, {formatSize(r.size)}
{r.contentType ?? tx("unknown type")}, {formatSize(r.size)}
</EmptyState>
) : (
<pre className={styles.pre} aria-label="Response body">
<pre className={styles.pre} aria-label={tx("Response body")}>
{body}
{r.truncated && (
<span className={styles.truncated}>{`\n… truncated (${formatSize(r.size)} received)`}</span>
@@ -143,10 +143,10 @@ export function HttpClientView() {
return (
<SplitGroup orientation="horizontal" className={styles.root}>
<SplitPanel id="http-history" defaultSize="32%" minSize="160px">
<div className={styles.list} role="list" aria-label="Response history">
<div className={styles.list} role="list" aria-label={tx("Response history")}>
{running && (
<div className={styles.runningRow}>
<Spinner size={12} /> Sending request #{running.index + 1}…
<Spinner size={12} /> {tx`Sending request #${running.index + 1}…`}
</div>
)}
{history.length === 0 && !running ? (
+1 -1
View File
@@ -98,7 +98,7 @@ function EditMacrosDialog({ onClose }: { onClose: () => void }) {
</Button>
</div>
</div>
<ol className={styles.steps} aria-label="Recorded steps">
<ol className={styles.steps} aria-label={tx("Recorded steps")}>
{macro?.steps.map((step, i) => (
<li key={i} className={styles.step}>
{describeStep(step, actionTitle)}
+1 -1
View File
@@ -89,7 +89,7 @@ export function MemoryDetails({ metrics: m }: { metrics: AppMetrics }) {
const sections = memorySections(m.memory ?? []);
return (
<div className={styles.panel}>
<div className={styles.header}>Memory &amp; Diagnostics</div>
<div className={styles.header}>{tx("Memory & Diagnostics")}</div>
{sections.map((sec) => (
<div key={sec.title} className={styles.section} aria-label={sec.title} role="group">
<div className={styles.groupRow}>
+3 -2
View File
@@ -7,6 +7,7 @@ import { FRAME_BUDGET_MS, keystrokeLatencies, supportsEntryType, type EventEntry
import { RollingWindow, type Summary } from "./stats";
import { refreshMetrics, usePerf } from "./store";
import styles from "./perf.module.css";
import { tx } from "@/platform/i18n";
const IPC_PING_MS = 2000;
const REPAINT_MS = 500;
@@ -70,7 +71,7 @@ function PerfPanel({ onClose }: { onClose: () => void }) {
}, [eventTiming, longTasks]);
return (
<section className={styles.overlay} aria-label="Performance overlay (dev diagnostics)">
<section className={styles.overlay} aria-label={tx("Performance overlay (dev diagnostics)")}>
<div className={styles.overlayHeader}>
<span className={styles.overlayTitle}>
<T>Performance (dev diagnostics)</T>
@@ -109,7 +110,7 @@ function Block({ title, summary, budget, note }: { title: string; summary?: Summ
<span className={styles.value}>{s ? [s.last, s.p50, s.p95, s.max].map(formatMs).join(" · ") : "–"}</span>
</div>
<div className={styles.row}>
<span className={styles.label}>Over budget ({budget})</span>
<span className={styles.label}>{tx`Over budget (${budget})`}</span>
<span className={s && s.overBudget > 0 ? `${styles.value} ${styles.warn}` : styles.value}>
{s ? s.overBudget : "–"}
</span>
+15 -11
View File
@@ -124,7 +124,11 @@ function InstalledRow({ plugin, selected, onSelect }: { plugin: PluginInfo; sele
<span className={styles.rowMeta}>
<span className={styles.rowVendor}>{vendorOf(plugin.authors) || plugin.id}</span>
<span>{plugin.version}</span>
{plugin.source === "dev" && <span className={styles.devTag}>dev</span>}
{plugin.source === "dev" && (
<span className={styles.devTag}>
<T>dev</T>
</span>
)}
{plugin.status === "failed" && (
<Badge dot tone="danger">
<T>Failed</T>
@@ -149,7 +153,7 @@ function CatalogButton({ entry, compact }: { entry: CatalogPlugin; compact?: boo
const installing = usePlugins((s) => s.installing);
const state = catalogState(entry, plugins);
if (state === "installed" || state === "dev") {
return <span className={styles.installedTag}>{state === "dev" ? "Dev plugin loaded" : "Installed"}</span>;
return <span className={styles.installedTag}>{state === "dev" ? tx("Dev plugin loaded") : tx("Installed")}</span>;
}
const label = state === "update" ? "Update" : "Install";
return (
@@ -261,7 +265,7 @@ function InstalledDetails({ plugin }: { plugin: PluginInfo }) {
<dt>
<T>Code</T>
</dt>
<dd>{plugin.hasCode ? "WebAssembly component (sandboxed)" : "None — contributions only"}</dd>
<dd>{plugin.hasCode ? tx("WebAssembly component (sandboxed)") : tx("None — contributions only")}</dd>
{plugin.homepage && (
<>
<dt>
@@ -300,7 +304,7 @@ function InstalledDetails({ plugin }: { plugin: PluginInfo }) {
<li key={c.id} className={styles.item}>
<span className={styles.itemMain}>{c.title}</span>
<Kbd action={actionId} />
<span className={styles.mono} title="Action id (bind it in Keymap)">
<span className={styles.mono} title={tx("Action id (bind it in Keymap)")}>
{actionId}
</span>
<Button
@@ -328,7 +332,7 @@ function InstalledDetails({ plugin }: { plugin: PluginInfo }) {
return (
<li key={t.id} className={styles.item}>
<span className={styles.itemMain}>{t.name}</span>
<span className={styles.muted}>{t.kind === "dark" ? "Dark" : "Light"}</span>
<span className={styles.muted}>{t.kind === "dark" ? tx("Dark") : tx("Light")}</span>
{isActive ? (
<Button size="small" variant="secondary" onClick={() => void selectPluginTheme(null, null)}>
<T>Use Default Theme</T>
@@ -456,7 +460,7 @@ function CatalogDetails({ entry }: { entry: CatalogPlugin }) {
<dt>
<T>Code</T>
</dt>
<dd>{entry.hasCode ? "WebAssembly component (sandboxed)" : "None — contributions only"}</dd>
<dd>{entry.hasCode ? tx("WebAssembly component (sandboxed)") : tx("None — contributions only")}</dd>
{contributes.length > 0 && (
<>
<dt>
@@ -602,7 +606,7 @@ export function PluginsPage() {
aria-label="Filter plugins"
/>
{tab === "installed" ? (
<ul className={styles.list} aria-label="Installed plugins">
<ul className={styles.list} aria-label={tx("Installed plugins")}>
{downloaded.length > 0 && <ListHeader title="Downloaded" count={downloaded.length} />}
{downloaded.map((p) => (
<InstalledRow
@@ -623,15 +627,15 @@ export function PluginsPage() {
))}
{loaded && shown.length === 0 && (
<li className={styles.listEmpty}>
{plugins.length === 0 ? "No plugins installed." : "No plugins match."}
{plugins.length === 0 ? tx("No plugins installed.") : tx("No plugins match.")}
</li>
)}
</ul>
) : (
<ul className={styles.list} aria-label="Available plugins">
<ul className={styles.list} aria-label={tx("Available plugins")}>
<li className={styles.notice} role="note">
<Icon name="info" size={14} />
<span>{NO_REPOSITORY}. Showing plugin packages available on this machine.</span>
<span>{tx`${NO_REPOSITORY}. Showing plugin packages available on this machine.`}</span>
</li>
{catalogShown.length > 0 && <ListHeader title="Local" count={catalogShown.length} />}
{catalogShown.map((c) => (
@@ -644,7 +648,7 @@ export function PluginsPage() {
))}
{catalog && catalogShown.length === 0 && (
<li className={styles.listEmpty}>
{catalog.length === 0 ? "No local plugin packages." : "No plugins match."}
{catalog.length === 0 ? tx("No local plugin packages.") : tx("No plugins match.")}
</li>
)}
{catalogError && <li className={clsx(styles.listEmpty, styles.errorText)}>{catalogError}</li>}
+3 -2
View File
@@ -11,6 +11,7 @@ import { editorService } from "@/workbench/contributions";
import { colorSlot, flameLayout, formatWeight, frameLocation, matchingFrames, share } from "./model";
import { activeSnapshot, closeSnapshot, useProfiler, type ProfilerTab } from "./store";
import styles from "./Profiler.module.css";
import { tx } from "@/platform/i18n";
const ROW = 18;
@@ -67,11 +68,11 @@ function FlameGraph({ profile, query }: { profile: Profile; query: string }) {
{focus !== 0 && (
<div className={styles.flameBar}>
<IconButton icon="close" size="small" title="Reset Zoom" onClick={() => setFocus(0)} />
<span className={styles.muted}>Zoomed into {profile.frames[profile.nodes[focus]!.frame]!.name}</span>
<span className={styles.muted}>{tx`Zoomed into ${profile.frames[profile.nodes[focus]!.frame]!.name}`}</span>
</div>
)}
<div className={styles.flameScroll} ref={ref}>
<div className={styles.flame} style={{ height: layout.depth * ROW }} role="tree" aria-label="Flame graph">
<div className={styles.flame} style={{ height: layout.depth * ROW }} role="tree" aria-label={tx("Flame graph")}>
{width > 0 &&
layout.rects.map((r) => {
const node = profile.nodes[r.node]!;
@@ -20,6 +20,7 @@ import {
} from "./paths";
import { useProjectTree } from "./store";
import styles from "./FileRefactor.module.css";
import { tx } from "@/platform/i18n";
export type FileRefactorRequest =
| {
@@ -220,7 +221,9 @@ function RenameDialog({ request }: { request: Extract<FileRefactorRequest, { kin
}}
>
<label className={styles.fieldLabel} htmlFor="file-rename-name">
Rename {request.dir ? "directory" : "file"} '{current}' and its usages to:
{request.dir
? tx`Rename directory '${current}' and its usages to:`
: tx`Rename file '${current}' and its usages to:`}
</label>
<Input
id="file-rename-name"
@@ -307,7 +310,7 @@ function MoveDialog({ request }: { request: Extract<FileRefactorRequest, { kind:
aria-label="Target directory"
onChange={(e) => setTarget(e.target.value)}
/>
<div className={styles.hint}>{relativeTo(root, dest) || (dest === root ? "Project root" : dest)}</div>
<div className={styles.hint}>{relativeTo(root, dest) || (dest === root ? tx("Project root") : dest)}</div>
{error && (
<div className={styles.error} role="alert">
{error}
@@ -418,7 +421,7 @@ function CopyDialog({ request }: { request: Extract<FileRefactorRequest, { kind:
aria-label="Target directory"
onChange={(e) => setTarget(e.target.value)}
/>
<div className={styles.hint}>{relativeTo(root, dest) || (dest === root ? "Project root" : dest)}</div>
<div className={styles.hint}>{relativeTo(root, dest) || (dest === root ? tx("Project root") : dest)}</div>
{error && (
<div className={styles.error} role="alert">
{error}
@@ -132,10 +132,9 @@ function PreviewDialog({ request }: { request: PreviewRequest }) {
]}
>
<div className={styles.previewSummary}>
References to update: {occurrences} {occurrences === 1 ? "occurrence" : "occurrences"} in {kept.length}{" "}
{kept.length === 1 ? "file" : "files"}
{tx`References to update: ${occurrences === 1 ? tx`${occurrences} occurrence` : tx`${occurrences} occurrences`} in ${kept.length === 1 ? tx`${kept.length} file` : tx`${kept.length} files`}`}
</div>
<ul className={styles.previewList} aria-label="Changes">
<ul className={styles.previewList} aria-label={tx("Changes")}>
{files.map((f) => (
<FileGroup
key={f.path}
+10 -4
View File
@@ -238,7 +238,7 @@ function ConfigChooser() {
return (
<span className={clsx(styles.chooserRow, styles.chooserEdit)}>
<Icon name="check" size={13} />
<span>Save &apos;{r.config.name}&apos; Configuration</span>
<span>{tx`Save '${r.config.name}' Configuration`}</span>
</span>
);
}
@@ -277,9 +277,15 @@ function ConfigChooser() {
{r.indices ? <HighlightedText text={c.name} indices={r.indices} /> : c.name}
</span>
{c.target && <span className={styles.chooserMuted}>@ {targetName(c, runTargets)}</span>}
{c.isTest && <span className={styles.badge}>test</span>}
{!c.detected && <span className={styles.chooserMuted}>user</span>}
{temporary.includes(c.id) && <span className={styles.chooserMuted}>temporary</span>}
{c.isTest && <span className={styles.badge}>
<T>test</T>
</span>}
{!c.detected && <span className={styles.chooserMuted}>
<T>user</T>
</span>}
{temporary.includes(c.id) && <span className={styles.chooserMuted}>
<T>temporary</T>
</span>}
{isActive && (
<span className={styles.chooserActions}>
<IconButton
+4 -3
View File
@@ -19,6 +19,7 @@ import { findOutputLinks, type OutputLink } from "./links";
import { openTarget, openUrl } from "./navigate";
import { segmentLine } from "./segments";
import styles from "./Run.module.css";
import { tx } from "@/platform/i18n";
const ROW = 20;
@@ -248,12 +249,12 @@ export function ConsoleView({ model, version, softWrap, follow, onFollowChange,
);
const foldButton = (fold: Fold, attached: boolean) =>
fold.expanded ? (
<button type="button" className={styles.foldToggle} title="Collapse folded lines" aria-label="Collapse folded lines" onClick={() => toggleFold(fold.id)}>
<button type="button" className={styles.foldToggle} title={tx("Collapse folded lines")} aria-label={tx("Collapse folded lines")} onClick={() => toggleFold(fold.id)}>
<Icon name="chevron-up" size={10} />
</button>
) : (
<button type="button" className={styles.fold} title="Expand folded lines" onClick={() => toggleFold(fold.id)}>
{`${attached ? " " : ""}<${fold.count} internal line${fold.count === 1 ? "" : "s"}>`}
<button type="button" className={styles.fold} title={tx("Expand folded lines")} onClick={() => toggleFold(fold.id)}>
{`${attached ? " " : ""}${fold.count === 1 ? tx`<${fold.count} internal line>` : tx`<${fold.count} internal lines>`}`}
</button>
);
+4 -3
View File
@@ -29,6 +29,7 @@ import { targetName } from "./remote";
import { saveUserConfigs, selectConfig, useRun } from "./store";
import { blankTemplate, COMPOUND, missingParams, newConfigOfType, templateId, typeFor } from "./types";
import styles from "./Run.module.css";
import { tx } from "@/platform/i18n";
export { envFromRows } from "./EnvDialog";
@@ -364,7 +365,7 @@ function EditConfigsDialog() {
<div className={styles.editSidebar}>
{mode === "configs" ? (
<>
<div className={styles.editToolbar} role="toolbar" aria-label="Configurations">
<div className={styles.editToolbar} role="toolbar" aria-label={tx("Configurations")}>
<IconButton
ref={addRef}
icon="plus"
@@ -420,7 +421,7 @@ function EditConfigsDialog() {
onClick={() => move(1)}
/>
</div>
<div className={styles.editList} role="tree" aria-label="Run configurations">
<div className={styles.editList} role="tree" aria-label={tx("Run configurations")}>
{rows.map((r) => {
if (r.type === "group")
return (
@@ -522,7 +523,7 @@ function EditConfigsDialog() {
<T>Templates</T>
</span>
</div>
<div className={styles.editList} role="listbox" aria-label="Configuration templates">
<div className={styles.editList} role="listbox" aria-label={tx("Configuration templates")}>
{templateTypes.map((t) => {
const sel = selection?.type === "template" && selection.kind === t.id;
return (
+2 -2
View File
@@ -69,7 +69,7 @@ function SessionPanel({ session }: { session: RunSession }) {
const consoleColumn = (
<div className={styles.consoleColumn}>
{rt.console.dropped > 0 && !testModel && (
<div className={styles.notice}>{rt.console.dropped.toLocaleString()} earlier lines were discarded</div>
<div className={styles.notice}>{tx`${rt.console.dropped.toLocaleString()} earlier lines were discarded`}</div>
)}
{testModel && (
<div className={styles.notice}>
@@ -113,7 +113,7 @@ function SessionPanel({ session }: { session: RunSession }) {
return (
<div className={styles.session}>
<div className={styles.toolbar} role="toolbar" aria-orientation="vertical" aria-label="Run actions">
<div className={styles.toolbar} role="toolbar" aria-orientation="vertical" aria-label={tx("Run actions")}>
<IconButton
icon="rerun"
iconSize={14}
+5 -5
View File
@@ -179,10 +179,10 @@ export function TestTreeView({ results, version, cwd, selected, onSelect, runnin
<div className={styles.testHeader}>
<StatusIcon status={status} />
<span className={styles.testCounts}>
{counts.failed > 0 && <span className={styles.failed}>{counts.failed} failed</span>}
{counts.passed > 0 && <span>{counts.passed} passed</span>}
{counts.skipped > 0 && <span className={styles.skipped}>{counts.skipped} ignored</span>}
{counts.total === 0 && <span className={styles.skipped}>{running ? "Running…" : "No tests"}</span>}
{counts.failed > 0 && <span className={styles.failed}>{tx`${counts.failed} failing`}</span>}
{counts.passed > 0 && <span>{tx`${counts.passed} passed`}</span>}
{counts.skipped > 0 && <span className={styles.skipped}>{tx`${counts.skipped} ignored`}</span>}
{counts.total === 0 && <span className={styles.skipped}>{running ? tx("Running…") : tx("No tests")}</span>}
{duration !== undefined && <span className={styles.skipped}>{formatDuration(duration)}</span>}
</span>
<span className={styles.spacer} />
@@ -235,7 +235,7 @@ export function TestTreeView({ results, version, cwd, selected, onSelect, runnin
}}
empty={
<div className={styles.testEmpty}>
{running ? "Waiting for test results…" : results.isEmpty ? "No test results" : "Passed tests are hidden"}
{running ? tx("Waiting for test results…") : results.isEmpty ? tx("No test results") : tx("Passed tests are hidden")}
</div>
}
/>
@@ -186,7 +186,7 @@ export function BookmarksView() {
if (!items.length && !paths.length && lists.length === 1) {
return (
<EmptyState>
No bookmarks yet. Toggle a line bookmark with {shortcutLabel("navigate.toggleBookmark") ?? "F3"}
{tx`No bookmarks yet. Toggle a line bookmark with ${shortcutLabel("navigate.toggleBookmark") ?? "F3"}`}
</EmptyState>
);
}
+6 -6
View File
@@ -3,7 +3,7 @@ import clsx from "clsx";
import { openEditorPaths, useEditorRegistry } from "@/editor/api";
import { executeAction } from "@/platform/actions";
import { setContext } from "@/platform/context";
import { useTxTemplate } from "@/platform/i18n";
import { useTxTemplate, tx } from "@/platform/i18n";
import { call, commands, type SearchScope } from "@/platform/ipc";
import { useProject } from "@/platform/project";
import { useProjectModules } from "@/platform/projectRoots";
@@ -477,7 +477,7 @@ function FindInFilesDialog() {
>
<div className={styles.root} onKeyDown={onKeyDown}>
<header className={styles.header}>
<span className={styles.title}>{s.replace ? "Replace in Files" : "Find in Files"}</span>
<span className={styles.title}>{s.replace ? tx("Replace in Files") : tx("Find in Files")}</span>
<span className={styles.status} aria-live="polite">
{state.status === "searching" || state.status === "validating" ? <Spinner size={12} /> : null}
{summary}
@@ -490,7 +490,7 @@ function FindInFilesDialog() {
className={styles.maskInput}
value={s.mask}
placeholder="*.kt, !*Test*"
aria-label="File mask"
aria-label={tx("File mask")}
spellCheck={false}
onChange={(e) => set({ mask: e.target.value, maskEnabled: true })}
onBlur={() => s.maskEnabled && rememberMask(s.mask)}
@@ -604,8 +604,8 @@ function FindInFilesDialog() {
<input
className={styles.dirInput}
value={displayDirectory(s.directory, root)}
placeholder="Directory (relative to project root)"
aria-label="Directory"
placeholder={tx("Directory (relative to project root)")}
aria-label={tx("Directory")}
spellCheck={false}
// Keep the raw text (resolved only when searching), or a typed "/" would be stripped.
onChange={(e) => set({ directory: e.target.value })}
@@ -746,7 +746,7 @@ function FindInFilesDialog() {
<span className={styles.spacer} />
{s.replace && progress && (
<span className={styles.hint} aria-live="polite">
Replacing… {progress.done}/{progress.total} files
{tx`Replacing… ${progress.done}/${progress.total} files`}
</span>
)}
{s.replace && running && (
+6 -6
View File
@@ -52,6 +52,7 @@ import {
type FindTab,
} from "./windowStore";
import styles from "./FindToolWindow.module.css";
import { tx } from "@/platform/i18n";
type Node =
| { kind: "dir"; key: string; label: string; count: number; children: Node[] }
@@ -199,7 +200,7 @@ export function FindToolWindow() {
if (!tab) {
return (
<EmptyState title="No search results" icon="search">
Use Find in Files ({shortcutLabel("find.inPath") ?? "⇧⌘F"}) and choose “Open in Find Window”.
{tx`Use Find in Files (${shortcutLabel("find.inPath") ?? "⇧⌘F"}) and choose “Open in Find Window”.`}
</EmptyState>
);
}
@@ -367,12 +368,11 @@ function FindTabView({ tab }: { tab: FindTab }) {
<span className={styles.title}>{tab.title}</span>
<span className={styles.count}>
{st.status === "invalid" || st.status === "error"
? (st.error ?? "Search failed")
? (st.error ?? tx("Search failed"))
: progress
? `Replacing… ${progress.done}/${progress.total} files`
: `${st.matchCount}${st.summary?.limitHit ? "+" : ""} matches in ${st.files.length} files${
marks.excluded.size ? ` · ${marks.excluded.size} excluded` : ""
}`}
? tx`Replacing… ${progress.done}/${progress.total} files`
: tx`${`${st.matchCount}${st.summary?.limitHit ? "+" : ""}`} matches in ${st.files.length} files` +
(marks.excluded.size ? ` · ${tx`${marks.excluded.size} excluded`}` : "")}
</span>
<span className={styles.spacer} />
<Button size="small" variant="ghost" disabled={!selection.size} onClick={exclude} title="Exclude / include (⌫)">
+3 -2
View File
@@ -9,6 +9,7 @@ import { loadPreviewText } from "./preview";
import type { PreviewLine } from "./replaceFlow";
import styles from "./parts.module.css";
import { T } from "@/ui";
import { tx } from "@/platform/i18n";
/** A result line with matches highlighted; in replace mode shows `old → new` inline. */
export function MatchText({
@@ -169,7 +170,7 @@ export function CodePreview({
const placeholder = !path ? <T>Select a result to preview</T> : error && !current ? error : null;
return (
<div className={styles.preview} role="region" aria-label="Preview">
<div className={styles.preview} role="region" aria-label={tx("Preview")}>
<div ref={hostRef} className={clsx(styles.previewEditor, (!current || placeholder) && styles.previewHidden)} />
{placeholder && <div className={styles.previewEmpty}>{placeholder}</div>}
</div>
@@ -189,7 +190,7 @@ export function ReplaceDiffPreview({ lines, line }: { lines: readonly PreviewLin
const from = Math.max(0, focus - WINDOW);
const shown = lines.slice(from, Math.max(focus, 0) + WINDOW);
return (
<div ref={scrollRef} className={styles.preview} role="region" aria-label="Replace preview">
<div ref={scrollRef} className={styles.preview} role="region" aria-label={tx("Replace preview")}>
<pre className={styles.pre}>
{shown.map((l, i) => (
<div
+14 -11
View File
@@ -33,6 +33,7 @@ import {
} from "./model";
import { cycleSwitcher, hideRecentPopup, showRecentPopup, useRecentPopup } from "./store";
import styles from "./RecentPopups.module.css";
import { tx } from "@/platform/i18n";
export function RecentPopupsOverlay() {
const open = useRecentPopup((s) => s.open);
@@ -233,7 +234,7 @@ function RecentFilesDialog() {
value={query}
spellCheck={false}
autoComplete="off"
aria-label="Filter recent files"
aria-label={tx("Filter recent files")}
onChange={(e) => {
setQuery(e.target.value);
setFileIndex(null);
@@ -248,7 +249,7 @@ function RecentFilesDialog() {
showRecentPopup("files");
inputRef.current?.focus();
}}
label={<>Show edited only {recentShortcut && <span className={styles.hint}>{recentShortcut}</span>}</>}
label={<><T>Show edited only</T> {recentShortcut && <span className={styles.hint}>{recentShortcut}</span>}</>}
/>
</>
)}
@@ -304,12 +305,12 @@ function RecentFilesDialog() {
paths && (
<span>
{switcher
? "No open files"
? tx("No open files")
: editedOnly
? "No edited files"
? tx("No edited files")
: query
? "No matching files"
: "No recent files"}
? tx("No matching files")
: tx("No recent files")}
</span>
)
}
@@ -506,7 +507,7 @@ function RecentLocationsDialog() {
return (
<Dialog open onClose={hideRecentPopup} width={720} aria-label="Recent Locations" className={styles.dialog}>
<header className={styles.header}>
<span className={styles.title}>{changedOnly ? "Recently Changed Locations" : "Recent Locations"}</span>
<span className={styles.title}>{changedOnly ? tx("Recently Changed Locations") : tx("Recent Locations")}</span>
<span className={clsx(styles.speed, styles.speedWide, query && styles.speedActive)}>
<Icon name="search" size={14} className={styles.filterIcon} />
<input
@@ -514,10 +515,10 @@ function RecentLocationsDialog() {
data-autofocus
className={styles.filter}
value={query}
placeholder="Type to filter by file name or code"
placeholder={tx("Type to filter by file name or code")}
spellCheck={false}
autoComplete="off"
aria-label="Filter recent locations"
aria-label={tx("Filter recent locations")}
onChange={(e) => {
setQuery(e.target.value);
setIndex(0);
@@ -552,7 +553,7 @@ function RecentLocationsDialog() {
focusable={false}
rowHeight={LOCATION_ROW}
aria-label="Recent locations"
empty={<span>{changedOnly ? "No changed locations" : "No recent locations"}</span>}
empty={<span>{changedOnly ? tx("No changed locations") : tx("No recent locations")}</span>}
renderItem={(l) => (
<div className={styles.location}>
<div className={styles.locationHeader}>
@@ -564,7 +565,9 @@ function RecentLocationsDialog() {
tone="strong"
/>
<span className={styles.dir}>{breadcrumbOf(outlines.get(l.path), l) || l.rel}</span>
{l.changed && <span className={styles.changed}>changed</span>}
{l.changed && <span className={styles.changed}>
<T>changed</T>
</span>}
<span className={styles.time}>{locationTime(l.time, now)}</span>
</div>
<SnippetCode path={l.path} lines={dedent(l.snippet)} first={l.snippetStart} caret={l.line} />
@@ -36,6 +36,7 @@ import {
} from "./outline";
import { useLiveOutline, useOutline, useStructureOptions } from "./store";
import styles from "./StructureView.module.css";
import { tx } from "@/platform/i18n";
function useOutlineNodes() {
const items = useOutline((s) => s.items);
@@ -228,7 +229,7 @@ function StructureToolbar() {
const narrowed = useStructureOptions((s) => !s.showFields || !s.showNonPublic || s.sortByVisibility);
const hasOptions = viewOptionItems(items ?? []).length > 0;
return (
<div className={styles.toolbar} role="toolbar" aria-label="Structure actions">
<div className={styles.toolbar} role="toolbar" aria-label={tx("Structure actions")}>
<IconButton
icon="sort"
title="Sort Alphabetically"
@@ -417,7 +418,7 @@ function FileStructureDialog() {
aria-label="File Structure"
title={
<span className={styles.popupTitle}>
{path ? basename(path) : "File Structure"}
{path ? basename(path) : tx("File Structure")}
<span className={styles.popupHint}>{shortcutLabel("navigate.fileStructure")}</span>
</span>
}
@@ -428,7 +429,7 @@ function FileStructureDialog() {
className={styles.filter}
value={query}
placeholder={path ? "Type to filter symbols" : "No active editor"}
aria-label="Filter symbols"
aria-label={tx("Filter symbols")}
spellCheck={false}
autoComplete="off"
onChange={(e) => {
@@ -490,7 +491,7 @@ function FileStructureDialog() {
onActivate={(n) => choose(n)}
rowHeight={POPUP_ROW}
indent={16}
empty={<span className={styles.popupEmpty}>{!path ? "Open a file first" : loading && !items ? "Loading…" : "No symbols"}</span>}
empty={<span className={styles.popupEmpty}>{!path ? tx("Open a file first") : loading && !items ? "Loading…" : "No symbols"}</span>}
renderNode={(n, s) => {
const matched = filter.matches.get(n.key);
return <SymbolRow node={n} indices={matched} selected={s.selected} dim={!!query.trim() && !matched} />;
+2 -2
View File
@@ -6,7 +6,7 @@
import { useEffect, useMemo, useRef } from "react";
import clsx from "clsx";
import { setContext } from "@/platform/context";
import { useTxTemplate } from "@/platform/i18n";
import { useTxTemplate, tx } from "@/platform/i18n";
import { shortcutLabel } from "@/platform/keymap";
import {
EmptyState,
@@ -93,7 +93,7 @@ export function ServicesTreeView() {
{providers.map((p) => (
<ProviderRoots key={p.id} provider={p} />
))}
<div className={styles.toolbar} role="toolbar" aria-label="Services actions">
<div className={styles.toolbar} role="toolbar" aria-label={tx("Services actions")}>
<IconButton
ref={addRef}
data-services-add
+3 -2
View File
@@ -250,8 +250,9 @@ function Unavailable({ availability, conn }: { availability: DockerAvailability;
>
<div>{availability.message}</div>
<div className={styles.hint}>
Start Docker{availability.context ? ` (context '${availability.context}')` : ""}; this view connects
automatically.
{availability.context
? tx`Start Docker (context '${availability.context}'); this view connects automatically.`
: tx`Start Docker; this view connects automatically.`}
</div>
</EmptyState>
);
+2 -2
View File
@@ -225,7 +225,7 @@ export function ColorSwatches({ value, presets, onChange, allowCustom = true, ..
<>
<label
className={clsx(styles.swatch, styles.custom, !isPreset && styles.swatchOn)}
title="Custom colour"
title={tx("Custom colour")}
aria-checked={!isPreset}
role="radio"
>
@@ -233,7 +233,7 @@ export function ColorSwatches({ value, presets, onChange, allowCustom = true, ..
<input
type="color"
className={styles.colorNative}
aria-label="Custom colour"
aria-label={tx("Custom colour")}
value={HEX_COLOR.test(value) && value.length === 7 ? value : "#808080"}
onChange={(e) => onChange(e.target.value)}
/>
@@ -350,7 +350,7 @@ function SettingsStatus() {
<span className={styles.footerStatus} role="status">
{error && (
<span className={styles.error}>
<Icon name="error" size={14} /> Could not save: {error}
<Icon name="error" size={14} /> {tx`Could not save: ${error}`}
</span>
)}
</span>
+1 -1
View File
@@ -295,7 +295,7 @@ export function KeymapPage() {
<span className={styles.spacer} />
{overrideCount > 0 && (
<span className={styles.modifiedNote}>
{overrideCount} modified {overrideCount === 1 ? "action" : "actions"}
{overrideCount === 1 ? tx`${overrideCount} modified action` : tx`${overrideCount} modified actions`}
</span>
)}
</div>
@@ -8,6 +8,7 @@ import { Dialog, IconButton, Input, Select, Spinner, type SelectOption, T } from
import { choosePath, createVenv, rememberSdk } from "./api";
import { tildify, useDetected } from "./components";
import styles from "./Sdk.module.css";
import { tx } from "@/platform/i18n";
type Mode = "new" | "existing";
@@ -80,7 +81,7 @@ export function AddInterpreterDialog({
]}
>
<div className={styles.dialogBody}>
<ul className={styles.kinds} aria-label="Interpreter type">
<ul className={styles.kinds} aria-label={tx("Interpreter type")}>
<li>
<button type="button" className={`${styles.kind} ${styles.kindOn}`} aria-current="true">
<T>Virtualenv Environment</T>
@@ -88,14 +89,14 @@ export function AddInterpreterDialog({
</li>
</ul>
<div className={styles.form}>
<div className={styles.radios} role="radiogroup" aria-label="Environment">
<div className={styles.radios} role="radiogroup" aria-label={tx("Environment")}>
<label className={styles.radio}>
<input type="radio" name="env" checked={mode === "new"} onChange={() => setMode("new")} />{" "}
<T>New environment</T>
</label>
<label className={styles.radio}>
<input type="radio" name="env" checked={mode === "existing"} onChange={() => setMode("existing")} />{" "}
Existing environment
<T>Existing environment</T>
</label>
</div>
{mode === "new" ? (
@@ -143,7 +144,7 @@ export function AddInterpreterDialog({
/>
</div>
<div className={styles.note}>
Runs “&lt;base&gt; -m venv &lt;location&gt;”; the new interpreter becomes the project interpreter.
{tx("Runs “<base> -m venv <location>”; the new interpreter becomes the project interpreter.")}
</div>
</>
) : (
@@ -8,6 +8,7 @@ import { downloadJdk, TEMURIN, type JdkDownload } from "@/platform/jdk";
import { Dialog, IconButton, Input, ProgressBar, Select, type SelectOption, T } from "@/ui";
import { choosePath, rememberSdk } from "./api";
import styles from "./Sdk.module.css";
import { tx } from "@/platform/i18n";
const errorText = (e: unknown) => (e instanceof Error ? e.message : String(e));
@@ -148,8 +149,7 @@ export function DownloadJdkDialog({
</div>
</div>
<div className={styles.note}>
{vendorName} {version ?? ""} is unpacked into a folder of its own in {location || "…"}, where JDK detection
finds it.
{tx`${vendorName} ${version ?? ""} is unpacked into a folder of its own in ${location || "…"}, where JDK detection finds it.`}
</div>
{progress && (
<div className={styles.downloadProgress}>
@@ -236,7 +236,7 @@ export function ProjectStructurePage() {
<div className={styles.page}>
<div className={styles.structure}>
<div className={styles.structureTree}>
<div className={styles.markBar} role="toolbar" aria-label="Mark as">
<div className={styles.markBar} role="toolbar" aria-label={tx("Mark as")}>
<span>
<T>Mark as:</T>
</span>
@@ -279,7 +279,7 @@ export function ProjectStructurePage() {
</div>
<ExcludeFiles />
</div>
<div className={styles.roots} aria-label="Marked folders">
<div className={styles.roots} aria-label={tx("Marked folders")}>
<ContentRoots root={rootNode.path} />
{ROLES.map((r) => {
const list = roots.filter((x) => x.kind === r.role);
@@ -300,7 +300,9 @@ export function ProjectStructurePage() {
<div key={x.path} className={styles.rootItem}>
<span className={styles.rootPath} title={x.path || root || undefined}>
{shown}
{x.detectedFrom && <span className={styles.rootHint}> detected from {x.detectedFrom}</span>}
{x.detectedFrom && (
<span className={styles.rootHint}> {tx`detected from ${x.detectedFrom}`}</span>
)}
</span>
<IconButton
icon="close"
@@ -318,8 +320,8 @@ export function ProjectStructurePage() {
{unmarkedDetected.length > 0 && (
<div className={styles.note}>
{unmarkedDetected.length === 1
? "1 detected folder is unmarked."
: `${unmarkedDetected.length} detected folders are unmarked.`}{" "}
? tx("1 detected folder is unmarked.")
: tx`${unmarkedDetected.length} detected folders are unmarked.`}{" "}
<button type="button" className={styles.linkButton} onClick={restore}>
<T>Restore detected roots</T>
</button>
@@ -15,6 +15,7 @@ import { AddInterpreterDialog } from "./AddInterpreterDialog";
import { useSetting } from "../components/context";
import { SdkField, tildify } from "./components";
import styles from "./Sdk.module.css";
import { tx } from "@/platform/i18n";
type Op = "install" | "upgrade" | "uninstall";
@@ -167,7 +168,7 @@ export function PythonInterpreterPage() {
)}
<div className={styles.tablePanel}>
<div className={styles.tableToolbar} role="toolbar" aria-label="Packages">
<div className={styles.tableToolbar} role="toolbar" aria-label={tx("Packages")}>
<IconButton
icon="plus"
size="small"
@@ -235,7 +236,7 @@ export function PythonInterpreterPage() {
</form>
)}
<div className={styles.tableScroll}>
<table className={styles.table} aria-label="Installed packages">
<table className={styles.table} aria-label={tx("Installed packages")}>
<thead>
<tr>
<th scope="col">
@@ -274,7 +275,7 @@ export function PythonInterpreterPage() {
</div>
)}
{python && !error && packages && shown.length === 0 && (
<div className={styles.empty}>{query ? "No packages match" : "No packages installed"}</div>
<div className={styles.empty}>{query ? tx("No packages match") : tx("No packages installed")}</div>
)}
{python && !error && !packages && loading && (
<div className={styles.empty}>
@@ -288,7 +289,8 @@ export function PythonInterpreterPage() {
<div className={styles.status}>
<Spinner size={14} label="Working" />
<span>
{job.op === "install" ? "Installing" : job.op === "upgrade" ? "Upgrading" : "Uninstalling"} {job.target}…
{job.op === "install" ? tx("Installing") : job.op === "upgrade" ? tx("Upgrading") : tx("Uninstalling")}{" "}
{job.target}…
</span>
<Button size="small" variant="ghost" onClick={job.cancel}>
<T>Cancel</T>
@@ -301,7 +303,7 @@ export function PythonInterpreterPage() {
</div>
)}
{log.length > 0 && (
<pre className={styles.log} aria-label="pip output">
<pre className={styles.log} aria-label={tx("pip output")}>
{log.join("\n")}
</pre>
)}
+6 -2
View File
@@ -82,7 +82,7 @@ export function RecognizedFileTypes() {
aria-label="Filter file types"
onChange={(e) => setFilter(e.target.value)}
/>
<div className={styles.typeList} role="listbox" aria-label="Recognized file types">
<div className={styles.typeList} role="listbox" aria-label={tx("Recognized file types")}>
{shown.map((l) => {
const sample = sampleFileName(l);
return (
@@ -96,7 +96,11 @@ export function RecognizedFileTypes() {
>
<Icon name={sample ? fileIconName(sample) : "file-text"} size={16} />
<span className={styles.typeName}>{l.name}</span>
{current[l.id] && <span className={styles.modified}>modified</span>}
{current[l.id] && (
<span className={styles.modified}>
<T>modified</T>
</span>
)}
</button>
);
})}
+5 -3
View File
@@ -306,7 +306,7 @@ function OpenProjectInRow() {
const name = useId();
return (
<Row label="Open project in">
<div className={styles.radios} role="radiogroup" aria-label="Open project in">
<div className={styles.radios} role="radiogroup" aria-label={tx("Open project in")}>
{OPEN_IN.map((o) => (
<label key={o.value} className={styles.radio}>
<input type="radio" name={name} checked={value === o.value} onChange={() => set(o.value)} />
@@ -365,7 +365,9 @@ function IdleSaveRow() {
<div className={clsx(styles.fontRow, matched && styles.matched)}>
<Checkbox checked={on} onChange={setOn} label={<SearchLabel text="Save files if the IDE is idle for" />} />
<NumberInput value={seconds} onChange={setSeconds} {...IDLE_LIMITS} integer aria-label="Idle seconds" />
<span className={clsx(!on && styles.disabled)}>seconds</span>
<span className={clsx(!on && styles.disabled)}>
<T>seconds</T>
</span>
</div>
);
}
@@ -438,7 +440,7 @@ function NotificationGroupsTable() {
setGroups(all);
};
return (
<table className={styles.table} aria-label="Notification groups">
<table className={styles.table} aria-label={tx("Notification groups")}>
<thead>
<tr>
<th>
+6 -6
View File
@@ -6,7 +6,7 @@
import { useEffect, useId, useState, type KeyboardEvent } from "react";
import clsx from "clsx";
import { call, commands, IpcError, type RemoteEntry } from "@/platform/ipc";
import { useTx } from "@/platform/i18n";
import { useTx, tx } from "@/platform/i18n";
import type { DeploymentServerValid } from "@/platform/remoteSettings";
import { Badge, Button, Checkbox, Dialog, Icon, IconButton, Input, Spinner, T, Tabs } from "@/ui";
import { GlobListEditor, Row } from "../components/Form";
@@ -198,7 +198,7 @@ function RemoteFolderDialog({
<ul
className={styles.folderList}
role="listbox"
aria-label="Remote folders"
aria-label={tx("Remote folders")}
tabIndex={0}
onKeyDown={onListKey}
>
@@ -369,7 +369,7 @@ function MappingsTab({
return (
<div className={styles.panel}>
<div className={workflow.tableBox}>
<div className={workflow.toolbar} role="toolbar" aria-label="Mappings actions">
<div className={workflow.toolbar} role="toolbar" aria-label={tx("Mappings actions")}>
<IconButton
icon="plus"
title="Add"
@@ -391,7 +391,7 @@ function MappingsTab({
}}
/>
</div>
<table className={styles.mappingTable} aria-label="Mappings">
<table className={styles.mappingTable} aria-label={tx("Mappings")}>
<thead>
<tr>
{MAPPING_COLUMNS.map((c) => (
@@ -505,7 +505,7 @@ function DeploymentServersEditor() {
return (
<div className={styles.layout}>
<div className={workflow.tableBox}>
<div className={workflow.toolbar} role="toolbar" aria-label="Deployment servers actions">
<div className={workflow.toolbar} role="toolbar" aria-label={tx("Deployment servers actions")}>
<IconButton icon="plus" title="Add" size="small" onClick={() => add()} />
<IconButton icon="minus" title="Remove" size="small" disabled={!current} onClick={remove} />
<IconButton
@@ -523,7 +523,7 @@ function DeploymentServersEditor() {
onClick={() => current && setDefaultServer(current.id)}
/>
</div>
<div className={styles.serverList} role="listbox" aria-label="Deployment servers">
<div className={styles.serverList} role="listbox" aria-label={tx("Deployment servers")}>
{list.length === 0 && (
<div className={styles.empty}>
<T>No servers. Click + to add one.</T>
@@ -17,6 +17,7 @@ import type { PageSpec } from "../spec";
import { CODE_STYLE_SCOPE, CodeStyleMain, SchemeRow, formatterLabels } from "./codeStyleScheme";
import { editorPage, sectionLabels, type EditorSection } from "./kit";
import styles from "./Editor.module.css";
import { tx } from "@/platform/i18n";
function VisualGuidesRow() {
const [guides, setGuides] = useSetting<number[]>("editor.codeStyle.visualGuides");
@@ -44,7 +45,9 @@ function VisualGuidesRow() {
}}
onBlur={() => setDraft(null)}
/>
<span className={styles.suffix}>columns</span>
<span className={styles.suffix}>
<T>columns</T>
</span>
</Row>
);
}
@@ -155,7 +158,7 @@ function LanguageStyle({ lang }: { lang: LanguageInfo }) {
return (
<div className={styles.page}>
<SchemeRow />
<section aria-label="Tabs and Indents">
<section aria-label={tx("Tabs and Indents")}>
<div className={styles.previewTitle}>
<T>Tabs and Indents</T>
</div>
@@ -183,7 +186,7 @@ function LanguageStyle({ lang }: { lang: LanguageInfo }) {
/>
</Row>
</section>
<section aria-label="Wrapping">
<section aria-label={tx("Wrapping")}>
<div className={styles.previewTitle}>
<T>Wrapping</T>
</div>
@@ -200,7 +203,7 @@ function LanguageStyle({ lang }: { lang: LanguageInfo }) {
</Row>
</section>
{lang.lineComment && (
<section aria-label="Comment Code">
<section aria-label={tx("Comment Code")}>
<div className={styles.previewTitle}>
<T>Comment Code</T>
</div>
@@ -225,7 +228,9 @@ function LanguageStyle({ lang }: { lang: LanguageInfo }) {
<T>Reset to Language Defaults</T>
</Button>
<span className={styles.inherited}>
Defaults from the {lang.name} language definition: {base.useTab ? "tab" : `${base.tabSize} spaces`}.
{base.useTab
? tx`Defaults from the ${lang.name} language definition: tab.`
: tx`Defaults from the ${lang.name} language definition: ${base.tabSize} spaces.`}
</span>
</div>
</div>
@@ -13,6 +13,7 @@ import { usePageContext, useSetting } from "../../components/context";
import { setPageScope, stage, stagedValue, useSettingsDialog, type Scope } from "../../model/staging";
import { EditorSections, sectionLabels, type EditorSection } from "./kit";
import styles from "./CodeStyle.module.css";
import { tx } from "@/platform/i18n";
export const CODE_STYLE_SCOPE = "editor.codeStyle";
@@ -125,7 +126,7 @@ export function CodeStyleMain({ general }: { general: EditorSection[] }) {
return (
<div className={styles.page}>
<SchemeRow />
<div className={styles.tabs} role="tablist" aria-label="Code Style">
<div className={styles.tabs} role="tablist" aria-label={tx("Code Style")}>
{tabs.map(([id, label]) => (
<button
key={id}
@@ -306,7 +306,9 @@ function ColorSchemePage() {
<T>Restore Defaults</T>
</Button>
</div>
{scheme === "theme-default" && <div className={styles.note}>Follows the IDE theme: currently {schemeName}.</div>}
{scheme === "theme-default" && (
<div className={styles.note}>{tx`Follows the IDE theme: currently ${schemeName}.`}</div>
)}
<div className={styles.schemeBody}>
<div className={styles.attrTree}>
<Tree<Node>
@@ -335,7 +337,7 @@ function ColorSchemePage() {
<div />
)}
</div>
<pre className={styles.schemePreview} style={vars} aria-label="Color scheme preview">
<pre className={styles.schemePreview} style={vars} aria-label={tx("Color scheme preview")}>
{SAMPLE.map((line, i) => (
<div key={i} className={`${styles.previewLine} ${i === 6 ? styles.previewCaretLine : ""}`}>
<span className={styles.previewGutter}>{i + 1}</span>
+1 -1
View File
@@ -209,7 +209,7 @@ function Preview() {
</div>
<textarea
className={styles.previewArea}
aria-label="Font preview"
aria-label={tx("Font preview")}
spellCheck={false}
value={text}
onChange={(e) => setText(e.target.value)}
@@ -13,6 +13,7 @@ import { SearchLabel, Select } from "../../components/Form";
import { useLabelMatch, useSetting } from "../../components/context";
import { editorPage, type Item } from "./kit";
import styles from "./Editor.module.css";
import { tx } from "@/platform/i18n";
const P = "editor";
const ms = (key: string, max = 5000) => ({ kind: "number" as const, key, min: 0, max, unit: "ms" });
@@ -472,13 +473,13 @@ function TabRows() {
</label>
);
return (
<div className={clsx(styles.radioBlock, matched && styles.matched)} role="radiogroup" aria-label="Show tabs in">
<div className={clsx(styles.radioBlock, matched && styles.matched)} role="radiogroup" aria-label={tx("Show tabs in")}>
<div className={clsx(styles.radioLabel, disabled && styles.dim)}>
<SearchLabel text="Show tabs in" />:
</div>
<div className={styles.radios}>
{radio("One row, and if tabs don't fit:", !multiple, () => setMultiple(false))}
<div className={styles.children} role="radiogroup" aria-label="If tabs don't fit">
<div className={styles.children} role="radiogroup" aria-label={tx("If tabs don't fit")}>
{radio("Scroll the tabs panel", !multiple && overflow !== "squeeze", () => setOverflow("scroll"), disabled || multiple)}
{radio("Squeeze tabs", !multiple && overflow === "squeeze", () => setOverflow("squeeze"), disabled || multiple)}
</div>
+3 -3
View File
@@ -5,7 +5,7 @@
import { useCallback, useEffect, useId, useState } from "react";
import clsx from "clsx";
import { call, commands, isTauri, type RemoteEntry } from "@/platform/ipc";
import { useTx } from "@/platform/i18n";
import { useTx, tx } from "@/platform/i18n";
import { useProject } from "@/platform/project";
import { DEFAULT_RUN_TARGET, newRemoteId, type RunTargetValid } from "@/platform/remoteSettings";
import { Button, Checkbox, Dialog, Icon, IconButton, Input, T } from "@/ui";
@@ -297,7 +297,7 @@ function RunTargetsEditor() {
return (
<div className={styles.toolsLayout}>
<div className={styles.tableBox}>
<div className={styles.toolbar} role="toolbar" aria-label="Run targets actions">
<div className={styles.toolbar} role="toolbar" aria-label={tx("Run targets actions")}>
<IconButton icon="plus" title="Add" size="small" onClick={() => add()} />
<IconButton icon="minus" title="Remove" size="small" disabled={!current} onClick={remove} />
<IconButton
@@ -316,7 +316,7 @@ function RunTargetsEditor() {
onClick={() => move(1)}
/>
</div>
<div className={styles.toolList} role="listbox" aria-label="Run targets">
<div className={styles.toolList} role="listbox" aria-label={tx("Run targets")}>
{targets.length === 0 && (
<div className={clsx(styles.toolRow, styles.empty)}>
<T>No run targets. Click + to add one.</T>
+3 -3
View File
@@ -5,7 +5,7 @@
import { useId, useState } from "react";
import clsx from "clsx";
import { call, commands, IpcError, isTauri } from "@/platform/ipc";
import { useTx } from "@/platform/i18n";
import { useTx, tx } from "@/platform/i18n";
import {
DEFAULT_SSH_CONFIG,
newRemoteId,
@@ -285,7 +285,7 @@ function SshConfigsEditor() {
return (
<div className={styles.toolsLayout}>
<div className={styles.tableBox}>
<div className={styles.toolbar} role="toolbar" aria-label="SSH configurations actions">
<div className={styles.toolbar} role="toolbar" aria-label={tx("SSH configurations actions")}>
<IconButton icon="plus" title="Add" size="small" onClick={() => add()} />
<IconButton icon="minus" title="Remove" size="small" disabled={!current} onClick={remove} />
<IconButton
@@ -296,7 +296,7 @@ function SshConfigsEditor() {
onClick={() => current && add(current)}
/>
</div>
<div className={styles.toolList} role="listbox" aria-label="SSH configurations">
<div className={styles.toolList} role="listbox" aria-label={tx("SSH configurations")}>
{configs.length === 0 && (
<div className={clsx(styles.toolRow, styles.empty)}>
<T>No SSH configurations. Click + to add one.</T>
+2 -2
View File
@@ -435,7 +435,7 @@ function ExternalToolsEditor() {
return (
<div className={styles.toolsLayout}>
<div className={styles.tableBox}>
<div className={styles.toolbar} role="toolbar" aria-label="External tools actions">
<div className={styles.toolbar} role="toolbar" aria-label={tx("External tools actions")}>
<IconButton icon="plus" title="Add" size="small" onClick={() => add()} />
<IconButton icon="minus" title="Remove" size="small" disabled={!current} onClick={remove} />
<IconButton
@@ -454,7 +454,7 @@ function ExternalToolsEditor() {
onClick={() => move(1)}
/>
</div>
<div className={styles.toolList} role="listbox" aria-label="External tools">
<div className={styles.toolList} role="listbox" aria-label={tx("External tools")}>
{tools.length === 0 && (
<div className={clsx(styles.toolRow, styles.empty)}>
<T>No external tools. Click + to add one.</T>
+7 -6
View File
@@ -36,7 +36,7 @@ function MappingsTable() {
const project = useProject((s) => s.project);
return (
<div className={styles.tableBox}>
<table className={styles.table} aria-label="Directory mappings">
<table className={styles.table} aria-label={tx("Directory mappings")}>
<thead>
<tr>
<th>
@@ -58,9 +58,9 @@ function MappingsTable() {
<tr>
<td title={project.root}>
<Icon name="folder" size={16} className={styles.mappingIcon} />
&lt;Project&gt; {project.root}
{tx("<Project>")} {project.root}
</td>
<td>{project.isGit ? "Git" : "<none>"}</td>
<td>{project.isGit ? "Git" : tx("<none>")}</td>
</tr>
)}
</tbody>
@@ -221,10 +221,11 @@ function IssueTable() {
stacked
hint={
<>
Matches of an Issue ID regular expression in commit messages (Git log, commit details) become links; $0 is the
whole match, $1… the groups.
{tx(
"Matches of an Issue ID regular expression in commit messages (Git log, commit details) become links; $0 is the whole match, $1… the groups.",
)}
{invalid.length > 0 && (
<span className={styles.error}> Invalid pattern: {invalid.map((l) => l.pattern).join(", ")}</span>
<span className={styles.error}> {tx`Invalid pattern: ${invalid.map((l) => l.pattern).join(", ")}`}</span>
)}
</>
}
@@ -3,6 +3,7 @@ import { useState } from "react";
import { create } from "zustand";
import { Dialog, Input } from "@/ui";
import styles from "./Emmet.module.css";
import { tx } from "@/platform/i18n";
interface Pending {
tag: string;
@@ -36,7 +37,7 @@ function PromptDialog({ tag, resolve }: Pending) {
>
<div className={styles.body}>
<label className={styles.label} htmlFor="emmet-update-tag">
Abbreviation for &lt;{tag}&gt;, e.g. <code>section.card#main[data-x=1]</code>
{tx`Abbreviation for <${tag}>, e.g.`} <code>section.card#main[data-x=1]</code>
</label>
<Input
id="emmet-update-tag"
@@ -205,7 +205,7 @@ function NameStep({
onKeyDown={onKeyDown}
/>
{template.kinds.length > 1 && (
<div className={styles.kinds} role="listbox" aria-label="Kind">
<div className={styles.kinds} role="listbox" aria-label={tx("Kind")}>
{template.kinds.map((k, i) => (
<div
key={k.id}
+2 -1
View File
@@ -3,6 +3,7 @@ import clsx from "clsx";
import { HighlightedText, List, Popup, fuzzyMatch, listNavigate } from "@/ui";
import { closeChooser, useChooser, type ChooserItem, type ChooserRequest } from "./store";
import styles from "./Chooser.module.css";
import { tx } from "@/platform/i18n";
/** Overlay contribution: renders the pending chooser, if any. */
export function ChooserHost() {
@@ -126,7 +127,7 @@ function ChooserPopup({ request }: { request: ChooserRequest }) {
rowHeight={22}
className={styles.list}
aria-label={request.title}
empty={<div className={styles.empty}>{request.emptyText ?? "Nothing to show"}</div>}
empty={<div className={styles.empty}>{request.emptyText ?? tx("Nothing to show")}</div>}
renderItem={(r, state) => (
<div className={clsx(styles.row, state.active && styles.active)}>
<span className={styles.label}>
@@ -88,7 +88,7 @@ export function LiveTemplatesPage() {
aria-label="Filter templates"
/>
</div>
<div className={styles.items} role="listbox" aria-label="Live templates">
<div className={styles.items} role="listbox" aria-label={tx("Live templates")}>
{groups.map(([group, list]) => (
<div key={group} role="group" aria-label={group}>
<div className={styles.groupHeader}>{group}</div>
@@ -180,7 +180,7 @@ function TemplateEditor({ template: t }: { template: LiveTemplate }) {
className={styles.body}
value={t.body}
spellCheck={false}
aria-label="Template text"
aria-label={tx("Template text")}
onChange={(e) => update({ body: e.target.value })}
onKeyDown={(e) => {
if (e.key !== "Tab") return;
@@ -193,8 +193,9 @@ function TemplateEditor({ template: t }: { template: LiveTemplate }) {
}}
/>
<div className={styles.hint}>
<code>$NAME$</code> variables (repeated ones are linked), <code>$END$</code> caret, <code>$SELECTION$</code>{" "}
surrounded text, <code>$$</code> a dollar sign. Leading tabs are indent units.
<code>$NAME$</code> <T>variables (repeated ones are linked),</T> <code>$END$</code> <T>caret,</T>{" "}
<code>$SELECTION$</code> <T>surrounded text,</T> <code>$$</code>{" "}
<T>a dollar sign. Leading tabs are indent units.</T>
</div>
<div className={styles.row}>
<span className={styles.label}>
@@ -269,7 +270,7 @@ function TemplateEditor({ template: t }: { template: LiveTemplate }) {
</table>
)}
<div className={styles.hint}>
Functions: <code>className()</code> <code>methodName()</code> <code>fileName()</code>{" "}
<T>Functions:</T> <code>className()</code> <code>methodName()</code> <code>fileName()</code>{" "}
<code>fileNameWithoutExtension()</code> <code>date(&quot;yyyy-MM-dd&quot;)</code> <code>time()</code>{" "}
<code>user()</code> <code>clipboard()</code> <code>complete()</code> <code>suggestVariableName()</code>{" "}
<code>commentStart()</code> <code>capitalize(…)</code> <code>camelCase(…)</code> <code>snakeCase(…)</code>
@@ -39,7 +39,7 @@ export function PostfixPage() {
<code>items.for</code>
<T>, then press Tab.</T>
</div>
<div className={styles.tabs} role="tablist" aria-label="Languages">
<div className={styles.tabs} role="tablist" aria-label={tx("Languages")}>
{POSTFIX_GROUPS.map((g) => (
<button
key={g.id}
@@ -53,7 +53,7 @@ export function PostfixPage() {
</button>
))}
</div>
<div className={styles.list} role="list" aria-label="Postfix templates">
<div className={styles.list} role="list" aria-label={tx("Postfix templates")}>
{list.map((t) => (
<div key={t.id} className={styles.listRow} role="listitem">
<Checkbox
+2 -1
View File
@@ -7,6 +7,7 @@ import type { TerminalSearchOptions, TerminalSearchResult } from "./session";
import { activeSession } from "./store";
import styles from "./Terminal.module.css";
import { hasPrimaryModifier } from "@/platform/keymap";
import { tx } from "@/platform/i18n";
export const useTerminalFind = create<{ open: boolean; seq: number; query: string; options: TerminalSearchOptions }>(
() => ({
@@ -101,7 +102,7 @@ export function TerminalFindBar({ sessionKey }: { sessionKey: string | null }) {
<div
className={styles.find}
role="search"
aria-label="Find in terminal"
aria-label={tx("Find in terminal")}
data-terminal-find=""
onKeyDown={onKeyDown}
>
+1 -1
View File
@@ -50,7 +50,7 @@ export function TodoSettingsPage() {
}
/>
</div>
<div className={styles.list} role="list" aria-label="TODO patterns">
<div className={styles.list} role="list" aria-label={tx("TODO patterns")}>
{s.patterns.map((p) => (
<div key={p.id} className={styles.row} role="listitem">
<Checkbox
+1 -1
View File
@@ -414,7 +414,7 @@ export function TodoView() {
<span className={clsx(styles.fileName, state.selected && styles.selectedText)}>{name}</span>
<span className={styles.muted}>{relativeDir(root, n.group.path)}</span>
<span className={styles.count}>
{n.group.items.length} item{n.group.items.length === 1 ? "" : "s"}
{n.group.items.length === 1 ? tx`${n.group.items.length} item` : tx`${n.group.items.length} items`}
</span>
</span>
);
+5 -4
View File
@@ -313,8 +313,9 @@ export function LanguageServersPage() {
return (
<div className={styles.page}>
<p className={styles.intro}>
Language servers and debug adapters used by the IDE. Missing tools can be installed into the IDE&apos;s own
tools folder; nothing is downloaded until you click Install.
{tx(
"Language servers and debug adapters used by the IDE. Missing tools can be installed into the IDE's own tools folder; nothing is downloaded until you click Install.",
)}
</p>
{error && (
<div className={styles.error} role="alert">
@@ -351,7 +352,7 @@ export function LanguageServersPage() {
aria-label="Filter language servers"
/>
</div>
<div className={styles.segmented} role="radiogroup" aria-label="Show">
<div className={styles.segmented} role="radiogroup" aria-label={tx("Show")}>
{(["all", "missing", "installed"] as const).map((f) => (
<button
key={f}
@@ -365,7 +366,7 @@ export function LanguageServersPage() {
</button>
))}
</div>
<ul className={styles.list} aria-label="Language servers and debug adapters" onKeyDown={onListKey}>
<ul className={styles.list} aria-label={tx("Language servers and debug adapters")} onKeyDown={onListKey}>
{group("Available", ready)}
{group("Missing", missing)}
{loaded && shown.length === 0 && (
+21 -8
View File
@@ -19,6 +19,7 @@ import {
SearchField,
firstSelectable,
type MenuItem,
T,
} from "@/ui";
import { buildBranchRows, type BranchRow, type PopupAction } from "./branches";
import { branchMenu, commonBranchMenu, inRepo, tagMenu } from "./branchMenu";
@@ -331,7 +332,7 @@ function BranchesPanel({ onClose, actions: baseActions, root, fallback, onBack }
items={rows}
getKey={(r) => r.key}
isSection={isSection}
renderSection={(r) => <span className={styles.section}>{r.type === "section" ? r.label : ""}</span>}
renderSection={(r) => <span className={styles.section}>{r.type === "section" ? tx(r.label) : ""}</span>}
activeIndex={activeIndex}
onActiveChange={setActive}
onActivate={(_r, i) => activate(i)}
@@ -340,7 +341,7 @@ function BranchesPanel({ onClose, actions: baseActions, root, fallback, onBack }
rowHeight={ROW_HEIGHT}
aria-label="Branches"
className={styles.list}
empty={<span className={styles.empty}>{loaded ? "No matching branches" : "Loading…"}</span>}
empty={<span className={styles.empty}>{loaded ? tx("No matching branches") : "Loading…"}</span>}
renderItem={(r) => <BranchRowView row={r} />}
/>
</div>
@@ -599,7 +600,7 @@ function MultiRepoPanel({ onClose }: { onClose: () => void }) {
sectionHeightOf={(r) => (r.type === "section" && !r.label ? SEPARATOR_HEIGHT : undefined)}
renderSection={(r) =>
r.type === "section" && r.label ? (
<span className={styles.section}>{r.label}</span>
<span className={styles.section}>{tx(r.label)}</span>
) : (
<span className={styles.separator} role="separator" />
)
@@ -612,7 +613,11 @@ function MultiRepoPanel({ onClose }: { onClose: () => void }) {
rowHeight={ROW_HEIGHT}
aria-label="Repositories and branches"
className={styles.list}
empty={<span className={styles.empty}>No matching repositories or branches</span>}
empty={
<span className={styles.empty}>
<T>No matching repositories or branches</T>
</span>
}
renderItem={(r) => (
<RepoRowView row={r} operation={r.type === "repo" ? operations[r.repo.root] : undefined} />
)}
@@ -674,7 +679,7 @@ function RepoRowView({ row, operation }: { row: RepoRow; operation?: string }) {
>
<Icon name={row.expanded ? "chevron-down" : "chevron-right"} size={12} className={styles.rowIcon} />
{row.depth > 0 && <Icon name="folder" size={14} className={styles.rowIcon} />}
<span className={styles.label}>{row.label}</span>
<span className={styles.label}>{tx(row.label)}</span>
</span>
);
case "common":
@@ -691,7 +696,11 @@ function RepoRowView({ row, operation }: { row: RepoRow; operation?: string }) {
indices={row.matches}
tone="accent"
/>
{row.branch.current && <span className={styles.tag}>current</span>}
{row.branch.current && (
<span className={styles.tag}>
<T>current</T>
</span>
)}
<Icon name="chevron-right" size={10} className={styles.chevron} />
</span>
);
@@ -707,7 +716,7 @@ function BranchRowView({ row }: { row: BranchRow | Extract<RepoRow, { type: "act
return (
<span className={styles.row} data-row-key={row.key}>
<Icon name={row.action.icon ?? "more"} size={14} className={styles.rowIcon} />
<HighlightedText className={styles.label} text={row.action.label} indices={row.matches} tone="accent" />
<HighlightedText className={styles.label} text={tx(row.action.label)} indices={row.matches} tone="accent" />
{keys && <span className={styles.keys}>{keys}</span>}
</span>
);
@@ -756,7 +765,11 @@ function BranchRowView({ row }: { row: BranchRow | Extract<RepoRow, { type: "act
indices={row.matches}
tone="accent"
/>
{b.current && <span className={styles.tag}>current</span>}
{b.current && (
<span className={styles.tag}>
<T>current</T>
</span>
)}
{(b.ahead > 0 || b.behind > 0) && (
<span className={styles.syncSmall}>
{b.ahead > 0 && `↑${b.ahead}`}
+8 -12
View File
@@ -42,7 +42,7 @@ import { useChangelists, withListDefaults } from "./changelists";
import { moveToChangelist } from "./changelistsUi";
import { messageHistory, setAmend, setGroupByDir, useCommit, type CommitTab } from "./commitStore";
import { openDiff, withChain, workingDiffData } from "./diffInput";
import { KIND_COLOR, KIND_LABEL, formatTime, stashLabel, stripSlash } from "./model";
import { KIND_COLOR, KIND_LABEL, fileCount, formatTime, stashLabel, stripSlash } from "./model";
import { call, commands } from "@/platform/ipc";
import { OperationBanner } from "./operations";
import { ShelfPanel, loadShelves, useShelf } from "./shelf";
@@ -423,7 +423,7 @@ function ChangesPanel() {
style={n.type === "file" && n.kind ? { color: KIND_COLOR[n.kind] } : undefined}
title={n.type === "file" && n.kind ? KIND_LABEL[n.kind] : undefined}
>
{n.label}
{n.type === "group" && !n.changelist ? tx(n.label) : n.label}
</span>
);
// Pointer-driven (HTML5 drag and drop never reaches the Tauri webview):
@@ -480,14 +480,10 @@ function ChangesPanel() {
/>
)}
{label}
{n.type === "group" && (
<span className={styles.detail}>
{n.files.length} file{n.files.length === 1 ? "" : "s"}
</span>
)}
{n.type === "group" && <span className={styles.detail}>{fileCount(n.files.length)}</span>}
{n.type === "file" && n.files[0] && isPartiallyStaged(n.files[0]) && (
<span className={styles.partial} title="Partially staged: only staged hunks are committed">
partial
<span className={styles.partial} title={tx("Partially staged: only staged hunks are committed")}>
<T>partial</T>
</span>
)}
{n.detail && <span className={styles.detail}>{n.detail}</span>}
@@ -631,7 +627,7 @@ function CommitMessage({ included }: { included: number }) {
/>
<span className={styles.grow} />
{tooLong && (
<span className={clsx(styles.lineLen, styles.lineLenWarn)} title="Subject line length">
<span className={clsx(styles.lineLen, styles.lineLenWarn)} title={tx("Subject line length")}>
{first.length}/{cfg.subjectLimit}
</span>
)}
@@ -642,8 +638,8 @@ function CommitMessage({ included }: { included: number }) {
aria-invalid={emptyError || undefined}
style={{ ["--guide" as string]: `${margin}ch` }}
value={message}
placeholder="Commit Message"
aria-label="Commit message"
placeholder={tx("Commit Message")}
aria-label={tx("Commit message")}
spellCheck
onChange={(e) => {
const el = e.target;
+6 -4
View File
@@ -19,7 +19,7 @@ import { setContext } from "@/platform/context";
import { call, commands } from "@/platform/ipc";
import { shortcutLabel } from "@/platform/keymap";
import { tx } from "@/platform/i18n";
import { IconButton, Select, Spinner } from "@/ui";
import { IconButton, Select, Spinner, T } from "@/ui";
import { editorService, type EditorInput } from "@/workbench/contributions";
import { languageFor, viewerBase } from "./cm";
import { applyChunk, arrowControls, hoverRevert } from "./diffControls";
@@ -591,7 +591,7 @@ export function DiffEditor({ input }: { input: EditorInput }) {
? ""
: chunkInfo.count === 0
? identical
? "Contents are identical"
? tx("Contents are identical")
: "No differences"
: chunkInfo.current >= 0
? `${chunkInfo.current + 1} of ${chunkInfo.count} differences`
@@ -602,11 +602,13 @@ export function DiffEditor({ input }: { input: EditorInput }) {
<div className={styles.labels}>
<span className={styles.label} title={sideLabel(left)}>
{sideLabel(left)}
{editableA && <span className={styles.badge}>editable</span>}
{editableA && <span className={styles.badge}>
<T>editable</T>
</span>}
</span>
<span className={styles.label} title={sideLabel(right)}>
{sideLabel(right)}
{editableB && <span className={styles.badge}>{left.type === "rev" ? "working copy" : "editable"}</span>}
{editableB && <span className={styles.badge}>{left.type === "rev" ? tx("working copy") : "editable"}</span>}
</span>
</div>
) : (
+19 -21
View File
@@ -53,7 +53,7 @@ import {
useGitLog,
} from "./logStore";
import { refLabels, useLogOptions, type LogOptions } from "./logOptions";
import { KIND_COLOR, baseName, formatTime, relTo, type EffectiveKind } from "./model";
import { KIND_COLOR, baseName, fileCount, formatTime, relTo, type EffectiveKind } from "./model";
import { clearConsole, useVcsConsole } from "./ops";
import { currentRepo, loadBranches, useVcs } from "./store";
import { loadFavoriteBranches, newBranch, useFavoriteBranches } from "./branchOps";
@@ -166,7 +166,7 @@ function branchDetail(n: BranchTreeNode): string | undefined {
const b = n.branch;
if (!b) return undefined;
const sync = [b.behind > 0 ? `↓${b.behind}` : "", b.ahead > 0 ? `↑${b.ahead}` : ""].filter(Boolean).join(" ");
return [b.current ? "current" : "", sync].filter(Boolean).join(" ") || undefined;
return [b.current ? tx("current") : "", sync].filter(Boolean).join(" ") || undefined;
}
export function BranchesPanel() {
@@ -219,7 +219,7 @@ export function BranchesPanel() {
};
return (
<div className={styles.branches} onKeyDownCapture={onKeyDownCapture}>
<div className={styles.branchesBar} role="toolbar" aria-label="Branches">
<div className={styles.branchesBar} role="toolbar" aria-label={tx("Branches")}>
<SearchField
className={styles.branchSearch}
value={query}
@@ -235,7 +235,7 @@ export function BranchesPanel() {
key={`${root ?? ""}:${branchesLoaded}`}
roots={roots}
getKey={(n) => n.key}
getLabel={(n) => n.label}
getLabel={(n) => (n.kind === "head" || n.kind === "group" ? tx(n.label) : n.label)}
getChildren={(n) => n.children}
isExpandable={(n) => !!n.children}
defaultExpanded={[
@@ -317,15 +317,15 @@ function FilterButton({
<button type="button" className={styles.filterMain} onClick={onOpen} aria-haspopup="menu">
{value ? (
<>
{label}: <span className={styles.filterValue}>{value}</span>
{tx(label)}: <span className={styles.filterValue}>{value}</span>
</>
) : (
label
tx(label)
)}
{!value && <Icon name="chevron-down" size={12} className={styles.filterChevron} />}
</button>
{value && (
<button type="button" className={styles.filterClear} aria-label={`Clear ${label} filter`} onClick={onClear}>
<button type="button" className={styles.filterClear} aria-label={tx`Clear ${tx(label)} filter`} onClick={onClear}>
<Icon name="close" size={12} />
</button>
)}
@@ -443,7 +443,7 @@ function FilterBar() {
]);
return (
<div className={styles.filters} role="toolbar" aria-label="Log filters">
<div className={styles.filters} role="toolbar" aria-label={tx("Log filters")}>
<SearchField
className={styles.textFilter}
value={text}
@@ -724,14 +724,14 @@ function CommitList() {
ref={scrollRef}
className={styles.list}
role="listbox"
aria-label="Commits"
aria-label={tx("Commits")}
aria-multiselectable
aria-activedescendant={selectedIndex >= 0 ? `commit-${selectedIndex}` : undefined}
tabIndex={0}
onKeyDown={onKeyDown}
>
{!commits.length && !loading && finished && (
<div className={styles.message}>{headCommit ? "No commits match the filters" : "No commits yet"}</div>
<div className={styles.message}>{headCommit ? tx("No commits match the filters") : tx("No commits yet")}</div>
)}
<div style={{ height: virtualizer.getTotalSize(), position: "relative" }}>
{items.map((v) => {
@@ -986,10 +986,10 @@ export function CommitDetails({ commits }: { commits: Commit[] }) {
<SplitGroup orientation="vertical" id="git-log-details">
<SplitPanel id="files" defaultSize="60%" minSize="60px">
<div className={styles.filesPane}>
<div className={styles.detailsBar} role="toolbar" aria-label="Changed files">
<div className={styles.detailsBar} role="toolbar" aria-label={tx("Changed files")}>
<span className={styles.detailsCount}>
{files ? `${files.length} file${files.length === 1 ? "" : "s"}` : ""}
{many ? ` in ${chosen.length} commits` : ""}
{files ? fileCount(files.length) : ""}
{many ? " " + tx`in ${chosen.length} commits` : ""}
</span>
<span className={styles.grow} />
<IconButton
@@ -1062,16 +1062,16 @@ function CommitMessage({ commit, compact = false }: { commit: Commit; compact?:
<button
type="button"
className={clsx(styles.linkButton, styles.hash)}
title="Copy revision number"
title={tx("Copy revision number")}
onClick={() => void navigator.clipboard?.writeText(commit.id)}
>
{commit.shortId}
</button>{" "}
{commit.author} &lt;{commit.email}&gt;
{commit.timeMs ? ` on ${new Date(commit.timeMs).toLocaleString()}` : ""}
{commit.timeMs ? " " + tx`on ${new Date(commit.timeMs).toLocaleString()}` : ""}
</div>
{commit.parents.length > 1 && (
<div className={styles.meta}>Merge of {commit.parents.map((p) => p.slice(0, 8)).join(", ")}</div>
<div className={styles.meta}>{tx`Merge of ${commit.parents.map((p) => p.slice(0, 8)).join(", ")}`}</div>
)}
{commit.refs.length > 0 && (
<div className={styles.meta}>
@@ -1088,9 +1088,7 @@ function ChangedFileRow({ node }: { node: FileTreeNode<ChangedFile> }) {
<span className={styles.fileRow}>
<Icon name="folder" size={16} className={styles.fileIcon} />
<span className={styles.fileName}>{node.name}</span>
<span className={styles.fileDir}>
{node.count} file{node.count === 1 ? "" : "s"}
</span>
<span className={styles.fileDir}>{fileCount(node.count)}</span>
</span>
);
const f = node.file;
@@ -1101,7 +1099,7 @@ function ChangedFileRow({ node }: { node: FileTreeNode<ChangedFile> }) {
<span className={styles.fileName} style={{ color: KIND_COLOR[kind] }}>
{node.name}
</span>
{f.oldPath && <span className={styles.fileDir}>renamed from {baseName(f.oldPath)}</span>}
{f.oldPath && <span className={styles.fileDir}>{tx`renamed from ${baseName(f.oldPath)}`}</span>}
</span>
);
}
@@ -1126,7 +1124,7 @@ function ConsoleView() {
ref={ref}
className={styles.console}
role="log"
aria-label="Git console"
aria-label={tx("Git console")}
onScroll={(e) => {
const el = e.currentTarget;
stick.current = el.scrollTop + el.clientHeight >= el.scrollHeight - 8;
+2 -1
View File
@@ -7,6 +7,7 @@ import { shortcutLabel } from "@/platform/keymap";
import { IconButton, Popup } from "@/ui";
import { editorHunks, gotoChange, setHunkPopupHooks, type EditorHunk } from "./gutter";
import styles from "./HunkPopup.module.css";
import { tx } from "@/platform/i18n";
interface PopupState {
view: EditorView;
@@ -110,7 +111,7 @@ function HunkPopupView({ state }: { state: PopupState }) {
aria-label={KIND_TITLE[hunk.kind]}
className={styles.popup}
>
<div className={styles.toolbar} role="toolbar" aria-label="Change actions">
<div className={styles.toolbar} role="toolbar" aria-label={tx("Change actions")}>
<IconButton
icon="arrow-up"
title="Previous Change"
+5 -3
View File
@@ -179,9 +179,9 @@ export function LocalHistoryEditor({ input }: { input: EditorInput }) {
else
detail = (
<div className={styles.message}>
Label '{current.label}'
{tx`Label '${current.label}'`}
{view?.folder && atLabel?.size
? ` · ${atLabel.size} ${atLabel.size === 1 ? "file" : "files"} recorded before it`
? ` · ${atLabel.size === 1 ? tx`${atLabel.size} file recorded before it` : tx`${atLabel.size} files recorded before it`}`
: ""}
</div>
);
@@ -249,7 +249,9 @@ export function LocalHistoryEditor({ input }: { input: EditorInput }) {
aria-label="Revisions"
className={styles.list}
empty={
<span className={styles.message}>No local history for this {view.folder ? "folder" : "path"} yet</span>
<span className={styles.message}>
{view.folder ? tx`No local history for this folder yet` : tx`No local history for this path yet`}
</span>
}
renderItem={(e) =>
isLabel(e) ? (
+6 -6
View File
@@ -296,7 +296,7 @@ export function MergeEditor({ input }: { input: EditorInput }) {
if (resolved)
return (
<div className={styles.message}>
{baseName(path)} has been resolved and staged.
{tx`${baseName(path)} has been resolved and staged.`}
<Button onClick={() => void editorService().openFile(path, { focus: true })}>
<T>Open File</T>
</Button>
@@ -305,7 +305,7 @@ export function MergeEditor({ input }: { input: EditorInput }) {
return (
<div className={styles.root}>
<div className={styles.toolbar} role="toolbar" aria-label="Merge">
<div className={styles.toolbar} role="toolbar" aria-label={tx("Merge")}>
<IconButton icon="arrow-up" title="Previous Conflict" size="small" onClick={() => goto(-1)} />
<IconButton icon="arrow-down" title="Next Conflict" size="small" onClick={() => goto(1)} />
<span className={styles.sep} />
@@ -317,7 +317,7 @@ export function MergeEditor({ input }: { input: EditorInput }) {
</Button>
<IconButton icon="refresh" title="Reload from Disk" size="small" onClick={() => void load()} />
<span className={clsx(styles.status, !left && styles.statusDone)}>
{left ? `${left} conflict${left === 1 ? "" : "s"} left` : "All conflicts resolved"}
{left ? `${left} conflict${left === 1 ? "" : "s"} left` : tx("All conflicts resolved")}
</span>
<Button size="small" variant="primary" onClick={() => void apply()}>
<T>Apply</T>
@@ -336,9 +336,9 @@ export function MergeEditor({ input }: { input: EditorInput }) {
</div>
) : null}
<div className={styles.panes} hidden={!state || lang === null}>
<div ref={oursRef} className={styles.pane} aria-label="Your version" role="region" />
<div ref={resultRef} className={styles.pane} aria-label="Result" role="region" />
<div ref={theirsRef} className={styles.pane} aria-label="Their version" role="region" />
<div ref={oursRef} className={styles.pane} aria-label={tx("Your version")} role="region" />
<div ref={resultRef} className={styles.pane} aria-label={tx("Result")} role="region" />
<div ref={theirsRef} className={styles.pane} aria-label={tx("Their version")} role="region" />
</div>
</div>
);
+2 -1
View File
@@ -1,5 +1,6 @@
/** Branches popup model: grouping, ordering and filtering (pure). */
import type { Branch } from "@/bindings";
import { tx } from "@/platform/i18n";
import { fuzzyMatch } from "@/ui/match";
export interface PopupAction {
@@ -69,7 +70,7 @@ export function buildBranchRows(
};
for (const a of actions) {
const m = match(a.label);
const m = match(tx(a.label));
if (m === false) continue;
rows.push({ type: "action", key: `action:${a.id}`, action: a, matches: m });
}
+18 -9
View File
@@ -2,7 +2,7 @@
import { useState } from "react";
import type { FileStatus, PartialCommitFile } from "@/bindings";
import { call, commands } from "@/platform/ipc";
import { Checkbox, Dialog, Icon, Input, fileIconName } from "@/ui";
import { Checkbox, Dialog, Icon, Input, T, fileIconName } from "@/ui";
import { notifications } from "@/workbench/contributions";
import { commitRepos, groupOf, planCommit, renamedFromPath } from "./changes";
import { useChangelists, withListDefaults } from "./changelists";
@@ -222,10 +222,11 @@ function RollbackDialog({ files, onClose }: { files: FileStatus[]; onClose: () =
]}
>
<div className={styles.section}>
{selected.size} of {files.length} file{files.length === 1 ? "" : "s"} will be reverted to their last committed
state. Uncommitted changes are lost.
{files.length === 1
? tx`${selected.size} of ${files.length} file will be reverted to their last committed state. Uncommitted changes are lost.`
: tx`${selected.size} of ${files.length} files will be reverted to their last committed state. Uncommitted changes are lost.`}
</div>
<div className={styles.fileList} role="group" aria-label="Files to roll back">
<div className={styles.fileList} role="group" aria-label={tx("Files to roll back")}>
{files.map((f) => {
const kind = effectiveKind(f) ?? "modified";
return (
@@ -337,8 +338,10 @@ function StashDialog({
submit();
}}
>
{branch && <div className={styles.hint}>Current branch: {branch}</div>}
<label className={styles.fieldLabel}>Message:</label>
{branch && <div className={styles.hint}>{tx`Current branch: ${branch}`}</div>}
<label className={styles.fieldLabel}>
<T>Message:</T>
</label>
<Input
data-autofocus
aria-label="Message"
@@ -428,8 +431,12 @@ function UnstashDialog({ initial, onClose }: { initial: number; onClose: () => v
submit();
}}
>
<div className={styles.choiceList} role="radiogroup" aria-label="Stashes">
{stashes.length === 0 && <div className={styles.hint}>No stashes</div>}
<div className={styles.choiceList} role="radiogroup" aria-label={tx("Stashes")}>
{stashes.length === 0 && (
<div className={styles.hint}>
<T>No stashes</T>
</div>
)}
{stashes.map((s) => (
<label key={s.index} className={styles.radioRow}>
<input type="radio" name="stash" checked={chosen?.index === s.index} onChange={() => setIndex(s.index)} />
@@ -446,7 +453,9 @@ function UnstashDialog({ initial, onClose }: { initial: number; onClose: () => v
onChange={setReinstateIndex}
label="Reinstate index"
/>
<label className={styles.fieldLabel}>As new branch:</label>
<label className={styles.fieldLabel}>
<T>As new branch:</T>
</label>
<Input mono aria-label="As new branch" value={branch} onChange={(e) => setBranch(e.target.value)} />
</form>
</Dialog>
+3 -4
View File
@@ -17,6 +17,7 @@ import {
} from "./changelists";
import { useCommit } from "./commitStore";
import { confirmDialog, openDialog } from "./dialogs";
import { fileCount } from "./model";
import { useVcs } from "./store";
import styles from "./common.module.css";
import { tx } from "@/platform/i18n";
@@ -230,10 +231,8 @@ function MoveDialog({ paths, onClose }: { paths: readonly string[]; onClose: ()
{ label: "Cancel", role: "cancel" },
]}
>
<div className={styles.form} role="radiogroup" aria-label="Target changelist">
<div className={styles.hint}>
{paths.length} file{paths.length === 1 ? "" : "s"}
</div>
<div className={styles.form} role="radiogroup" aria-label={tx("Target changelist")}>
<div className={styles.hint}>{fileCount(paths.length)}</div>
<label className={styles.radioRow}>
<input
type="radio"
+9 -8
View File
@@ -180,10 +180,6 @@ export interface CommitProblem {
details?: { label: string; run: () => void };
}
function plural(n: number, word: string) {
return `${n} ${word}${n === 1 ? "" : "s"}`;
}
// ---------------------------------------------------------------------------
export interface CommitCandidate {
@@ -289,7 +285,11 @@ export async function runCommitChecks(files: readonly CommitCandidate[], amend:
title: "TODO",
text: (
<>
<div>{plural(hits.length, "TODO item")} found in the changed lines:</div>
<div>
{hits.length === 1
? tx`${hits.length} TODO item found in the changed lines:`
: tx`${hits.length} TODO items found in the changed lines:`}
</div>
<ul className={styles.todoList}>
{hits.slice(0, 20).map((h, i) => (
<li key={i}>
@@ -325,13 +325,14 @@ export async function runCommitChecks(files: readonly CommitCandidate[], amend:
}
}
if (crlf.length) {
const crlfNames = crlf.slice(0, 5).map(baseName).join(", ") + (crlf.length > 5 ? ", …" : "");
problems.push({
title: "Line separators",
text: (
<>
{plural(crlf.length, "file")} ({crlf.slice(0, 5).map(baseName).join(", ")}
{crlf.length > 5 ? ", …" : ""}) contain CRLF line separators and core.autocrlf is not set, so they would be
committed with CRLF. Convert them with the line separator widget in the status bar, or set{" "}
{crlf.length === 1
? tx`${crlf.length} file (${crlfNames}) contain CRLF line separators and core.autocrlf is not set, so they would be committed with CRLF. Convert them with the line separator widget in the status bar, or set`
: tx`${crlf.length} files (${crlfNames}) contain CRLF line separators and core.autocrlf is not set, so they would be committed with CRLF. Convert them with the line separator widget in the status bar, or set`}{" "}
<code>git config core.autocrlf input</code>.
</>
),
+5 -2
View File
@@ -14,6 +14,7 @@ import { runOp, writeFileText } from "./ops";
import { executeAction } from "@/platform/actions";
import { conflictedFiles, useVcs } from "./store";
import styles from "./common.module.css";
import { tx } from "@/platform/i18n";
export function showConflictsDialog() {
if (!conflictedFiles().length) {
@@ -99,8 +100,10 @@ function ConflictsDialog({ onClose }: { onClose: () => void }) {
>
<div className={styles.section}>
{files.length
? `${files.length} file${files.length === 1 ? "" : "s"} with conflicts`
: "All conflicts resolved"}
? files.length === 1
? tx`${files.length} file with conflicts`
: tx`${files.length} files with conflicts`
: tx("All conflicts resolved")}
</div>
<div style={{ height: 240, display: "flex", flexDirection: "column", padding: "0 14px 14px" }}>
<List
+7 -3
View File
@@ -1,7 +1,7 @@
/** Imperative dialogs for VCS actions, rendered by one overlay contribution. */
import { Fragment, useState, type ReactNode } from "react";
import { create } from "zustand";
import { Checkbox, Dialog, Input } from "@/ui";
import { Checkbox, Dialog, Input, T } from "@/ui";
import { confirm } from "@/workbench/messages";
import styles from "./common.module.css";
@@ -279,9 +279,13 @@ function DateRangeDialog({ onDone }: { onDone: (r: { after: number | null; befor
submit();
}}
>
<label className={styles.fieldLabel}>From:</label>
<label className={styles.fieldLabel}>
<T>From:</T>
</label>
<Input data-autofocus type="date" aria-label="From" value={from} onChange={(e) => setFrom(e.target.value)} />
<label className={styles.fieldLabel}>To:</label>
<label className={styles.fieldLabel}>
<T>To:</T>
</label>
<Input type="date" aria-label="To" value={to} onChange={(e) => setTo(e.target.value)} />
{(from || to) && error && <div className={styles.error}>{error}</div>}
</form>
+6 -6
View File
@@ -62,7 +62,7 @@ function MergeFlags({
}) {
const disabled = disabledMergeFlags(on, rebase);
return (
<div className={styles.optionGrid} role="group" aria-label="Merge options">
<div className={styles.optionGrid} role="group" aria-label={tx("Merge options")}>
{FLAG_LABELS.map(([flag, label]) => (
<Checkbox
key={flag}
@@ -317,7 +317,7 @@ function RebaseDialog({ onClose }: { onClose: () => void }) {
>
<div className={styles.form}>
<div className={styles.fieldLabel}>
{useOnto ? `Rebase commits of ${head ?? "HEAD"} not on:` : `Rebase ${head ?? "HEAD"} onto:`}
{useOnto ? tx`Rebase commits of ${head ?? "HEAD"} not on:` : `Rebase ${head ?? "HEAD"} onto:`}
</div>
<Select aria-label="Upstream" value={upstream} placeholder="Select branch" options={options} onChange={setUpstream} />
<Checkbox label="--onto" checked={useOnto} onChange={setUseOnto} />
@@ -409,12 +409,12 @@ function RemotesDialog({ onClose }: { onClose: () => void }) {
buttons={[{ label: "OK", role: "default", autoFocus: true, onClick: onClose }]}
>
<div className={styles.form}>
<div className={styles.manageBar} role="toolbar" aria-label="Remotes">
<div className={styles.manageBar} role="toolbar" aria-label={tx("Remotes")}>
<IconButton icon="plus" title="Add Remote…" size="small" onClick={() => edit()} />
<IconButton icon="minus" title="Remove Remote" size="small" disabled={!current} onClick={() => void remove()} />
<IconButton icon="edit" title="Edit Remote…" size="small" disabled={!current} onClick={() => edit(current)} />
</div>
<div className={styles.choiceList} role="listbox" aria-label="Remotes">
<div className={styles.choiceList} role="listbox" aria-label={tx("Remotes")}>
{!remotes.length && (
<div className={styles.empty}>
<T>No remotes</T>
@@ -565,7 +565,7 @@ function WorktreesDialog({ onClose }: { onClose: () => void }) {
buttons={[{ label: "Close", role: "default", autoFocus: true, onClick: onClose }]}
>
<div className={styles.form}>
<div className={styles.manageBar} role="toolbar" aria-label="Worktrees">
<div className={styles.manageBar} role="toolbar" aria-label={tx("Worktrees")}>
<IconButton icon="plus" title="New Worktree…" size="small" onClick={() => showNewWorktree(() => void reload())} />
<IconButton
icon="minus"
@@ -582,7 +582,7 @@ function WorktreesDialog({ onClose }: { onClose: () => void }) {
onClick={() => current && void executeAction("file.openProjectPath", current.path)}
/>
</div>
<div className={styles.choiceList} role="listbox" aria-label="Worktrees">
<div className={styles.choiceList} role="listbox" aria-label={tx("Worktrees")}>
{list.map((w) => (
<button
key={w.path}
+5
View File
@@ -172,3 +172,8 @@ export function commitWebUrl(remoteUrl: string, sha: string): string | null {
if (!base) return null;
return `${base}/${/bitbucket\.org\//.test(base) ? "commits" : "commit"}/${sha}`;
}
/** "N file(s)", worded for the locale. */
export function fileCount(n: number): string {
return n === 1 ? tx`${n} file` : tx`${n} files`;
}
+4 -4
View File
@@ -145,7 +145,7 @@ export function OperationBanner() {
{conflicts
? ` · ${conflicts} conflict${conflicts === 1 ? "" : "s"}`
: op === "merge"
? " · commit to conclude"
? tx(" · commit to conclude")
: ""}
</span>
{conflicts > 0 && (
@@ -161,7 +161,7 @@ export function OperationBanner() {
disabled={!!busy || (c === "continue" && conflicts > 0)}
onClick={() => void controlOperation(c, op)}
>
{c === "continue" ? "Continue" : c === "skip" ? "Skip" : "Abort"}
{c === "continue" ? tx("Continue") : c === "skip" ? tx("Skip") : tx("Abort")}
</button>
))}
</div>
@@ -266,7 +266,7 @@ function ResetDialog({ commit, onClose }: { commit?: Commit; onClose: () => void
{ label: "Cancel", role: "cancel" },
]}
>
<div className={styles.form} role="radiogroup" aria-label="Reset type">
<div className={styles.form} role="radiogroup" aria-label={tx("Reset type")}>
{commit ? (
<div className={styles.hint}>
<T>
@@ -398,7 +398,7 @@ function TagDialog({ onClose }: { onClose: () => void }) {
className={styles.textarea}
rows={4}
value={message}
placeholder="Optional; creates an annotated tag"
placeholder={tx("Optional; creates an annotated tag")}
onChange={(e) => setMessage(e.target.value)}
/>
</div>
+4 -2
View File
@@ -114,7 +114,7 @@ function CreatePatchDialog({ origin, onClose }: { origin: PatchOrigin; onClose:
: `${origin.commits.length} commits`}
</div>
)}
<div className={styles.form} role="radiogroup" aria-label="Patch destination">
<div className={styles.form} role="radiogroup" aria-label={tx("Patch destination")}>
<label className={styles.radioRow}>
<input type="radio" name="dest" checked={toFile} onChange={() => setToFile(true)} />
<span className={styles.radioText}>
@@ -220,7 +220,9 @@ function ApplyPatchDialog({ patch, source, onClose }: { patch: string; source: s
footerStart={
files && failing > 0 ? (
<span className={styles.hint}>
{failing} file{failing === 1 ? "" : "s"} will be merged; conflicts open in the merge tool
{failing === 1
? tx`${failing} file will be merged; conflicts open in the merge tool`
: tx`${failing} files will be merged; conflicts open in the merge tool`}
</span>
) : undefined
}
+2 -2
View File
@@ -260,7 +260,7 @@ function MessageDialog({
<div className={styles.form}>
<textarea
data-autofocus
aria-label="Commit message"
aria-label={tx("Commit message")}
className={styles.textarea}
rows={8}
value={message}
@@ -306,7 +306,7 @@ function InteractiveRebaseDialog({ range, onClose }: { range: RebaseRange; onClo
<div className={styles.section}>
<T>Commits are applied from top to bottom.</T>
</div>
<div className={styles.rebaseList} role="list" aria-label="Commits to rebase">
<div className={styles.rebaseList} role="list" aria-label={tx("Commits to rebase")}>
{rows.map((r, i) => (
<div key={r.commit.id} className={styles.rebaseRow} role="listitem">
<Select<RebaseAction>
+10 -6
View File
@@ -48,7 +48,7 @@ function UpdateDialog({ onClose }: { onClose: () => void }) {
<div className={styles.hint}>
{branch?.upstream
? `Update ${branch.head ?? "HEAD"} from ${branch.upstream}${branch.behind ? ` (${branch.behind} incoming)` : ""}`
: "The current branch has no tracked branch; git pull may fail."}
: tx("The current branch has no tracked branch; git pull may fail.")}
</div>
<div className={styles.fieldLabel}>
<T>Update type:</T>
@@ -237,7 +237,11 @@ function MultiPushDialog({ onClose }: { onClose: () => void }) {
onActivate={(r, _i, e) => {
if (r.type === "repo" && !(e.target as HTMLElement | null)?.closest?.("label")) toggle(r.repo.root);
}}
empty={<span>Everything is up to date</span>}
empty={
<span>
<T>Everything is up to date</T>
</span>
}
renderItem={(r) =>
r.type === "repo" ? (
<span className={styles.commitRow} style={{ flex: 1, padding: 0 }}>
@@ -390,12 +394,12 @@ function PushDialog({ target: pushTarget, onClose }: { target: PushTarget; onClo
>
<div className={styles.section}>
{!head
? "HEAD is detached; check out a branch to push."
? tx("HEAD is detached; check out a branch to push.")
: upstream
? `${head}${revision ? ` up to ${revision.slice(0, 8)}` : ""} → ${remote}/`
: revision
? `${head} has no upstream: commits up to ${revision.slice(0, 8)} will be pushed as a new branch:`
: `${head} has no upstream: it will be pushed as a new branch and tracked:`}
? tx`${head} has no upstream: commits up to ${revision.slice(0, 8)} will be pushed as a new branch:`
: tx`${head} has no upstream: it will be pushed as a new branch and tracked:`}
{head && (
<Input
aria-label="Target branch"
@@ -421,7 +425,7 @@ function PushDialog({ target: pushTarget, onClose }: { target: PushTarget; onClo
rowHeight={22}
aria-label="Outgoing commits"
className={styles.boxList}
empty={<span>{upstream ? "Everything is up to date" : "No commits"}</span>}
empty={<span>{upstream ? tx("Everything is up to date") : "No commits"}</span>}
renderItem={(c) => (
<span className={styles.commitRow} style={{ flex: 1, padding: 0 }}>
<span className={styles.commitHash}>{c.shortId}</span>
+8 -7
View File
@@ -14,7 +14,7 @@ import { groupOf } from "./changes";
import { activeList, listOf, moveFiles, relOf, updateChangelists, useChangelists } from "./changelists";
import { useCommit } from "./commitStore";
import { confirmDialog, openDialog, promptText } from "./dialogs";
import { KIND_COLOR, baseName, dirName, effectiveKind, formatTime, relTo, stripSlash } from "./model";
import { KIND_COLOR, baseName, dirName, effectiveKind, fileCount, formatTime, relTo, stripSlash } from "./model";
import { errorMessage, logConsole, runOp, saveAll } from "./ops";
import { conflictedFiles, refreshNow, useVcs } from "./store";
import styles from "./common.module.css";
@@ -155,8 +155,9 @@ function ShelveDialog({
]}
>
<div className={styles.section}>
{selected.size} of {files.length} file{files.length === 1 ? "" : "s"} will be shelved and reverted in the
working tree.
{files.length === 1
? tx`${selected.size} of ${files.length} file will be shelved and reverted in the working tree.`
: tx`${selected.size} of ${files.length} files will be shelved and reverted in the working tree.`}
</div>
<FileChecklist files={files} selected={selected} onChange={setSelected} label="Files to shelve" />
<div className={styles.form}>
@@ -221,7 +222,7 @@ function UnshelveDialog({ entry, onClose }: { entry: ShelvedChangeList; onClose:
]}
>
<div className={styles.section}>
{entry.name} · {entry.files.length} file{entry.files.length === 1 ? "" : "s"}
{entry.name} · {fileCount(entry.files.length)}
</div>
<div className={styles.form}>
<label className={styles.fieldLabel}>
@@ -316,7 +317,7 @@ export function ShelfPanel() {
const current = roots.find((r) => selected.has(r.key) || r.children?.some((c) => selected.has(c.key)))?.entry;
return (
<div className={viewStyles.changes}>
<div className={viewStyles.toolbar} role="toolbar" aria-label="Shelf">
<div className={viewStyles.toolbar} role="toolbar" aria-label={tx("Shelf")}>
<IconButton icon="refresh" title="Refresh" size="small" onClick={() => void loadShelves()} />
<IconButton icon="shelve" action="vcs.shelve" size="small" />
<span className={viewStyles.sep} />
@@ -354,7 +355,7 @@ export function ShelfPanel() {
}}
onContextMenu={(node, _sel, e: MouseEvent) => node && showContextMenu(e, shelfMenu(node.entry))}
aria-label="Shelved changes"
empty={<span className={viewStyles.empty}>{loaded ? "No shelved changes" : "Loading…"}</span>}
empty={<span className={viewStyles.empty}>{loaded ? tx("No shelved changes") : "Loading…"}</span>}
renderNode={(n) =>
n.file ? (
<>
@@ -372,7 +373,7 @@ export function ShelfPanel() {
<Icon name="shelve" size={16} className={viewStyles.icon} />
<span className={viewStyles.label}>{n.entry.name}</span>
<span className={viewStyles.detail}>
{n.entry.files.length} file{n.entry.files.length === 1 ? "" : "s"}
{fileCount(n.entry.files.length)}
{n.entry.timeMs ? ` · ${formatTime(n.entry.timeMs)}` : ""}
</span>
</>

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