From 81a5e5bc61d6dddddf23011429f32238191f7b07 Mon Sep 17 00:00:00 2001 From: "paseo-bot[bot]" <266920839+paseo-bot[bot]@users.noreply.github.com> Date: Tue, 6 Oct 2026 09:19:52 +0200 Subject: [PATCH] Show the PR checks summary, sidebar status, and "Worked for" line in the app language (#6196) * fix(app): translate the PR checks summary, sidebar status labels, and "Worked for" line These were written in the components, so they stayed English in every other language. They now come from the translation files, with copy for all nine supported languages. Closes #6190 * fix(app): rebuild translated labels when the app language changes The checks summary, sidebar status groups, and turn footer now take the translation function, so their memoized labels rebuild on a language switch. Russian one-check group labels use singular wording. * test(app): compare compact footer targets at sub-pixel precision The browser reports the 44pt footer buttons as 43.99999 wide on some runs, which fails the finger-size check on main as well. --------- Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com> --- packages/app/e2e/browser/sidebar-help.spec.ts | 7 ++- .../assistant-turn-footer-label.test.ts | 19 +++++++ .../components/assistant-turn-footer-label.ts | 6 ++ packages/app/src/components/message.tsx | 8 +-- .../sidebar/project-leading-visual.tsx | 8 ++- .../src/components/sidebar/sidebar-model.tsx | 4 ++ .../sidebar/sidebar-projection.test.ts | 2 + .../components/sidebar/sidebar-projection.ts | 4 +- .../sidebar/sidebar-workspace-menu.tsx | 1 + .../sidebar/sidebar-workspace-title.test.ts | 4 ++ .../sidebar/sidebar-workspace-title.ts | 6 +- .../git/pull-request-panel/checks-section.tsx | 7 ++- .../pull-request-panel/checks-summary.test.ts | 57 ++++++++++++++----- .../git/pull-request-panel/checks-summary.ts | 56 +++++++++--------- .../hooks/sidebar-status-view-model.test.ts | 36 +++++++++--- .../src/hooks/sidebar-status-view-model.ts | 22 ++++--- packages/app/src/i18n/resources/ar.ts | 48 ++++++++++++++++ packages/app/src/i18n/resources/en.ts | 48 ++++++++++++++++ packages/app/src/i18n/resources/es.ts | 48 ++++++++++++++++ packages/app/src/i18n/resources/fr.ts | 48 ++++++++++++++++ packages/app/src/i18n/resources/ja.ts | 48 ++++++++++++++++ packages/app/src/i18n/resources/ko.ts | 48 ++++++++++++++++ packages/app/src/i18n/resources/pt-BR.ts | 48 ++++++++++++++++ packages/app/src/i18n/resources/ru.ts | 48 ++++++++++++++++ packages/app/src/i18n/resources/zh-CN.ts | 48 ++++++++++++++++ .../app/src/utils/sidebar-shortcuts.test.ts | 4 +- 26 files changed, 607 insertions(+), 76 deletions(-) create mode 100644 packages/app/src/components/assistant-turn-footer-label.test.ts create mode 100644 packages/app/src/components/assistant-turn-footer-label.ts diff --git a/packages/app/e2e/browser/sidebar-help.spec.ts b/packages/app/e2e/browser/sidebar-help.spec.ts index 2d038abbdb..4c0162e8d8 100644 --- a/packages/app/e2e/browser/sidebar-help.spec.ts +++ b/packages/app/e2e/browser/sidebar-help.spec.ts @@ -183,13 +183,16 @@ async function openCompactSidebar(page: Page): Promise { await expect(page.getByTestId("sidebar-footer-bottom-line")).toBeVisible(); } +const MIN_TARGET_SIZE = 44; + async function expectFingerSizedFooterTargets(page: Page): Promise { const buttons = page.getByTestId("sidebar-footer-bottom-line").getByRole("button"); await expect(buttons).toHaveCount(5); for (const button of await buttons.all()) { const bounds = await button.boundingBox(); - expect(bounds?.width).toBeGreaterThanOrEqual(44); - expect(bounds?.height).toBeGreaterThanOrEqual(44); + // Layout can report a 44pt box as 43.99999, so compare at a hundredth of a point. + expect(bounds?.width).toBeGreaterThanOrEqual(MIN_TARGET_SIZE - 0.01); + expect(bounds?.height).toBeGreaterThanOrEqual(MIN_TARGET_SIZE - 0.01); // Glyphs stay at the composer toolbar size (some optically smaller); only the target grows. const glyphWidth = await button.locator("svg").first().getAttribute("width"); expect(Number(glyphWidth)).toBeLessThanOrEqual(20); diff --git a/packages/app/src/components/assistant-turn-footer-label.test.ts b/packages/app/src/components/assistant-turn-footer-label.test.ts new file mode 100644 index 0000000000..7e3e918df8 --- /dev/null +++ b/packages/app/src/components/assistant-turn-footer-label.test.ts @@ -0,0 +1,19 @@ +import { describe, expect, it } from "vitest"; + +import { i18n } from "@/i18n/i18next"; +import { getTurnDurationLabel } from "./assistant-turn-footer-label"; + +describe("getTurnDurationLabel", () => { + it("says how long the turn worked", () => { + expect(getTurnDurationLabel(14_000, i18n.t)).toBe("Worked for 14s"); + }); + + it("reads in the active app language", async () => { + await i18n.changeLanguage("fr"); + try { + expect(getTurnDurationLabel(14_000, i18n.t)).toBe("A travaillé pendant 14s"); + } finally { + await i18n.changeLanguage("en"); + } + }); +}); diff --git a/packages/app/src/components/assistant-turn-footer-label.ts b/packages/app/src/components/assistant-turn-footer-label.ts new file mode 100644 index 0000000000..2e2a088f0c --- /dev/null +++ b/packages/app/src/components/assistant-turn-footer-label.ts @@ -0,0 +1,6 @@ +import type { TFunction } from "i18next"; +import { formatDuration } from "@/utils/time"; + +export function getTurnDurationLabel(durationMs: number, t: TFunction): string { + return t("message.turnFooter.workedFor", { duration: formatDuration(durationMs) }); +} diff --git a/packages/app/src/components/message.tsx b/packages/app/src/components/message.tsx index 1bbeca891f..583585b782 100644 --- a/packages/app/src/components/message.tsx +++ b/packages/app/src/components/message.tsx @@ -74,6 +74,7 @@ import { useRevealedText } from "@/hooks/use-revealed-text"; import { colorMarkdownLinkChildren } from "@/components/markdown/link-children"; import { createAssistantMarkdownParser } from "@/utils/assistant-markdown-parser"; import { formatDuration, formatMessageTimestamp } from "@/utils/time"; +import { getTurnDurationLabel } from "./assistant-turn-footer-label"; import { writeMarkdownToRichClipboard } from "@/utils/rich-clipboard"; import { getDefaultMarkdownClipboardEnvironment } from "@/utils/rich-clipboard-default-environment"; import { setAssistantMarkdownBlockHeight } from "@/utils/assistant-message-height-estimate"; @@ -629,6 +630,7 @@ export const AssistantTurnFooter = memo(function AssistantTurnFooter({ durationMs, onFork, }: AssistantTurnFooterProps) { + const { t } = useTranslation(); const [hovered, setHovered] = useState(false); const [pressedReveal, setPressedReveal] = useState(false); const revealTimerRef = useRef | null>(null); @@ -644,10 +646,8 @@ export const AssistantTurnFooter = memo(function AssistantTurnFooter({ const durationLabel = useMemo( () => - durationMs !== undefined && durationMs !== null - ? `Worked for ${formatDuration(durationMs)}` - : "", - [durationMs], + durationMs !== undefined && durationMs !== null ? getTurnDurationLabel(durationMs, t) : "", + [durationMs, t], ); const timestampLabel = useMemo( () => (completedAt ? formatMessageTimestamp(completedAt) : ""), diff --git a/packages/app/src/components/sidebar/project-leading-visual.tsx b/packages/app/src/components/sidebar/project-leading-visual.tsx index c3950efe2d..e8c1e3a5d6 100644 --- a/packages/app/src/components/sidebar/project-leading-visual.tsx +++ b/packages/app/src/components/sidebar/project-leading-visual.tsx @@ -1,8 +1,9 @@ import { ActivityIndicator, View, type ViewStyle } from "react-native"; import { StyleSheet, withUnistyles } from "react-native-unistyles"; +import { useTranslation } from "react-i18next"; import { ChevronDown, ChevronRight, CircleAlert } from "lucide-react-native"; import { ProjectIconView } from "@/components/project-icon-view"; -import { STATUS_BUCKET_LABELS } from "@/hooks/sidebar-status-view-model"; +import { getStatusBucketLabel } from "@/hooks/sidebar-status-view-model"; import { ICON_SIZE, type Theme } from "@/styles/theme"; import type { SidebarStateBucket } from "@/utils/sidebar-agent-state"; import { @@ -170,6 +171,7 @@ function ProjectStatusBadge({ statusBucket: SidebarStateBucket; backdrop: SidebarSurfaceBackdrop; }) { + const { t } = useTranslation(); // Running skips the shell. The ring is wider than the 12pt shell and carries its own knockout, // so nesting it inside would clip it against the very thing that was meant to separate it from // the icon. It anchors to the same corner instead, growing around the centre the dot had. @@ -177,7 +179,7 @@ function ProjectStatusBadge({ return ( @@ -188,7 +190,7 @@ function ProjectStatusBadge({ return ( diff --git a/packages/app/src/components/sidebar/sidebar-model.tsx b/packages/app/src/components/sidebar/sidebar-model.tsx index 9399a3bd13..0cd7dc2b72 100644 --- a/packages/app/src/components/sidebar/sidebar-model.tsx +++ b/packages/app/src/components/sidebar/sidebar-model.tsx @@ -1,3 +1,4 @@ +import { useTranslation } from "react-i18next"; import React, { createContext, useContext, useEffect, useMemo, type ReactNode } from "react"; import { useSidebarWorkspacesList, @@ -54,6 +55,7 @@ export function SidebarModelProvider({ active?: boolean; children: ReactNode; }) { + const { t } = useTranslation(); const list = useSidebarWorkspacesList({ enabled: active }); const groupMode = useSidebarViewStore((state) => state.groupMode); const labelFilter = useSidebarViewStore((state) => state.labelFilter); @@ -150,6 +152,7 @@ export function SidebarModelProvider({ pinnedCollapsed, collapsedProjectKeys, collapsedWorkspaceGroupKeys, + t, }), [ collapsedProjectKeys, @@ -161,6 +164,7 @@ export function SidebarModelProvider({ pinnedKeys, pinnedWorkspaceOrder, filteredWorkspaceEntriesByKey, + t, ], ); const projection = useMemo(() => buildSidebarProjection(projectionInput), [projectionInput]); diff --git a/packages/app/src/components/sidebar/sidebar-projection.test.ts b/packages/app/src/components/sidebar/sidebar-projection.test.ts index 55882a0c49..c2dc31f2ee 100644 --- a/packages/app/src/components/sidebar/sidebar-projection.test.ts +++ b/packages/app/src/components/sidebar/sidebar-projection.test.ts @@ -1,3 +1,4 @@ +import { i18n } from "@/i18n/i18next"; import { describe, expect, it } from "vitest"; import type { SidebarProjectEntry, @@ -85,6 +86,7 @@ function projectionInput(options?: { pinnedCollapsed: options?.pinnedCollapsed ?? false, collapsedProjectKeys: new Set(), collapsedWorkspaceGroupKeys: new Set(), + t: i18n.t, }; } diff --git a/packages/app/src/components/sidebar/sidebar-projection.ts b/packages/app/src/components/sidebar/sidebar-projection.ts index c67042b45e..2e84f02aae 100644 --- a/packages/app/src/components/sidebar/sidebar-projection.ts +++ b/packages/app/src/components/sidebar/sidebar-projection.ts @@ -1,3 +1,4 @@ +import type { TFunction } from "i18next"; import { buildStatusGroups } from "@/hooks/sidebar-status-view-model"; import { splitPinnedSidebarGroups, @@ -45,6 +46,7 @@ export interface SidebarProjectionInput { pinnedCollapsed: boolean; collapsedProjectKeys: ReadonlySet; collapsedWorkspaceGroupKeys: ReadonlySet; + t: TFunction; } export function buildSidebarProjection(input: SidebarProjectionInput): SidebarProjection { @@ -100,7 +102,7 @@ function buildWorkspaceGroups( return []; case "status": return statusWorkspaceGroups( - buildStatusGroups(unpinnedWorkspaces, input.projectNamesByViewKey), + buildStatusGroups(unpinnedWorkspaces, input.projectNamesByViewKey, input.t), ); } } diff --git a/packages/app/src/components/sidebar/sidebar-workspace-menu.tsx b/packages/app/src/components/sidebar/sidebar-workspace-menu.tsx index 115003a3f2..e1543fac41 100644 --- a/packages/app/src/components/sidebar/sidebar-workspace-menu.tsx +++ b/packages/app/src/components/sidebar/sidebar-workspace-menu.tsx @@ -380,6 +380,7 @@ export function SidebarWorkspaceContextMenu({ serviceLabel: serviceSummary ? t(workspaceServiceLabelKey(serviceSummary), { name: serviceSummary.name }) : null, + t, }); const workspaceTarget = useMemo( () => ({ diff --git a/packages/app/src/components/sidebar/sidebar-workspace-title.test.ts b/packages/app/src/components/sidebar/sidebar-workspace-title.test.ts index 2068dbe271..465caf1344 100644 --- a/packages/app/src/components/sidebar/sidebar-workspace-title.test.ts +++ b/packages/app/src/components/sidebar/sidebar-workspace-title.test.ts @@ -1,3 +1,4 @@ +import { i18n } from "@/i18n/i18next"; import { describe, expect, it } from "vitest"; import { resolveSidebarWorkspaceAccessibilityLabel, @@ -39,6 +40,7 @@ describe("resolveSidebarWorkspaceAccessibilityLabel", () => { workspace: { name: "Investigate search", currentBranch: "fix/search", statusBucket: "done" }, workspaceTitleSource: "title", hostBadgeLabel: "Build host", + t: i18n.t, }); expect(label).toBe("Investigate search, Build host"); @@ -56,6 +58,7 @@ describe("resolveSidebarWorkspaceAccessibilityLabel", () => { hostBadgeLabel: "Build host", pullRequestLabel: "Pull request 42", serviceLabel: "Service web running", + t: i18n.t, }); expect(label).toBe( @@ -69,6 +72,7 @@ describe("resolveSidebarWorkspaceAccessibilityLabel", () => { workspaceTitleSource: "title", leadingProjectName: "Search project", hostBadgeLabel: "Build host", + t: i18n.t, }); expect(label).toBe("Search project, Investigate search, Build host"); diff --git a/packages/app/src/components/sidebar/sidebar-workspace-title.ts b/packages/app/src/components/sidebar/sidebar-workspace-title.ts index 955c9d2636..da3782f082 100644 --- a/packages/app/src/components/sidebar/sidebar-workspace-title.ts +++ b/packages/app/src/components/sidebar/sidebar-workspace-title.ts @@ -1,6 +1,7 @@ +import type { TFunction } from "i18next"; import type { SidebarWorkspaceEntry } from "@/hooks/use-sidebar-workspaces-list"; import type { WorkspaceTitleSource } from "@/hooks/use-settings"; -import { STATUS_BUCKET_LABELS } from "@/hooks/sidebar-status-view-model"; +import { getStatusBucketLabel } from "@/hooks/sidebar-status-view-model"; export function resolveSidebarWorkspacePrimaryLabel(input: { workspace: Pick; @@ -19,6 +20,7 @@ export function resolveSidebarWorkspaceAccessibilityLabel(input: { hostBadgeLabel?: string | null; pullRequestLabel?: string | null; serviceLabel?: string | null; + t: TFunction; }): string { return [ input.leadingProjectName, @@ -28,7 +30,7 @@ export function resolveSidebarWorkspaceAccessibilityLabel(input: { input.serviceLabel, input.workspace.statusBucket === "done" ? null - : STATUS_BUCKET_LABELS[input.workspace.statusBucket], + : getStatusBucketLabel(input.workspace.statusBucket, input.t), ] .filter((label): label is string => Boolean(label)) .join(", "); diff --git a/packages/app/src/git/pull-request-panel/checks-section.tsx b/packages/app/src/git/pull-request-panel/checks-section.tsx index 9e4f2dd942..96b2238fd2 100644 --- a/packages/app/src/git/pull-request-panel/checks-section.tsx +++ b/packages/app/src/git/pull-request-panel/checks-section.tsx @@ -1,6 +1,7 @@ import { useCallback, useMemo } from "react"; import { Pressable, ScrollView, Text, View, type GestureResponderEvent } from "react-native"; import { StyleSheet, withUnistyles } from "react-native-unistyles"; +import { useTranslation } from "react-i18next"; import { ChevronDown, ChevronRight, MessageSquarePlus } from "lucide-react-native"; import { Button } from "@/components/ui/button"; import { openExternalUrl } from "@/utils/open-external-url"; @@ -71,7 +72,8 @@ export function ChecksSection({ loadingCheckKeys, onAddLogsToChat, }: ChecksSectionProps) { - const summary = useMemo(() => summarizeChecks(checks), [checks]); + const { t } = useTranslation(); + const summary = useMemo(() => summarizeChecks(checks, t), [checks, t]); const { collapsedGroups, toggle: handleToggleGroup } = useCheckGroupState(); return ( @@ -91,13 +93,14 @@ export function ChecksSection({ {summary.parts.length > 0 ? ( + {summary.detailLead} {summary.parts.map((part, index) => ( {index > 0 ? ", " : ""} {part.text} ))} - {` ${summary.countNoun}`} + {summary.detailTrail} ) : null} diff --git a/packages/app/src/git/pull-request-panel/checks-summary.test.ts b/packages/app/src/git/pull-request-panel/checks-summary.test.ts index f014500753..747947b907 100644 --- a/packages/app/src/git/pull-request-panel/checks-summary.test.ts +++ b/packages/app/src/git/pull-request-panel/checks-summary.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it } from "vitest"; +import { i18n } from "@/i18n/i18next"; import type { CheckStatus } from "./check-status"; import { summarizeChecks } from "./checks-summary"; import type { PrPaneCheck } from "./data"; @@ -9,7 +10,7 @@ function check(name: string, status: CheckStatus): PrPaneCheck { describe("summarizeChecks", () => { it("reports no checks without claiming the run passed", () => { - const summary = summarizeChecks([]); + const summary = summarizeChecks([], i18n.t); expect(summary.outcome).toBe("none"); expect(summary.headline).toBe("No checks"); @@ -18,11 +19,10 @@ describe("summarizeChecks", () => { }); it("leads with failures when anything failed", () => { - const summary = summarizeChecks([ - check("build", "success"), - check("lint", "failure"), - check("e2e", "pending"), - ]); + const summary = summarizeChecks( + [check("build", "success"), check("lint", "failure"), check("e2e", "pending")], + i18n.t, + ); expect(summary.outcome).toBe("failure"); expect(summary.headline).toBe("Some checks were not successful"); @@ -31,7 +31,7 @@ describe("summarizeChecks", () => { }); it("reports an unfinished run as in progress when nothing failed", () => { - const summary = summarizeChecks([check("build", "success"), check("e2e", "pending")]); + const summary = summarizeChecks([check("build", "success"), check("e2e", "pending")], i18n.t); expect(summary.outcome).toBe("pending"); expect(summary.headline).toBe("Some checks haven't completed yet"); @@ -39,7 +39,10 @@ describe("summarizeChecks", () => { }); it("treats a run of only successes and skips as passed", () => { - const summary = summarizeChecks([check("build", "success"), check("deploy", "skipped")]); + const summary = summarizeChecks( + [check("build", "success"), check("deploy", "skipped")], + i18n.t, + ); expect(summary.outcome).toBe("success"); expect(summary.headline).toBe("All checks have passed"); @@ -47,11 +50,10 @@ describe("summarizeChecks", () => { }); it("keeps the checks of each group together under a counted label", () => { - const summary = summarizeChecks([ - check("build", "failure"), - check("lint", "success"), - check("test", "failure"), - ]); + const summary = summarizeChecks( + [check("build", "failure"), check("lint", "success"), check("test", "failure")], + i18n.t, + ); const [failing, successful] = summary.groups; expect(failing?.label).toBe("2 failing checks"); @@ -60,9 +62,34 @@ describe("summarizeChecks", () => { }); it("says check rather than checks for a run of one", () => { - const summary = summarizeChecks([check("build", "failure")]); + const summary = summarizeChecks([check("build", "failure")], i18n.t); - expect(summary.countNoun).toBe("check"); expect(summary.detail).toBe("1 failing check"); }); + + it("reads in the active app language", async () => { + await i18n.changeLanguage("fr"); + try { + const summary = summarizeChecks( + [ + check("build", "success"), + check("deploy", "skipped"), + check("lint", "failure"), + check("test", "failure"), + ], + i18n.t, + ); + + expect(summary.headline).toBe("Certaines vérifications ont échoué"); + expect(summary.detail).toBe("Vérifications : 2 en échec, 1 réussie(s), 1 ignorée(s)"); + expect(summary.groups.map((group) => group.label)).toEqual([ + "2 vérifications en échec", + "1 vérification réussie", + "1 vérification ignorée", + ]); + expect(summarizeChecks([], i18n.t).headline).toBe("Aucune vérification"); + } finally { + await i18n.changeLanguage("en"); + } + }); }); diff --git a/packages/app/src/git/pull-request-panel/checks-summary.ts b/packages/app/src/git/pull-request-panel/checks-summary.ts index f2f9b30a38..977706a9af 100644 --- a/packages/app/src/git/pull-request-panel/checks-summary.ts +++ b/packages/app/src/git/pull-request-panel/checks-summary.ts @@ -1,4 +1,5 @@ import { classifyCheck, type CheckPresentation } from "@/git/check-presentation"; +import type { TFunction } from "i18next"; import type { PrPaneCheck } from "./data"; /** @@ -16,27 +17,10 @@ const PRESENTATION_ORDER = [ "ignored", ] as const satisfies readonly CheckPresentation[]; -/** How each status reads inside a count phrase: "2 failing", "4 in progress". */ -const PRESENTATION_NOUN: Record = { - actionRequired: "needs action", - warning: "warning", - failure: "failing", - pending: "in progress", - manual: "manual", - success: "successful", - ignored: "skipped", -}; - /** The worst thing happening in the run, which is what the headline and the ring report. */ export type ChecksOutcome = "actionRequired" | "failure" | "pending" | "success" | "none"; -const OUTCOME_HEADLINE: Record = { - actionRequired: "Some checks need your attention", - failure: "Some checks were not successful", - pending: "Some checks haven't completed yet", - success: "All checks have passed", - none: "No checks", -}; +const COPY = "workspace.git.pr.checksOverview"; export interface ChecksCountPart { status: CheckPresentation; @@ -58,8 +42,12 @@ export interface ChecksSummary { headline: string; /** The count phrases behind `detail`, kept apart so the header can label each one. */ parts: readonly ChecksCountPart[]; - /** The noun closing the detail line — "checks", or "check" for a run of one. */ - countNoun: string; + /** + * The words around the count phrases in `detail`: "" and " checks" in English, where the noun + * closes the line, while other languages lead with it. + */ + detailLead: string; + detailTrail: string; /** The whole line: "2 failing, 4 in progress, 1 successful checks". Empty with no checks. */ detail: string; total: number; @@ -72,7 +60,7 @@ export interface ChecksSummary { * header, the ring, and the grouped list all read from one derivation instead of each * filtering the array again with its own idea of what counts. */ -export function summarizeChecks(checks: readonly PrPaneCheck[]): ChecksSummary { +export function summarizeChecks(checks: readonly PrPaneCheck[], t: TFunction): ChecksSummary { const groups: ChecksGroup[] = []; const parts: ChecksCountPart[] = []; @@ -83,24 +71,27 @@ export function summarizeChecks(checks: readonly PrPaneCheck[]): ChecksSummary { } groups.push({ status, - label: `${matching.length} ${PRESENTATION_NOUN[status]} ${countNoun(matching.length)}`, + label: t(`${COPY}.${matching.length === 1 ? "groupOne" : "groupMany"}.${status}`, { + count: matching.length, + }), checks: matching, }); parts.push({ status, count: matching.length, - text: `${matching.length} ${PRESENTATION_NOUN[status]}`, + text: t(`${COPY}.count.${status}`, { count: matching.length }), }); } const outcome = selectOutcome(checks); - const noun = countNoun(checks.length); + const { lead, trail } = detailFrame(checks.length, t); return { outcome, - headline: OUTCOME_HEADLINE[outcome], + headline: t(`${COPY}.headline.${outcome}`), parts, - countNoun: noun, - detail: parts.length === 0 ? "" : `${parts.map((part) => part.text).join(", ")} ${noun}`, + detailLead: lead, + detailTrail: trail, + detail: parts.length === 0 ? "" : `${lead}${parts.map((part) => part.text).join(", ")}${trail}`, total: checks.length, groups, }; @@ -131,6 +122,13 @@ function selectOutcome(checks: readonly PrPaneCheck[]): ChecksOutcome { return "success"; } -function countNoun(count: number): string { - return count === 1 ? "check" : "checks"; +/** + * The header renders each count phrase on its own, so the translated line is split around + * where the phrases go. + */ +function detailFrame(total: number, t: TFunction): { lead: string; trail: string } { + const marker = "\u0000"; + const line = t(`${COPY}.${total === 1 ? "detailOne" : "detailMany"}`, { parts: marker }); + const [lead = "", trail = ""] = line.split(marker); + return { lead, trail }; } diff --git a/packages/app/src/hooks/sidebar-status-view-model.test.ts b/packages/app/src/hooks/sidebar-status-view-model.test.ts index 62dc7142a8..c5bd8dcd11 100644 --- a/packages/app/src/hooks/sidebar-status-view-model.test.ts +++ b/packages/app/src/hooks/sidebar-status-view-model.test.ts @@ -1,9 +1,10 @@ import { describe, expect, it } from "vitest"; +import { i18n } from "@/i18n/i18next"; import type { SidebarWorkspaceEntry } from "./sidebar-workspaces-view-model"; import { buildStatusGroups, buildStatusShortcutIndex, - STATUS_BUCKET_LABELS, + getStatusBucketLabel, STATUS_BUCKET_ORDER, type StatusGroup, } from "./sidebar-status-view-model"; @@ -55,7 +56,7 @@ describe("buildStatusGroups", () => { ws({ workspaceKey: "srv:running-ws", statusBucket: "running", name: "running-ws" }), ]; - const groups = buildStatusGroups(workspaces, emptyProjectNames); + const groups = buildStatusGroups(workspaces, emptyProjectNames, i18n.t); expect(groups.map((g) => g.bucket)).toEqual(["needs_input", "running", "done"]); expect(groups[0]?.label).toBe("Needs input"); @@ -69,7 +70,7 @@ describe("buildStatusGroups", () => { ws({ workspaceKey: "srv:b", statusBucket: "running" }), ]; - const groups = buildStatusGroups(workspaces, emptyProjectNames); + const groups = buildStatusGroups(workspaces, emptyProjectNames, i18n.t); expect(groups.map((g) => g.bucket)).toEqual(["running", "done"]); }); @@ -93,7 +94,7 @@ describe("buildStatusGroups", () => { }), ]; - const groups = buildStatusGroups(workspaces, emptyProjectNames); + const groups = buildStatusGroups(workspaces, emptyProjectNames, i18n.t); expect(groups[0]?.rows.map((r) => r.workspaceKey)).toEqual(["srv:new", "srv:mid", "srv:old"]); }); @@ -109,7 +110,7 @@ describe("buildStatusGroups", () => { ws({ workspaceKey: "srv:null-b", statusBucket: "done", statusEnteredAt: null }), ]; - const groups = buildStatusGroups(workspaces, emptyProjectNames); + const groups = buildStatusGroups(workspaces, emptyProjectNames, i18n.t); expect(groups[0]?.rows.map((r) => r.workspaceKey)).toEqual([ "srv:ts", @@ -145,13 +146,13 @@ describe("buildStatusGroups", () => { }), ]; - const groups = buildStatusGroups(workspaces, projectNames); + const groups = buildStatusGroups(workspaces, projectNames, i18n.t); expect(groups[0]?.rows.map((r) => r.workspaceKey)).toEqual(["srv:2", "srv:3", "srv:1"]); }); it("returns empty array for no workspaces", () => { - const groups = buildStatusGroups([], emptyProjectNames); + const groups = buildStatusGroups([], emptyProjectNames, i18n.t); expect(groups).toEqual([]); }); @@ -180,11 +181,11 @@ describe("buildStatusGroups", () => { ws({ workspaceKey: "srv:dn", statusBucket: "done", statusEnteredAt: null }), ]; - const groups = buildStatusGroups(workspaces, emptyProjectNames); + const groups = buildStatusGroups(workspaces, emptyProjectNames, i18n.t); expect(groups.map((g) => g.bucket)).toEqual(STATUS_BUCKET_ORDER); expect(groups.map((g) => g.label)).toEqual( - STATUS_BUCKET_ORDER.map((b) => STATUS_BUCKET_LABELS[b]), + STATUS_BUCKET_ORDER.map((b) => getStatusBucketLabel(b, i18n.t)), ); // Each group has exactly one row with the matching bucket for (const group of groups) { @@ -230,3 +231,20 @@ describe("buildStatusShortcutIndex", () => { expect(index.size).toBe(0); }); }); + +describe("getStatusBucketLabel", () => { + it("reads in the active app language", async () => { + await i18n.changeLanguage("fr"); + try { + expect(STATUS_BUCKET_ORDER.map((bucket) => getStatusBucketLabel(bucket, i18n.t))).toEqual([ + "Attend une réponse", + "Échec", + "À relire", + "En cours", + "Terminé", + ]); + } finally { + await i18n.changeLanguage("en"); + } + }); +}); diff --git a/packages/app/src/hooks/sidebar-status-view-model.ts b/packages/app/src/hooks/sidebar-status-view-model.ts index 7bcbe3d345..5736c7f391 100644 --- a/packages/app/src/hooks/sidebar-status-view-model.ts +++ b/packages/app/src/hooks/sidebar-status-view-model.ts @@ -1,3 +1,4 @@ +import type { TFunction } from "i18next"; import { STATUS_BUCKET_ORDER } from "@/utils/sidebar-agent-state"; import type { SidebarWorkspaceEntry } from "@/hooks/sidebar-workspaces-view-model"; @@ -5,13 +6,17 @@ export type StatusBucket = SidebarWorkspaceEntry["statusBucket"]; export { STATUS_BUCKET_ORDER }; -export const STATUS_BUCKET_LABELS: Record = { - needs_input: "Needs input", - failed: "Failed", - attention: "Ready to review", - running: "Working", - done: "Done", -}; +const STATUS_BUCKET_LABEL_KEYS = { + needs_input: "sidebar.statusBucket.needsInput", + failed: "sidebar.statusBucket.failed", + attention: "sidebar.statusBucket.readyToReview", + running: "sidebar.statusBucket.working", + done: "sidebar.statusBucket.done", +} as const satisfies Record; + +export function getStatusBucketLabel(bucket: StatusBucket, t: TFunction): string { + return t(STATUS_BUCKET_LABEL_KEYS[bucket]); +} export interface StatusGroup { bucket: StatusBucket; @@ -22,6 +27,7 @@ export interface StatusGroup { export function buildStatusGroups( workspaces: SidebarWorkspaceEntry[], projectNamesByViewKey: Map, + t: TFunction, ): StatusGroup[] { const bucketRows = new Map(); @@ -42,7 +48,7 @@ export function buildStatusGroups( if (!rows || rows.length === 0) continue; rows.sort((a, b) => compareStatusRows(a, b, projectNamesByViewKey)); - groups.push({ bucket, label: STATUS_BUCKET_LABELS[bucket], rows }); + groups.push({ bucket, label: getStatusBucketLabel(bucket, t), rows }); } return groups; diff --git a/packages/app/src/i18n/resources/ar.ts b/packages/app/src/i18n/resources/ar.ts index f001a177ef..8a6338fe1e 100644 --- a/packages/app/src/i18n/resources/ar.ts +++ b/packages/app/src/i18n/resources/ar.ts @@ -382,6 +382,9 @@ export const ar: TranslationResources = { completed: "اكتملت", }, }, + turnFooter: { + workedFor: "عمل لمدة {{duration}}", + }, compaction: { loading: "الضغط...", auto: "يتم ضغط السياق تلقائيًا", @@ -971,6 +974,44 @@ export const ar: TranslationResources = { viewPullRequest: "عرض", openOn: "فتح على {{brand}}", }, + checksOverview: { + headline: { + actionRequired: "بعض الفحوصات تحتاج إلى انتباهك", + failure: "بعض الفحوصات لم تنجح", + pending: "بعض الفحوصات لم تكتمل بعد", + success: "نجحت جميع الفحوصات", + none: "لا توجد فحوصات", + }, + count: { + actionRequired: "{{count}} تحتاج إجراءً", + warning: "{{count}} مع تحذير", + failure: "{{count}} فاشلة", + pending: "{{count}} قيد التشغيل", + manual: "{{count}} يدوية", + success: "{{count}} ناجحة", + ignored: "{{count}} متخطاة", + }, + detailOne: "الفحص: {{parts}}", + detailMany: "الفحوصات: {{parts}}", + groupOne: { + actionRequired: "{{count}} فحص يحتاج إجراءً", + warning: "{{count}} فحص مع تحذير", + failure: "{{count}} فحص فاشل", + pending: "{{count}} فحص قيد التشغيل", + manual: "{{count}} فحص يدوي", + success: "{{count}} فحص ناجح", + ignored: "{{count}} فحص متخطى", + }, + groupMany: { + actionRequired: "{{count}} فحوصات تحتاج إجراءً", + warning: "{{count}} فحوصات مع تحذير", + failure: "{{count}} فحوصات فاشلة", + pending: "{{count}} فحوصات قيد التشغيل", + manual: "{{count}} فحوصات يدوية", + success: "{{count}} فحوصات ناجحة", + ignored: "{{count}} فحوصات متخطاة", + }, + }, checksSummary: { passedLabel: "نجح", failedLabel: "فشل", @@ -1088,6 +1129,13 @@ export const ar: TranslationResources = { }, }, sidebar: { + statusBucket: { + needsInput: "تحتاج إدخالاً", + failed: "فشل", + readyToReview: "جاهزة للمراجعة", + working: "قيد العمل", + done: "تم", + }, display: { trigger: "تفضيلات العرض", heading: "العرض", diff --git a/packages/app/src/i18n/resources/en.ts b/packages/app/src/i18n/resources/en.ts index 1bf5598dfd..5ae4bc29cd 100644 --- a/packages/app/src/i18n/resources/en.ts +++ b/packages/app/src/i18n/resources/en.ts @@ -379,6 +379,9 @@ export const en = { completed: "Completed", }, }, + turnFooter: { + workedFor: "Worked for {{duration}}", + }, compaction: { loading: "Compacting...", auto: "Context automatically compacted", @@ -979,6 +982,44 @@ export const en = { viewPullRequest: "View", openOn: "Open on {{brand}}", }, + checksOverview: { + headline: { + actionRequired: "Some checks need your attention", + failure: "Some checks were not successful", + pending: "Some checks haven't completed yet", + success: "All checks have passed", + none: "No checks", + }, + count: { + actionRequired: "{{count}} needs action", + warning: "{{count}} warning", + failure: "{{count}} failing", + pending: "{{count}} in progress", + manual: "{{count}} manual", + success: "{{count}} successful", + ignored: "{{count}} skipped", + }, + detailOne: "{{parts}} check", + detailMany: "{{parts}} checks", + groupOne: { + actionRequired: "{{count}} needs action check", + warning: "{{count}} warning check", + failure: "{{count}} failing check", + pending: "{{count}} in progress check", + manual: "{{count}} manual check", + success: "{{count}} successful check", + ignored: "{{count}} skipped check", + }, + groupMany: { + actionRequired: "{{count}} needs action checks", + warning: "{{count}} warning checks", + failure: "{{count}} failing checks", + pending: "{{count}} in progress checks", + manual: "{{count}} manual checks", + success: "{{count}} successful checks", + ignored: "{{count}} skipped checks", + }, + }, checksSummary: { passedLabel: "passed", failedLabel: "failed", @@ -1096,6 +1137,13 @@ export const en = { }, }, sidebar: { + statusBucket: { + needsInput: "Needs input", + failed: "Failed", + readyToReview: "Ready to review", + working: "Working", + done: "Done", + }, display: { trigger: "Display preferences", heading: "Display", diff --git a/packages/app/src/i18n/resources/es.ts b/packages/app/src/i18n/resources/es.ts index f3e0fc9e17..1c34d7f5b6 100644 --- a/packages/app/src/i18n/resources/es.ts +++ b/packages/app/src/i18n/resources/es.ts @@ -387,6 +387,9 @@ export const es: TranslationResources = { completed: "Completada", }, }, + turnFooter: { + workedFor: "Trabajó durante {{duration}}", + }, compaction: { loading: "Compactando...", auto: "Contexto compactado automáticamente", @@ -1004,6 +1007,44 @@ export const es: TranslationResources = { viewPullRequest: "Ver", openOn: "Abrir en {{brand}}", }, + checksOverview: { + headline: { + actionRequired: "Algunas comprobaciones requieren tu atención", + failure: "Algunas comprobaciones no se superaron", + pending: "Algunas comprobaciones aún no han terminado", + success: "Todas las comprobaciones se superaron", + none: "Sin comprobaciones", + }, + count: { + actionRequired: "{{count}} con acción pendiente", + warning: "{{count}} con advertencia", + failure: "{{count}} con error", + pending: "{{count}} en curso", + manual: "{{count}} manual(es)", + success: "{{count}} superada(s)", + ignored: "{{count}} omitida(s)", + }, + detailOne: "Comprobación: {{parts}}", + detailMany: "Comprobaciones: {{parts}}", + groupOne: { + actionRequired: "{{count}} comprobación con acción pendiente", + warning: "{{count}} comprobación con advertencia", + failure: "{{count}} comprobación con error", + pending: "{{count}} comprobación en curso", + manual: "{{count}} comprobación manual", + success: "{{count}} comprobación superada", + ignored: "{{count}} comprobación omitida", + }, + groupMany: { + actionRequired: "{{count}} comprobaciones con acción pendiente", + warning: "{{count}} comprobaciones con advertencia", + failure: "{{count}} comprobaciones con error", + pending: "{{count}} comprobaciones en curso", + manual: "{{count}} comprobaciones manuales", + success: "{{count}} comprobaciones superadas", + ignored: "{{count}} comprobaciones omitidas", + }, + }, checksSummary: { passedLabel: "pasó", failedLabel: "falló", @@ -1124,6 +1165,13 @@ export const es: TranslationResources = { }, }, sidebar: { + statusBucket: { + needsInput: "Necesita datos", + failed: "Con error", + readyToReview: "Para revisar", + working: "En ejecución", + done: "Terminado", + }, display: { trigger: "Preferencias de visualización", heading: "Visualización", diff --git a/packages/app/src/i18n/resources/fr.ts b/packages/app/src/i18n/resources/fr.ts index 19deced196..c682d05baf 100644 --- a/packages/app/src/i18n/resources/fr.ts +++ b/packages/app/src/i18n/resources/fr.ts @@ -387,6 +387,9 @@ export const fr: TranslationResources = { completed: "Terminée", }, }, + turnFooter: { + workedFor: "A travaillé pendant {{duration}}", + }, compaction: { loading: "Compaction…", auto: "Contexte compacté automatiquement", @@ -995,6 +998,44 @@ export const fr: TranslationResources = { viewPullRequest: "Voir", openOn: "Ouvrir sur {{brand}}", }, + checksOverview: { + headline: { + actionRequired: "Certaines vérifications demandent votre attention", + failure: "Certaines vérifications ont échoué", + pending: "Certaines vérifications ne sont pas terminées", + success: "Toutes les vérifications ont réussi", + none: "Aucune vérification", + }, + count: { + actionRequired: "{{count}} à traiter", + warning: "{{count}} avec avertissement", + failure: "{{count}} en échec", + pending: "{{count}} en cours", + manual: "{{count}} manuelle(s)", + success: "{{count}} réussie(s)", + ignored: "{{count}} ignorée(s)", + }, + detailOne: "Vérification : {{parts}}", + detailMany: "Vérifications : {{parts}}", + groupOne: { + actionRequired: "{{count}} vérification à traiter", + warning: "{{count}} vérification avec avertissement", + failure: "{{count}} vérification en échec", + pending: "{{count}} vérification en cours", + manual: "{{count}} vérification manuelle", + success: "{{count}} vérification réussie", + ignored: "{{count}} vérification ignorée", + }, + groupMany: { + actionRequired: "{{count}} vérifications à traiter", + warning: "{{count}} vérifications avec avertissement", + failure: "{{count}} vérifications en échec", + pending: "{{count}} vérifications en cours", + manual: "{{count}} vérifications manuelles", + success: "{{count}} vérifications réussies", + ignored: "{{count}} vérifications ignorées", + }, + }, checksSummary: { passedLabel: "réussies", failedLabel: "en échec", @@ -1115,6 +1156,13 @@ export const fr: TranslationResources = { }, }, sidebar: { + statusBucket: { + needsInput: "Attend une réponse", + failed: "Échec", + readyToReview: "À relire", + working: "En cours", + done: "Terminé", + }, display: { trigger: "Préférences d’affichage", heading: "Affichage", diff --git a/packages/app/src/i18n/resources/ja.ts b/packages/app/src/i18n/resources/ja.ts index 59ac1cc600..7d562247b2 100644 --- a/packages/app/src/i18n/resources/ja.ts +++ b/packages/app/src/i18n/resources/ja.ts @@ -387,6 +387,9 @@ export const ja: TranslationResources = { completed: "完了", }, }, + turnFooter: { + workedFor: "作業時間 {{duration}}", + }, compaction: { loading: "コンテキストを圧縮中...", auto: "コンテキストが自動的に圧縮されました", @@ -984,6 +987,44 @@ export const ja: TranslationResources = { viewPullRequest: "表示", openOn: "{{brand}}で開く", }, + checksOverview: { + headline: { + actionRequired: "対応が必要なチェックがあります", + failure: "失敗したチェックがあります", + pending: "完了していないチェックがあります", + success: "すべてのチェックに合格しました", + none: "チェックなし", + }, + count: { + actionRequired: "{{count}} 件要対応", + warning: "{{count}} 件警告", + failure: "{{count}} 件失敗", + pending: "{{count}} 件実行中", + manual: "{{count}} 件手動", + success: "{{count}} 件成功", + ignored: "{{count}} 件スキップ", + }, + detailOne: "チェック: {{parts}}", + detailMany: "チェック: {{parts}}", + groupOne: { + actionRequired: "要対応のチェック {{count}} 件", + warning: "警告のチェック {{count}} 件", + failure: "失敗したチェック {{count}} 件", + pending: "実行中のチェック {{count}} 件", + manual: "手動のチェック {{count}} 件", + success: "成功したチェック {{count}} 件", + ignored: "スキップされたチェック {{count}} 件", + }, + groupMany: { + actionRequired: "要対応のチェック {{count}} 件", + warning: "警告のチェック {{count}} 件", + failure: "失敗したチェック {{count}} 件", + pending: "実行中のチェック {{count}} 件", + manual: "手動のチェック {{count}} 件", + success: "成功したチェック {{count}} 件", + ignored: "スキップされたチェック {{count}} 件", + }, + }, checksSummary: { passedLabel: "成功", failedLabel: "失敗", @@ -1101,6 +1142,13 @@ export const ja: TranslationResources = { }, }, sidebar: { + statusBucket: { + needsInput: "入力待ち", + failed: "失敗", + readyToReview: "レビュー待ち", + working: "実行中", + done: "完了", + }, display: { trigger: "表示設定", heading: "表示", diff --git a/packages/app/src/i18n/resources/ko.ts b/packages/app/src/i18n/resources/ko.ts index 7528bd26c6..5b325a5be6 100644 --- a/packages/app/src/i18n/resources/ko.ts +++ b/packages/app/src/i18n/resources/ko.ts @@ -383,6 +383,9 @@ export const ko: TranslationResources = { completed: "완료됨", }, }, + turnFooter: { + workedFor: "작업 시간 {{duration}}", + }, compaction: { loading: "압축하는 중...", auto: "컨텍스트가 자동으로 압축되었습니다", @@ -978,6 +981,44 @@ export const ko: TranslationResources = { viewPullRequest: "보기", openOn: "{{brand}}에서 열기", }, + checksOverview: { + headline: { + actionRequired: "확인이 필요한 검사가 있습니다", + failure: "실패한 검사가 있습니다", + pending: "아직 완료되지 않은 검사가 있습니다", + success: "모든 검사를 통과했습니다", + none: "검사 없음", + }, + count: { + actionRequired: "{{count}}개 조치 필요", + warning: "{{count}}개 경고", + failure: "{{count}}개 실패", + pending: "{{count}}개 진행 중", + manual: "{{count}}개 수동", + success: "{{count}}개 성공", + ignored: "{{count}}개 건너뜀", + }, + detailOne: "검사: {{parts}}", + detailMany: "검사: {{parts}}", + groupOne: { + actionRequired: "조치 필요 검사 {{count}}개", + warning: "경고 검사 {{count}}개", + failure: "실패한 검사 {{count}}개", + pending: "진행 중인 검사 {{count}}개", + manual: "수동 검사 {{count}}개", + success: "성공한 검사 {{count}}개", + ignored: "건너뛴 검사 {{count}}개", + }, + groupMany: { + actionRequired: "조치 필요 검사 {{count}}개", + warning: "경고 검사 {{count}}개", + failure: "실패한 검사 {{count}}개", + pending: "진행 중인 검사 {{count}}개", + manual: "수동 검사 {{count}}개", + success: "성공한 검사 {{count}}개", + ignored: "건너뛴 검사 {{count}}개", + }, + }, checksSummary: { passedLabel: "통과", failedLabel: "실패", @@ -1095,6 +1136,13 @@ export const ko: TranslationResources = { }, }, sidebar: { + statusBucket: { + needsInput: "입력 필요", + failed: "실패", + readyToReview: "검토 대기", + working: "실행 중", + done: "완료", + }, display: { trigger: "표시 설정", heading: "표시", diff --git a/packages/app/src/i18n/resources/pt-BR.ts b/packages/app/src/i18n/resources/pt-BR.ts index 5726d75c8b..d86b0f58d3 100644 --- a/packages/app/src/i18n/resources/pt-BR.ts +++ b/packages/app/src/i18n/resources/pt-BR.ts @@ -386,6 +386,9 @@ export const ptBR: TranslationResources = { completed: "Concluída", }, }, + turnFooter: { + workedFor: "Trabalhou por {{duration}}", + }, compaction: { loading: "Compactando...", auto: "Contexto compactado automaticamente", @@ -994,6 +997,44 @@ export const ptBR: TranslationResources = { viewPullRequest: "Ver", openOn: "Abrir no {{brand}}", }, + checksOverview: { + headline: { + actionRequired: "Algumas verificações precisam da sua atenção", + failure: "Algumas verificações não foram bem-sucedidas", + pending: "Algumas verificações ainda não terminaram", + success: "Todas as verificações foram aprovadas", + none: "Nenhuma verificação", + }, + count: { + actionRequired: "{{count}} com ação pendente", + warning: "{{count}} com aviso", + failure: "{{count}} com falha", + pending: "{{count}} em andamento", + manual: "{{count}} manual(is)", + success: "{{count}} aprovada(s)", + ignored: "{{count}} ignorada(s)", + }, + detailOne: "Verificação: {{parts}}", + detailMany: "Verificações: {{parts}}", + groupOne: { + actionRequired: "{{count}} verificação com ação pendente", + warning: "{{count}} verificação com aviso", + failure: "{{count}} verificação com falha", + pending: "{{count}} verificação em andamento", + manual: "{{count}} verificação manual", + success: "{{count}} verificação aprovada", + ignored: "{{count}} verificação ignorada", + }, + groupMany: { + actionRequired: "{{count}} verificações com ação pendente", + warning: "{{count}} verificações com aviso", + failure: "{{count}} verificações com falha", + pending: "{{count}} verificações em andamento", + manual: "{{count}} verificações manuais", + success: "{{count}} verificações aprovadas", + ignored: "{{count}} verificações ignoradas", + }, + }, checksSummary: { passedLabel: "passou", failedLabel: "falhou", @@ -1114,6 +1155,13 @@ export const ptBR: TranslationResources = { }, }, sidebar: { + statusBucket: { + needsInput: "Precisa de resposta", + failed: "Com falha", + readyToReview: "Para revisar", + working: "Em execução", + done: "Concluído", + }, display: { trigger: "Preferências de exibição", heading: "Exibição", diff --git a/packages/app/src/i18n/resources/ru.ts b/packages/app/src/i18n/resources/ru.ts index 4f558ca79f..ac5ab8d2e0 100644 --- a/packages/app/src/i18n/resources/ru.ts +++ b/packages/app/src/i18n/resources/ru.ts @@ -386,6 +386,9 @@ export const ru: TranslationResources = { completed: "Завершена", }, }, + turnFooter: { + workedFor: "Время работы: {{duration}}", + }, compaction: { loading: "Сжатие контекста...", auto: "Контекст сжат автоматически", @@ -988,6 +991,44 @@ export const ru: TranslationResources = { viewPullRequest: "Просмотреть", openOn: "Открыть на {{brand}}", }, + checksOverview: { + headline: { + actionRequired: "Некоторые проверки требуют внимания", + failure: "Некоторые проверки не пройдены", + pending: "Некоторые проверки ещё не завершены", + success: "Все проверки пройдены", + none: "Нет проверок", + }, + count: { + actionRequired: "требует действий: {{count}}", + warning: "с предупреждением: {{count}}", + failure: "не пройдено: {{count}}", + pending: "выполняется: {{count}}", + manual: "вручную: {{count}}", + success: "пройдено: {{count}}", + ignored: "пропущено: {{count}}", + }, + detailOne: "Проверка ({{parts}})", + detailMany: "Проверки ({{parts}})", + groupOne: { + actionRequired: "Требует действий: {{count}}", + warning: "С предупреждением: {{count}}", + failure: "Не пройдена: {{count}}", + pending: "Выполняется: {{count}}", + manual: "Ручная: {{count}}", + success: "Пройдена: {{count}}", + ignored: "Пропущена: {{count}}", + }, + groupMany: { + actionRequired: "Требуют действий: {{count}}", + warning: "С предупреждением: {{count}}", + failure: "Не пройдены: {{count}}", + pending: "Выполняются: {{count}}", + manual: "Ручные: {{count}}", + success: "Пройдены: {{count}}", + ignored: "Пропущены: {{count}}", + }, + }, checksSummary: { passedLabel: "успешно", failedLabel: "ошибка", @@ -1105,6 +1146,13 @@ export const ru: TranslationResources = { }, }, sidebar: { + statusBucket: { + needsInput: "Ожидает ввода", + failed: "Ошибка", + readyToReview: "На проверку", + working: "В работе", + done: "Готово", + }, display: { trigger: "Настройки отображения", heading: "Отображение", diff --git a/packages/app/src/i18n/resources/zh-CN.ts b/packages/app/src/i18n/resources/zh-CN.ts index d1b5511dd6..7120b7d80e 100644 --- a/packages/app/src/i18n/resources/zh-CN.ts +++ b/packages/app/src/i18n/resources/zh-CN.ts @@ -382,6 +382,9 @@ export const zhCN: TranslationResources = { completed: "已完成", }, }, + turnFooter: { + workedFor: "工作了 {{duration}}", + }, compaction: { loading: "正在压缩...", auto: "上下文已自动压缩", @@ -963,6 +966,44 @@ export const zhCN: TranslationResources = { viewPullRequest: "查看", openOn: "在 {{brand}} 上打开", }, + checksOverview: { + headline: { + actionRequired: "部分检查需要你处理", + failure: "部分检查未通过", + pending: "部分检查尚未完成", + success: "所有检查均已通过", + none: "没有检查", + }, + count: { + actionRequired: "{{count}} 个需处理", + warning: "{{count}} 个警告", + failure: "{{count}} 个失败", + pending: "{{count}} 个进行中", + manual: "{{count}} 个手动", + success: "{{count}} 个成功", + ignored: "{{count}} 个已跳过", + }, + detailOne: "检查:{{parts}}", + detailMany: "检查:{{parts}}", + groupOne: { + actionRequired: "{{count}} 个需处理的检查", + warning: "{{count}} 个有警告的检查", + failure: "{{count}} 个失败的检查", + pending: "{{count}} 个进行中的检查", + manual: "{{count}} 个手动检查", + success: "{{count}} 个成功的检查", + ignored: "{{count}} 个已跳过的检查", + }, + groupMany: { + actionRequired: "{{count}} 个需处理的检查", + warning: "{{count}} 个有警告的检查", + failure: "{{count}} 个失败的检查", + pending: "{{count}} 个进行中的检查", + manual: "{{count}} 个手动检查", + success: "{{count}} 个成功的检查", + ignored: "{{count}} 个已跳过的检查", + }, + }, checksSummary: { passedLabel: "通过", failedLabel: "失败", @@ -1080,6 +1121,13 @@ export const zhCN: TranslationResources = { }, }, sidebar: { + statusBucket: { + needsInput: "需要输入", + failed: "失败", + readyToReview: "待查看", + working: "运行中", + done: "已完成", + }, display: { trigger: "显示偏好", heading: "显示", diff --git a/packages/app/src/utils/sidebar-shortcuts.test.ts b/packages/app/src/utils/sidebar-shortcuts.test.ts index 6b008e3dc6..ac9663c6da 100644 --- a/packages/app/src/utils/sidebar-shortcuts.test.ts +++ b/packages/app/src/utils/sidebar-shortcuts.test.ts @@ -1,3 +1,4 @@ +import { i18n } from "@/i18n/i18next"; import { describe, expect, it } from "vitest"; import type { SidebarProjectEntry, @@ -211,6 +212,7 @@ describe("buildStatusSidebarShortcutModel", () => { ["p1", "Project 1"], ["p2", "Project 2"], ]), + i18n.t, ), }); @@ -247,7 +249,7 @@ describe("buildStatusSidebarShortcutModel", () => { ]; const model = buildStatusSidebarShortcutModel({ - groups: buildStatusGroups(workspaces, new Map([["p1", "Project 1"]])), + groups: buildStatusGroups(workspaces, new Map([["p1", "Project 1"]]), i18n.t), collapsedStatusGroupKeys: new Set(["needs_input"]), });