Merged
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
79 changes: 79 additions & 0 deletions .changeset/7173-ai-pending-actions-inbox-i18n.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
---
'@object-ui/plugin-chatbot': patch
'@object-ui/i18n': patch
---

`AiPendingActionsInbox` speaks the session locale — every string in it, not only its timestamps (objectui#7173).

The AI HITL approval inbox held its own relative-time helper returning hardcoded
English (`'just now'`, `` `${min}m ago` ``), so a zh / ja / ar session read English
relative times on every row. It is the fifth spelling of that helper in the repo,
and the file had **no translation wiring at all** — the unwired-component shape,
not the lookup-swap shape.

It is therefore swept whole. objectui#7142 wired one string into an otherwise
untranslated component and shipped something visibly half-done, and objectui#7149
is what finishing that afterwards cost; the triage ruling on this card (2026-09-01)
carried that forward as *sweep the file whole or leave it*. Everything the user can
read now resolves from the locale packs: the card heading and description, the three
tabs, the refresh button, all five status badges, the six column headings, the empty
state, the row and drawer buttons, all nine drawer field labels, the outcome banner
and the whole reject-reason dialog.

**No new rows for the four relative-time branches.** `detail.justNow`,
`detail.minutesAgo`, `detail.hoursAgo` and `detail.daysAgo` already existed,
translated, in all ten packs, and cross-package key borrowing is this repo's settled
convention rather than an open question — `ObjectGrid`, `ObjectKanban`, `ObjectTree`,
`ListView`, `ObjectView`, `NavigationOverlay`, `RecordAttachmentsPanel`,
`RecordDetailView` and `apps/console` all resolve `detail.*` from outside
`plugin-detail`. One phrase on one kind of control should not get a second
translation that can drift from the first.

The rest of the sweep needed copy no pack had, so `@object-ui/i18n` gains an
`aiApprovals` namespace: 38 keys, translated in all ten packs. It is deliberately
separate from `approvalsInbox`, which is the human approval-**process** inbox — a
different surface and a different feature, so no rows are shared with it. Four
generic verbs are reused rather than forked (`common.refresh`, `common.cancel`,
`common.loading`, `common.ok`).

**⛔ The five relative-time helpers are not unified.** They differ in real behaviour
— `Math.round` here against `Math.floor` in `plugin-detail`, thresholds 45s/30d
against 60s/7d, different tails — so normalising them is a behaviour change wearing
a refactor's clothes and needs its own card. This inbox's arithmetic is untouched,
and three rows in the new suite exist only to pin it: 50s renders `1m ago` (a 60s
threshold would still say "just now"), 90s renders `2m ago` (`Math.floor` gives
`1m ago`), and 20d renders `20d ago` (a 7d threshold would already show a date).

Two assembled English sentences became single interpolated keys — the outcome banner
(`Approve for {{id}}: {{message}}`) and the drawer subtitle
(`Tool {{tool}} on {{object}}`). Their word order differs per locale, which fragments
around a `<code>` element cannot express, so the two identifiers lose their monospace
styling. That is the deliberate cost of making those sentences translatable.

Evidence: an `en`-only assertion cannot discriminate here, because each key's `en`
value is byte-identical to the literal it replaced. The suite asserts in **zh and
ar**, and the provider-less path separately, in its own file (`createI18n` installs
itself as react-i18next's module-level global, so a provider-less render in a file
that has already mounted a provider silently reads that pack instead of the defaults
map). No inline `defaultValue` anywhere (objectui#3517).

Two consequences of the sweep, both landed here rather than left for CI to find:

`packages/app-shell/src/console/ai/__tests__/ConversationsSidebar.test.tsx` froze its
`vi.mock('@object-ui/i18n', ...)` factory to a hand-written object. Its import graph
reaches `plugin-chatbot`, which now resolves `createSafeTranslation` at module scope, so
the frozen surface made that read `undefined` and the file died during COLLECTION — the
objectui#6849 shape, which does not look like a test failure. It now spreads
`importOriginal()` and overrides only `useObjectTranslation`. Measured, not guessed: of
the 41 frozen `@object-ui/i18n` factories in the repo, running every one of them showed
this to be the only file whose graph reaches the package.

The ten pack blocks are locale DATA, and locale data lands in the console's eager
`framework` chunk, so `scripts/check-eager-closure-budget.mjs` raises that chunk's
ceiling from 512,000 to 524,000 gzipped bytes and re-pins its baseline onto a fresh
measurement (502,405 to 514,863). Attributed by three console builds: the merge parent
reads 510,192, this branch with the ten `aiApprovals` blocks cut reads 510,192 again, and
this branch reads 514,863 — so the whole 4,671-byte delta is the pack data and nothing
else. Headroom is kept at the line's own convention (9,137 bytes, 0.10x the regression
the gate must catch) rather than widened; most of the overage was pre-existing drift, with
the merge parent already at 510,192 of the 512,000 allowed.
Original file line numberDiff line numberDiff line change
Expand Up@@ -30,7 +30,15 @@ vi.mock('../../../hooks/useConversationList', () => ({
}),
}));

