Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/localize-core-agent-chat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@agent-native/core": minor
"@agent-native/toolkit": minor
---

Localize the Core agent-chat interface and Toolkit composer across every supported locale, provide built-in Core translations with app-level catalog overrides, and guard the complete chat surface against new raw visible strings.
23 changes: 13 additions & 10 deletions packages/core/src/client/AgentPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,7 @@ function ChatLoadingSkeleton({
composerAreaClassName?: string;
composerLayoutVariant?: AssistantChatProps["composerLayoutVariant"];
}) {
const t = useT();
// Provide empty no-op implementations so renderHeader can render the real
// tab/mode buttons without needing actual chat state.
const noop = useCallback(() => {}, []);
Expand Down Expand Up @@ -354,7 +355,7 @@ function ChatLoadingSkeleton({
>
<div className="agent-chat-scroll flex-1 overflow-y-auto overflow-x-hidden min-h-0">
<div className="agent-empty-state sr-only" aria-busy="true">
Loading chat...
{t("agentChat.empty.loadingChat")}
</div>
</div>
{composerSlot}
Expand Down Expand Up @@ -1311,7 +1312,7 @@ function AgentPanelInner({
<ShareButton
resourceType="chat_thread"
resourceId={activeTab.id}
resourceTitle={activeTab.label || "Chat"}
resourceTitle={activeTab.label || t("agentPanel.chat")}
shareUrl={getChatThreadShareUrl(activeTab.id)}
trigger="icon"
triggerClassName="h-7 w-7"
Expand Down Expand Up @@ -1623,7 +1624,7 @@ function AgentPanelInner({
<ShareButton
resourceType="chat_thread"
resourceId={activeTab.id}
resourceTitle={activeTab.label || "Chat"}
resourceTitle={activeTab.label || t("agentPanel.chat")}
shareUrl={getChatThreadShareUrl(activeTab.id)}
trigger="icon"
triggerClassName="h-8 w-8 border border-border bg-background/95 shadow-sm backdrop-blur hover:bg-accent"
Expand Down Expand Up @@ -1753,7 +1754,7 @@ function AgentPanelInner({
)}
<button
type="button"
aria-label="Close tab"
aria-label={t("agentPanel.closeTab")}
onClick={(e) => {
e.stopPropagation();
closeTab(tab.id);
Expand Down Expand Up @@ -1851,7 +1852,7 @@ function AgentPanelInner({
)}
<button
type="button"
aria-label="Close tab"
aria-label={t("agentPanel.closeTab")}
onClick={(e) => {
e.stopPropagation();
closeTab(tab.id);
Expand Down Expand Up @@ -1898,7 +1899,7 @@ function AgentPanelInner({
<span>Terminal {i + 1}</span>
<button
type="button"
aria-label="Close tab"
aria-label={t("agentPanel.closeTab")}
onClick={(e) => {
e.stopPropagation();
closeCliTab(id);
Expand Down Expand Up @@ -1977,7 +1978,7 @@ function AgentPanelInner({
)}
<button
type="button"
aria-label="Close tab"
aria-label={t("agentPanel.closeTab")}
onClick={(e) => {
e.stopPropagation();
closeTab(tab.id);
Expand Down Expand Up @@ -2023,6 +2024,7 @@ function AgentPanelInner({
activeTabRefCb,
activateOnKeyDown,
closeCliTab,
t,
],
);

Expand Down Expand Up @@ -2159,7 +2161,7 @@ function AgentPanelInner({
<Suspense
fallback={
<div className="flex items-center justify-center h-full text-muted-foreground text-sm">
Loading terminal...
{t("agentPanel.loadingTerminal")}
</div>
}
>
Expand Down Expand Up @@ -3689,6 +3691,7 @@ export function focusAgentChat() {
* Dispatches a custom event that AgentSidebar listens for.
*/
export function AgentToggleButton({ className }: { className?: string }) {
const t = useT();
const [open, setOpen] = useState(false);
useEffect(() => {
const handler = (event: Event) => {
Expand All @@ -3708,7 +3711,7 @@ export function AgentToggleButton({ className }: { className?: string }) {
trigger={
<button
type="button"
aria-label="Toggle agent"
aria-label={t("agentPanel.toggleAgent")}
onClick={() => window.dispatchEvent(new Event("agent-panel:toggle"))}
className={cn(
"inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent/40 hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
Expand All @@ -3718,7 +3721,7 @@ export function AgentToggleButton({ className }: { className?: string }) {
<IconMessageDots size={20} aria-hidden />
</button>
}
content="Toggle agent"
content={t("agentPanel.toggleAgent")}
delayMs={200}
/>
);
Expand Down
40 changes: 36 additions & 4 deletions packages/core/src/client/AssistantChat.display.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1343,7 +1343,7 @@ describe("missing agent engine setup", () => {
expect(source).toContain(
"if (!hideUserMessage) resumeFollowingRef.current()",
);
expect(source).toContain('aria-label="Scroll to bottom"');
expect(source).toContain('"agentChat.composer.scrollToBottom"');
expect(source).toContain("autoScroll={false}");
expect(source).not.toContain("scrollAnchor");
expect(source).toContain("composerContextItems.length > 0");
Expand All @@ -1356,7 +1356,7 @@ describe("missing agent engine setup", () => {
expect(source).toContain("willQueue={engineSetupRequired || isRunning}");
expect(source).toContain("<BuilderSetupCard");
expect(source).toContain("showInlineMissingKeySetup");
expect(source).toContain("Connect AI above to start chatting...");
expect(source).toContain('"agentChat.setup.connectPlaceholder"');
expect(source).toContain('className="agent-composer-missing-key-trigger"');
expect(source).toContain('className="agent-composer-missing-key-cta"');
expect(source).toContain("<BuilderSetupContent");
Expand Down Expand Up @@ -1678,15 +1678,42 @@ describe("resolveAssistantChatRunningStatusLabel", () => {
).toBe("Preparing generate-design action");
});

it("localizes Core-owned activity labels while preserving the tool name", () => {
expect(
resolveAssistantChatRunningStatusLabel({
runningActivityLabel: "Preparing generate-design...",
isAutoResuming: false,
isReconnecting: false,
hasReconnectContent: false,
labels: {
thinking: "Denkt nach",
resuming: "Wird fortgesetzt",
stillWorking: "Arbeitet weiter",
working: "Arbeitet",
contactingModel: "Modell wird kontaktiert",
starting: (activity) => `${activity} wird gestartet...`,
preparing: (activity) => `${activity} wird vorbereitet...`,
writing: (activity) => `${activity} wird geschrieben...`,
stillGenerating: (activity) => `${activity} wird weiterhin generiert`,
},
}),
).toBe("generate-design wird vorbereitet...");
});

it("shows replayed recovery as still working instead of reconnecting", () => {
expect(
resolveAssistantChatRunningStatusLabel({
runningActivityLabel: null,
isAutoResuming: false,
isReconnecting: true,
hasReconnectContent: true,
labels: {
thinking: "Denkt nach",
resuming: "Wird fortgesetzt",
stillWorking: "Arbeitet weiter",
},
}),
).toBe("Still working");
).toBe("Arbeitet weiter");
});

it("keeps bare reconnect recovery as thinking", () => {
Expand All @@ -1696,8 +1723,13 @@ describe("resolveAssistantChatRunningStatusLabel", () => {
isAutoResuming: false,
isReconnecting: true,
hasReconnectContent: false,
labels: {
thinking: "Denkt nach",
resuming: "Wird fortgesetzt",
stillWorking: "Arbeitet weiter",
},
}),
).toBe("Thinking");
).toBe("Denkt nach");
});
});

Expand Down
Loading
Loading