mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-06 00:07:50 +00:00
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:
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"))}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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 & 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}>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -238,7 +238,7 @@ function ConfigChooser() {
|
||||
return (
|
||||
<span className={clsx(styles.chooserRow, styles.chooserEdit)}>
|
||||
<Icon name="check" size={13} />
|
||||
<span>Save '{r.config.name}' 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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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 (⌫)">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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} />;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 “<base> -m venv <location>”; 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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -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,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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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} />
|
||||
<Project> {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 <{tag}>, 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}
|
||||
|
||||
@@ -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("yyyy-MM-dd")</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
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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'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 && (
|
||||
|
||||
@@ -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}`}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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} <{commit.email}>
|
||||
{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;
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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) ? (
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>.
|
||||
</>
|
||||
),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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`;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user