vi.mock('@object-ui/i18n', () => ({
// `importOriginal` spread, not a hand-written object: this file's import graph
// reaches `@object-ui/plugin-chatbot`, whose `AiPendingActionsInbox` resolves
// `createSafeTranslation` from this package AT MODULE SCOPE. A frozen factory
// makes that read `undefined` and the file dies during COLLECTION — before a
// single test runs, so it does not look like a test failure (objectui#6849,
// the shape `scripts/check-vi-mock-inherit.mjs` exists to stop). Only
// `useObjectTranslation` is overridden; everything else is the real module.
vi.mock('@object-ui/i18n', async (importOriginal) => ({
...(await importOriginal<Record<string, unknown>>()),
useObjectTranslation: () => ({
t: (key: string) => ({
'common.loading': 'Loading…',
Expand Down
14 changes: 10 additions & 4 deletions packages/i18n/src/__tests__/de-quote-pairing-3876.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -273,8 +273,13 @@ describe('objectui#3876 — de pack closes „ with “ and not with a straight
// keys: `detail.activityFieldChanged` quotes the OLD and NEW field values
// („{{old}}“ / „{{new}}“, two spans in one value, like `navigationSync.
// renamedPage` above) and `detail.activityStatusChanged` the new status
// („{{value}}“) — three interpolated spans, all runtime data.
expect(okSpans, 'correctly paired spans').toBe(58);
// („{{value}}“) — three interpolated spans, all runtime data,
// 59 once objectui#7173 gave `AiPendingActionsInbox` pack keys:
// `aiApprovals.rejectPlaceholder` quotes the EXAMPLE rejection reason the
// placeholder suggests („Falsche Datensatz-ID — …“) — one LITERAL span, like
// `timeline.unsupported.objectBoundGantt` above rather than the interpolated
// ones, because the quoted thing is sample prose this pack authored.
expect(okSpans, 'correctly paired spans').toBe(59);
});

it('keeps the count identity that replaces the card’s count(„) === count(“)', () => {
Expand All@@ -298,8 +303,9 @@ describe('objectui#3876 — de pack closes „ with “ and not with a straight
// after objectui#7149 added the two quoted `ActivityTimeline` sentences
// (three spans between them). `rdq` staying at 0 is the load-bearing half:
// each new value added a MATCHED „…“ pair, not a stray closer that would
// have made `close === open` true for the wrong reason.
expect({ open, close, rdq }).toEqual({ open: 58, close: 58, rdq: 0 });
// have made `close === open` true for the wrong reason. 59 / 59 / 0 after
// objectui#7173 added `aiApprovals.rejectPlaceholder`, one more matched pair.
expect({ open, close, rdq }).toEqual({ open: 59, close: 59, rdq: 0 });
// The durable shape: every „ closed by a “, every surplus “ an English
// opener answered by a ”. Survived translating the two English values.
expect(close).toBe(open + rdq);
Expand Down
40 changes: 40 additions & 0 deletions packages/i18n/src/locales/ar.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -2796,6 +2796,46 @@ const ar = {
openProduction: "فتح بيئة الإنتاج",
manageEnvironments: "إدارة البيئات",
},
aiApprovals: {
title: "موافقات الذكاء الاصطناعي",
description: "إجراءات اقترحها وكيل ذكاء اصطناعي وتحتاج إلى مراجعة بشرية قبل تنفيذها.",
tabPending: "قيد الانتظار",
tabDecided: "تم البت فيها",
tabAll: "الكل",
statusPending: "قيد الانتظار",
statusApproved: "تمت الموافقة",
statusExecuted: "تم التنفيذ",
statusFailed: "فشل",
statusRejected: "مرفوض",
colTool: "الأداة",
colAction: "الإجراء",
colObject: "الكائن",
colStatus: "الحالة",
colProposed: "وقت الاقتراح",
colDecision: "القرار",
emptyTitle: "لا توجد إجراءات في الانتظار",
emptyDescription: "عندما يقترح الذكاء الاصطناعي إجراءً حساسًا سيظهر هنا للمراجعة.",
view: "عرض",
approve: "موافقة",
reject: "رفض",
working: "جارٍ التنفيذ…",
approveAndExecute: "الموافقة والتنفيذ",
outcomeApprove: "موافقة على {{id}}: {{message}}",
outcomeReject: "رفض {{id}}: {{message}}",
outcomeExecuteFailed: "فشل الإجراء أثناء التنفيذ",
drawerFallbackTitle: "إجراء قيد الانتظار",
drawerSubtitle: "الأداة {{tool}} على {{object}}",
fieldProposedBy: "اقترحه",
fieldDecidedBy: "قرّره",
fieldConversation: "المحادثة",
fieldToolInput: "مدخلات الأداة",
fieldResult: "النتيجة",
fieldError: "خطأ",
fieldRejectionReason: "سبب الرفض",
rejectTitle: "هل تريد رفض هذا الإجراء؟",
rejectBody: "يُعاد السبب إلى الذكاء الاصطناعي ليعدّل ردّه التالي.",
rejectPlaceholder: "سبب اختياري (مثال: «معرّف السجل غير صحيح — يرجى التأكيد مع المستخدم أولاً.»)",
},
aiModelStatus: {
summary: "يستخدم الإنشاء / السؤال {{conversational}} ({{conversationalSource}})؛ ويستخدم الإخراج المهيكل {{structured}} ({{structuredSource}}).",
summaryRouting: "سياسة التوجيه: الخطط المجانية ← {{free}}، الخطط المدفوعة ← {{paid}}.",
Expand Down
40 changes: 40 additions & 0 deletions packages/i18n/src/locales/de.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -2789,6 +2789,46 @@ const de = {
openProduction: "Produktion öffnen",
manageEnvironments: "Umgebungen verwalten",
},
aiApprovals: {
title: "KI-Genehmigungen",
description: "Von einem KI-Agenten vorgeschlagene Aktionen, die vor der Ausführung eine menschliche Prüfung benötigen.",
tabPending: "Ausstehend",
tabDecided: "Entschieden",
tabAll: "Alle",
statusPending: "Ausstehend",
statusApproved: "Genehmigt",
statusExecuted: "Ausgeführt",
statusFailed: "Fehlgeschlagen",
statusRejected: "Abgelehnt",
colTool: "Werkzeug",
colAction: "Aktion",
colObject: "Objekt",
colStatus: "Status",
colProposed: "Vorgeschlagen",
colDecision: "Entscheidung",
emptyTitle: "Keine wartenden Aktionen",
emptyDescription: "Wenn die KI eine sensible Aktion vorschlägt, erscheint sie hier zur Prüfung.",
view: "Ansehen",
approve: "Genehmigen",
reject: "Ablehnen",
working: "Wird bearbeitet…",
approveAndExecute: "Genehmigen & ausführen",
outcomeApprove: "Genehmigung für {{id}}: {{message}}",
outcomeReject: "Ablehnung für {{id}}: {{message}}",
outcomeExecuteFailed: "Die Aktion ist bei der Ausführung fehlgeschlagen",
drawerFallbackTitle: "Ausstehende Aktion",
drawerSubtitle: "Werkzeug {{tool}} auf {{object}}",
fieldProposedBy: "Vorgeschlagen von",
fieldDecidedBy: "Entschieden von",
fieldConversation: "Konversation",
fieldToolInput: "Werkzeugeingabe",
fieldResult: "Ergebnis",
fieldError: "Fehler",
fieldRejectionReason: "Ablehnungsgrund",
rejectTitle: "Diese Aktion ablehnen?",
rejectBody: "Der Grund wird an die KI zurückgemeldet, damit sie ihre nächste Antwort anpassen kann.",
rejectPlaceholder: "Optionaler Grund (z. B. „Falsche Datensatz-ID — bitte zuerst mit der Nutzerin oder dem Nutzer klären.“)",
},
aiModelStatus: {
summary: "Erstellen / Fragen nutzt {{conversational}} ({{conversationalSource}}); strukturiert nutzt {{structured}} ({{structuredSource}}).",
summaryRouting: "Routing-Richtlinie: kostenlose Tarife → {{free}}, kostenpflichtige Tarife → {{paid}}.",
Expand Down
47 changes: 47 additions & 0 deletions packages/i18n/src/locales/en.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -3135,6 +3135,53 @@ const en = {
openProduction: 'Open Production',
manageEnvironments: 'Manage environments',
},
// The AI HITL approval inbox (`@object-ui/plugin-chatbot`'s
// `AiPendingActionsInbox`) — objectui#7173. Its four relative-time phrases
// are NOT here: it borrows `detail.justNow` / `minutesAgo` / `hoursAgo` /
// `daysAgo`, already translated in all ten packs, the way `ObjectGrid` and
// `ObjectKanban` borrow `detail.recordDetail`. Distinct from
// `approvalsInbox` above, which is the human approval-PROCESS inbox:
// different surface, different feature, so no rows are shared with it.
aiApprovals: {
title: 'AI Approvals',
description: 'Actions an AI agent proposed that need a human review before execution.',
tabPending: 'Pending',
tabDecided: 'Decided',
tabAll: 'All',
statusPending: 'Pending',
statusApproved: 'Approved',
statusExecuted: 'Executed',
statusFailed: 'Failed',
statusRejected: 'Rejected',
colTool: 'Tool',
colAction: 'Action',
colObject: 'Object',
colStatus: 'Status',
colProposed: 'Proposed',
colDecision: 'Decision',
emptyTitle: 'No actions waiting',
emptyDescription: 'When the AI proposes a sensitive action it will appear here for review.',
view: 'View',
approve: 'Approve',
reject: 'Reject',
working: 'Working…',
approveAndExecute: 'Approve & Execute',
outcomeApprove: 'Approve for {{id}}: {{message}}',
outcomeReject: 'Reject for {{id}}: {{message}}',
outcomeExecuteFailed: 'Action failed during execution',
drawerFallbackTitle: 'Pending action',
drawerSubtitle: 'Tool {{tool}} on {{object}}',
fieldProposedBy: 'Proposed by',
fieldDecidedBy: 'Decided by',
fieldConversation: 'Conversation',
fieldToolInput: 'Tool input',
fieldResult: 'Result',
fieldError: 'Error',
fieldRejectionReason: 'Rejection reason',
rejectTitle: 'Reject this action?',
rejectBody: 'The reason is shown back to the AI so it can adjust its next response.',
rejectPlaceholder: "Optional reason (e.g. 'Wrong record id — please confirm with the user first.')",
},
aiModelStatus: {
summary: 'Build / Ask uses {{conversational}} ({{conversationalSource}}); structured uses {{structured}} ({{structuredSource}}).',
summaryRouting: 'Routing policy: free plans → {{free}}, paid plans → {{paid}}.',
Expand Down
40 changes: 40 additions & 0 deletions packages/i18n/src/locales/es.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -2793,6 +2793,46 @@ const es = {
openProduction: "Abrir producción",
manageEnvironments: "Gestionar entornos",
},
aiApprovals: {
title: "Aprobaciones de IA",
description: "Acciones propuestas por un agente de IA que necesitan revisión humana antes de ejecutarse.",
tabPending: "Pendientes",
tabDecided: "Decididas",
tabAll: "Todas",
statusPending: "Pendiente",
statusApproved: "Aprobada",
statusExecuted: "Ejecutada",
statusFailed: "Fallida",
statusRejected: "Rechazada",
colTool: "Herramienta",
colAction: "Acción",
colObject: "Objeto",
colStatus: "Estado",
colProposed: "Propuesta",
colDecision: "Decisión",
emptyTitle: "No hay acciones en espera",
emptyDescription: "Cuando la IA proponga una acción sensible, aparecerá aquí para su revisión.",
view: "Ver",
approve: "Aprobar",
reject: "Rechazar",
working: "Procesando…",
approveAndExecute: "Aprobar y ejecutar",
outcomeApprove: "Aprobación de {{id}}: {{message}}",
outcomeReject: "Rechazo de {{id}}: {{message}}",
outcomeExecuteFailed: "La acción falló durante su ejecución",
drawerFallbackTitle: "Acción pendiente",
drawerSubtitle: "Herramienta {{tool}} sobre {{object}}",
fieldProposedBy: "Propuesta por",
fieldDecidedBy: "Decidida por",
fieldConversation: "Conversación",
fieldToolInput: "Entrada de la herramienta",
fieldResult: "Resultado",
fieldError: "Error",
fieldRejectionReason: "Motivo del rechazo",
rejectTitle: "¿Rechazar esta acción?",
rejectBody: "El motivo se devuelve a la IA para que ajuste su siguiente respuesta.",
rejectPlaceholder: "Motivo opcional (p. ej.: «ID de registro incorrecto — confírmalo antes con la persona usuaria.»)",
},
aiModelStatus: {
summary: "Crear / Preguntar usa {{conversational}} ({{conversationalSource}}); estructurado usa {{structured}} ({{structuredSource}}).",
summaryRouting: "Política de enrutamiento: planes gratuitos → {{free}}, planes de pago → {{paid}}.",
Expand Down
40 changes: 40 additions & 0 deletions packages/i18n/src/locales/fr.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -2791,6 +2791,46 @@ const fr = {
openProduction: "Ouvrir la production",
manageEnvironments: "Gérer les environnements",
},
aiApprovals: {
title: "Approbations IA",
description: "Actions proposées par un agent IA qui nécessitent une validation humaine avant exécution.",
tabPending: "En attente",
tabDecided: "Traitées",
tabAll: "Toutes",
statusPending: "En attente",
statusApproved: "Approuvée",
statusExecuted: "Exécutée",
statusFailed: "Échouée",
statusRejected: "Rejetée",
colTool: "Outil",
colAction: "Action",
colObject: "Objet",
colStatus: "Statut",
colProposed: "Proposée",
colDecision: "Décision",
emptyTitle: "Aucune action en attente",
emptyDescription: "Lorsque l'IA propose une action sensible, elle apparaît ici pour validation.",
view: "Voir",
approve: "Approuver",
reject: "Rejeter",
working: "En cours…",
approveAndExecute: "Approuver et exécuter",
outcomeApprove: "Approbation de {{id}} : {{message}}",
outcomeReject: "Rejet de {{id}} : {{message}}",
outcomeExecuteFailed: "L'action a échoué pendant son exécution",
drawerFallbackTitle: "Action en attente",
drawerSubtitle: "Outil {{tool}} sur {{object}}",
fieldProposedBy: "Proposée par",
fieldDecidedBy: "Décidée par",
fieldConversation: "Conversation",
fieldToolInput: "Entrée de l'outil",
fieldResult: "Résultat",
fieldError: "Erreur",
fieldRejectionReason: "Motif du rejet",
rejectTitle: "Rejeter cette action ?",
rejectBody: "Le motif est renvoyé à l'IA pour qu'elle ajuste sa prochaine réponse.",
rejectPlaceholder: "Motif facultatif (par ex. « Identifiant d'enregistrement erroné — merci de confirmer d'abord avec l'utilisateur. »)",
},
aiModelStatus: {
summary: "Créer / Demander utilise {{conversational}} ({{conversationalSource}}) ; structuré utilise {{structured}} ({{structuredSource}}).",
summaryRouting: "Politique de routage : offres gratuites → {{free}}, offres payantes → {{paid}}.",
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
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
79 changes: 79 additions & 0 deletions .changeset/7173-ai-pending-actions-inbox-i18n.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
---
'@object-ui/plugin-chatbot': patch
'@object-ui/i18n': patch
---

`AiPendingActionsInbox` speaks the session locale — every string in it, not only its timestamps (objectui#7173).

The AI HITL approval inbox held its own relative-time helper returning hardcoded
English (`'just now'`, `` `${min}m ago` ``), so a zh / ja / ar session read English
relative times on every row. It is the fifth spelling of that helper in the repo,
and the file had **no translation wiring at all** — the unwired-component shape,
not the lookup-swap shape.

It is therefore swept whole. objectui#7142 wired one string into an otherwise
untranslated component and shipped something visibly half-done, and objectui#7149
is what finishing that afterwards cost; the triage ruling on this card (2026-09-01)
carried that forward as *sweep the file whole or leave it*. Everything the user can
read now resolves from the locale packs: the card heading and description, the three
tabs, the refresh button, all five status badges, the six column headings, the empty
state, the row and drawer buttons, all nine drawer field labels, the outcome banner
and the whole reject-reason dialog.

**No new rows for the four relative-time branches.** `detail.justNow`,
`detail.minutesAgo`, `detail.hoursAgo` and `detail.daysAgo` already existed,
translated, in all ten packs, and cross-package key borrowing is this repo's settled
convention rather than an open question — `ObjectGrid`, `ObjectKanban`, `ObjectTree`,
`ListView`, `ObjectView`, `NavigationOverlay`, `RecordAttachmentsPanel`,
`RecordDetailView` and `apps/console` all resolve `detail.*` from outside
`plugin-detail`. One phrase on one kind of control should not get a second
translation that can drift from the first.

The rest of the sweep needed copy no pack had, so `@object-ui/i18n` gains an
`aiApprovals` namespace: 38 keys, translated in all ten packs. It is deliberately
separate from `approvalsInbox`, which is the human approval-**process** inbox — a
different surface and a different feature, so no rows are shared with it. Four
generic verbs are reused rather than forked (`common.refresh`, `common.cancel`,
`common.loading`, `common.ok`).

**⛔ The five relative-time helpers are not unified.** They differ in real behaviour
— `Math.round` here against `Math.floor` in `plugin-detail`, thresholds 45s/30d
against 60s/7d, different tails — so normalising them is a behaviour change wearing
a refactor's clothes and needs its own card. This inbox's arithmetic is untouched,
and three rows in the new suite exist only to pin it: 50s renders `1m ago` (a 60s
threshold would still say "just now"), 90s renders `2m ago` (`Math.floor` gives
`1m ago`), and 20d renders `20d ago` (a 7d threshold would already show a date).

Two assembled English sentences became single interpolated keys — the outcome banner
(`Approve for {{id}}: {{message}}`) and the drawer subtitle
(`Tool {{tool}} on {{object}}`). Their word order differs per locale, which fragments
around a `<code>` element cannot express, so the two identifiers lose their monospace
styling. That is the deliberate cost of making those sentences translatable.

Evidence: an `en`-only assertion cannot discriminate here, because each key's `en`
value is byte-identical to the literal it replaced. The suite asserts in **zh and
ar**, and the provider-less path separately, in its own file (`createI18n` installs
itself as react-i18next's module-level global, so a provider-less render in a file
that has already mounted a provider silently reads that pack instead of the defaults
map). No inline `defaultValue` anywhere (objectui#3517).

Two consequences of the sweep, both landed here rather than left for CI to find:

`packages/app-shell/src/console/ai/__tests__/ConversationsSidebar.test.tsx` froze its
`vi.mock('@object-ui/i18n', ...)` factory to a hand-written object. Its import graph
reaches `plugin-chatbot`, which now resolves `createSafeTranslation` at module scope, so
the frozen surface made that read `undefined` and the file died during COLLECTION — the
objectui#6849 shape, which does not look like a test failure. It now spreads
`importOriginal()` and overrides only `useObjectTranslation`. Measured, not guessed: of
the 41 frozen `@object-ui/i18n` factories in the repo, running every one of them showed
this to be the only file whose graph reaches the package.

The ten pack blocks are locale DATA, and locale data lands in the console's eager
`framework` chunk, so `scripts/check-eager-closure-budget.mjs` raises that chunk's
ceiling from 512,000 to 524,000 gzipped bytes and re-pins its baseline onto a fresh
measurement (502,405 to 514,863). Attributed by three console builds: the merge parent
reads 510,192, this branch with the ten `aiApprovals` blocks cut reads 510,192 again, and
this branch reads 514,863 — so the whole 4,671-byte delta is the pack data and nothing
else. Headroom is kept at the line's own convention (9,137 bytes, 0.10x the regression
the gate must catch) rather than widened; most of the overage was pre-existing drift, with
the merge parent already at 510,192 of the 512,000 allowed.
Original file line numberDiff line numberDiff line change
Expand Up@@ -30,7 +30,15 @@ vi.mock('../../../hooks/useConversationList', () => ({
}),
}));

vi.mock('@object-ui/i18n', () => ({
// `importOriginal` spread, not a hand-written object: this file's import graph
// reaches `@object-ui/plugin-chatbot`, whose `AiPendingActionsInbox` resolves
// `createSafeTranslation` from this package AT MODULE SCOPE. A frozen factory
// makes that read `undefined` and the file dies during COLLECTION — before a
// single test runs, so it does not look like a test failure (objectui#6849,
// the shape `scripts/check-vi-mock-inherit.mjs` exists to stop). Only
// `useObjectTranslation` is overridden; everything else is the real module.
vi.mock('@object-ui/i18n', async (importOriginal) => ({
...(await importOriginal<Record<string, unknown>>()),
useObjectTranslation: () => ({
t: (key: string) => ({
'common.loading': 'Loading…',
Expand Down
14 changes: 10 additions & 4 deletions packages/i18n/src/__tests__/de-quote-pairing-3876.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -273,8 +273,13 @@ describe('objectui#3876 — de pack closes „ with “ and not with a straight
// keys: `detail.activityFieldChanged` quotes the OLD and NEW field values
// („{{old}}“ / „{{new}}“, two spans in one value, like `navigationSync.
// renamedPage` above) and `detail.activityStatusChanged` the new status
// („{{value}}“) — three interpolated spans, all runtime data.
expect(okSpans, 'correctly paired spans').toBe(58);
// („{{value}}“) — three interpolated spans, all runtime data,
// 59 once objectui#7173 gave `AiPendingActionsInbox` pack keys:
// `aiApprovals.rejectPlaceholder` quotes the EXAMPLE rejection reason the
// placeholder suggests („Falsche Datensatz-ID — …“) — one LITERAL span, like
// `timeline.unsupported.objectBoundGantt` above rather than the interpolated
// ones, because the quoted thing is sample prose this pack authored.
expect(okSpans, 'correctly paired spans').toBe(59);
});

it('keeps the count identity that replaces the card’s count(„) === count(“)', () => {
Expand All@@ -298,8 +303,9 @@ describe('objectui#3876 — de pack closes „ with “ and not with a straight
// after objectui#7149 added the two quoted `ActivityTimeline` sentences
// (three spans between them). `rdq` staying at 0 is the load-bearing half:
// each new value added a MATCHED „…“ pair, not a stray closer that would
// have made `close === open` true for the wrong reason.
expect({ open, close, rdq }).toEqual({ open: 58, close: 58, rdq: 0 });
// have made `close === open` true for the wrong reason. 59 / 59 / 0 after
// objectui#7173 added `aiApprovals.rejectPlaceholder`, one more matched pair.
expect({ open, close, rdq }).toEqual({ open: 59, close: 59, rdq: 0 });
// The durable shape: every „ closed by a “, every surplus “ an English
// opener answered by a ”. Survived translating the two English values.
expect(close).toBe(open + rdq);
Expand Down
40 changes: 40 additions & 0 deletions packages/i18n/src/locales/ar.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -2796,6 +2796,46 @@ const ar = {
openProduction: "فتح بيئة الإنتاج",
manageEnvironments: "إدارة البيئات",
},
aiApprovals: {
title: "موافقات الذكاء الاصطناعي",
description: "إجراءات اقترحها وكيل ذكاء اصطناعي وتحتاج إلى مراجعة بشرية قبل تنفيذها.",
tabPending: "قيد الانتظار",
tabDecided: "تم البت فيها",
tabAll: "الكل",
statusPending: "قيد الانتظار",
statusApproved: "تمت الموافقة",
statusExecuted: "تم التنفيذ",
statusFailed: "فشل",
statusRejected: "مرفوض",
colTool: "الأداة",
colAction: "الإجراء",
colObject: "الكائن",
colStatus: "الحالة",
colProposed: "وقت الاقتراح",
colDecision: "القرار",
emptyTitle: "لا توجد إجراءات في الانتظار",
emptyDescription: "عندما يقترح الذكاء الاصطناعي إجراءً حساسًا سيظهر هنا للمراجعة.",
view: "عرض",
approve: "موافقة",
reject: "رفض",
working: "جارٍ التنفيذ…",
approveAndExecute: "الموافقة والتنفيذ",
outcomeApprove: "موافقة على {{id}}: {{message}}",
outcomeReject: "رفض {{id}}: {{message}}",
outcomeExecuteFailed: "فشل الإجراء أثناء التنفيذ",
drawerFallbackTitle: "إجراء قيد الانتظار",
drawerSubtitle: "الأداة {{tool}} على {{object}}",
fieldProposedBy: "اقترحه",
fieldDecidedBy: "قرّره",
fieldConversation: "المحادثة",
fieldToolInput: "مدخلات الأداة",
fieldResult: "النتيجة",
fieldError: "خطأ",
fieldRejectionReason: "سبب الرفض",
rejectTitle: "هل تريد رفض هذا الإجراء؟",
rejectBody: "يُعاد السبب إلى الذكاء الاصطناعي ليعدّل ردّه التالي.",
rejectPlaceholder: "سبب اختياري (مثال: «معرّف السجل غير صحيح — يرجى التأكيد مع المستخدم أولاً.»)",
},
aiModelStatus: {
summary: "يستخدم الإنشاء / السؤال {{conversational}} ({{conversationalSource}})؛ ويستخدم الإخراج المهيكل {{structured}} ({{structuredSource}}).",
summaryRouting: "سياسة التوجيه: الخطط المجانية ← {{free}}، الخطط المدفوعة ← {{paid}}.",
Expand Down
40 changes: 40 additions & 0 deletions packages/i18n/src/locales/de.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -2789,6 +2789,46 @@ const de = {
openProduction: "Produktion öffnen",
manageEnvironments: "Umgebungen verwalten",
},
aiApprovals: {
title: "KI-Genehmigungen",
description: "Von einem KI-Agenten vorgeschlagene Aktionen, die vor der Ausführung eine menschliche Prüfung benötigen.",
tabPending: "Ausstehend",
tabDecided: "Entschieden",
tabAll: "Alle",
statusPending: "Ausstehend",
statusApproved: "Genehmigt",
statusExecuted: "Ausgeführt",
statusFailed: "Fehlgeschlagen",
statusRejected: "Abgelehnt",
colTool: "Werkzeug",
colAction: "Aktion",
colObject: "Objekt",
colStatus: "Status",
colProposed: "Vorgeschlagen",
colDecision: "Entscheidung",
emptyTitle: "Keine wartenden Aktionen",
emptyDescription: "Wenn die KI eine sensible Aktion vorschlägt, erscheint sie hier zur Prüfung.",
view: "Ansehen",
approve: "Genehmigen",
reject: "Ablehnen",
working: "Wird bearbeitet…",
approveAndExecute: "Genehmigen & ausführen",
outcomeApprove: "Genehmigung für {{id}}: {{message}}",
outcomeReject: "Ablehnung für {{id}}: {{message}}",
outcomeExecuteFailed: "Die Aktion ist bei der Ausführung fehlgeschlagen",
drawerFallbackTitle: "Ausstehende Aktion",
drawerSubtitle: "Werkzeug {{tool}} auf {{object}}",
fieldProposedBy: "Vorgeschlagen von",
fieldDecidedBy: "Entschieden von",
fieldConversation: "Konversation",
fieldToolInput: "Werkzeugeingabe",
fieldResult: "Ergebnis",
fieldError: "Fehler",
fieldRejectionReason: "Ablehnungsgrund",
rejectTitle: "Diese Aktion ablehnen?",
rejectBody: "Der Grund wird an die KI zurückgemeldet, damit sie ihre nächste Antwort anpassen kann.",
rejectPlaceholder: "Optionaler Grund (z. B. „Falsche Datensatz-ID — bitte zuerst mit der Nutzerin oder dem Nutzer klären.“)",
},
aiModelStatus: {
summary: "Erstellen / Fragen nutzt {{conversational}} ({{conversationalSource}}); strukturiert nutzt {{structured}} ({{structuredSource}}).",
summaryRouting: "Routing-Richtlinie: kostenlose Tarife → {{free}}, kostenpflichtige Tarife → {{paid}}.",
Expand Down
47 changes: 47 additions & 0 deletions packages/i18n/src/locales/en.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -3135,6 +3135,53 @@ const en = {
openProduction: 'Open Production',
manageEnvironments: 'Manage environments',
},
// The AI HITL approval inbox (`@object-ui/plugin-chatbot`'s
// `AiPendingActionsInbox`) — objectui#7173. Its four relative-time phrases
// are NOT here: it borrows `detail.justNow` / `minutesAgo` / `hoursAgo` /
// `daysAgo`, already translated in all ten packs, the way `ObjectGrid` and
// `ObjectKanban` borrow `detail.recordDetail`. Distinct from
// `approvalsInbox` above, which is the human approval-PROCESS inbox:
// different surface, different feature, so no rows are shared with it.
aiApprovals: {
title: 'AI Approvals',
description: 'Actions an AI agent proposed that need a human review before execution.',
tabPending: 'Pending',
tabDecided: 'Decided',
tabAll: 'All',
statusPending: 'Pending',
statusApproved: 'Approved',
statusExecuted: 'Executed',
statusFailed: 'Failed',
statusRejected: 'Rejected',
colTool: 'Tool',
colAction: 'Action',
colObject: 'Object',
colStatus: 'Status',
colProposed: 'Proposed',
colDecision: 'Decision',
emptyTitle: 'No actions waiting',
emptyDescription: 'When the AI proposes a sensitive action it will appear here for review.',
view: 'View',
approve: 'Approve',
reject: 'Reject',
working: 'Working…',
approveAndExecute: 'Approve & Execute',
outcomeApprove: 'Approve for {{id}}: {{message}}',
outcomeReject: 'Reject for {{id}}: {{message}}',
outcomeExecuteFailed: 'Action failed during execution',
drawerFallbackTitle: 'Pending action',
drawerSubtitle: 'Tool {{tool}} on {{object}}',
fieldProposedBy: 'Proposed by',
fieldDecidedBy: 'Decided by',
fieldConversation: 'Conversation',
fieldToolInput: 'Tool input',
fieldResult: 'Result',
fieldError: 'Error',
fieldRejectionReason: 'Rejection reason',
rejectTitle: 'Reject this action?',
rejectBody: 'The reason is shown back to the AI so it can adjust its next response.',
rejectPlaceholder: "Optional reason (e.g. 'Wrong record id — please confirm with the user first.')",
},
aiModelStatus: {
summary: 'Build / Ask uses {{conversational}} ({{conversationalSource}}); structured uses {{structured}} ({{structuredSource}}).',
summaryRouting: 'Routing policy: free plans → {{free}}, paid plans → {{paid}}.',
Expand Down
40 changes: 40 additions & 0 deletions packages/i18n/src/locales/es.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -2793,6 +2793,46 @@ const es = {
openProduction: "Abrir producción",
manageEnvironments: "Gestionar entornos",
},
aiApprovals: {
title: "Aprobaciones de IA",
description: "Acciones propuestas por un agente de IA que necesitan revisión humana antes de ejecutarse.",
tabPending: "Pendientes",
tabDecided: "Decididas",
tabAll: "Todas",
statusPending: "Pendiente",
statusApproved: "Aprobada",
statusExecuted: "Ejecutada",
statusFailed: "Fallida",
statusRejected: "Rechazada",
colTool: "Herramienta",
colAction: "Acción",
colObject: "Objeto",
colStatus: "Estado",
colProposed: "Propuesta",
colDecision: "Decisión",
emptyTitle: "No hay acciones en espera",
emptyDescription: "Cuando la IA proponga una acción sensible, aparecerá aquí para su revisión.",
view: "Ver",
approve: "Aprobar",
reject: "Rechazar",
working: "Procesando…",
approveAndExecute: "Aprobar y ejecutar",
outcomeApprove: "Aprobación de {{id}}: {{message}}",
outcomeReject: "Rechazo de {{id}}: {{message}}",
outcomeExecuteFailed: "La acción falló durante su ejecución",
drawerFallbackTitle: "Acción pendiente",
drawerSubtitle: "Herramienta {{tool}} sobre {{object}}",
fieldProposedBy: "Propuesta por",
fieldDecidedBy: "Decidida por",
fieldConversation: "Conversación",
fieldToolInput: "Entrada de la herramienta",
fieldResult: "Resultado",
fieldError: "Error",
fieldRejectionReason: "Motivo del rechazo",
rejectTitle: "¿Rechazar esta acción?",
rejectBody: "El motivo se devuelve a la IA para que ajuste su siguiente respuesta.",
rejectPlaceholder: "Motivo opcional (p. ej.: «ID de registro incorrecto — confírmalo antes con la persona usuaria.»)",
},
aiModelStatus: {
summary: "Crear / Preguntar usa {{conversational}} ({{conversationalSource}}); estructurado usa {{structured}} ({{structuredSource}}).",
summaryRouting: "Política de enrutamiento: planes gratuitos → {{free}}, planes de pago → {{paid}}.",
Expand Down
40 changes: 40 additions & 0 deletions packages/i18n/src/locales/fr.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -2791,6 +2791,46 @@ const fr = {
openProduction: "Ouvrir la production",
manageEnvironments: "Gérer les environnements",
},
aiApprovals: {
title: "Approbations IA",
description: "Actions proposées par un agent IA qui nécessitent une validation humaine avant exécution.",
tabPending: "En attente",
tabDecided: "Traitées",
tabAll: "Toutes",
statusPending: "En attente",
statusApproved: "Approuvée",
statusExecuted: "Exécutée",
statusFailed: "Échouée",
statusRejected: "Rejetée",
colTool: "Outil",
colAction: "Action",
colObject: "Objet",
colStatus: "Statut",
colProposed: "Proposée",
colDecision: "Décision",
emptyTitle: "Aucune action en attente",
emptyDescription: "Lorsque l'IA propose une action sensible, elle apparaît ici pour validation.",
view: "Voir",
approve: "Approuver",
reject: "Rejeter",
working: "En cours…",
approveAndExecute: "Approuver et exécuter",
outcomeApprove: "Approbation de {{id}} : {{message}}",
outcomeReject: "Rejet de {{id}} : {{message}}",
outcomeExecuteFailed: "L'action a échoué pendant son exécution",
drawerFallbackTitle: "Action en attente",
drawerSubtitle: "Outil {{tool}} sur {{object}}",
fieldProposedBy: "Proposée par",
fieldDecidedBy: "Décidée par",
fieldConversation: "Conversation",
fieldToolInput: "Entrée de l'outil",
fieldResult: "Résultat",
fieldError: "Erreur",
fieldRejectionReason: "Motif du rejet",
rejectTitle: "Rejeter cette action ?",
rejectBody: "Le motif est renvoyé à l'IA pour qu'elle ajuste sa prochaine réponse.",
rejectPlaceholder: "Motif facultatif (par ex. « Identifiant d'enregistrement erroné — merci de confirmer d'abord avec l'utilisateur. »)",
},
aiModelStatus: {
summary: "Créer / Demander utilise {{conversational}} ({{conversationalSource}}) ; structuré utilise {{structured}} ({{structuredSource}}).",
summaryRouting: "Politique de routage : offres gratuites → {{free}}, offres payantes → {{paid}}.",
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
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
79 changes: 79 additions & 0 deletions .changeset/7173-ai-pending-actions-inbox-i18n.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
---
'@object-ui/plugin-chatbot': patch
'@object-ui/i18n': patch
---

`AiPendingActionsInbox` speaks the session locale — every string in it, not only its timestamps (objectui#7173).

The AI HITL approval inbox held its own relative-time helper returning hardcoded
English (`'just now'`, `` `${min}m ago` ``), so a zh / ja / ar session read English
relative times on every row. It is the fifth spelling of that helper in the repo,
and the file had **no translation wiring at all** — the unwired-component shape,
not the lookup-swap shape.

It is therefore swept whole. objectui#7142 wired one string into an otherwise
untranslated component and shipped something visibly half-done, and objectui#7149
is what finishing that afterwards cost; the triage ruling on this card (2026-09-01)
carried that forward as *sweep the file whole or leave it*. Everything the user can
read now resolves from the locale packs: the card heading and description, the three
tabs, the refresh button, all five status badges, the six column headings, the empty
state, the row and drawer buttons, all nine drawer field labels, the outcome banner
and the whole reject-reason dialog.

**No new rows for the four relative-time branches.** `detail.justNow`,
`detail.minutesAgo`, `detail.hoursAgo` and `detail.daysAgo` already existed,
translated, in all ten packs, and cross-package key borrowing is this repo's settled
convention rather than an open question — `ObjectGrid`, `ObjectKanban`, `ObjectTree`,
`ListView`, `ObjectView`, `NavigationOverlay`, `RecordAttachmentsPanel`,
`RecordDetailView` and `apps/console` all resolve `detail.*` from outside
`plugin-detail`. One phrase on one kind of control should not get a second
translation that can drift from the first.

The rest of the sweep needed copy no pack had, so `@object-ui/i18n` gains an
`aiApprovals` namespace: 38 keys, translated in all ten packs. It is deliberately
separate from `approvalsInbox`, which is the human approval-**process** inbox — a
different surface and a different feature, so no rows are shared with it. Four
generic verbs are reused rather than forked (`common.refresh`, `common.cancel`,
`common.loading`, `common.ok`).

**⛔ The five relative-time helpers are not unified.** They differ in real behaviour
— `Math.round` here against `Math.floor` in `plugin-detail`, thresholds 45s/30d
against 60s/7d, different tails — so normalising them is a behaviour change wearing
a refactor's clothes and needs its own card. This inbox's arithmetic is untouched,
and three rows in the new suite exist only to pin it: 50s renders `1m ago` (a 60s
threshold would still say "just now"), 90s renders `2m ago` (`Math.floor` gives
`1m ago`), and 20d renders `20d ago` (a 7d threshold would already show a date).

Two assembled English sentences became single interpolated keys — the outcome banner
(`Approve for {{id}}: {{message}}`) and the drawer subtitle
(`Tool {{tool}} on {{object}}`). Their word order differs per locale, which fragments
around a `<code>` element cannot express, so the two identifiers lose their monospace
styling. That is the deliberate cost of making those sentences translatable.

Evidence: an `en`-only assertion cannot discriminate here, because each key's `en`
value is byte-identical to the literal it replaced. The suite asserts in **zh and
ar**, and the provider-less path separately, in its own file (`createI18n` installs
itself as react-i18next's module-level global, so a provider-less render in a file
that has already mounted a provider silently reads that pack instead of the defaults
map). No inline `defaultValue` anywhere (objectui#3517).

Two consequences of the sweep, both landed here rather than left for CI to find:

`packages/app-shell/src/console/ai/__tests__/ConversationsSidebar.test.tsx` froze its
`vi.mock('@object-ui/i18n', ...)` factory to a hand-written object. Its import graph
reaches `plugin-chatbot`, which now resolves `createSafeTranslation` at module scope, so
the frozen surface made that read `undefined` and the file died during COLLECTION — the
objectui#6849 shape, which does not look like a test failure. It now spreads
`importOriginal()` and overrides only `useObjectTranslation`. Measured, not guessed: of
the 41 frozen `@object-ui/i18n` factories in the repo, running every one of them showed
this to be the only file whose graph reaches the package.

The ten pack blocks are locale DATA, and locale data lands in the console's eager
`framework` chunk, so `scripts/check-eager-closure-budget.mjs` raises that chunk's
ceiling from 512,000 to 524,000 gzipped bytes and re-pins its baseline onto a fresh
measurement (502,405 to 514,863). Attributed by three console builds: the merge parent
reads 510,192, this branch with the ten `aiApprovals` blocks cut reads 510,192 again, and
this branch reads 514,863 — so the whole 4,671-byte delta is the pack data and nothing
else. Headroom is kept at the line's own convention (9,137 bytes, 0.10x the regression
the gate must catch) rather than widened; most of the overage was pre-existing drift, with
the merge parent already at 510,192 of the 512,000 allowed.
Original file line numberDiff line numberDiff line change
Expand Up@@ -30,7 +30,15 @@ vi.mock('../../../hooks/useConversationList', () => ({
}),
}));

vi.mock('@object-ui/i18n', () => ({
// `importOriginal` spread, not a hand-written object: this file's import graph
// reaches `@object-ui/plugin-chatbot`, whose `AiPendingActionsInbox` resolves
// `createSafeTranslation` from this package AT MODULE SCOPE. A frozen factory
// makes that read `undefined` and the file dies during COLLECTION — before a
// single test runs, so it does not look like a test failure (objectui#6849,
// the shape `scripts/check-vi-mock-inherit.mjs` exists to stop). Only
// `useObjectTranslation` is overridden; everything else is the real module.
vi.mock('@object-ui/i18n', async (importOriginal) => ({
...(await importOriginal<Record<string, unknown>>()),
useObjectTranslation: () => ({
t: (key: string) => ({
'common.loading': 'Loading…',
Expand Down
14 changes: 10 additions & 4 deletions packages/i18n/src/__tests__/de-quote-pairing-3876.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -273,8 +273,13 @@ describe('objectui#3876 — de pack closes „ with “ and not with a straight
// keys: `detail.activityFieldChanged` quotes the OLD and NEW field values
// („{{old}}“ / „{{new}}“, two spans in one value, like `navigationSync.
// renamedPage` above) and `detail.activityStatusChanged` the new status
// („{{value}}“) — three interpolated spans, all runtime data.
expect(okSpans, 'correctly paired spans').toBe(58);
// („{{value}}“) — three interpolated spans, all runtime data,
// 59 once objectui#7173 gave `AiPendingActionsInbox` pack keys:
// `aiApprovals.rejectPlaceholder` quotes the EXAMPLE rejection reason the
// placeholder suggests („Falsche Datensatz-ID — …“) — one LITERAL span, like
// `timeline.unsupported.objectBoundGantt` above rather than the interpolated
// ones, because the quoted thing is sample prose this pack authored.
expect(okSpans, 'correctly paired spans').toBe(59);
});

it('keeps the count identity that replaces the card’s count(„) === count(“)', () => {
Expand All@@ -298,8 +303,9 @@ describe('objectui#3876 — de pack closes „ with “ and not with a straight
// after objectui#7149 added the two quoted `ActivityTimeline` sentences
// (three spans between them). `rdq` staying at 0 is the load-bearing half:
// each new value added a MATCHED „…“ pair, not a stray closer that would
// have made `close === open` true for the wrong reason.
expect({ open, close, rdq }).toEqual({ open: 58, close: 58, rdq: 0 });
// have made `close === open` true for the wrong reason. 59 / 59 / 0 after
// objectui#7173 added `aiApprovals.rejectPlaceholder`, one more matched pair.
expect({ open, close, rdq }).toEqual({ open: 59, close: 59, rdq: 0 });
// The durable shape: every „ closed by a “, every surplus “ an English
// opener answered by a ”. Survived translating the two English values.
expect(close).toBe(open + rdq);
Expand Down
40 changes: 40 additions & 0 deletions packages/i18n/src/locales/ar.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -2796,6 +2796,46 @@ const ar = {
openProduction: "فتح بيئة الإنتاج",
manageEnvironments: "إدارة البيئات",
},
aiApprovals: {
title: "موافقات الذكاء الاصطناعي",
description: "إجراءات اقترحها وكيل ذكاء اصطناعي وتحتاج إلى مراجعة بشرية قبل تنفيذها.",
tabPending: "قيد الانتظار",
tabDecided: "تم البت فيها",
tabAll: "الكل",
statusPending: "قيد الانتظار",
statusApproved: "تمت الموافقة",
statusExecuted: "تم التنفيذ",
statusFailed: "فشل",
statusRejected: "مرفوض",
colTool: "الأداة",
colAction: "الإجراء",
colObject: "الكائن",
colStatus: "الحالة",
colProposed: "وقت الاقتراح",
colDecision: "القرار",
emptyTitle: "لا توجد إجراءات في الانتظار",
emptyDescription: "عندما يقترح الذكاء الاصطناعي إجراءً حساسًا سيظهر هنا للمراجعة.",
view: "عرض",
approve: "موافقة",
reject: "رفض",
working: "جارٍ التنفيذ…",
approveAndExecute: "الموافقة والتنفيذ",
outcomeApprove: "موافقة على {{id}}: {{message}}",
outcomeReject: "رفض {{id}}: {{message}}",
outcomeExecuteFailed: "فشل الإجراء أثناء التنفيذ",
drawerFallbackTitle: "إجراء قيد الانتظار",
drawerSubtitle: "الأداة {{tool}} على {{object}}",
fieldProposedBy: "اقترحه",
fieldDecidedBy: "قرّره",
fieldConversation: "المحادثة",
fieldToolInput: "مدخلات الأداة",
fieldResult: "النتيجة",
fieldError: "خطأ",
fieldRejectionReason: "سبب الرفض",
rejectTitle: "هل تريد رفض هذا الإجراء؟",
rejectBody: "يُعاد السبب إلى الذكاء الاصطناعي ليعدّل ردّه التالي.",
rejectPlaceholder: "سبب اختياري (مثال: «معرّف السجل غير صحيح — يرجى التأكيد مع المستخدم أولاً.»)",
},
aiModelStatus: {
summary: "يستخدم الإنشاء / السؤال {{conversational}} ({{conversationalSource}})؛ ويستخدم الإخراج المهيكل {{structured}} ({{structuredSource}}).",
summaryRouting: "سياسة التوجيه: الخطط المجانية ← {{free}}، الخطط المدفوعة ← {{paid}}.",
Expand Down
40 changes: 40 additions & 0 deletions packages/i18n/src/locales/de.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -2789,6 +2789,46 @@ const de = {
openProduction: "Produktion öffnen",
manageEnvironments: "Umgebungen verwalten",
},
aiApprovals: {
title: "KI-Genehmigungen",
description: "Von einem KI-Agenten vorgeschlagene Aktionen, die vor der Ausführung eine menschliche Prüfung benötigen.",
tabPending: "Ausstehend",
tabDecided: "Entschieden",
tabAll: "Alle",
statusPending: "Ausstehend",
statusApproved: "Genehmigt",
statusExecuted: "Ausgeführt",
statusFailed: "Fehlgeschlagen",
statusRejected: "Abgelehnt",
colTool: "Werkzeug",
colAction: "Aktion",
colObject: "Objekt",
colStatus: "Status",
colProposed: "Vorgeschlagen",
colDecision: "Entscheidung",
emptyTitle: "Keine wartenden Aktionen",
emptyDescription: "Wenn die KI eine sensible Aktion vorschlägt, erscheint sie hier zur Prüfung.",
view: "Ansehen",
approve: "Genehmigen",
reject: "Ablehnen",
working: "Wird bearbeitet…",
approveAndExecute: "Genehmigen & ausführen",
outcomeApprove: "Genehmigung für {{id}}: {{message}}",
outcomeReject: "Ablehnung für {{id}}: {{message}}",
outcomeExecuteFailed: "Die Aktion ist bei der Ausführung fehlgeschlagen",
drawerFallbackTitle: "Ausstehende Aktion",
drawerSubtitle: "Werkzeug {{tool}} auf {{object}}",
fieldProposedBy: "Vorgeschlagen von",
fieldDecidedBy: "Entschieden von",
fieldConversation: "Konversation",
fieldToolInput: "Werkzeugeingabe",
fieldResult: "Ergebnis",
fieldError: "Fehler",
fieldRejectionReason: "Ablehnungsgrund",
rejectTitle: "Diese Aktion ablehnen?",
rejectBody: "Der Grund wird an die KI zurückgemeldet, damit sie ihre nächste Antwort anpassen kann.",
rejectPlaceholder: "Optionaler Grund (z. B. „Falsche Datensatz-ID — bitte zuerst mit der Nutzerin oder dem Nutzer klären.“)",
},
aiModelStatus: {
summary: "Erstellen / Fragen nutzt {{conversational}} ({{conversationalSource}}); strukturiert nutzt {{structured}} ({{structuredSource}}).",
summaryRouting: "Routing-Richtlinie: kostenlose Tarife → {{free}}, kostenpflichtige Tarife → {{paid}}.",
Expand Down
47 changes: 47 additions & 0 deletions packages/i18n/src/locales/en.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -3135,6 +3135,53 @@ const en = {
openProduction: 'Open Production',
manageEnvironments: 'Manage environments',
},
// The AI HITL approval inbox (`@object-ui/plugin-chatbot`'s
// `AiPendingActionsInbox`) — objectui#7173. Its four relative-time phrases
// are NOT here: it borrows `detail.justNow` / `minutesAgo` / `hoursAgo` /
// `daysAgo`, already translated in all ten packs, the way `ObjectGrid` and
// `ObjectKanban` borrow `detail.recordDetail`. Distinct from
// `approvalsInbox` above, which is the human approval-PROCESS inbox:
// different surface, different feature, so no rows are shared with it.
aiApprovals: {
title: 'AI Approvals',
description: 'Actions an AI agent proposed that need a human review before execution.',
tabPending: 'Pending',
tabDecided: 'Decided',
tabAll: 'All',
statusPending: 'Pending',
statusApproved: 'Approved',
statusExecuted: 'Executed',
statusFailed: 'Failed',
statusRejected: 'Rejected',
colTool: 'Tool',
colAction: 'Action',
colObject: 'Object',
colStatus: 'Status',
colProposed: 'Proposed',
colDecision: 'Decision',
emptyTitle: 'No actions waiting',
emptyDescription: 'When the AI proposes a sensitive action it will appear here for review.',
view: 'View',
approve: 'Approve',
reject: 'Reject',
working: 'Working…',
approveAndExecute: 'Approve & Execute',
outcomeApprove: 'Approve for {{id}}: {{message}}',
outcomeReject: 'Reject for {{id}}: {{message}}',
outcomeExecuteFailed: 'Action failed during execution',
drawerFallbackTitle: 'Pending action',
drawerSubtitle: 'Tool {{tool}} on {{object}}',
fieldProposedBy: 'Proposed by',
fieldDecidedBy: 'Decided by',
fieldConversation: 'Conversation',
fieldToolInput: 'Tool input',
fieldResult: 'Result',
fieldError: 'Error',
fieldRejectionReason: 'Rejection reason',
rejectTitle: 'Reject this action?',
rejectBody: 'The reason is shown back to the AI so it can adjust its next response.',
rejectPlaceholder: "Optional reason (e.g. 'Wrong record id — please confirm with the user first.')",
},
aiModelStatus: {
summary: 'Build / Ask uses {{conversational}} ({{conversationalSource}}); structured uses {{structured}} ({{structuredSource}}).',
summaryRouting: 'Routing policy: free plans → {{free}}, paid plans → {{paid}}.',
Expand Down
40 changes: 40 additions & 0 deletions packages/i18n/src/locales/es.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -2793,6 +2793,46 @@ const es = {
openProduction: "Abrir producción",
manageEnvironments: "Gestionar entornos",
},
aiApprovals: {
title: "Aprobaciones de IA",
description: "Acciones propuestas por un agente de IA que necesitan revisión humana antes de ejecutarse.",
tabPending: "Pendientes",
tabDecided: "Decididas",
tabAll: "Todas",
statusPending: "Pendiente",
statusApproved: "Aprobada",
statusExecuted: "Ejecutada",
statusFailed: "Fallida",
statusRejected: "Rechazada",
colTool: "Herramienta",
colAction: "Acción",
colObject: "Objeto",
colStatus: "Estado",
colProposed: "Propuesta",
colDecision: "Decisión",
emptyTitle: "No hay acciones en espera",
emptyDescription: "Cuando la IA proponga una acción sensible, aparecerá aquí para su revisión.",
view: "Ver",
approve: "Aprobar",
reject: "Rechazar",
working: "Procesando…",
approveAndExecute: "Aprobar y ejecutar",
outcomeApprove: "Aprobación de {{id}}: {{message}}",
outcomeReject: "Rechazo de {{id}}: {{message}}",
outcomeExecuteFailed: "La acción falló durante su ejecución",
drawerFallbackTitle: "Acción pendiente",
drawerSubtitle: "Herramienta {{tool}} sobre {{object}}",
fieldProposedBy: "Propuesta por",
fieldDecidedBy: "Decidida por",
fieldConversation: "Conversación",
fieldToolInput: "Entrada de la herramienta",
fieldResult: "Resultado",
fieldError: "Error",
fieldRejectionReason: "Motivo del rechazo",
rejectTitle: "¿Rechazar esta acción?",
rejectBody: "El motivo se devuelve a la IA para que ajuste su siguiente respuesta.",
rejectPlaceholder: "Motivo opcional (p. ej.: «ID de registro incorrecto — confírmalo antes con la persona usuaria.»)",
},
aiModelStatus: {
summary: "Crear / Preguntar usa {{conversational}} ({{conversationalSource}}); estructurado usa {{structured}} ({{structuredSource}}).",
summaryRouting: "Política de enrutamiento: planes gratuitos → {{free}}, planes de pago → {{paid}}.",
Expand Down
40 changes: 40 additions & 0 deletions packages/i18n/src/locales/fr.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -2791,6 +2791,46 @@ const fr = {
openProduction: "Ouvrir la production",
manageEnvironments: "Gérer les environnements",
},
aiApprovals: {
title: "Approbations IA",
description: "Actions proposées par un agent IA qui nécessitent une validation humaine avant exécution.",
tabPending: "En attente",
tabDecided: "Traitées",
tabAll: "Toutes",
statusPending: "En attente",
statusApproved: "Approuvée",
statusExecuted: "Exécutée",
statusFailed: "Échouée",
statusRejected: "Rejetée",
colTool: "Outil",
colAction: "Action",
colObject: "Objet",
colStatus: "Statut",
colProposed: "Proposée",
colDecision: "Décision",
emptyTitle: "Aucune action en attente",
emptyDescription: "Lorsque l'IA propose une action sensible, elle apparaît ici pour validation.",
view: "Voir",
approve: "Approuver",
reject: "Rejeter",
working: "En cours…",
approveAndExecute: "Approuver et exécuter",
outcomeApprove: "Approbation de {{id}} : {{message}}",
outcomeReject: "Rejet de {{id}} : {{message}}",
outcomeExecuteFailed: "L'action a échoué pendant son exécution",
drawerFallbackTitle: "Action en attente",
drawerSubtitle: "Outil {{tool}} sur {{object}}",
fieldProposedBy: "Proposée par",
fieldDecidedBy: "Décidée par",
fieldConversation: "Conversation",
fieldToolInput: "Entrée de l'outil",
fieldResult: "Résultat",
fieldError: "Erreur",
fieldRejectionReason: "Motif du rejet",
rejectTitle: "Rejeter cette action ?",
rejectBody: "Le motif est renvoyé à l'IA pour qu'elle ajuste sa prochaine réponse.",
rejectPlaceholder: "Motif facultatif (par ex. « Identifiant d'enregistrement erroné — merci de confirmer d'abord avec l'utilisateur. »)",
},
aiModelStatus: {
summary: "Créer / Demander utilise {{conversational}} ({{conversationalSource}}) ; structuré utilise {{structured}} ({{structuredSource}}).",
summaryRouting: "Politique de routage : offres gratuites → {{free}}, offres payantes → {{paid}}.",
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
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
79 changes: 79 additions & 0 deletions .changeset/7173-ai-pending-actions-inbox-i18n.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
---
'@object-ui/plugin-chatbot': patch
'@object-ui/i18n': patch
---

`AiPendingActionsInbox` speaks the session locale — every string in it, not only its timestamps (objectui#7173).

The AI HITL approval inbox held its own relative-time helper returning hardcoded
English (`'just now'`, `` `${min}m ago` ``), so a zh / ja / ar session read English
relative times on every row. It is the fifth spelling of that helper in the repo,
and the file had **no translation wiring at all** — the unwired-component shape,
not the lookup-swap shape.

It is therefore swept whole. objectui#7142 wired one string into an otherwise
untranslated component and shipped something visibly half-done, and objectui#7149
is what finishing that afterwards cost; the triage ruling on this card (2026-09-01)
carried that forward as *sweep the file whole or leave it*. Everything the user can
read now resolves from the locale packs: the card heading and description, the three
tabs, the refresh button, all five status badges, the six column headings, the empty
state, the row and drawer buttons, all nine drawer field labels, the outcome banner
and the whole reject-reason dialog.

**No new rows for the four relative-time branches.** `detail.justNow`,
`detail.minutesAgo`, `detail.hoursAgo` and `detail.daysAgo` already existed,
translated, in all ten packs, and cross-package key borrowing is this repo's settled
convention rather than an open question — `ObjectGrid`, `ObjectKanban`, `ObjectTree`,
`ListView`, `ObjectView`, `NavigationOverlay`, `RecordAttachmentsPanel`,
`RecordDetailView` and `apps/console` all resolve `detail.*` from outside
`plugin-detail`. One phrase on one kind of control should not get a second
translation that can drift from the first.

The rest of the sweep needed copy no pack had, so `@object-ui/i18n` gains an
`aiApprovals` namespace: 38 keys, translated in all ten packs. It is deliberately
separate from `approvalsInbox`, which is the human approval-**process** inbox — a
different surface and a different feature, so no rows are shared with it. Four
generic verbs are reused rather than forked (`common.refresh`, `common.cancel`,
`common.loading`, `common.ok`).

**⛔ The five relative-time helpers are not unified.** They differ in real behaviour
— `Math.round` here against `Math.floor` in `plugin-detail`, thresholds 45s/30d
against 60s/7d, different tails — so normalising them is a behaviour change wearing
a refactor's clothes and needs its own card. This inbox's arithmetic is untouched,
and three rows in the new suite exist only to pin it: 50s renders `1m ago` (a 60s
threshold would still say "just now"), 90s renders `2m ago` (`Math.floor` gives
`1m ago`), and 20d renders `20d ago` (a 7d threshold would already show a date).

Two assembled English sentences became single interpolated keys — the outcome banner
(`Approve for {{id}}: {{message}}`) and the drawer subtitle
(`Tool {{tool}} on {{object}}`). Their word order differs per locale, which fragments
around a `<code>` element cannot express, so the two identifiers lose their monospace
styling. That is the deliberate cost of making those sentences translatable.

Evidence: an `en`-only assertion cannot discriminate here, because each key's `en`
value is byte-identical to the literal it replaced. The suite asserts in **zh and
ar**, and the provider-less path separately, in its own file (`createI18n` installs
itself as react-i18next's module-level global, so a provider-less render in a file
that has already mounted a provider silently reads that pack instead of the defaults
map). No inline `defaultValue` anywhere (objectui#3517).

Two consequences of the sweep, both landed here rather than left for CI to find:

`packages/app-shell/src/console/ai/__tests__/ConversationsSidebar.test.tsx` froze its
`vi.mock('@object-ui/i18n', ...)` factory to a hand-written object. Its import graph
reaches `plugin-chatbot`, which now resolves `createSafeTranslation` at module scope, so
the frozen surface made that read `undefined` and the file died during COLLECTION — the
objectui#6849 shape, which does not look like a test failure. It now spreads
`importOriginal()` and overrides only `useObjectTranslation`. Measured, not guessed: of
the 41 frozen `@object-ui/i18n` factories in the repo, running every one of them showed
this to be the only file whose graph reaches the package.

The ten pack blocks are locale DATA, and locale data lands in the console's eager
`framework` chunk, so `scripts/check-eager-closure-budget.mjs` raises that chunk's
ceiling from 512,000 to 524,000 gzipped bytes and re-pins its baseline onto a fresh
measurement (502,405 to 514,863). Attributed by three console builds: the merge parent
reads 510,192, this branch with the ten `aiApprovals` blocks cut reads 510,192 again, and
this branch reads 514,863 — so the whole 4,671-byte delta is the pack data and nothing
else. Headroom is kept at the line's own convention (9,137 bytes, 0.10x the regression
the gate must catch) rather than widened; most of the overage was pre-existing drift, with
the merge parent already at 510,192 of the 512,000 allowed.
Original file line numberDiff line numberDiff line change
Expand Up@@ -30,7 +30,15 @@ vi.mock('../../../hooks/useConversationList', () => ({
}),
}));

vi.mock('@object-ui/i18n', () => ({
// `importOriginal` spread, not a hand-written object: this file's import graph
// reaches `@object-ui/plugin-chatbot`, whose `AiPendingActionsInbox` resolves
// `createSafeTranslation` from this package AT MODULE SCOPE. A frozen factory
// makes that read `undefined` and the file dies during COLLECTION — before a
// single test runs, so it does not look like a test failure (objectui#6849,
// the shape `scripts/check-vi-mock-inherit.mjs` exists to stop). Only
// `useObjectTranslation` is overridden; everything else is the real module.
vi.mock('@object-ui/i18n', async (importOriginal) => ({
...(await importOriginal<Record<string, unknown>>()),
useObjectTranslation: () => ({
t: (key: string) => ({
'common.loading': 'Loading…',
Expand Down
14 changes: 10 additions & 4 deletions packages/i18n/src/__tests__/de-quote-pairing-3876.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -273,8 +273,13 @@ describe('objectui#3876 — de pack closes „ with “ and not with a straight
// keys: `detail.activityFieldChanged` quotes the OLD and NEW field values
// („{{old}}“ / „{{new}}“, two spans in one value, like `navigationSync.
// renamedPage` above) and `detail.activityStatusChanged` the new status
// („{{value}}“) — three interpolated spans, all runtime data.
expect(okSpans, 'correctly paired spans').toBe(58);
// („{{value}}“) — three interpolated spans, all runtime data,
// 59 once objectui#7173 gave `AiPendingActionsInbox` pack keys:
// `aiApprovals.rejectPlaceholder` quotes the EXAMPLE rejection reason the
// placeholder suggests („Falsche Datensatz-ID — …“) — one LITERAL span, like
// `timeline.unsupported.objectBoundGantt` above rather than the interpolated
// ones, because the quoted thing is sample prose this pack authored.
expect(okSpans, 'correctly paired spans').toBe(59);
});

it('keeps the count identity that replaces the card’s count(„) === count(“)', () => {
Expand All@@ -298,8 +303,9 @@ describe('objectui#3876 — de pack closes „ with “ and not with a straight
// after objectui#7149 added the two quoted `ActivityTimeline` sentences
// (three spans between them). `rdq` staying at 0 is the load-bearing half:
// each new value added a MATCHED „…“ pair, not a stray closer that would
// have made `close === open` true for the wrong reason.
expect({ open, close, rdq }).toEqual({ open: 58, close: 58, rdq: 0 });
// have made `close === open` true for the wrong reason. 59 / 59 / 0 after
// objectui#7173 added `aiApprovals.rejectPlaceholder`, one more matched pair.
expect({ open, close, rdq }).toEqual({ open: 59, close: 59, rdq: 0 });
// The durable shape: every „ closed by a “, every surplus “ an English
// opener answered by a ”. Survived translating the two English values.
expect(close).toBe(open + rdq);
Expand Down
40 changes: 40 additions & 0 deletions packages/i18n/src/locales/ar.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -2796,6 +2796,46 @@ const ar = {
openProduction: "فتح بيئة الإنتاج",
manageEnvironments: "إدارة البيئات",
},
aiApprovals: {
title: "موافقات الذكاء الاصطناعي",
description: "إجراءات اقترحها وكيل ذكاء اصطناعي وتحتاج إلى مراجعة بشرية قبل تنفيذها.",
tabPending: "قيد الانتظار",
tabDecided: "تم البت فيها",
tabAll: "الكل",
statusPending: "قيد الانتظار",
statusApproved: "تمت الموافقة",
statusExecuted: "تم التنفيذ",
statusFailed: "فشل",
statusRejected: "مرفوض",
colTool: "الأداة",
colAction: "الإجراء",
colObject: "الكائن",
colStatus: "الحالة",
colProposed: "وقت الاقتراح",
colDecision: "القرار",
emptyTitle: "لا توجد إجراءات في الانتظار",
emptyDescription: "عندما يقترح الذكاء الاصطناعي إجراءً حساسًا سيظهر هنا للمراجعة.",
view: "عرض",
approve: "موافقة",
reject: "رفض",
working: "جارٍ التنفيذ…",
approveAndExecute: "الموافقة والتنفيذ",
outcomeApprove: "موافقة على {{id}}: {{message}}",
outcomeReject: "رفض {{id}}: {{message}}",
outcomeExecuteFailed: "فشل الإجراء أثناء التنفيذ",
drawerFallbackTitle: "إجراء قيد الانتظار",
drawerSubtitle: "الأداة {{tool}} على {{object}}",
fieldProposedBy: "اقترحه",
fieldDecidedBy: "قرّره",
fieldConversation: "المحادثة",
fieldToolInput: "مدخلات الأداة",
fieldResult: "النتيجة",
fieldError: "خطأ",
fieldRejectionReason: "سبب الرفض",
rejectTitle: "هل تريد رفض هذا الإجراء؟",
rejectBody: "يُعاد السبب إلى الذكاء الاصطناعي ليعدّل ردّه التالي.",
rejectPlaceholder: "سبب اختياري (مثال: «معرّف السجل غير صحيح — يرجى التأكيد مع المستخدم أولاً.»)",
},
aiModelStatus: {
summary: "يستخدم الإنشاء / السؤال {{conversational}} ({{conversationalSource}})؛ ويستخدم الإخراج المهيكل {{structured}} ({{structuredSource}}).",
summaryRouting: "سياسة التوجيه: الخطط المجانية ← {{free}}، الخطط المدفوعة ← {{paid}}.",
Expand Down
40 changes: 40 additions & 0 deletions packages/i18n/src/locales/de.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -2789,6 +2789,46 @@ const de = {
openProduction: "Produktion öffnen",
manageEnvironments: "Umgebungen verwalten",
},
aiApprovals: {
title: "KI-Genehmigungen",
description: "Von einem KI-Agenten vorgeschlagene Aktionen, die vor der Ausführung eine menschliche Prüfung benötigen.",
tabPending: "Ausstehend",
tabDecided: "Entschieden",
tabAll: "Alle",
statusPending: "Ausstehend",
statusApproved: "Genehmigt",
statusExecuted: "Ausgeführt",
statusFailed: "Fehlgeschlagen",
statusRejected: "Abgelehnt",
colTool: "Werkzeug",
colAction: "Aktion",
colObject: "Objekt",
colStatus: "Status",
colProposed: "Vorgeschlagen",
colDecision: "Entscheidung",
emptyTitle: "Keine wartenden Aktionen",
emptyDescription: "Wenn die KI eine sensible Aktion vorschlägt, erscheint sie hier zur Prüfung.",
view: "Ansehen",
approve: "Genehmigen",
reject: "Ablehnen",
working: "Wird bearbeitet…",
approveAndExecute: "Genehmigen & ausführen",
outcomeApprove: "Genehmigung für {{id}}: {{message}}",
outcomeReject: "Ablehnung für {{id}}: {{message}}",
outcomeExecuteFailed: "Die Aktion ist bei der Ausführung fehlgeschlagen",
drawerFallbackTitle: "Ausstehende Aktion",
drawerSubtitle: "Werkzeug {{tool}} auf {{object}}",
fieldProposedBy: "Vorgeschlagen von",
fieldDecidedBy: "Entschieden von",
fieldConversation: "Konversation",
fieldToolInput: "Werkzeugeingabe",
fieldResult: "Ergebnis",
fieldError: "Fehler",
fieldRejectionReason: "Ablehnungsgrund",
rejectTitle: "Diese Aktion ablehnen?",
rejectBody: "Der Grund wird an die KI zurückgemeldet, damit sie ihre nächste Antwort anpassen kann.",
rejectPlaceholder: "Optionaler Grund (z. B. „Falsche Datensatz-ID — bitte zuerst mit der Nutzerin oder dem Nutzer klären.“)",
},
aiModelStatus: {
summary: "Erstellen / Fragen nutzt {{conversational}} ({{conversationalSource}}); strukturiert nutzt {{structured}} ({{structuredSource}}).",
summaryRouting: "Routing-Richtlinie: kostenlose Tarife → {{free}}, kostenpflichtige Tarife → {{paid}}.",
Expand Down
47 changes: 47 additions & 0 deletions packages/i18n/src/locales/en.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -3135,6 +3135,53 @@ const en = {
openProduction: 'Open Production',
manageEnvironments: 'Manage environments',
},
// The AI HITL approval inbox (`@object-ui/plugin-chatbot`'s
// `AiPendingActionsInbox`) — objectui#7173. Its four relative-time phrases
// are NOT here: it borrows `detail.justNow` / `minutesAgo` / `hoursAgo` /
// `daysAgo`, already translated in all ten packs, the way `ObjectGrid` and
// `ObjectKanban` borrow `detail.recordDetail`. Distinct from
// `approvalsInbox` above, which is the human approval-PROCESS inbox:
// different surface, different feature, so no rows are shared with it.
aiApprovals: {
title: 'AI Approvals',
description: 'Actions an AI agent proposed that need a human review before execution.',
tabPending: 'Pending',
tabDecided: 'Decided',
tabAll: 'All',
statusPending: 'Pending',
statusApproved: 'Approved',
statusExecuted: 'Executed',
statusFailed: 'Failed',
statusRejected: 'Rejected',
colTool: 'Tool',
colAction: 'Action',
colObject: 'Object',
colStatus: 'Status',
colProposed: 'Proposed',
colDecision: 'Decision',
emptyTitle: 'No actions waiting',
emptyDescription: 'When the AI proposes a sensitive action it will appear here for review.',
view: 'View',
approve: 'Approve',
reject: 'Reject',
working: 'Working…',
approveAndExecute: 'Approve & Execute',
outcomeApprove: 'Approve for {{id}}: {{message}}',
outcomeReject: 'Reject for {{id}}: {{message}}',
outcomeExecuteFailed: 'Action failed during execution',
drawerFallbackTitle: 'Pending action',
drawerSubtitle: 'Tool {{tool}} on {{object}}',
fieldProposedBy: 'Proposed by',
fieldDecidedBy: 'Decided by',
fieldConversation: 'Conversation',
fieldToolInput: 'Tool input',
fieldResult: 'Result',
fieldError: 'Error',
fieldRejectionReason: 'Rejection reason',
rejectTitle: 'Reject this action?',
rejectBody: 'The reason is shown back to the AI so it can adjust its next response.',
rejectPlaceholder: "Optional reason (e.g. 'Wrong record id — please confirm with the user first.')",
},
aiModelStatus: {
summary: 'Build / Ask uses {{conversational}} ({{conversationalSource}}); structured uses {{structured}} ({{structuredSource}}).',
summaryRouting: 'Routing policy: free plans → {{free}}, paid plans → {{paid}}.',
Expand Down
40 changes: 40 additions & 0 deletions packages/i18n/src/locales/es.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -2793,6 +2793,46 @@ const es = {
openProduction: "Abrir producción",
manageEnvironments: "Gestionar entornos",
},
aiApprovals: {
title: "Aprobaciones de IA",
description: "Acciones propuestas por un agente de IA que necesitan revisión humana antes de ejecutarse.",
tabPending: "Pendientes",
tabDecided: "Decididas",
tabAll: "Todas",
statusPending: "Pendiente",
statusApproved: "Aprobada",
statusExecuted: "Ejecutada",
statusFailed: "Fallida",
statusRejected: "Rechazada",
colTool: "Herramienta",
colAction: "Acción",
colObject: "Objeto",
colStatus: "Estado",
colProposed: "Propuesta",
colDecision: "Decisión",
emptyTitle: "No hay acciones en espera",
emptyDescription: "Cuando la IA proponga una acción sensible, aparecerá aquí para su revisión.",
view: "Ver",
approve: "Aprobar",
reject: "Rechazar",
working: "Procesando…",
approveAndExecute: "Aprobar y ejecutar",
outcomeApprove: "Aprobación de {{id}}: {{message}}",
outcomeReject: "Rechazo de {{id}}: {{message}}",
outcomeExecuteFailed: "La acción falló durante su ejecución",
drawerFallbackTitle: "Acción pendiente",
drawerSubtitle: "Herramienta {{tool}} sobre {{object}}",
fieldProposedBy: "Propuesta por",
fieldDecidedBy: "Decidida por",
fieldConversation: "Conversación",
fieldToolInput: "Entrada de la herramienta",
fieldResult: "Resultado",
fieldError: "Error",
fieldRejectionReason: "Motivo del rechazo",
rejectTitle: "¿Rechazar esta acción?",
rejectBody: "El motivo se devuelve a la IA para que ajuste su siguiente respuesta.",
rejectPlaceholder: "Motivo opcional (p. ej.: «ID de registro incorrecto — confírmalo antes con la persona usuaria.»)",
},
aiModelStatus: {
summary: "Crear / Preguntar usa {{conversational}} ({{conversationalSource}}); estructurado usa {{structured}} ({{structuredSource}}).",
summaryRouting: "Política de enrutamiento: planes gratuitos → {{free}}, planes de pago → {{paid}}.",
Expand Down
40 changes: 40 additions & 0 deletions packages/i18n/src/locales/fr.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -2791,6 +2791,46 @@ const fr = {
openProduction: "Ouvrir la production",
manageEnvironments: "Gérer les environnements",
},
aiApprovals: {
title: "Approbations IA",
description: "Actions proposées par un agent IA qui nécessitent une validation humaine avant exécution.",
tabPending: "En attente",
tabDecided: "Traitées",
tabAll: "Toutes",
statusPending: "En attente",
statusApproved: "Approuvée",
statusExecuted: "Exécutée",
statusFailed: "Échouée",
statusRejected: "Rejetée",
colTool: "Outil",
colAction: "Action",
colObject: "Objet",
colStatus: "Statut",
colProposed: "Proposée",
colDecision: "Décision",
emptyTitle: "Aucune action en attente",
emptyDescription: "Lorsque l'IA propose une action sensible, elle apparaît ici pour validation.",
view: "Voir",
approve: "Approuver",
reject: "Rejeter",
working: "En cours…",
approveAndExecute: "Approuver et exécuter",
outcomeApprove: "Approbation de {{id}} : {{message}}",
outcomeReject: "Rejet de {{id}} : {{message}}",
outcomeExecuteFailed: "L'action a échoué pendant son exécution",
drawerFallbackTitle: "Action en attente",
drawerSubtitle: "Outil {{tool}} sur {{object}}",
fieldProposedBy: "Proposée par",
fieldDecidedBy: "Décidée par",
fieldConversation: "Conversation",
fieldToolInput: "Entrée de l'outil",
fieldResult: "Résultat",
fieldError: "Erreur",
fieldRejectionReason: "Motif du rejet",
rejectTitle: "Rejeter cette action ?",
rejectBody: "Le motif est renvoyé à l'IA pour qu'elle ajuste sa prochaine réponse.",
rejectPlaceholder: "Motif facultatif (par ex. « Identifiant d'enregistrement erroné — merci de confirmer d'abord avec l'utilisateur. »)",
},
aiModelStatus: {
summary: "Créer / Demander utilise {{conversational}} ({{conversationalSource}}) ; structuré utilise {{structured}} ({{structuredSource}}).",
summaryRouting: "Politique de routage : offres gratuites → {{free}}, offres payantes → {{paid}}.",
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
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
79 changes: 79 additions & 0 deletions .changeset/7173-ai-pending-actions-inbox-i18n.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
---
'@object-ui/plugin-chatbot': patch
'@object-ui/i18n': patch
---

`AiPendingActionsInbox` speaks the session locale — every string in it, not only its timestamps (objectui#7173).

The AI HITL approval inbox held its own relative-time helper returning hardcoded
English (`'just now'`, `` `${min}m ago` ``), so a zh / ja / ar session read English
relative times on every row. It is the fifth spelling of that helper in the repo,
and the file had **no translation wiring at all** — the unwired-component shape,
not the lookup-swap shape.

It is therefore swept whole. objectui#7142 wired one string into an otherwise
untranslated component and shipped something visibly half-done, and objectui#7149
is what finishing that afterwards cost; the triage ruling on this card (2026-09-01)
carried that forward as *sweep the file whole or leave it*. Everything the user can
read now resolves from the locale packs: the card heading and description, the three
tabs, the refresh button, all five status badges, the six column headings, the empty
state, the row and drawer buttons, all nine drawer field labels, the outcome banner
and the whole reject-reason dialog.

**No new rows for the four relative-time branches.** `detail.justNow`,
`detail.minutesAgo`, `detail.hoursAgo` and `detail.daysAgo` already existed,
translated, in all ten packs, and cross-package key borrowing is this repo's settled
convention rather than an open question — `ObjectGrid`, `ObjectKanban`, `ObjectTree`,
`ListView`, `ObjectView`, `NavigationOverlay`, `RecordAttachmentsPanel`,
`RecordDetailView` and `apps/console` all resolve `detail.*` from outside
`plugin-detail`. One phrase on one kind of control should not get a second
translation that can drift from the first.

The rest of the sweep needed copy no pack had, so `@object-ui/i18n` gains an
`aiApprovals` namespace: 38 keys, translated in all ten packs. It is deliberately
separate from `approvalsInbox`, which is the human approval-**process** inbox — a
different surface and a different feature, so no rows are shared with it. Four
generic verbs are reused rather than forked (`common.refresh`, `common.cancel`,
`common.loading`, `common.ok`).

**⛔ The five relative-time helpers are not unified.** They differ in real behaviour
— `Math.round` here against `Math.floor` in `plugin-detail`, thresholds 45s/30d
against 60s/7d, different tails — so normalising them is a behaviour change wearing
a refactor's clothes and needs its own card. This inbox's arithmetic is untouched,
and three rows in the new suite exist only to pin it: 50s renders `1m ago` (a 60s
threshold would still say "just now"), 90s renders `2m ago` (`Math.floor` gives
`1m ago`), and 20d renders `20d ago` (a 7d threshold would already show a date).

Two assembled English sentences became single interpolated keys — the outcome banner
(`Approve for {{id}}: {{message}}`) and the drawer subtitle
(`Tool {{tool}} on {{object}}`). Their word order differs per locale, which fragments
around a `<code>` element cannot express, so the two identifiers lose their monospace
styling. That is the deliberate cost of making those sentences translatable.

Evidence: an `en`-only assertion cannot discriminate here, because each key's `en`
value is byte-identical to the literal it replaced. The suite asserts in **zh and
ar**, and the provider-less path separately, in its own file (`createI18n` installs
itself as react-i18next's module-level global, so a provider-less render in a file
that has already mounted a provider silently reads that pack instead of the defaults
map). No inline `defaultValue` anywhere (objectui#3517).

Two consequences of the sweep, both landed here rather than left for CI to find:

`packages/app-shell/src/console/ai/__tests__/ConversationsSidebar.test.tsx` froze its
`vi.mock('@object-ui/i18n', ...)` factory to a hand-written object. Its import graph
reaches `plugin-chatbot`, which now resolves `createSafeTranslation` at module scope, so
the frozen surface made that read `undefined` and the file died during COLLECTION — the
objectui#6849 shape, which does not look like a test failure. It now spreads
`importOriginal()` and overrides only `useObjectTranslation`. Measured, not guessed: of
the 41 frozen `@object-ui/i18n` factories in the repo, running every one of them showed
this to be the only file whose graph reaches the package.

The ten pack blocks are locale DATA, and locale data lands in the console's eager
`framework` chunk, so `scripts/check-eager-closure-budget.mjs` raises that chunk's
ceiling from 512,000 to 524,000 gzipped bytes and re-pins its baseline onto a fresh
measurement (502,405 to 514,863). Attributed by three console builds: the merge parent
reads 510,192, this branch with the ten `aiApprovals` blocks cut reads 510,192 again, and
this branch reads 514,863 — so the whole 4,671-byte delta is the pack data and nothing
else. Headroom is kept at the line's own convention (9,137 bytes, 0.10x the regression
the gate must catch) rather than widened; most of the overage was pre-existing drift, with
the merge parent already at 510,192 of the 512,000 allowed.
Original file line numberDiff line numberDiff line change
Expand Up@@ -30,7 +30,15 @@ vi.mock('../../../hooks/useConversationList', () => ({
}),
}));

vi.mock('@object-ui/i18n', () => ({
// `importOriginal` spread, not a hand-written object: this file's import graph
// reaches `@object-ui/plugin-chatbot`, whose `AiPendingActionsInbox` resolves
// `createSafeTranslation` from this package AT MODULE SCOPE. A frozen factory
// makes that read `undefined` and the file dies during COLLECTION — before a
// single test runs, so it does not look like a test failure (objectui#6849,
// the shape `scripts/check-vi-mock-inherit.mjs` exists to stop). Only
// `useObjectTranslation` is overridden; everything else is the real module.
vi.mock('@object-ui/i18n', async (importOriginal) => ({
...(await importOriginal<Record<string, unknown>>()),
useObjectTranslation: () => ({
t: (key: string) => ({
'common.loading': 'Loading…',
Expand Down
14 changes: 10 additions & 4 deletions packages/i18n/src/__tests__/de-quote-pairing-3876.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -273,8 +273,13 @@ describe('objectui#3876 — de pack closes „ with “ and not with a straight
// keys: `detail.activityFieldChanged` quotes the OLD and NEW field values
// („{{old}}“ / „{{new}}“, two spans in one value, like `navigationSync.
// renamedPage` above) and `detail.activityStatusChanged` the new status
// („{{value}}“) — three interpolated spans, all runtime data.
expect(okSpans, 'correctly paired spans').toBe(58);
// („{{value}}“) — three interpolated spans, all runtime data,
// 59 once objectui#7173 gave `AiPendingActionsInbox` pack keys:
// `aiApprovals.rejectPlaceholder` quotes the EXAMPLE rejection reason the
// placeholder suggests („Falsche Datensatz-ID — …“) — one LITERAL span, like
// `timeline.unsupported.objectBoundGantt` above rather than the interpolated
// ones, because the quoted thing is sample prose this pack authored.
expect(okSpans, 'correctly paired spans').toBe(59);
});

it('keeps the count identity that replaces the card’s count(„) === count(“)', () => {
Expand All@@ -298,8 +303,9 @@ describe('objectui#3876 — de pack closes „ with “ and not with a straight
// after objectui#7149 added the two quoted `ActivityTimeline` sentences
// (three spans between them). `rdq` staying at 0 is the load-bearing half:
// each new value added a MATCHED „…“ pair, not a stray closer that would
// have made `close === open` true for the wrong reason.
expect({ open, close, rdq }).toEqual({ open: 58, close: 58, rdq: 0 });
// have made `close === open` true for the wrong reason. 59 / 59 / 0 after
// objectui#7173 added `aiApprovals.rejectPlaceholder`, one more matched pair.
expect({ open, close, rdq }).toEqual({ open: 59, close: 59, rdq: 0 });
// The durable shape: every „ closed by a “, every surplus “ an English
// opener answered by a ”. Survived translating the two English values.
expect(close).toBe(open + rdq);
Expand Down
40 changes: 40 additions & 0 deletions packages/i18n/src/locales/ar.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -2796,6 +2796,46 @@ const ar = {
openProduction: "فتح بيئة الإنتاج",
manageEnvironments: "إدارة البيئات",
},
aiApprovals: {
title: "موافقات الذكاء الاصطناعي",
description: "إجراءات اقترحها وكيل ذكاء اصطناعي وتحتاج إلى مراجعة بشرية قبل تنفيذها.",
tabPending: "قيد الانتظار",
tabDecided: "تم البت فيها",
tabAll: "الكل",
statusPending: "قيد الانتظار",
statusApproved: "تمت الموافقة",
statusExecuted: "تم التنفيذ",
statusFailed: "فشل",
statusRejected: "مرفوض",
colTool: "الأداة",
colAction: "الإجراء",
colObject: "الكائن",
colStatus: "الحالة",
colProposed: "وقت الاقتراح",
colDecision: "القرار",
emptyTitle: "لا توجد إجراءات في الانتظار",
emptyDescription: "عندما يقترح الذكاء الاصطناعي إجراءً حساسًا سيظهر هنا للمراجعة.",
view: "عرض",
approve: "موافقة",
reject: "رفض",
working: "جارٍ التنفيذ…",
approveAndExecute: "الموافقة والتنفيذ",
outcomeApprove: "موافقة على {{id}}: {{message}}",
outcomeReject: "رفض {{id}}: {{message}}",
outcomeExecuteFailed: "فشل الإجراء أثناء التنفيذ",
drawerFallbackTitle: "إجراء قيد الانتظار",
drawerSubtitle: "الأداة {{tool}} على {{object}}",
fieldProposedBy: "اقترحه",
fieldDecidedBy: "قرّره",
fieldConversation: "المحادثة",
fieldToolInput: "مدخلات الأداة",
fieldResult: "النتيجة",
fieldError: "خطأ",
fieldRejectionReason: "سبب الرفض",
rejectTitle: "هل تريد رفض هذا الإجراء؟",
rejectBody: "يُعاد السبب إلى الذكاء الاصطناعي ليعدّل ردّه التالي.",
rejectPlaceholder: "سبب اختياري (مثال: «معرّف السجل غير صحيح — يرجى التأكيد مع المستخدم أولاً.»)",
},
aiModelStatus: {
summary: "يستخدم الإنشاء / السؤال {{conversational}} ({{conversationalSource}})؛ ويستخدم الإخراج المهيكل {{structured}} ({{structuredSource}}).",
summaryRouting: "سياسة التوجيه: الخطط المجانية ← {{free}}، الخطط المدفوعة ← {{paid}}.",
Expand Down
40 changes: 40 additions & 0 deletions packages/i18n/src/locales/de.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -2789,6 +2789,46 @@ const de = {
openProduction: "Produktion öffnen",
manageEnvironments: "Umgebungen verwalten",
},
aiApprovals: {
title: "KI-Genehmigungen",
description: "Von einem KI-Agenten vorgeschlagene Aktionen, die vor der Ausführung eine menschliche Prüfung benötigen.",
tabPending: "Ausstehend",
tabDecided: "Entschieden",
tabAll: "Alle",
statusPending: "Ausstehend",
statusApproved: "Genehmigt",
statusExecuted: "Ausgeführt",
statusFailed: "Fehlgeschlagen",
statusRejected: "Abgelehnt",
colTool: "Werkzeug",
colAction: "Aktion",
colObject: "Objekt",
colStatus: "Status",
colProposed: "Vorgeschlagen",
colDecision: "Entscheidung",
emptyTitle: "Keine wartenden Aktionen",
emptyDescription: "Wenn die KI eine sensible Aktion vorschlägt, erscheint sie hier zur Prüfung.",
view: "Ansehen",
approve: "Genehmigen",
reject: "Ablehnen",
working: "Wird bearbeitet…",
approveAndExecute: "Genehmigen & ausführen",
outcomeApprove: "Genehmigung für {{id}}: {{message}}",
outcomeReject: "Ablehnung für {{id}}: {{message}}",
outcomeExecuteFailed: "Die Aktion ist bei der Ausführung fehlgeschlagen",
drawerFallbackTitle: "Ausstehende Aktion",
drawerSubtitle: "Werkzeug {{tool}} auf {{object}}",
fieldProposedBy: "Vorgeschlagen von",
fieldDecidedBy: "Entschieden von",
fieldConversation: "Konversation",
fieldToolInput: "Werkzeugeingabe",
fieldResult: "Ergebnis",
fieldError: "Fehler",
fieldRejectionReason: "Ablehnungsgrund",
rejectTitle: "Diese Aktion ablehnen?",
rejectBody: "Der Grund wird an die KI zurückgemeldet, damit sie ihre nächste Antwort anpassen kann.",
rejectPlaceholder: "Optionaler Grund (z. B. „Falsche Datensatz-ID — bitte zuerst mit der Nutzerin oder dem Nutzer klären.“)",
},
aiModelStatus: {
summary: "Erstellen / Fragen nutzt {{conversational}} ({{conversationalSource}}); strukturiert nutzt {{structured}} ({{structuredSource}}).",
summaryRouting: "Routing-Richtlinie: kostenlose Tarife → {{free}}, kostenpflichtige Tarife → {{paid}}.",
Expand Down
47 changes: 47 additions & 0 deletions packages/i18n/src/locales/en.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -3135,6 +3135,53 @@ const en = {
openProduction: 'Open Production',
manageEnvironments: 'Manage environments',
},
// The AI HITL approval inbox (`@object-ui/plugin-chatbot`'s
// `AiPendingActionsInbox`) — objectui#7173. Its four relative-time phrases
// are NOT here: it borrows `detail.justNow` / `minutesAgo` / `hoursAgo` /
// `daysAgo`, already translated in all ten packs, the way `ObjectGrid` and
// `ObjectKanban` borrow `detail.recordDetail`. Distinct from
// `approvalsInbox` above, which is the human approval-PROCESS inbox:
// different surface, different feature, so no rows are shared with it.
aiApprovals: {
title: 'AI Approvals',
description: 'Actions an AI agent proposed that need a human review before execution.',
tabPending: 'Pending',
tabDecided: 'Decided',
tabAll: 'All',
statusPending: 'Pending',
statusApproved: 'Approved',
statusExecuted: 'Executed',
statusFailed: 'Failed',
statusRejected: 'Rejected',
colTool: 'Tool',
colAction: 'Action',
colObject: 'Object',
colStatus: 'Status',
colProposed: 'Proposed',
colDecision: 'Decision',
emptyTitle: 'No actions waiting',
emptyDescription: 'When the AI proposes a sensitive action it will appear here for review.',
view: 'View',
approve: 'Approve',
reject: 'Reject',
working: 'Working…',
approveAndExecute: 'Approve & Execute',
outcomeApprove: 'Approve for {{id}}: {{message}}',
outcomeReject: 'Reject for {{id}}: {{message}}',
outcomeExecuteFailed: 'Action failed during execution',
drawerFallbackTitle: 'Pending action',
drawerSubtitle: 'Tool {{tool}} on {{object}}',
fieldProposedBy: 'Proposed by',
fieldDecidedBy: 'Decided by',
fieldConversation: 'Conversation',
fieldToolInput: 'Tool input',
fieldResult: 'Result',
fieldError: 'Error',
fieldRejectionReason: 'Rejection reason',
rejectTitle: 'Reject this action?',
rejectBody: 'The reason is shown back to the AI so it can adjust its next response.',
rejectPlaceholder: "Optional reason (e.g. 'Wrong record id — please confirm with the user first.')",
},
aiModelStatus: {
summary: 'Build / Ask uses {{conversational}} ({{conversationalSource}}); structured uses {{structured}} ({{structuredSource}}).',
summaryRouting: 'Routing policy: free plans → {{free}}, paid plans → {{paid}}.',
Expand Down
40 changes: 40 additions & 0 deletions packages/i18n/src/locales/es.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -2793,6 +2793,46 @@ const es = {
openProduction: "Abrir producción",
manageEnvironments: "Gestionar entornos",
},
aiApprovals: {
title: "Aprobaciones de IA",
description: "Acciones propuestas por un agente de IA que necesitan revisión humana antes de ejecutarse.",
tabPending: "Pendientes",
tabDecided: "Decididas",
tabAll: "Todas",
statusPending: "Pendiente",
statusApproved: "Aprobada",
statusExecuted: "Ejecutada",
statusFailed: "Fallida",
statusRejected: "Rechazada",
colTool: "Herramienta",
colAction: "Acción",
colObject: "Objeto",
colStatus: "Estado",
colProposed: "Propuesta",
colDecision: "Decisión",
emptyTitle: "No hay acciones en espera",
emptyDescription: "Cuando la IA proponga una acción sensible, aparecerá aquí para su revisión.",
view: "Ver",
approve: "Aprobar",
reject: "Rechazar",
working: "Procesando…",
approveAndExecute: "Aprobar y ejecutar",
outcomeApprove: "Aprobación de {{id}}: {{message}}",
outcomeReject: "Rechazo de {{id}}: {{message}}",
outcomeExecuteFailed: "La acción falló durante su ejecución",
drawerFallbackTitle: "Acción pendiente",
drawerSubtitle: "Herramienta {{tool}} sobre {{object}}",
fieldProposedBy: "Propuesta por",
fieldDecidedBy: "Decidida por",
fieldConversation: "Conversación",
fieldToolInput: "Entrada de la herramienta",
fieldResult: "Resultado",
fieldError: "Error",
fieldRejectionReason: "Motivo del rechazo",
rejectTitle: "¿Rechazar esta acción?",
rejectBody: "El motivo se devuelve a la IA para que ajuste su siguiente respuesta.",
rejectPlaceholder: "Motivo opcional (p. ej.: «ID de registro incorrecto — confírmalo antes con la persona usuaria.»)",
},
aiModelStatus: {
summary: "Crear / Preguntar usa {{conversational}} ({{conversationalSource}}); estructurado usa {{structured}} ({{structuredSource}}).",
summaryRouting: "Política de enrutamiento: planes gratuitos → {{free}}, planes de pago → {{paid}}.",
Expand Down
40 changes: 40 additions & 0 deletions packages/i18n/src/locales/fr.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -2791,6 +2791,46 @@ const fr = {
openProduction: "Ouvrir la production",
manageEnvironments: "Gérer les environnements",
},
aiApprovals: {
title: "Approbations IA",
description: "Actions proposées par un agent IA qui nécessitent une validation humaine avant exécution.",
tabPending: "En attente",
tabDecided: "Traitées",
tabAll: "Toutes",
statusPending: "En attente",
statusApproved: "Approuvée",
statusExecuted: "Exécutée",
statusFailed: "Échouée",
statusRejected: "Rejetée",
colTool: "Outil",
colAction: "Action",
colObject: "Objet",
colStatus: "Statut",
colProposed: "Proposée",
colDecision: "Décision",
emptyTitle: "Aucune action en attente",
emptyDescription: "Lorsque l'IA propose une action sensible, elle apparaît ici pour validation.",
view: "Voir",
approve: "Approuver",
reject: "Rejeter",
working: "En cours…",
approveAndExecute: "Approuver et exécuter",
outcomeApprove: "Approbation de {{id}} : {{message}}",
outcomeReject: "Rejet de {{id}} : {{message}}",
outcomeExecuteFailed: "L'action a échoué pendant son exécution",
drawerFallbackTitle: "Action en attente",
drawerSubtitle: "Outil {{tool}} sur {{object}}",
fieldProposedBy: "Proposée par",
fieldDecidedBy: "Décidée par",
fieldConversation: "Conversation",
fieldToolInput: "Entrée de l'outil",
fieldResult: "Résultat",
fieldError: "Erreur",
fieldRejectionReason: "Motif du rejet",
rejectTitle: "Rejeter cette action ?",
rejectBody: "Le motif est renvoyé à l'IA pour qu'elle ajuste sa prochaine réponse.",
rejectPlaceholder: "Motif facultatif (par ex. « Identifiant d'enregistrement erroné — merci de confirmer d'abord avec l'utilisateur. »)",
},
aiModelStatus: {
summary: "Créer / Demander utilise {{conversational}} ({{conversationalSource}}) ; structuré utilise {{structured}} ({{structuredSource}}).",
summaryRouting: "Politique de routage : offres gratuites → {{free}}, offres payantes → {{paid}}.",
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
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
79 changes: 79 additions & 0 deletions .changeset/7173-ai-pending-actions-inbox-i18n.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
---
'@object-ui/plugin-chatbot': patch
'@object-ui/i18n': patch
---

`AiPendingActionsInbox` speaks the session locale — every string in it, not only its timestamps (objectui#7173).

The AI HITL approval inbox held its own relative-time helper returning hardcoded
English (`'just now'`, `` `${min}m ago` ``), so a zh / ja / ar session read English
relative times on every row. It is the fifth spelling of that helper in the repo,
and the file had **no translation wiring at all** — the unwired-component shape,
not the lookup-swap shape.

It is therefore swept whole. objectui#7142 wired one string into an otherwise
untranslated component and shipped something visibly half-done, and objectui#7149
is what finishing that afterwards cost; the triage ruling on this card (2026-09-01)
carried that forward as *sweep the file whole or leave it*. Everything the user can
read now resolves from the locale packs: the card heading and description, the three
tabs, the refresh button, all five status badges, the six column headings, the empty
state, the row and drawer buttons, all nine drawer field labels, the outcome banner
and the whole reject-reason dialog.

**No new rows for the four relative-time branches.** `detail.justNow`,
`detail.minutesAgo`, `detail.hoursAgo` and `detail.daysAgo` already existed,
translated, in all ten packs, and cross-package key borrowing is this repo's settled
convention rather than an open question — `ObjectGrid`, `ObjectKanban`, `ObjectTree`,
`ListView`, `ObjectView`, `NavigationOverlay`, `RecordAttachmentsPanel`,
`RecordDetailView` and `apps/console` all resolve `detail.*` from outside
`plugin-detail`. One phrase on one kind of control should not get a second
translation that can drift from the first.

The rest of the sweep needed copy no pack had, so `@object-ui/i18n` gains an
`aiApprovals` namespace: 38 keys, translated in all ten packs. It is deliberately
separate from `approvalsInbox`, which is the human approval-**process** inbox — a
different surface and a different feature, so no rows are shared with it. Four
generic verbs are reused rather than forked (`common.refresh`, `common.cancel`,
`common.loading`, `common.ok`).

**⛔ The five relative-time helpers are not unified.** They differ in real behaviour
— `Math.round` here against `Math.floor` in `plugin-detail`, thresholds 45s/30d
against 60s/7d, different tails — so normalising them is a behaviour change wearing
a refactor's clothes and needs its own card. This inbox's arithmetic is untouched,
and three rows in the new suite exist only to pin it: 50s renders `1m ago` (a 60s
threshold would still say "just now"), 90s renders `2m ago` (`Math.floor` gives
`1m ago`), and 20d renders `20d ago` (a 7d threshold would already show a date).

Two assembled English sentences became single interpolated keys — the outcome banner
(`Approve for {{id}}: {{message}}`) and the drawer subtitle
(`Tool {{tool}} on {{object}}`). Their word order differs per locale, which fragments
around a `<code>` element cannot express, so the two identifiers lose their monospace
styling. That is the deliberate cost of making those sentences translatable.

Evidence: an `en`-only assertion cannot discriminate here, because each key's `en`
value is byte-identical to the literal it replaced. The suite asserts in **zh and
ar**, and the provider-less path separately, in its own file (`createI18n` installs
itself as react-i18next's module-level global, so a provider-less render in a file
that has already mounted a provider silently reads that pack instead of the defaults
map). No inline `defaultValue` anywhere (objectui#3517).

Two consequences of the sweep, both landed here rather than left for CI to find:

`packages/app-shell/src/console/ai/__tests__/ConversationsSidebar.test.tsx` froze its
`vi.mock('@object-ui/i18n', ...)` factory to a hand-written object. Its import graph
reaches `plugin-chatbot`, which now resolves `createSafeTranslation` at module scope, so
the frozen surface made that read `undefined` and the file died during COLLECTION — the
objectui#6849 shape, which does not look like a test failure. It now spreads
`importOriginal()` and overrides only `useObjectTranslation`. Measured, not guessed: of
the 41 frozen `@object-ui/i18n` factories in the repo, running every one of them showed
this to be the only file whose graph reaches the package.

The ten pack blocks are locale DATA, and locale data lands in the console's eager
`framework` chunk, so `scripts/check-eager-closure-budget.mjs` raises that chunk's
ceiling from 512,000 to 524,000 gzipped bytes and re-pins its baseline onto a fresh
measurement (502,405 to 514,863). Attributed by three console builds: the merge parent
reads 510,192, this branch with the ten `aiApprovals` blocks cut reads 510,192 again, and
this branch reads 514,863 — so the whole 4,671-byte delta is the pack data and nothing
else. Headroom is kept at the line's own convention (9,137 bytes, 0.10x the regression
the gate must catch) rather than widened; most of the overage was pre-existing drift, with
the merge parent already at 510,192 of the 512,000 allowed.
Original file line numberDiff line numberDiff line change
Expand Up@@ -30,7 +30,15 @@ vi.mock('../../../hooks/useConversationList', () => ({
}),
}));

vi.mock('@object-ui/i18n', () => ({
// `importOriginal` spread, not a hand-written object: this file's import graph
// reaches `@object-ui/plugin-chatbot`, whose `AiPendingActionsInbox` resolves
// `createSafeTranslation` from this package AT MODULE SCOPE. A frozen factory
// makes that read `undefined` and the file dies during COLLECTION — before a
// single test runs, so it does not look like a test failure (objectui#6849,
// the shape `scripts/check-vi-mock-inherit.mjs` exists to stop). Only
// `useObjectTranslation` is overridden; everything else is the real module.
vi.mock('@object-ui/i18n', async (importOriginal) => ({
...(await importOriginal<Record<string, unknown>>()),
useObjectTranslation: () => ({
t: (key: string) => ({
'common.loading': 'Loading…',
Expand Down
14 changes: 10 additions & 4 deletions packages/i18n/src/__tests__/de-quote-pairing-3876.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -273,8 +273,13 @@ describe('objectui#3876 — de pack closes „ with “ and not with a straight
// keys: `detail.activityFieldChanged` quotes the OLD and NEW field values
// („{{old}}“ / „{{new}}“, two spans in one value, like `navigationSync.
// renamedPage` above) and `detail.activityStatusChanged` the new status
// („{{value}}“) — three interpolated spans, all runtime data.
expect(okSpans, 'correctly paired spans').toBe(58);
// („{{value}}“) — three interpolated spans, all runtime data,
// 59 once objectui#7173 gave `AiPendingActionsInbox` pack keys:
// `aiApprovals.rejectPlaceholder` quotes the EXAMPLE rejection reason the
// placeholder suggests („Falsche Datensatz-ID — …“) — one LITERAL span, like
// `timeline.unsupported.objectBoundGantt` above rather than the interpolated
// ones, because the quoted thing is sample prose this pack authored.
expect(okSpans, 'correctly paired spans').toBe(59);
});

it('keeps the count identity that replaces the card’s count(„) === count(“)', () => {
Expand All@@ -298,8 +303,9 @@ describe('objectui#3876 — de pack closes „ with “ and not with a straight
// after objectui#7149 added the two quoted `ActivityTimeline` sentences
// (three spans between them). `rdq` staying at 0 is the load-bearing half:
// each new value added a MATCHED „…“ pair, not a stray closer that would
// have made `close === open` true for the wrong reason.
expect({ open, close, rdq }).toEqual({ open: 58, close: 58, rdq: 0 });
// have made `close === open` true for the wrong reason. 59 / 59 / 0 after
// objectui#7173 added `aiApprovals.rejectPlaceholder`, one more matched pair.
expect({ open, close, rdq }).toEqual({ open: 59, close: 59, rdq: 0 });
// The durable shape: every „ closed by a “, every surplus “ an English
// opener answered by a ”. Survived translating the two English values.
expect(close).toBe(open + rdq);
Expand Down
40 changes: 40 additions & 0 deletions packages/i18n/src/locales/ar.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -2796,6 +2796,46 @@ const ar = {
openProduction: "فتح بيئة الإنتاج",
manageEnvironments: "إدارة البيئات",
},
aiApprovals: {
title: "موافقات الذكاء الاصطناعي",
description: "إجراءات اقترحها وكيل ذكاء اصطناعي وتحتاج إلى مراجعة بشرية قبل تنفيذها.",
tabPending: "قيد الانتظار",
tabDecided: "تم البت فيها",
tabAll: "الكل",
statusPending: "قيد الانتظار",
statusApproved: "تمت الموافقة",
statusExecuted: "تم التنفيذ",
statusFailed: "فشل",
statusRejected: "مرفوض",
colTool: "الأداة",
colAction: "الإجراء",
colObject: "الكائن",
colStatus: "الحالة",
colProposed: "وقت الاقتراح",
colDecision: "القرار",
emptyTitle: "لا توجد إجراءات في الانتظار",
emptyDescription: "عندما يقترح الذكاء الاصطناعي إجراءً حساسًا سيظهر هنا للمراجعة.",
view: "عرض",
approve: "موافقة",
reject: "رفض",
working: "جارٍ التنفيذ…",
approveAndExecute: "الموافقة والتنفيذ",
outcomeApprove: "موافقة على {{id}}: {{message}}",
outcomeReject: "رفض {{id}}: {{message}}",
outcomeExecuteFailed: "فشل الإجراء أثناء التنفيذ",
drawerFallbackTitle: "إجراء قيد الانتظار",
drawerSubtitle: "الأداة {{tool}} على {{object}}",
fieldProposedBy: "اقترحه",
fieldDecidedBy: "قرّره",
fieldConversation: "المحادثة",
fieldToolInput: "مدخلات الأداة",
fieldResult: "النتيجة",
fieldError: "خطأ",
fieldRejectionReason: "سبب الرفض",
rejectTitle: "هل تريد رفض هذا الإجراء؟",
rejectBody: "يُعاد السبب إلى الذكاء الاصطناعي ليعدّل ردّه التالي.",
rejectPlaceholder: "سبب اختياري (مثال: «معرّف السجل غير صحيح — يرجى التأكيد مع المستخدم أولاً.»)",
},
aiModelStatus: {
summary: "يستخدم الإنشاء / السؤال {{conversational}} ({{conversationalSource}})؛ ويستخدم الإخراج المهيكل {{structured}} ({{structuredSource}}).",
summaryRouting: "سياسة التوجيه: الخطط المجانية ← {{free}}، الخطط المدفوعة ← {{paid}}.",
Expand Down
40 changes: 40 additions & 0 deletions packages/i18n/src/locales/de.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -2789,6 +2789,46 @@ const de = {
openProduction: "Produktion öffnen",
manageEnvironments: "Umgebungen verwalten",
},
aiApprovals: {
title: "KI-Genehmigungen",
description: "Von einem KI-Agenten vorgeschlagene Aktionen, die vor der Ausführung eine menschliche Prüfung benötigen.",
tabPending: "Ausstehend",
tabDecided: "Entschieden",
tabAll: "Alle",
statusPending: "Ausstehend",
statusApproved: "Genehmigt",
statusExecuted: "Ausgeführt",
statusFailed: "Fehlgeschlagen",
statusRejected: "Abgelehnt",
colTool: "Werkzeug",
colAction: "Aktion",
colObject: "Objekt",
colStatus: "Status",
colProposed: "Vorgeschlagen",
colDecision: "Entscheidung",
emptyTitle: "Keine wartenden Aktionen",
emptyDescription: "Wenn die KI eine sensible Aktion vorschlägt, erscheint sie hier zur Prüfung.",
view: "Ansehen",
approve: "Genehmigen",
reject: "Ablehnen",
working: "Wird bearbeitet…",
approveAndExecute: "Genehmigen & ausführen",
outcomeApprove: "Genehmigung für {{id}}: {{message}}",
outcomeReject: "Ablehnung für {{id}}: {{message}}",
outcomeExecuteFailed: "Die Aktion ist bei der Ausführung fehlgeschlagen",
drawerFallbackTitle: "Ausstehende Aktion",
drawerSubtitle: "Werkzeug {{tool}} auf {{object}}",
fieldProposedBy: "Vorgeschlagen von",
fieldDecidedBy: "Entschieden von",
fieldConversation: "Konversation",
fieldToolInput: "Werkzeugeingabe",
fieldResult: "Ergebnis",
fieldError: "Fehler",
fieldRejectionReason: "Ablehnungsgrund",
rejectTitle: "Diese Aktion ablehnen?",
rejectBody: "Der Grund wird an die KI zurückgemeldet, damit sie ihre nächste Antwort anpassen kann.",
rejectPlaceholder: "Optionaler Grund (z. B. „Falsche Datensatz-ID — bitte zuerst mit der Nutzerin oder dem Nutzer klären.“)",
},
aiModelStatus: {
summary: "Erstellen / Fragen nutzt {{conversational}} ({{conversationalSource}}); strukturiert nutzt {{structured}} ({{structuredSource}}).",
summaryRouting: "Routing-Richtlinie: kostenlose Tarife → {{free}}, kostenpflichtige Tarife → {{paid}}.",
Expand Down
47 changes: 47 additions & 0 deletions packages/i18n/src/locales/en.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -3135,6 +3135,53 @@ const en = {
openProduction: 'Open Production',
manageEnvironments: 'Manage environments',
},
// The AI HITL approval inbox (`@object-ui/plugin-chatbot`'s
// `AiPendingActionsInbox`) — objectui#7173. Its four relative-time phrases
// are NOT here: it borrows `detail.justNow` / `minutesAgo` / `hoursAgo` /
// `daysAgo`, already translated in all ten packs, the way `ObjectGrid` and
// `ObjectKanban` borrow `detail.recordDetail`. Distinct from
// `approvalsInbox` above, which is the human approval-PROCESS inbox:
// different surface, different feature, so no rows are shared with it.
aiApprovals: {
title: 'AI Approvals',
description: 'Actions an AI agent proposed that need a human review before execution.',
tabPending: 'Pending',
tabDecided: 'Decided',
tabAll: 'All',
statusPending: 'Pending',
statusApproved: 'Approved',
statusExecuted: 'Executed',
statusFailed: 'Failed',
statusRejected: 'Rejected',
colTool: 'Tool',
colAction: 'Action',
colObject: 'Object',
colStatus: 'Status',
colProposed: 'Proposed',
colDecision: 'Decision',
emptyTitle: 'No actions waiting',
emptyDescription: 'When the AI proposes a sensitive action it will appear here for review.',
view: 'View',
approve: 'Approve',
reject: 'Reject',
working: 'Working…',
approveAndExecute: 'Approve & Execute',
outcomeApprove: 'Approve for {{id}}: {{message}}',
outcomeReject: 'Reject for {{id}}: {{message}}',
outcomeExecuteFailed: 'Action failed during execution',
drawerFallbackTitle: 'Pending action',
drawerSubtitle: 'Tool {{tool}} on {{object}}',
fieldProposedBy: 'Proposed by',
fieldDecidedBy: 'Decided by',
fieldConversation: 'Conversation',
fieldToolInput: 'Tool input',
fieldResult: 'Result',
fieldError: 'Error',
fieldRejectionReason: 'Rejection reason',
rejectTitle: 'Reject this action?',
rejectBody: 'The reason is shown back to the AI so it can adjust its next response.',
rejectPlaceholder: "Optional reason (e.g. 'Wrong record id — please confirm with the user first.')",
},
aiModelStatus: {
summary: 'Build / Ask uses {{conversational}} ({{conversationalSource}}); structured uses {{structured}} ({{structuredSource}}).',
summaryRouting: 'Routing policy: free plans → {{free}}, paid plans → {{paid}}.',
Expand Down
40 changes: 40 additions & 0 deletions packages/i18n/src/locales/es.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -2793,6 +2793,46 @@ const es = {
openProduction: "Abrir producción",
manageEnvironments: "Gestionar entornos",
},
aiApprovals: {
title: "Aprobaciones de IA",
description: "Acciones propuestas por un agente de IA que necesitan revisión humana antes de ejecutarse.",
tabPending: "Pendientes",
tabDecided: "Decididas",
tabAll: "Todas",
statusPending: "Pendiente",
statusApproved: "Aprobada",
statusExecuted: "Ejecutada",
statusFailed: "Fallida",
statusRejected: "Rechazada",
colTool: "Herramienta",
colAction: "Acción",
colObject: "Objeto",
colStatus: "Estado",
colProposed: "Propuesta",
colDecision: "Decisión",
emptyTitle: "No hay acciones en espera",
emptyDescription: "Cuando la IA proponga una acción sensible, aparecerá aquí para su revisión.",
view: "Ver",
approve: "Aprobar",
reject: "Rechazar",
working: "Procesando…",
approveAndExecute: "Aprobar y ejecutar",
outcomeApprove: "Aprobación de {{id}}: {{message}}",
outcomeReject: "Rechazo de {{id}}: {{message}}",
outcomeExecuteFailed: "La acción falló durante su ejecución",
drawerFallbackTitle: "Acción pendiente",
drawerSubtitle: "Herramienta {{tool}} sobre {{object}}",
fieldProposedBy: "Propuesta por",
fieldDecidedBy: "Decidida por",
fieldConversation: "Conversación",
fieldToolInput: "Entrada de la herramienta",
fieldResult: "Resultado",
fieldError: "Error",
fieldRejectionReason: "Motivo del rechazo",
rejectTitle: "¿Rechazar esta acción?",
rejectBody: "El motivo se devuelve a la IA para que ajuste su siguiente respuesta.",
rejectPlaceholder: "Motivo opcional (p. ej.: «ID de registro incorrecto — confírmalo antes con la persona usuaria.»)",
},
aiModelStatus: {
summary: "Crear / Preguntar usa {{conversational}} ({{conversationalSource}}); estructurado usa {{structured}} ({{structuredSource}}).",
summaryRouting: "Política de enrutamiento: planes gratuitos → {{free}}, planes de pago → {{paid}}.",
Expand Down
40 changes: 40 additions & 0 deletions packages/i18n/src/locales/fr.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -2791,6 +2791,46 @@ const fr = {
openProduction: "Ouvrir la production",
manageEnvironments: "Gérer les environnements",
},
aiApprovals: {
title: "Approbations IA",
description: "Actions proposées par un agent IA qui nécessitent une validation humaine avant exécution.",
tabPending: "En attente",
tabDecided: "Traitées",
tabAll: "Toutes",
statusPending: "En attente",
statusApproved: "Approuvée",
statusExecuted: "Exécutée",
statusFailed: "Échouée",
statusRejected: "Rejetée",
colTool: "Outil",
colAction: "Action",
colObject: "Objet",
colStatus: "Statut",
colProposed: "Proposée",
colDecision: "Décision",
emptyTitle: "Aucune action en attente",
emptyDescription: "Lorsque l'IA propose une action sensible, elle apparaît ici pour validation.",
view: "Voir",
approve: "Approuver",
reject: "Rejeter",
working: "En cours…",
approveAndExecute: "Approuver et exécuter",
outcomeApprove: "Approbation de {{id}} : {{message}}",
outcomeReject: "Rejet de {{id}} : {{message}}",
outcomeExecuteFailed: "L'action a échoué pendant son exécution",
drawerFallbackTitle: "Action en attente",
drawerSubtitle: "Outil {{tool}} sur {{object}}",
fieldProposedBy: "Proposée par",
fieldDecidedBy: "Décidée par",
fieldConversation: "Conversation",
fieldToolInput: "Entrée de l'outil",
fieldResult: "Résultat",
fieldError: "Erreur",
fieldRejectionReason: "Motif du rejet",
rejectTitle: "Rejeter cette action ?",
rejectBody: "Le motif est renvoyé à l'IA pour qu'elle ajuste sa prochaine réponse.",
rejectPlaceholder: "Motif facultatif (par ex. « Identifiant d'enregistrement erroné — merci de confirmer d'abord avec l'utilisateur. »)",
},
aiModelStatus: {
summary: "Créer / Demander utilise {{conversational}} ({{conversationalSource}}) ; structuré utilise {{structured}} ({{structuredSource}}).",
summaryRouting: "Politique de routage : offres gratuites → {{free}}, offres payantes → {{paid}}.",
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
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
79 changes: 79 additions & 0 deletions .changeset/7173-ai-pending-actions-inbox-i18n.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
---
'@object-ui/plugin-chatbot': patch
'@object-ui/i18n': patch
---

`AiPendingActionsInbox` speaks the session locale — every string in it, not only its timestamps (objectui#7173).

The AI HITL approval inbox held its own relative-time helper returning hardcoded
English (`'just now'`, `` `${min}m ago` ``), so a zh / ja / ar session read English
relative times on every row. It is the fifth spelling of that helper in the repo,
and the file had **no translation wiring at all** — the unwired-component shape,
not the lookup-swap shape.

It is therefore swept whole. objectui#7142 wired one string into an otherwise
untranslated component and shipped something visibly half-done, and objectui#7149
is what finishing that afterwards cost; the triage ruling on this card (2026-09-01)
carried that forward as *sweep the file whole or leave it*. Everything the user can
read now resolves from the locale packs: the card heading and description, the three
tabs, the refresh button, all five status badges, the six column headings, the empty
state, the row and drawer buttons, all nine drawer field labels, the outcome banner
and the whole reject-reason dialog.

**No new rows for the four relative-time branches.** `detail.justNow`,
`detail.minutesAgo`, `detail.hoursAgo` and `detail.daysAgo` already existed,
translated, in all ten packs, and cross-package key borrowing is this repo's settled
convention rather than an open question — `ObjectGrid`, `ObjectKanban`, `ObjectTree`,
`ListView`, `ObjectView`, `NavigationOverlay`, `RecordAttachmentsPanel`,
`RecordDetailView` and `apps/console` all resolve `detail.*` from outside
`plugin-detail`. One phrase on one kind of control should not get a second
translation that can drift from the first.

The rest of the sweep needed copy no pack had, so `@object-ui/i18n` gains an
`aiApprovals` namespace: 38 keys, translated in all ten packs. It is deliberately
separate from `approvalsInbox`, which is the human approval-**process** inbox — a
different surface and a different feature, so no rows are shared with it. Four
generic verbs are reused rather than forked (`common.refresh`, `common.cancel`,
`common.loading`, `common.ok`).

**⛔ The five relative-time helpers are not unified.** They differ in real behaviour
— `Math.round` here against `Math.floor` in `plugin-detail`, thresholds 45s/30d
against 60s/7d, different tails — so normalising them is a behaviour change wearing
a refactor's clothes and needs its own card. This inbox's arithmetic is untouched,
and three rows in the new suite exist only to pin it: 50s renders `1m ago` (a 60s
threshold would still say "just now"), 90s renders `2m ago` (`Math.floor` gives
`1m ago`), and 20d renders `20d ago` (a 7d threshold would already show a date).

Two assembled English sentences became single interpolated keys — the outcome banner
(`Approve for {{id}}: {{message}}`) and the drawer subtitle
(`Tool {{tool}} on {{object}}`). Their word order differs per locale, which fragments
around a `<code>` element cannot express, so the two identifiers lose their monospace
styling. That is the deliberate cost of making those sentences translatable.

Evidence: an `en`-only assertion cannot discriminate here, because each key's `en`
value is byte-identical to the literal it replaced. The suite asserts in **zh and
ar**, and the provider-less path separately, in its own file (`createI18n` installs
itself as react-i18next's module-level global, so a provider-less render in a file
that has already mounted a provider silently reads that pack instead of the defaults
map). No inline `defaultValue` anywhere (objectui#3517).

Two consequences of the sweep, both landed here rather than left for CI to find:

`packages/app-shell/src/console/ai/__tests__/ConversationsSidebar.test.tsx` froze its
`vi.mock('@object-ui/i18n', ...)` factory to a hand-written object. Its import graph
reaches `plugin-chatbot`, which now resolves `createSafeTranslation` at module scope, so
the frozen surface made that read `undefined` and the file died during COLLECTION — the
objectui#6849 shape, which does not look like a test failure. It now spreads
`importOriginal()` and overrides only `useObjectTranslation`. Measured, not guessed: of
the 41 frozen `@object-ui/i18n` factories in the repo, running every one of them showed
this to be the only file whose graph reaches the package.

The ten pack blocks are locale DATA, and locale data lands in the console's eager
`framework` chunk, so `scripts/check-eager-closure-budget.mjs` raises that chunk's
ceiling from 512,000 to 524,000 gzipped bytes and re-pins its baseline onto a fresh
measurement (502,405 to 514,863). Attributed by three console builds: the merge parent
reads 510,192, this branch with the ten `aiApprovals` blocks cut reads 510,192 again, and
this branch reads 514,863 — so the whole 4,671-byte delta is the pack data and nothing
else. Headroom is kept at the line's own convention (9,137 bytes, 0.10x the regression
the gate must catch) rather than widened; most of the overage was pre-existing drift, with
the merge parent already at 510,192 of the 512,000 allowed.
Original file line numberDiff line numberDiff line change
Expand Up@@ -30,7 +30,15 @@ vi.mock('../../../hooks/useConversationList', () => ({
}),
}));

vi.mock('@object-ui/i18n', () => ({
// `importOriginal` spread, not a hand-written object: this file's import graph
// reaches `@object-ui/plugin-chatbot`, whose `AiPendingActionsInbox` resolves
// `createSafeTranslation` from this package AT MODULE SCOPE. A frozen factory
// makes that read `undefined` and the file dies during COLLECTION — before a
// single test runs, so it does not look like a test failure (objectui#6849,
// the shape `scripts/check-vi-mock-inherit.mjs` exists to stop). Only
// `useObjectTranslation` is overridden; everything else is the real module.
vi.mock('@object-ui/i18n', async (importOriginal) => ({
...(await importOriginal<Record<string, unknown>>()),
useObjectTranslation: () => ({
t: (key: string) => ({
'common.loading': 'Loading…',
Expand Down
14 changes: 10 additions & 4 deletions packages/i18n/src/__tests__/de-quote-pairing-3876.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -273,8 +273,13 @@ describe('objectui#3876 — de pack closes „ with “ and not with a straight
// keys: `detail.activityFieldChanged` quotes the OLD and NEW field values
// („{{old}}“ / „{{new}}“, two spans in one value, like `navigationSync.
// renamedPage` above) and `detail.activityStatusChanged` the new status
// („{{value}}“) — three interpolated spans, all runtime data.
expect(okSpans, 'correctly paired spans').toBe(58);
// („{{value}}“) — three interpolated spans, all runtime data,
// 59 once objectui#7173 gave `AiPendingActionsInbox` pack keys:
// `aiApprovals.rejectPlaceholder` quotes the EXAMPLE rejection reason the
// placeholder suggests („Falsche Datensatz-ID — …“) — one LITERAL span, like
// `timeline.unsupported.objectBoundGantt` above rather than the interpolated
// ones, because the quoted thing is sample prose this pack authored.
expect(okSpans, 'correctly paired spans').toBe(59);
});

it('keeps the count identity that replaces the card’s count(„) === count(“)', () => {
Expand All@@ -298,8 +303,9 @@ describe('objectui#3876 — de pack closes „ with “ and not with a straight
// after objectui#7149 added the two quoted `ActivityTimeline` sentences
// (three spans between them). `rdq` staying at 0 is the load-bearing half:
// each new value added a MATCHED „…“ pair, not a stray closer that would
// have made `close === open` true for the wrong reason.
expect({ open, close, rdq }).toEqual({ open: 58, close: 58, rdq: 0 });
// have made `close === open` true for the wrong reason. 59 / 59 / 0 after
// objectui#7173 added `aiApprovals.rejectPlaceholder`, one more matched pair.
expect({ open, close, rdq }).toEqual({ open: 59, close: 59, rdq: 0 });
// The durable shape: every „ closed by a “, every surplus “ an English
// opener answered by a ”. Survived translating the two English values.
expect(close).toBe(open + rdq);
Expand Down
40 changes: 40 additions & 0 deletions packages/i18n/src/locales/ar.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -2796,6 +2796,46 @@ const ar = {
openProduction: "فتح بيئة الإنتاج",
manageEnvironments: "إدارة البيئات",
},
aiApprovals: {
title: "موافقات الذكاء الاصطناعي",
description: "إجراءات اقترحها وكيل ذكاء اصطناعي وتحتاج إلى مراجعة بشرية قبل تنفيذها.",
tabPending: "قيد الانتظار",
tabDecided: "تم البت فيها",
tabAll: "الكل",
statusPending: "قيد الانتظار",
statusApproved: "تمت الموافقة",
statusExecuted: "تم التنفيذ",
statusFailed: "فشل",
statusRejected: "مرفوض",
colTool: "الأداة",
colAction: "الإجراء",
colObject: "الكائن",
colStatus: "الحالة",
colProposed: "وقت الاقتراح",
colDecision: "القرار",
emptyTitle: "لا توجد إجراءات في الانتظار",
emptyDescription: "عندما يقترح الذكاء الاصطناعي إجراءً حساسًا سيظهر هنا للمراجعة.",
view: "عرض",
approve: "موافقة",
reject: "رفض",
working: "جارٍ التنفيذ…",
approveAndExecute: "الموافقة والتنفيذ",
outcomeApprove: "موافقة على {{id}}: {{message}}",
outcomeReject: "رفض {{id}}: {{message}}",
outcomeExecuteFailed: "فشل الإجراء أثناء التنفيذ",
drawerFallbackTitle: "إجراء قيد الانتظار",
drawerSubtitle: "الأداة {{tool}} على {{object}}",
fieldProposedBy: "اقترحه",
fieldDecidedBy: "قرّره",
fieldConversation: "المحادثة",
fieldToolInput: "مدخلات الأداة",
fieldResult: "النتيجة",
fieldError: "خطأ",
fieldRejectionReason: "سبب الرفض",
rejectTitle: "هل تريد رفض هذا الإجراء؟",
rejectBody: "يُعاد السبب إلى الذكاء الاصطناعي ليعدّل ردّه التالي.",
rejectPlaceholder: "سبب اختياري (مثال: «معرّف السجل غير صحيح — يرجى التأكيد مع المستخدم أولاً.»)",
},
aiModelStatus: {
summary: "يستخدم الإنشاء / السؤال {{conversational}} ({{conversationalSource}})؛ ويستخدم الإخراج المهيكل {{structured}} ({{structuredSource}}).",
summaryRouting: "سياسة التوجيه: الخطط المجانية ← {{free}}، الخطط المدفوعة ← {{paid}}.",
Expand Down
40 changes: 40 additions & 0 deletions packages/i18n/src/locales/de.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -2789,6 +2789,46 @@ const de = {
openProduction: "Produktion öffnen",
manageEnvironments: "Umgebungen verwalten",
},
aiApprovals: {
title: "KI-Genehmigungen",
description: "Von einem KI-Agenten vorgeschlagene Aktionen, die vor der Ausführung eine menschliche Prüfung benötigen.",
tabPending: "Ausstehend",
tabDecided: "Entschieden",
tabAll: "Alle",
statusPending: "Ausstehend",
statusApproved: "Genehmigt",
statusExecuted: "Ausgeführt",
statusFailed: "Fehlgeschlagen",
statusRejected: "Abgelehnt",
colTool: "Werkzeug",
colAction: "Aktion",
colObject: "Objekt",
colStatus: "Status",
colProposed: "Vorgeschlagen",
colDecision: "Entscheidung",
emptyTitle: "Keine wartenden Aktionen",
emptyDescription: "Wenn die KI eine sensible Aktion vorschlägt, erscheint sie hier zur Prüfung.",
view: "Ansehen",
approve: "Genehmigen",
reject: "Ablehnen",
working: "Wird bearbeitet…",
approveAndExecute: "Genehmigen & ausführen",
outcomeApprove: "Genehmigung für {{id}}: {{message}}",
outcomeReject: "Ablehnung für {{id}}: {{message}}",
outcomeExecuteFailed: "Die Aktion ist bei der Ausführung fehlgeschlagen",
drawerFallbackTitle: "Ausstehende Aktion",
drawerSubtitle: "Werkzeug {{tool}} auf {{object}}",
fieldProposedBy: "Vorgeschlagen von",
fieldDecidedBy: "Entschieden von",
fieldConversation: "Konversation",
fieldToolInput: "Werkzeugeingabe",
fieldResult: "Ergebnis",
fieldError: "Fehler",
fieldRejectionReason: "Ablehnungsgrund",
rejectTitle: "Diese Aktion ablehnen?",
rejectBody: "Der Grund wird an die KI zurückgemeldet, damit sie ihre nächste Antwort anpassen kann.",
rejectPlaceholder: "Optionaler Grund (z. B. „Falsche Datensatz-ID — bitte zuerst mit der Nutzerin oder dem Nutzer klären.“)",
},
aiModelStatus: {
summary: "Erstellen / Fragen nutzt {{conversational}} ({{conversationalSource}}); strukturiert nutzt {{structured}} ({{structuredSource}}).",
summaryRouting: "Routing-Richtlinie: kostenlose Tarife → {{free}}, kostenpflichtige Tarife → {{paid}}.",
Expand Down
47 changes: 47 additions & 0 deletions packages/i18n/src/locales/en.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -3135,6 +3135,53 @@ const en = {
openProduction: 'Open Production',
manageEnvironments: 'Manage environments',
},
// The AI HITL approval inbox (`@object-ui/plugin-chatbot`'s
// `AiPendingActionsInbox`) — objectui#7173. Its four relative-time phrases
// are NOT here: it borrows `detail.justNow` / `minutesAgo` / `hoursAgo` /
// `daysAgo`, already translated in all ten packs, the way `ObjectGrid` and
// `ObjectKanban` borrow `detail.recordDetail`. Distinct from
// `approvalsInbox` above, which is the human approval-PROCESS inbox:
// different surface, different feature, so no rows are shared with it.
aiApprovals: {
title: 'AI Approvals',
description: 'Actions an AI agent proposed that need a human review before execution.',
tabPending: 'Pending',
tabDecided: 'Decided',
tabAll: 'All',
statusPending: 'Pending',
statusApproved: 'Approved',
statusExecuted: 'Executed',
statusFailed: 'Failed',
statusRejected: 'Rejected',
colTool: 'Tool',
colAction: 'Action',
colObject: 'Object',
colStatus: 'Status',
colProposed: 'Proposed',
colDecision: 'Decision',
emptyTitle: 'No actions waiting',
emptyDescription: 'When the AI proposes a sensitive action it will appear here for review.',
view: 'View',
approve: 'Approve',
reject: 'Reject',
working: 'Working…',
approveAndExecute: 'Approve & Execute',
outcomeApprove: 'Approve for {{id}}: {{message}}',
outcomeReject: 'Reject for {{id}}: {{message}}',
outcomeExecuteFailed: 'Action failed during execution',
drawerFallbackTitle: 'Pending action',
drawerSubtitle: 'Tool {{tool}} on {{object}}',
fieldProposedBy: 'Proposed by',
fieldDecidedBy: 'Decided by',
fieldConversation: 'Conversation',
fieldToolInput: 'Tool input',
fieldResult: 'Result',
fieldError: 'Error',
fieldRejectionReason: 'Rejection reason',
rejectTitle: 'Reject this action?',
rejectBody: 'The reason is shown back to the AI so it can adjust its next response.',
rejectPlaceholder: "Optional reason (e.g. 'Wrong record id — please confirm with the user first.')",
},
aiModelStatus: {
summary: 'Build / Ask uses {{conversational}} ({{conversationalSource}}); structured uses {{structured}} ({{structuredSource}}).',
summaryRouting: 'Routing policy: free plans → {{free}}, paid plans → {{paid}}.',
Expand Down
40 changes: 40 additions & 0 deletions packages/i18n/src/locales/es.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -2793,6 +2793,46 @@ const es = {
openProduction: "Abrir producción",
manageEnvironments: "Gestionar entornos",
},
aiApprovals: {
title: "Aprobaciones de IA",
description: "Acciones propuestas por un agente de IA que necesitan revisión humana antes de ejecutarse.",
tabPending: "Pendientes",
tabDecided: "Decididas",
tabAll: "Todas",
statusPending: "Pendiente",
statusApproved: "Aprobada",
statusExecuted: "Ejecutada",
statusFailed: "Fallida",
statusRejected: "Rechazada",
colTool: "Herramienta",
colAction: "Acción",
colObject: "Objeto",
colStatus: "Estado",
colProposed: "Propuesta",
colDecision: "Decisión",
emptyTitle: "No hay acciones en espera",
emptyDescription: "Cuando la IA proponga una acción sensible, aparecerá aquí para su revisión.",
view: "Ver",
approve: "Aprobar",
reject: "Rechazar",
working: "Procesando…",
approveAndExecute: "Aprobar y ejecutar",
outcomeApprove: "Aprobación de {{id}}: {{message}}",
outcomeReject: "Rechazo de {{id}}: {{message}}",
outcomeExecuteFailed: "La acción falló durante su ejecución",
drawerFallbackTitle: "Acción pendiente",
drawerSubtitle: "Herramienta {{tool}} sobre {{object}}",
fieldProposedBy: "Propuesta por",
fieldDecidedBy: "Decidida por",
fieldConversation: "Conversación",
fieldToolInput: "Entrada de la herramienta",
fieldResult: "Resultado",
fieldError: "Error",
fieldRejectionReason: "Motivo del rechazo",
rejectTitle: "¿Rechazar esta acción?",
rejectBody: "El motivo se devuelve a la IA para que ajuste su siguiente respuesta.",
rejectPlaceholder: "Motivo opcional (p. ej.: «ID de registro incorrecto — confírmalo antes con la persona usuaria.»)",
},
aiModelStatus: {
summary: "Crear / Preguntar usa {{conversational}} ({{conversationalSource}}); estructurado usa {{structured}} ({{structuredSource}}).",
summaryRouting: "Política de enrutamiento: planes gratuitos → {{free}}, planes de pago → {{paid}}.",
Expand Down
40 changes: 40 additions & 0 deletions packages/i18n/src/locales/fr.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -2791,6 +2791,46 @@ const fr = {
openProduction: "Ouvrir la production",
manageEnvironments: "Gérer les environnements",
},
aiApprovals: {
title: "Approbations IA",
description: "Actions proposées par un agent IA qui nécessitent une validation humaine avant exécution.",
tabPending: "En attente",
tabDecided: "Traitées",
tabAll: "Toutes",
statusPending: "En attente",
statusApproved: "Approuvée",
statusExecuted: "Exécutée",
statusFailed: "Échouée",
statusRejected: "Rejetée",
colTool: "Outil",
colAction: "Action",
colObject: "Objet",
colStatus: "Statut",
colProposed: "Proposée",
colDecision: "Décision",
emptyTitle: "Aucune action en attente",
emptyDescription: "Lorsque l'IA propose une action sensible, elle apparaît ici pour validation.",
view: "Voir",
approve: "Approuver",
reject: "Rejeter",
working: "En cours…",
approveAndExecute: "Approuver et exécuter",
outcomeApprove: "Approbation de {{id}} : {{message}}",
outcomeReject: "Rejet de {{id}} : {{message}}",
outcomeExecuteFailed: "L'action a échoué pendant son exécution",
drawerFallbackTitle: "Action en attente",
drawerSubtitle: "Outil {{tool}} sur {{object}}",
fieldProposedBy: "Proposée par",
fieldDecidedBy: "Décidée par",
fieldConversation: "Conversation",
fieldToolInput: "Entrée de l'outil",
fieldResult: "Résultat",
fieldError: "Erreur",
fieldRejectionReason: "Motif du rejet",
rejectTitle: "Rejeter cette action ?",
rejectBody: "Le motif est renvoyé à l'IA pour qu'elle ajuste sa prochaine réponse.",
rejectPlaceholder: "Motif facultatif (par ex. « Identifiant d'enregistrement erroné — merci de confirmer d'abord avec l'utilisateur. »)",
},
aiModelStatus: {
summary: "Créer / Demander utilise {{conversational}} ({{conversationalSource}}) ; structuré utilise {{structured}} ({{structuredSource}}).",
summaryRouting: "Politique de routage : offres gratuites → {{free}}, offres payantes → {{paid}}.",
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
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
79 changes: 79 additions & 0 deletions .changeset/7173-ai-pending-actions-inbox-i18n.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
---
'@object-ui/plugin-chatbot': patch
'@object-ui/i18n': patch
---

`AiPendingActionsInbox` speaks the session locale — every string in it, not only its timestamps (objectui#7173).

The AI HITL approval inbox held its own relative-time helper returning hardcoded
English (`'just now'`, `` `${min}m ago` ``), so a zh / ja / ar session read English
relative times on every row. It is the fifth spelling of that helper in the repo,
and the file had **no translation wiring at all** — the unwired-component shape,
not the lookup-swap shape.

It is therefore swept whole. objectui#7142 wired one string into an otherwise
untranslated component and shipped something visibly half-done, and objectui#7149
is what finishing that afterwards cost; the triage ruling on this card (2026-09-01)
carried that forward as *sweep the file whole or leave it*. Everything the user can
read now resolves from the locale packs: the card heading and description, the three
tabs, the refresh button, all five status badges, the six column headings, the empty
state, the row and drawer buttons, all nine drawer field labels, the outcome banner
and the whole reject-reason dialog.

**No new rows for the four relative-time branches.** `detail.justNow`,
`detail.minutesAgo`, `detail.hoursAgo` and `detail.daysAgo` already existed,
translated, in all ten packs, and cross-package key borrowing is this repo's settled
convention rather than an open question — `ObjectGrid`, `ObjectKanban`, `ObjectTree`,
`ListView`, `ObjectView`, `NavigationOverlay`, `RecordAttachmentsPanel`,
`RecordDetailView` and `apps/console` all resolve `detail.*` from outside
`plugin-detail`. One phrase on one kind of control should not get a second
translation that can drift from the first.

The rest of the sweep needed copy no pack had, so `@object-ui/i18n` gains an
`aiApprovals` namespace: 38 keys, translated in all ten packs. It is deliberately
separate from `approvalsInbox`, which is the human approval-**process** inbox — a
different surface and a different feature, so no rows are shared with it. Four
generic verbs are reused rather than forked (`common.refresh`, `common.cancel`,
`common.loading`, `common.ok`).

**⛔ The five relative-time helpers are not unified.** They differ in real behaviour
— `Math.round` here against `Math.floor` in `plugin-detail`, thresholds 45s/30d
against 60s/7d, different tails — so normalising them is a behaviour change wearing
a refactor's clothes and needs its own card. This inbox's arithmetic is untouched,
and three rows in the new suite exist only to pin it: 50s renders `1m ago` (a 60s
threshold would still say "just now"), 90s renders `2m ago` (`Math.floor` gives
`1m ago`), and 20d renders `20d ago` (a 7d threshold would already show a date).

Two assembled English sentences became single interpolated keys — the outcome banner
(`Approve for {{id}}: {{message}}`) and the drawer subtitle
(`Tool {{tool}} on {{object}}`). Their word order differs per locale, which fragments
around a `<code>` element cannot express, so the two identifiers lose their monospace
styling. That is the deliberate cost of making those sentences translatable.

Evidence: an `en`-only assertion cannot discriminate here, because each key's `en`
value is byte-identical to the literal it replaced. The suite asserts in **zh and
ar**, and the provider-less path separately, in its own file (`createI18n` installs
itself as react-i18next's module-level global, so a provider-less render in a file
that has already mounted a provider silently reads that pack instead of the defaults
map). No inline `defaultValue` anywhere (objectui#3517).

Two consequences of the sweep, both landed here rather than left for CI to find:

`packages/app-shell/src/console/ai/__tests__/ConversationsSidebar.test.tsx` froze its
`vi.mock('@object-ui/i18n', ...)` factory to a hand-written object. Its import graph
reaches `plugin-chatbot`, which now resolves `createSafeTranslation` at module scope, so
the frozen surface made that read `undefined` and the file died during COLLECTION — the
objectui#6849 shape, which does not look like a test failure. It now spreads
`importOriginal()` and overrides only `useObjectTranslation`. Measured, not guessed: of
the 41 frozen `@object-ui/i18n` factories in the repo, running every one of them showed
this to be the only file whose graph reaches the package.

The ten pack blocks are locale DATA, and locale data lands in the console's eager
`framework` chunk, so `scripts/check-eager-closure-budget.mjs` raises that chunk's
ceiling from 512,000 to 524,000 gzipped bytes and re-pins its baseline onto a fresh
measurement (502,405 to 514,863). Attributed by three console builds: the merge parent
reads 510,192, this branch with the ten `aiApprovals` blocks cut reads 510,192 again, and
this branch reads 514,863 — so the whole 4,671-byte delta is the pack data and nothing
else. Headroom is kept at the line's own convention (9,137 bytes, 0.10x the regression
the gate must catch) rather than widened; most of the overage was pre-existing drift, with
the merge parent already at 510,192 of the 512,000 allowed.
Original file line numberDiff line numberDiff line change
Expand Up@@ -30,7 +30,15 @@ vi.mock('../../../hooks/useConversationList', () => ({
}),
}));

vi.mock('@object-ui/i18n', () => ({
// `importOriginal` spread, not a hand-written object: this file's import graph
// reaches `@object-ui/plugin-chatbot`, whose `AiPendingActionsInbox` resolves
// `createSafeTranslation` from this package AT MODULE SCOPE. A frozen factory
// makes that read `undefined` and the file dies during COLLECTION — before a
// single test runs, so it does not look like a test failure (objectui#6849,
// the shape `scripts/check-vi-mock-inherit.mjs` exists to stop). Only
// `useObjectTranslation` is overridden; everything else is the real module.
vi.mock('@object-ui/i18n', async (importOriginal) => ({
...(await importOriginal<Record<string, unknown>>()),
useObjectTranslation: () => ({
t: (key: string) => ({
'common.loading': 'Loading…',
Expand Down
14 changes: 10 additions & 4 deletions packages/i18n/src/__tests__/de-quote-pairing-3876.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -273,8 +273,13 @@ describe('objectui#3876 — de pack closes „ with “ and not with a straight
// keys: `detail.activityFieldChanged` quotes the OLD and NEW field values
// („{{old}}“ / „{{new}}“, two spans in one value, like `navigationSync.
// renamedPage` above) and `detail.activityStatusChanged` the new status
// („{{value}}“) — three interpolated spans, all runtime data.
expect(okSpans, 'correctly paired spans').toBe(58);
// („{{value}}“) — three interpolated spans, all runtime data,
// 59 once objectui#7173 gave `AiPendingActionsInbox` pack keys:
// `aiApprovals.rejectPlaceholder` quotes the EXAMPLE rejection reason the
// placeholder suggests („Falsche Datensatz-ID — …“) — one LITERAL span, like
// `timeline.unsupported.objectBoundGantt` above rather than the interpolated
// ones, because the quoted thing is sample prose this pack authored.
expect(okSpans, 'correctly paired spans').toBe(59);
});

it('keeps the count identity that replaces the card’s count(„) === count(“)', () => {
Expand All@@ -298,8 +303,9 @@ describe('objectui#3876 — de pack closes „ with “ and not with a straight
// after objectui#7149 added the two quoted `ActivityTimeline` sentences
// (three spans between them). `rdq` staying at 0 is the load-bearing half:
// each new value added a MATCHED „…“ pair, not a stray closer that would
// have made `close === open` true for the wrong reason.
expect({ open, close, rdq }).toEqual({ open: 58, close: 58, rdq: 0 });
// have made `close === open` true for the wrong reason. 59 / 59 / 0 after
// objectui#7173 added `aiApprovals.rejectPlaceholder`, one more matched pair.
expect({ open, close, rdq }).toEqual({ open: 59, close: 59, rdq: 0 });
// The durable shape: every „ closed by a “, every surplus “ an English
// opener answered by a ”. Survived translating the two English values.
expect(close).toBe(open + rdq);
Expand Down
40 changes: 40 additions & 0 deletions packages/i18n/src/locales/ar.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -2796,6 +2796,46 @@ const ar = {
openProduction: "فتح بيئة الإنتاج",
manageEnvironments: "إدارة البيئات",
},
aiApprovals: {
title: "موافقات الذكاء الاصطناعي",
description: "إجراءات اقترحها وكيل ذكاء اصطناعي وتحتاج إلى مراجعة بشرية قبل تنفيذها.",
tabPending: "قيد الانتظار",
tabDecided: "تم البت فيها",
tabAll: "الكل",
statusPending: "قيد الانتظار",
statusApproved: "تمت الموافقة",
statusExecuted: "تم التنفيذ",
statusFailed: "فشل",
statusRejected: "مرفوض",
colTool: "الأداة",
colAction: "الإجراء",
colObject: "الكائن",
colStatus: "الحالة",
colProposed: "وقت الاقتراح",
colDecision: "القرار",
emptyTitle: "لا توجد إجراءات في الانتظار",
emptyDescription: "عندما يقترح الذكاء الاصطناعي إجراءً حساسًا سيظهر هنا للمراجعة.",
view: "عرض",
approve: "موافقة",
reject: "رفض",
working: "جارٍ التنفيذ…",
approveAndExecute: "الموافقة والتنفيذ",
outcomeApprove: "موافقة على {{id}}: {{message}}",
outcomeReject: "رفض {{id}}: {{message}}",
outcomeExecuteFailed: "فشل الإجراء أثناء التنفيذ",
drawerFallbackTitle: "إجراء قيد الانتظار",
drawerSubtitle: "الأداة {{tool}} على {{object}}",
fieldProposedBy: "اقترحه",
fieldDecidedBy: "قرّره",
fieldConversation: "المحادثة",
fieldToolInput: "مدخلات الأداة",
fieldResult: "النتيجة",
fieldError: "خطأ",
fieldRejectionReason: "سبب الرفض",
rejectTitle: "هل تريد رفض هذا الإجراء؟",
rejectBody: "يُعاد السبب إلى الذكاء الاصطناعي ليعدّل ردّه التالي.",
rejectPlaceholder: "سبب اختياري (مثال: «معرّف السجل غير صحيح — يرجى التأكيد مع المستخدم أولاً.»)",
},
aiModelStatus: {
summary: "يستخدم الإنشاء / السؤال {{conversational}} ({{conversationalSource}})؛ ويستخدم الإخراج المهيكل {{structured}} ({{structuredSource}}).",
summaryRouting: "سياسة التوجيه: الخطط المجانية ← {{free}}، الخطط المدفوعة ← {{paid}}.",
Expand Down
40 changes: 40 additions & 0 deletions packages/i18n/src/locales/de.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -2789,6 +2789,46 @@ const de = {
openProduction: "Produktion öffnen",
manageEnvironments: "Umgebungen verwalten",
},
aiApprovals: {
title: "KI-Genehmigungen",
description: "Von einem KI-Agenten vorgeschlagene Aktionen, die vor der Ausführung eine menschliche Prüfung benötigen.",
tabPending: "Ausstehend",
tabDecided: "Entschieden",
tabAll: "Alle",
statusPending: "Ausstehend",
statusApproved: "Genehmigt",
statusExecuted: "Ausgeführt",
statusFailed: "Fehlgeschlagen",
statusRejected: "Abgelehnt",
colTool: "Werkzeug",
colAction: "Aktion",
colObject: "Objekt",
colStatus: "Status",
colProposed: "Vorgeschlagen",
colDecision: "Entscheidung",
emptyTitle: "Keine wartenden Aktionen",
emptyDescription: "Wenn die KI eine sensible Aktion vorschlägt, erscheint sie hier zur Prüfung.",
view: "Ansehen",
approve: "Genehmigen",
reject: "Ablehnen",
working: "Wird bearbeitet…",
approveAndExecute: "Genehmigen & ausführen",
outcomeApprove: "Genehmigung für {{id}}: {{message}}",
outcomeReject: "Ablehnung für {{id}}: {{message}}",
outcomeExecuteFailed: "Die Aktion ist bei der Ausführung fehlgeschlagen",
drawerFallbackTitle: "Ausstehende Aktion",
drawerSubtitle: "Werkzeug {{tool}} auf {{object}}",
fieldProposedBy: "Vorgeschlagen von",
fieldDecidedBy: "Entschieden von",
fieldConversation: "Konversation",
fieldToolInput: "Werkzeugeingabe",
fieldResult: "Ergebnis",
fieldError: "Fehler",
fieldRejectionReason: "Ablehnungsgrund",
rejectTitle: "Diese Aktion ablehnen?",
rejectBody: "Der Grund wird an die KI zurückgemeldet, damit sie ihre nächste Antwort anpassen kann.",
rejectPlaceholder: "Optionaler Grund (z. B. „Falsche Datensatz-ID — bitte zuerst mit der Nutzerin oder dem Nutzer klären.“)",
},
aiModelStatus: {
summary: "Erstellen / Fragen nutzt {{conversational}} ({{conversationalSource}}); strukturiert nutzt {{structured}} ({{structuredSource}}).",
summaryRouting: "Routing-Richtlinie: kostenlose Tarife → {{free}}, kostenpflichtige Tarife → {{paid}}.",
Expand Down
47 changes: 47 additions & 0 deletions packages/i18n/src/locales/en.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -3135,6 +3135,53 @@ const en = {
openProduction: 'Open Production',
manageEnvironments: 'Manage environments',
},
// The AI HITL approval inbox (`@object-ui/plugin-chatbot`'s
// `AiPendingActionsInbox`) — objectui#7173. Its four relative-time phrases
// are NOT here: it borrows `detail.justNow` / `minutesAgo` / `hoursAgo` /
// `daysAgo`, already translated in all ten packs, the way `ObjectGrid` and
// `ObjectKanban` borrow `detail.recordDetail`. Distinct from
// `approvalsInbox` above, which is the human approval-PROCESS inbox:
// different surface, different feature, so no rows are shared with it.
aiApprovals: {
title: 'AI Approvals',
description: 'Actions an AI agent proposed that need a human review before execution.',
tabPending: 'Pending',
tabDecided: 'Decided',
tabAll: 'All',
statusPending: 'Pending',
statusApproved: 'Approved',
statusExecuted: 'Executed',
statusFailed: 'Failed',
statusRejected: 'Rejected',
colTool: 'Tool',
colAction: 'Action',
colObject: 'Object',
colStatus: 'Status',
colProposed: 'Proposed',
colDecision: 'Decision',
emptyTitle: 'No actions waiting',
emptyDescription: 'When the AI proposes a sensitive action it will appear here for review.',
view: 'View',
approve: 'Approve',
reject: 'Reject',
working: 'Working…',
approveAndExecute: 'Approve & Execute',
outcomeApprove: 'Approve for {{id}}: {{message}}',
outcomeReject: 'Reject for {{id}}: {{message}}',
outcomeExecuteFailed: 'Action failed during execution',
drawerFallbackTitle: 'Pending action',
drawerSubtitle: 'Tool {{tool}} on {{object}}',
fieldProposedBy: 'Proposed by',
fieldDecidedBy: 'Decided by',
fieldConversation: 'Conversation',
fieldToolInput: 'Tool input',
fieldResult: 'Result',
fieldError: 'Error',
fieldRejectionReason: 'Rejection reason',
rejectTitle: 'Reject this action?',
rejectBody: 'The reason is shown back to the AI so it can adjust its next response.',
rejectPlaceholder: "Optional reason (e.g. 'Wrong record id — please confirm with the user first.')",
},
aiModelStatus: {
summary: 'Build / Ask uses {{conversational}} ({{conversationalSource}}); structured uses {{structured}} ({{structuredSource}}).',
summaryRouting: 'Routing policy: free plans → {{free}}, paid plans → {{paid}}.',
Expand Down
40 changes: 40 additions & 0 deletions packages/i18n/src/locales/es.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -2793,6 +2793,46 @@ const es = {
openProduction: "Abrir producción",
manageEnvironments: "Gestionar entornos",
},
aiApprovals: {
title: "Aprobaciones de IA",
description: "Acciones propuestas por un agente de IA que necesitan revisión humana antes de ejecutarse.",
tabPending: "Pendientes",
tabDecided: "Decididas",
tabAll: "Todas",
statusPending: "Pendiente",
statusApproved: "Aprobada",
statusExecuted: "Ejecutada",
statusFailed: "Fallida",
statusRejected: "Rechazada",
colTool: "Herramienta",
colAction: "Acción",
colObject: "Objeto",
colStatus: "Estado",
colProposed: "Propuesta",
colDecision: "Decisión",
emptyTitle: "No hay acciones en espera",
emptyDescription: "Cuando la IA proponga una acción sensible, aparecerá aquí para su revisión.",
view: "Ver",
approve: "Aprobar",
reject: "Rechazar",
working: "Procesando…",
approveAndExecute: "Aprobar y ejecutar",
outcomeApprove: "Aprobación de {{id}}: {{message}}",
outcomeReject: "Rechazo de {{id}}: {{message}}",
outcomeExecuteFailed: "La acción falló durante su ejecución",
drawerFallbackTitle: "Acción pendiente",
drawerSubtitle: "Herramienta {{tool}} sobre {{object}}",
fieldProposedBy: "Propuesta por",
fieldDecidedBy: "Decidida por",
fieldConversation: "Conversación",
fieldToolInput: "Entrada de la herramienta",
fieldResult: "Resultado",
fieldError: "Error",
fieldRejectionReason: "Motivo del rechazo",
rejectTitle: "¿Rechazar esta acción?",
rejectBody: "El motivo se devuelve a la IA para que ajuste su siguiente respuesta.",
rejectPlaceholder: "Motivo opcional (p. ej.: «ID de registro incorrecto — confírmalo antes con la persona usuaria.»)",
},
aiModelStatus: {
summary: "Crear / Preguntar usa {{conversational}} ({{conversationalSource}}); estructurado usa {{structured}} ({{structuredSource}}).",
summaryRouting: "Política de enrutamiento: planes gratuitos → {{free}}, planes de pago → {{paid}}.",
Expand Down
40 changes: 40 additions & 0 deletions packages/i18n/src/locales/fr.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -2791,6 +2791,46 @@ const fr = {
openProduction: "Ouvrir la production",
manageEnvironments: "Gérer les environnements",
},
aiApprovals: {
title: "Approbations IA",
description: "Actions proposées par un agent IA qui nécessitent une validation humaine avant exécution.",
tabPending: "En attente",
tabDecided: "Traitées",
tabAll: "Toutes",
statusPending: "En attente",
statusApproved: "Approuvée",
statusExecuted: "Exécutée",
statusFailed: "Échouée",
statusRejected: "Rejetée",
colTool: "Outil",
colAction: "Action",
colObject: "Objet",
colStatus: "Statut",
colProposed: "Proposée",
colDecision: "Décision",
emptyTitle: "Aucune action en attente",
emptyDescription: "Lorsque l'IA propose une action sensible, elle apparaît ici pour validation.",
view: "Voir",
approve: "Approuver",
reject: "Rejeter",
working: "En cours…",
approveAndExecute: "Approuver et exécuter",
outcomeApprove: "Approbation de {{id}} : {{message}}",
outcomeReject: "Rejet de {{id}} : {{message}}",
outcomeExecuteFailed: "L'action a échoué pendant son exécution",
drawerFallbackTitle: "Action en attente",
drawerSubtitle: "Outil {{tool}} sur {{object}}",
fieldProposedBy: "Proposée par",
fieldDecidedBy: "Décidée par",
fieldConversation: "Conversation",
fieldToolInput: "Entrée de l'outil",
fieldResult: "Résultat",
fieldError: "Erreur",
fieldRejectionReason: "Motif du rejet",
rejectTitle: "Rejeter cette action ?",
rejectBody: "Le motif est renvoyé à l'IA pour qu'elle ajuste sa prochaine réponse.",
rejectPlaceholder: "Motif facultatif (par ex. « Identifiant d'enregistrement erroné — merci de confirmer d'abord avec l'utilisateur. »)",
},
aiModelStatus: {
summary: "Créer / Demander utilise {{conversational}} ({{conversationalSource}}) ; structuré utilise {{structured}} ({{structuredSource}}).",
summaryRouting: "Politique de routage : offres gratuites → {{free}}, offres payantes → {{paid}}.",
Expand Down
Loading
Loading