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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
230 lines
8.5 KiB
TypeScript
230 lines
8.5 KiB
TypeScript
import { Trans, useLingui } from "@lingui/react/macro";
|
|
import type { Bot } from "@rakazo/contracts";
|
|
import {
|
|
BotAvatar,
|
|
Command,
|
|
CommandEmpty,
|
|
CommandGroup,
|
|
CommandItem,
|
|
CommandList,
|
|
CommandSeparator,
|
|
} from "@rakazo/ui-web";
|
|
import { Info, Lock, Plus, Users } from "lucide-react";
|
|
import { useMemo, useState } from "react";
|
|
|
|
/** 1:1 conversation: open an existing bot or create a new one. */
|
|
export function ConversationBotPicker({
|
|
bots,
|
|
onCreateBot,
|
|
onOpenBot,
|
|
}: {
|
|
bots: Bot[];
|
|
onCreateBot: () => void;
|
|
onOpenBot: (botId: string) => void;
|
|
}) {
|
|
const { t } = useLingui();
|
|
const [query, setQuery] = useState("");
|
|
const needle = query.trim().toLowerCase();
|
|
const matched = useMemo(() => {
|
|
if (!needle) return bots;
|
|
return bots.filter(
|
|
(bot) => bot.name.toLowerCase().includes(needle) || bot.title.toLowerCase().includes(needle),
|
|
);
|
|
}, [bots, needle]);
|
|
const createLabel = t`Create new Bot`;
|
|
const createText = createLabel.toLowerCase();
|
|
const showCreate =
|
|
!needle ||
|
|
createText.includes(needle) ||
|
|
needle.split(/\s+/).every((part) => createText.includes(part));
|
|
|
|
return (
|
|
<div data-testid="conversation-bot-picker" className="w-[min(320px,calc(100vw-2rem))]">
|
|
<label className="flex items-center gap-2 border-b border-border px-3 py-2">
|
|
<span className="shrink-0 text-ui-body-sm text-muted-foreground">
|
|
<Trans>With:</Trans>
|
|
</span>
|
|
<input
|
|
value={query}
|
|
onChange={(event) => setQuery(event.target.value)}
|
|
placeholder={t`Search`}
|
|
aria-label={t`Search`}
|
|
className="min-w-0 flex-1 bg-transparent text-ui-body text-foreground outline-none placeholder:text-muted-foreground"
|
|
/>
|
|
</label>
|
|
<Command shouldFilter={false} className="rounded-none border-0 bg-transparent p-0">
|
|
<CommandList className="max-h-72 p-1">
|
|
<CommandEmpty>
|
|
<Trans>No bots match</Trans>
|
|
</CommandEmpty>
|
|
<CommandGroup>
|
|
{showCreate ? (
|
|
<CommandItem
|
|
value="create-new-bot"
|
|
data-testid="conversation-create-bot"
|
|
onSelect={() => onCreateBot()}
|
|
className="gap-2"
|
|
>
|
|
<Plus strokeWidth={1.8} aria-hidden="true" className="size-icon-md" />
|
|
{createLabel}
|
|
</CommandItem>
|
|
) : null}
|
|
{matched.map((bot) => (
|
|
<CommandItem
|
|
key={bot.id}
|
|
value={bot.id}
|
|
data-testid={`conversation-picker-bot-${bot.id}`}
|
|
onSelect={() => onOpenBot(bot.id)}
|
|
className="gap-2"
|
|
>
|
|
<BotAvatar color={bot.color} identity={bot.id} size={22} status={bot.status} />
|
|
<span className="min-w-0 flex-1 truncate">{bot.name}</span>
|
|
</CommandItem>
|
|
))}
|
|
</CommandGroup>
|
|
</CommandList>
|
|
</Command>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function BotCreatePicker({
|
|
bots,
|
|
onCreateBot,
|
|
onOpenBot,
|
|
onCreateGroup,
|
|
onCreateSpace,
|
|
onShowGroupInfo,
|
|
onShowSpaceInfo,
|
|
}: {
|
|
bots: Bot[];
|
|
onCreateBot: () => void;
|
|
onOpenBot: (botId: string) => void;
|
|
onCreateGroup: () => void;
|
|
onCreateSpace: () => void;
|
|
onShowGroupInfo: () => void;
|
|
onShowSpaceInfo: () => void;
|
|
}) {
|
|
const { t } = useLingui();
|
|
const [query, setQuery] = useState("");
|
|
const needle = query.trim().toLowerCase();
|
|
const matched = useMemo(() => {
|
|
if (!needle) return bots;
|
|
return bots.filter(
|
|
(bot) => bot.name.toLowerCase().includes(needle) || bot.title.toLowerCase().includes(needle),
|
|
);
|
|
}, [bots, needle]);
|
|
const showCreate =
|
|
!needle ||
|
|
"create new bot".includes(needle) ||
|
|
needle.split(/\s+/).every((part) => "create new bot".includes(part));
|
|
|
|
return (
|
|
<div data-testid="bot-create-picker" className="w-[min(320px,calc(100vw-2rem))]">
|
|
<label className="flex items-center gap-2 border-b border-border px-3 py-2">
|
|
<span className="shrink-0 text-ui-body-sm text-muted-foreground">
|
|
<Trans>To:</Trans>
|
|
</span>
|
|
<input
|
|
value={query}
|
|
onChange={(event) => setQuery(event.target.value)}
|
|
placeholder={t`Search`}
|
|
aria-label={t`Search`}
|
|
className="min-w-0 flex-1 bg-transparent text-ui-body text-foreground outline-none placeholder:text-muted-foreground"
|
|
/>
|
|
</label>
|
|
<Command shouldFilter={false} className="rounded-none border-0 bg-transparent p-0">
|
|
<CommandList className="max-h-72 p-1">
|
|
<CommandEmpty>
|
|
<Trans>No bots match</Trans>
|
|
</CommandEmpty>
|
|
<CommandGroup>
|
|
{showCreate ? (
|
|
<CommandItem
|
|
value="create-new-bot"
|
|
data-testid="create-new-bot"
|
|
onSelect={() => onCreateBot()}
|
|
className="gap-2"
|
|
>
|
|
<Plus strokeWidth={1.8} aria-hidden="true" className="size-icon-md" />
|
|
<Trans>Create new Bot</Trans>
|
|
</CommandItem>
|
|
) : null}
|
|
{matched.map((bot) => (
|
|
<CommandItem
|
|
key={bot.id}
|
|
value={bot.id}
|
|
data-testid={`picker-bot-${bot.id}`}
|
|
onSelect={() => onOpenBot(bot.id)}
|
|
className="gap-2"
|
|
>
|
|
<BotAvatar color={bot.color} identity={bot.id} size={22} status={bot.status} />
|
|
<span className="min-w-0 flex-1 truncate">{bot.name}</span>
|
|
</CommandItem>
|
|
))}
|
|
</CommandGroup>
|
|
<CommandSeparator />
|
|
<CommandGroup>
|
|
<CommandItem
|
|
value="create-group"
|
|
data-testid="create-new-group"
|
|
onSelect={() => onCreateGroup()}
|
|
className="gap-2"
|
|
>
|
|
<Users strokeWidth={1.8} aria-hidden="true" className="size-icon-md" />
|
|
<span className="min-w-0 flex-1 truncate">
|
|
<Trans>Create new Group</Trans>
|
|
</span>
|
|
<button
|
|
type="button"
|
|
data-testid="picker-info-group"
|
|
aria-label={t`About groups`}
|
|
title={t`About groups`}
|
|
onPointerDown={(event) => event.stopPropagation()}
|
|
onMouseDown={(event) => event.stopPropagation()}
|
|
onClick={(event) => {
|
|
event.stopPropagation();
|
|
event.preventDefault();
|
|
onShowGroupInfo();
|
|
}}
|
|
onKeyDown={(event) => event.stopPropagation()}
|
|
className="-mr-2 shrink-0 rounded p-1 text-muted-foreground/70 opacity-0 transition-opacity duration-150 group-focus-within/command-item:opacity-100 group-hover/command-item:opacity-100 hover:text-foreground focus-visible:opacity-100 focus-visible:outline-2 focus-visible:outline-ring"
|
|
>
|
|
<Info strokeWidth={1.8} aria-hidden="true" className="size-icon-md" />
|
|
</button>
|
|
</CommandItem>
|
|
<CommandItem
|
|
value="create-space"
|
|
data-testid="create-new-space"
|
|
onSelect={() => onCreateSpace()}
|
|
className="gap-2"
|
|
>
|
|
<Lock strokeWidth={1.8} aria-hidden="true" className="size-icon-md" />
|
|
<span className="min-w-0 flex-1 truncate">
|
|
<Trans>Create new Space</Trans>
|
|
</span>
|
|
<button
|
|
type="button"
|
|
data-testid="picker-info-space"
|
|
aria-label={t`About spaces`}
|
|
title={t`About spaces`}
|
|
onPointerDown={(event) => event.stopPropagation()}
|
|
onMouseDown={(event) => event.stopPropagation()}
|
|
onClick={(event) => {
|
|
event.stopPropagation();
|
|
event.preventDefault();
|
|
onShowSpaceInfo();
|
|
}}
|
|
onKeyDown={(event) => event.stopPropagation()}
|
|
className="-mr-2 shrink-0 rounded p-1 text-muted-foreground/70 opacity-0 transition-opacity duration-150 group-focus-within/command-item:opacity-100 group-hover/command-item:opacity-100 hover:text-foreground focus-visible:opacity-100 focus-visible:outline-2 focus-visible:outline-ring"
|
|
>
|
|
<Info strokeWidth={1.8} aria-hidden="true" className="size-icon-md" />
|
|
</button>
|
|
</CommandItem>
|
|
</CommandGroup>
|
|
</CommandList>
|
|
</Command>
|
|
</div>
|
|
);
|
|
}
|