diff --git a/.changeset/ai-model-status-summary-i18n.md b/.changeset/ai-model-status-summary-i18n.md new file mode 100644 index 000000000..ada75a889 --- /dev/null +++ b/.changeset/ai-model-status-summary-i18n.md @@ -0,0 +1,45 @@ +--- +"@object-ui/i18n": patch +"@object-ui/app-shell": patch +--- + +fix(i18n): compose the AI-model diagnostics summary client-side instead of rendering the server's English string (objectui#2886) + +`CloudAiModelStatus` rendered `report.summary` verbatim — the most prominent +line on the panel, in English for every locale. + +Reading `objectstack-ai/cloud` settled how to fix it. The server **cannot** +localize that string as currently built: + +- `service-ai/src/effective-model.ts:117` assembles it as a hard-coded English + template literal, with no locale parameter; +- `service-ai/src/routes/ai-routes.ts:395` declares `handler: async () => …` — + it takes **no request argument**, so it cannot read `Accept-Language` even + though `createAuthenticatedFetch` has been sending it since objectui#1319. + +But no server change is needed, because every ingredient of the sentence is +already in the structured payload: `conversational.model`, +`conversational.source`, `structured.model`, `structured.pinned`, and +`routing.{free,paid}`. The issue proposed "return structured data instead of a +sentence" as the better fix — the server was already doing that; the client +just wasn't using it. + +The panel now composes the line from those fields. `sourceLabel()` already +produced exactly the two clauses the server hand-rolls — "pinned by X" / +"code default (no env override)", and "same as build/ask" for an unpinned +structured model — so no new source vocabulary was required. + +**A dropped diagnostic, not just untranslated text.** The client's +`EffectiveModelReport` never declared `routing`, which the server has always +sent conditionally. Its only appearance anywhere was inside the English summary, +so non-English admins could not see the plan→model routing policy **at all**. +It is now declared and surfaced. + +Also fixed: `attributeSource` emits the bare token `'unknown'` when the adapter +cannot report a model, and `sourceLabel` fell through to rendering it raw. + +Four keys added to all ten packs (`summary`, `summaryRouting`, `modelUnknown`, +`sourceUnknown`), so the full-parity guard from objectui#2909 stays green. + +The panel had **no test coverage at all**; it now has five, mutation-tested by +restoring `
{report.summary}
` — which fails four of them. diff --git a/packages/app-shell/src/console/diagnostics/CloudAiModelStatus.test.tsx b/packages/app-shell/src/console/diagnostics/CloudAiModelStatus.test.tsx new file mode 100644 index 000000000..54826090e --- /dev/null +++ b/packages/app-shell/src/console/diagnostics/CloudAiModelStatus.test.tsx @@ -0,0 +1,119 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * The AI-model diagnostics panel composes its own summary line (objectui#2886). + * + * `service-ai` builds `report.summary` as a hard-coded English template literal + * and its route handler takes no request argument, so it cannot honour + * `Accept-Language` even though the client has been sending it since + * objectui#1319. Every ingredient of that sentence is already in the structured + * fields, so the panel assembles a localized equivalent and ignores `summary`. + * + * These tests pin the two things that regressed before: the English string must + * not reach the DOM, and `routing` — which the server always sent but the client + * never declared — must be surfaced. + */ +import '@testing-library/jest-dom/vitest'; +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { render, screen, waitFor, cleanup } from '@testing-library/react'; +import { I18nProvider, createI18n } from '@object-ui/i18n'; + +vi.mock('@object-ui/auth', () => ({ + createAuthenticatedFetch: () => vi.fn().mockImplementation(() => Promise.resolve(mockResponse())), +})); + +let payload: Record{report.summary}
` put this verbatim on screen for + // every locale. + expect(line.textContent).not.toBe(SERVER_SUMMARY); + expect(document.body.textContent).not.toContain('build/ask uses'); + }); + + it('surfaces the routing policy the client type used to drop', async () => { + payload = baseReport({ routing: { free: 'gpt-5-mini', paid: 'gpt-5' } }); + renderPanel('en'); + const line = await screen.findByTestId('ai-model-summary'); + // Routing only ever appeared inside the English summary, so non-English + // admins could not see it at all. + expect(line).toHaveTextContent(/Routing policy/i); + expect(line).toHaveTextContent('gpt-5-mini'); + }); + + it('omits the routing clause when the host does not inject one', async () => { + renderPanel('en'); + const line = await screen.findByTestId('ai-model-summary'); + expect(line).not.toHaveTextContent(/Routing policy/i); + }); + + it('translates the summary — a zh render shares no sentence text with en', async () => { + payload = baseReport({ routing: { free: 'gpt-5-mini', paid: 'gpt-5' } }); + const { unmount } = renderPanel('en'); + const en = (await screen.findByTestId('ai-model-summary')).textContent ?? ''; + unmount(); + + renderPanel('zh'); + const zh = (await screen.findByTestId('ai-model-summary')).textContent ?? ''; + + expect(zh).not.toBe(en); + expect(zh).toMatch(/[一-鿿]/); + // Model ids are identifiers, not prose — they must survive translation. + expect(zh).toContain('gpt-5'); + }); + + it('falls back to a translated placeholder when the adapter reports no model', async () => { + payload = baseReport({ + conversational: { model: undefined, source: 'unknown' }, + structured: { model: undefined, pinned: false, source: 'inherits-conversational' }, + }); + renderPanel('en'); + const line = await screen.findByTestId('ai-model-summary'); + // `attributeSource` emits the bare token 'unknown'; it used to render raw. + expect(line.textContent).not.toMatch(/\bunknown\s*\)/); + expect(line).toHaveTextContent(/not reported by the adapter/i); + }); +}); diff --git a/packages/app-shell/src/console/diagnostics/CloudAiModelStatus.tsx b/packages/app-shell/src/console/diagnostics/CloudAiModelStatus.tsx index 444e0748d..ba1bbc138 100644 --- a/packages/app-shell/src/console/diagnostics/CloudAiModelStatus.tsx +++ b/packages/app-shell/src/console/diagnostics/CloudAiModelStatus.tsx @@ -31,6 +31,28 @@ interface EffectiveModelReport { adapter: string; provider?: string; overrides: Record{report.summary}
++ {t('aiModelStatus.summary', { + conversational: report.conversational.model ?? unknownModel, + conversationalSource: sourceLabel(report.conversational.source, t), + structured: report.structured.model ?? unknownModel, + structuredSource: sourceLabel(report.structured.source, t), + })} + {report.routing ? ( + <> + {' '} + {t('aiModelStatus.summaryRouting', { + free: report.routing.free, + paid: report.routing.paid, + })} + > + ) : null} +