From 8ae07cdf11fe5c742b5c11efccbf293e3857f4aa Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 3 Oct 2026 13:22:37 +0000 Subject: [PATCH 1/2] fix(web): use English Lingui sources for German UI text Several web surfaces hard-coded German (the project card, roster and activity status labels, the sidebar "Heute" label) or used German text as the Lingui source message (sidebar sections, project timeline, bot picker, voice entry, group stage, copy and read-aloud buttons). English users saw German, and because production builds keep only message ids, none of these strings could be translated per locale. Convert them to English source messages, keeping the meaning: - ProjectCreatedCard: Lingui Trans/Plural instead of literals. - agent-activity: labels go through i18n._ with literal ids so the module stays importable from plain unit tests; the extractor still collects them. - Shell, ProjectSessionTimeline, bot-picker, StageVoiceEntry, OoStage: English sources, reusing existing messages where one already fit ("just now", "{n}m ago", "Search", "No bots match", "Set up voice", "Group conversation", "Transcript"). - The conversation picker matches its create entry against the translated label instead of a fixed German string. The German wording returns through the de catalog in the next commit. Unit and E2E tests now assert the English labels. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u --- apps/web/e2e/empty-project-starten.spec.ts | 10 ++-- apps/web/e2e/heute-brief.spec.ts | 2 +- apps/web/e2e/sidebar-nav.spec.ts | 11 ++--- apps/web/e2e/smoke.spec.ts | 4 +- apps/web/e2e/spaces.spec.ts | 8 ++-- apps/web/e2e/unterhaltungen-sidebar.spec.ts | 6 +-- .../src/components/ai/ProjectCreatedCard.tsx | 11 +++-- apps/web/src/components/stage/OoStage.tsx | 4 +- .../src/components/stage/StageVoiceEntry.tsx | 8 ++-- apps/web/src/lib/agent-activity.test.ts | 20 +++++--- apps/web/src/lib/agent-activity.ts | 36 +++++++------- apps/web/src/pages/ProjectSessionTimeline.tsx | 47 +++++++++--------- apps/web/src/pages/Shell.tsx | 48 ++++++++----------- apps/web/src/pages/shell/bot-picker.tsx | 18 +++---- 14 files changed, 120 insertions(+), 113 deletions(-) diff --git a/apps/web/e2e/empty-project-starten.spec.ts b/apps/web/e2e/empty-project-starten.spec.ts index dcf80dc..17d7a16 100644 --- a/apps/web/e2e/empty-project-starten.spec.ts +++ b/apps/web/e2e/empty-project-starten.spec.ts @@ -14,7 +14,7 @@ async function projectSidebarStart(projekte: Locator, name: string) { return projekte.getByTestId(`project-start-${spaceId}`); } -test("empty project shows Starten instead of a fallback bot", async ({ page }) => { +test("empty project shows Start instead of a fallback bot", async ({ page }) => { const stamp = Date.now(); await signup(page, `empty-project-${stamp}@rakazo.test`, "password12", "Empty Project"); await completeOnboarding(page); @@ -29,14 +29,14 @@ test("empty project shows Starten instead of a fallback bot", async ({ page }) = await page.waitForURL(/\/app(?:\/)?$/); await expect(projekte.getByRole("button", { name: /^Chief/ })).toHaveCount(0); - await expect(projekte.getByRole("button", { name: "Starten", exact: true })).toBeVisible(); + await expect(projekte.getByRole("button", { name: "Start", exact: true })).toBeVisible(); await expect(page.getByTestId("project-start-main")).toBeVisible(); await page.getByTestId("project-start-main").click(); await expect(page.getByTestId("side-panel")).toHaveAttribute("data-panel", "create"); }); -test("Starten in an onboarded project opens the group create flow", async ({ page }) => { +test("Start in an onboarded project opens the group create flow", async ({ page }) => { const stamp = Date.now(); await signup(page, `project-start-${stamp}@rakazo.test`, "password12", "Project Start"); await completeOnboarding(page); @@ -70,7 +70,7 @@ test("Starten in an onboarded project opens the group create flow", async ({ pag ).toBeVisible(); }); -test("archiving the only bot keeps Starten visible in the project sidebar", async ({ page }) => { +test("archiving the only bot keeps Start visible in the project sidebar", async ({ page }) => { const stamp = Date.now(); await signup(page, `project-archive-${stamp}@rakazo.test`, "password12", "Project Archive"); await completeOnboarding(page); @@ -93,6 +93,6 @@ test("archiving the only bot keeps Starten visible in the project sidebar", asyn await page.goto("/app"); await page.waitForURL(/\/app(?:\/)?$/); - await expect(projekte.getByRole("button", { name: "Starten", exact: true })).toBeVisible(); + await expect(projekte.getByRole("button", { name: "Start", exact: true })).toBeVisible(); await expect(page.getByTestId("project-start-main")).toBeVisible(); }); diff --git a/apps/web/e2e/heute-brief.spec.ts b/apps/web/e2e/heute-brief.spec.ts index 28bf669..6347490 100644 --- a/apps/web/e2e/heute-brief.spec.ts +++ b/apps/web/e2e/heute-brief.spec.ts @@ -6,7 +6,7 @@ test("Heute shows native brief instead of Mission Control iframe", async ({ page await signup(page, `heute-${stamp}@rakazo.test`, "password12", "Heute"); await completeOnboarding(page); - await page.getByRole("button", { name: "Heute", exact: true }).click(); + await page.getByRole("button", { name: "Today", exact: true }).click(); await expect(page).toHaveURL(/\/app\/[^/]+$/); const brief = page.getByTestId("heute-brief"); diff --git a/apps/web/e2e/sidebar-nav.spec.ts b/apps/web/e2e/sidebar-nav.spec.ts index 95e3724..1321ae8 100644 --- a/apps/web/e2e/sidebar-nav.spec.ts +++ b/apps/web/e2e/sidebar-nav.spec.ts @@ -21,8 +21,8 @@ test("sidebar has expected navigation items and no integrations", async ({ page // Actual sidebar structure (verified by snapshot): // - button "Activity" (top action) // - textbox "Search" (search input) - // - button "Heute" (today) - // - text "Sprich mit deinen Oo's" (conversations section header) + // - button "Today" + // - text "Talk to your Oos" (conversations section header) // - status "Not configured" (trading bot URL unset) // - NO "Integration(en)" button or text @@ -30,12 +30,11 @@ test("sidebar has expected navigation items and no integrations", async ({ page const searchInput = sidebar.locator('input[placeholder="Search"], textbox[name="Search"]'); await expect(searchInput).toBeVisible({ timeout: 5000 }); - // "Heute" (Today) - const todayBtn = sidebar.getByRole("button", { name: "Heute", exact: true }); + const todayBtn = sidebar.getByRole("button", { name: "Today", exact: true }); await expect(todayBtn).toBeVisible({ timeout: 5000 }); - // "Sprich mit deinen Oo's" is plain text, not a button - const conversationsText = sidebar.getByText("Sprich mit deinen Oo's"); + // "Talk to your Oos" is plain text, not a button + const conversationsText = sidebar.getByText("Talk to your Oos"); await expect(conversationsText).toBeVisible({ timeout: 5000 }); // Trading mode comes from the bot API. With no URL configured the sidebar diff --git a/apps/web/e2e/smoke.spec.ts b/apps/web/e2e/smoke.spec.ts index 6d634bd..d5228e9 100644 --- a/apps/web/e2e/smoke.spec.ts +++ b/apps/web/e2e/smoke.spec.ts @@ -41,8 +41,8 @@ test("app loads without React hook errors", async ({ page }) => { bodyText.includes("Sign in") || bodyText.includes("Anmelden") || bodyText.includes("EvOo") || - bodyText.includes("Projekte") || - bodyText.includes("Heute") || + bodyText.includes("Projects") || + bodyText.includes("Today") || bodyText.includes("Team"); expect(hasContent).toBe(true); diff --git a/apps/web/e2e/spaces.spec.ts b/apps/web/e2e/spaces.spec.ts index d3b1dc6..5b29d7f 100644 --- a/apps/web/e2e/spaces.spec.ts +++ b/apps/web/e2e/spaces.spec.ts @@ -15,7 +15,7 @@ function projectSpace(projekte: Locator, name: string) { }); } -test("Projekte + opens new project dialog instead of group form", async ({ page }) => { +test("Projects + opens new project dialog instead of group form", async ({ page }) => { const stamp = Date.now(); await signup(page, `proj-create-${stamp}@rakazo.test`, "password12", "Project Create"); await completeOnboarding(page); @@ -42,7 +42,7 @@ test("Projekte + opens new project dialog instead of group form", async ({ page 0, ); await expect(page.getByTestId("project-start-main")).toBeVisible(); - await expect(page.getByTestId("project-start-main")).toHaveText(/Starten/); + await expect(page.getByTestId("project-start-main")).toHaveText(/Start/); }); test("spaces stay invisible by default and chat creation requires approval", async ({ @@ -104,7 +104,7 @@ test("spaces stay invisible by default and chat creation requires approval", asy await expect(projekte.getByText("Customer support", { exact: true })).toBeVisible(); await expect(unterhaltungenOnly(page).getByRole("button", { name: /^Chief/ })).toHaveCount(1); await expect(projekte.getByRole("button", { name: /^Chief/ })).toHaveCount(0); - await expect(projekte.getByRole("button", { name: "Starten", exact: true })).toBeVisible(); + await expect(projekte.getByRole("button", { name: "Start", exact: true })).toBeVisible(); await captureScreenshot(page, testInfo, "spaces-sidebar"); await page.evaluate((id) => { @@ -135,7 +135,7 @@ test("a new space opens empty and can be deleted from its menu", async ({ page } await expect(projekte.getByText("Temporary", { exact: true })).toBeVisible(); await expect(unterhaltungenOnly(page).getByRole("button", { name: /^Chief/ })).toHaveCount(1); await expect(projekte.getByRole("button", { name: /^Chief/ })).toHaveCount(0); - await expect(projekte.getByRole("button", { name: "Starten", exact: true })).toBeVisible(); + await expect(projekte.getByRole("button", { name: "Start", exact: true })).toBeVisible(); expect(await rpc(page, "bots/list", {})).toEqual([]); await captureScreenshot(page, testInfo, "new-space-empty"); diff --git a/apps/web/e2e/unterhaltungen-sidebar.spec.ts b/apps/web/e2e/unterhaltungen-sidebar.spec.ts index fbe99e4..ec03c7f 100644 --- a/apps/web/e2e/unterhaltungen-sidebar.spec.ts +++ b/apps/web/e2e/unterhaltungen-sidebar.spec.ts @@ -1,7 +1,7 @@ import { expect, test } from "@playwright/test"; import { completeOnboarding, openNewBot, signup, unterhaltungenOnly } from "./helpers"; -test("Unterhaltungen and Projekte stay separate; + opens bot picker", async ({ page }) => { +test("conversations and projects stay separate; + opens bot picker", async ({ page }) => { const stamp = Date.now(); await signup(page, `unterhaltungen-${stamp}@rakazo.test`, "password12", "Unterhaltungen UX"); await completeOnboarding(page); @@ -11,8 +11,8 @@ test("Unterhaltungen and Projekte stay separate; + opens bot picker", async ({ p const unterhaltungen = page.locator(".sidebar-unterhaltungen"); const projekte = page.locator(".sidebar-projekte"); - await expect(unterhaltungen.getByText("Sprich mit deinen Oo's", { exact: true })).toBeVisible(); - await expect(projekte.getByText("Projekte", { exact: true })).toBeVisible(); + await expect(unterhaltungen.getByText("Talk to your Oos", { exact: true })).toBeVisible(); + await expect(projekte.getByText("Projects", { exact: true })).toBeVisible(); await expect(unterhaltungen.getByRole("button", { name: /^Chief/ })).toBeVisible(); await unterhaltungen.getByTestId("conversation-picker-trigger").click(); diff --git a/apps/web/src/components/ai/ProjectCreatedCard.tsx b/apps/web/src/components/ai/ProjectCreatedCard.tsx index d6a6047..831e8c6 100644 --- a/apps/web/src/components/ai/ProjectCreatedCard.tsx +++ b/apps/web/src/components/ai/ProjectCreatedCard.tsx @@ -1,3 +1,4 @@ +import { Plural, Trans } from "@lingui/react/macro"; import { FolderKanban } from "lucide-react"; export interface ProjectCreatedCardProps { @@ -22,17 +23,19 @@ export function ProjectCreatedCard({ name, memberCount, onClick }: ProjectCreate
{name}
- Projekt + Project - Erstellt + Created

- {memberCount} Agent{memberCount === 1 ? "" : "en"} eingeladen +

-
Projekt öffnen →
+
+ Open project → +
); diff --git a/apps/web/src/components/stage/OoStage.tsx b/apps/web/src/components/stage/OoStage.tsx index 1fa1f26..a4ae59e 100644 --- a/apps/web/src/components/stage/OoStage.tsx +++ b/apps/web/src/components/stage/OoStage.tsx @@ -421,7 +421,7 @@ export function OoGroupStage({ if (sparkLabel && groupSpark?.kind === "peer-reply") return sparkLabel; if (awarenessLabel) return awarenessLabel; if (sparkLabel) return sparkLabel; - if (!speaker) return t`Gruppenunterhaltung`; + if (!speaker) return t`Group conversation`; if (running && workingBots.length > 1) { return t`${workingBots.length} Oos are working — ${speaker.name} has the floor`; } @@ -495,7 +495,7 @@ export function OoGroupStage({

{groupName ?? t`Group`}

-

{t`Gruppenunterhaltung`}

+

{t`Group conversation`}

{awarenessLabel && awarenessEvent && speaker ? ( diff --git a/apps/web/src/components/stage/StageVoiceEntry.tsx b/apps/web/src/components/stage/StageVoiceEntry.tsx index 14c4088..dd32f8b 100644 --- a/apps/web/src/components/stage/StageVoiceEntry.tsx +++ b/apps/web/src/components/stage/StageVoiceEntry.tsx @@ -21,8 +21,8 @@ export function StageVoiceEntry({ variant="outline" onClick={onStartVoice} data-testid="stage-voice-primary" - aria-label={voiceReady ? t`Sprechen starten` : t`Sprache einrichten`} - title={voiceReady ? t`Sprechen starten` : t`Sprache einrichten`} + aria-label={voiceReady ? t`Start talking` : t`Set up voice`} + title={voiceReady ? t`Start talking` : t`Set up voice`} className={`tactile-press h-control-lg w-full max-w-sm gap-2.5 rounded-full px-6 text-ui-body font-semibold transition-all ${ voiceReady ? "relative overflow-hidden border-border bg-foreground/5 text-foreground hover:bg-foreground/10" @@ -33,10 +33,10 @@ export function StageVoiceEntry({ ) : null}