Skip to content

Commit b3ed6c8

Browse files
objectstack-fleet[bot]claudeos-steve
authored
fix(app-shell): record create/update toasts name the saved record (#26202)
<!-- claude-projects-attribution: {"github_login":"os-steve"} --> _Requested by **Steve** · [project thread](https://claude.ai/code/project/chan_01FdL1XeUyvVNxLreh3F4Qi9?thread=cmsg_01FdL1XeUyvVNxLreh3F4Qi99R9hXJTYYUhUGGdHBZb4ty)_ Fixes #26172 Clause-②: no (adds or rewords message keys; no key is removed, so `TranslationKeys` only widens; no exported runtime contract changes) Metadata-protocol: none ## What The Console's record create and update success toasts now name the saved record by its display name (ADR-0079), as the ruled `ux-conventions.write-feedback` clause asks: `Task "Quarterly review" created`, `Task "Quarterly review" updated`. A record with nothing nameable keeps today's object-label toast (`Task created successfully`). - `ui/packages/app-shell/src/console/AppContent.tsx`: one message builder, `recordSaveMessage(saved)`, now serves both toast sites, the main path (`handleCrudSuccess`, which hands the message to the runner's `crud_success`) and the child-form task path (a subtable child opened over its parent's record page, which toasts directly). `handleRecordFormSuccess` passes the saved record it already receives to both. The name comes from `getRecordDisplayName` in `@object-ui/core`, read through the object being saved (`formObjectDef`), so it follows the object's declared `nameField`. The resolver's floor (`Record #ID`, or the empty fallback for a row with no id) is not a name, and falls back to the object-label string. This is the same floor test `recordToOption` in `LookupField` and `renderLookupColumnValue` already use. - `ui/packages/i18n/src/locales/*.ts` (all ten packs): two new keys, `form.createdNamed` and `form.updatedNamed`, each with `{{object}}` (object label) and `{{title}}` (record name), quoted the way each pack already quotes a name (`deleteConfirmTitle` is the model: curly quotes in zh, corner brackets in ja, low-high quotes in de, straight quotes elsewhere). `form.createSuccess` / `form.updateSuccess` are unchanged and stay the no-name toast. - `scripts/sdui-manifest.sources/i18n.json`: re-recorded by `node scripts/gen-sdui-manifest-node.mjs` (1 of 27 package records moved: i18n). `sdui.manifest.json` itself is unchanged. - Two count pins in `ui/packages/i18n` move with the diff: `de-quote-pairing-3876` (94 to 96 matched pairs, the two new de values) and `residue-namespaces-3546` (the `common.record` fallback in AppContent is written once now, 2 to 1). - Root changeset `.changeset/26172-record-save-toast-names-record.md` (patch, `@object-ui/app-shell`, `@object-ui/i18n`). Out of scope, per the ruling: settings saves and flow-completion toasts. ## Tests New: `ui/packages/app-shell/src/console/__tests__/AppContent.recordSaveToastNamesRecord-26172.test.tsx`, a real `AppContent` mount in a real i18next instance (the harness of `AppContent.createLandingOriginPlural-11733`). Nine cells: main path and child-form path, each for create and update, each with a name and with none, plus one zh-CN cell (`任务“季度复盘”已创建`). - New file: 9/9 passed. - Ablation (committed fix, `node scripts/ablation-replace.mjs`, anchor `const title = resolved && !(id` replaced by `const title = false && resolved && !(id`; anchor 1 to 0, blob `66183e3229a8` to `a12d7507a89f`): 5 failed / 4 passed. All five "names the record" cells went red (`expected 'Task created successfully' to be 'Task "Quarterly review" created'`), and the four no-name cells stayed green, the direction written in the test's header before the run. Restore: blob back to `66183e3229a8` equals HEAD, `git diff HEAD` empty. - `vitest run packages/i18n/ packages/app-shell/src/console/__tests__/`: first run 3 failed (the two count pins above), 1694 passed. After re-pinning, the two files: 52/52 passed. - Every other test file under `ui/` that names `AppContent.tsx` or `form.createSuccess` / `form.updateSuccess` (14 files): 89/89 passed. - Test runs were at `3c5713b5`. The one later commit (`e9bd0d06`) only re-records `scripts/sdui-manifest.sources/i18n.json`. Narrowed on purpose: the whole `app-shell` suite (about 1700 files) is left to CI `Test Core` / `Build & E2E UI`. ## Acceptance notes - On an edit, the name is read off what the save returned (`dataSource.update`'s `record`). The toast can only name the record if that response carries the name field. The ObjectStack adapter returns the server's `record`. A live server was not booted for this PR. - Serial: `ui/packages/i18n` is held by the batch PR #26158. This PR stays draft. Merging main, re-recording `i18n.json` and enqueuing wait until that batch has merged and the ui seat says go. The two count pins above may need a re-pin at that merge if the batch moves the same counts. --- _Generated by [Claude Code](https://claude.ai/code/session_01DiQzqGc2iCV6ZSmHBw4mwe)_ --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Steve Jobs <steve@objectstack.ai>
1 parent b727956 commit b3ed6c8

16 files changed

Lines changed: 322 additions & 19 deletions

File tree

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
'@object-ui/app-shell': patch
3+
'@object-ui/i18n': patch
4+
---
5+
6+
fix(app-shell): the record create and update toasts name the saved record
7+
8+
Clause-②: no (adds or rewords message keys; no key is removed, so `TranslationKeys` only widens; no exported runtime contract changes)
9+
Metadata-protocol: none
10+
11+
A user entering several records in a row saw "Task created successfully" after each save and could not tell which record was just written. The Console's record-form save toasts now carry the saved record's display name (ADR-0079), for example `Task "Quarterly review" created` and `Task "Quarterly review" updated`. This covers the record-form overlay on a list and the child-record form opened over a parent record's page.
12+
13+
- **`@object-ui/app-shell`:** the name comes from the same display-name resolver the record page title and lookups use, read through the saved object's declared name field. A record with nothing nameable (the resolver would only answer `Record #ID`) keeps the object-label toast it showed before.
14+
- **`@object-ui/i18n`:** two new keys under `form.` in all ten packs, `createdNamed` and `updatedNamed`, each with `{{object}}` (the object label) and `{{title}}` (the record's name). The existing `form.createSuccess` / `form.updateSuccess` strings are unchanged and remain the toast for a record with no name.
15+
16+
Settings saves and flow-completion toasts are unchanged.
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"package":"ui/packages/i18n","sources":31,"sha256":"7e7907af7823796538ad5a70e56fb172dc533ec3988a2318c8aeddf7069af14a","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"}
1+
{"package":"ui/packages/i18n","sources":31,"sha256":"83894d7c91938618b320bda4f6664c1b39a0d5d6e7c3238e8e5eac5c51e1ec39","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"}

‎ui/packages/app-shell/src/console/AppContent.tsx‎

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { ModalForm } from '@object-ui/plugin-form';
1515
import { Empty, EmptyTitle, EmptyDescription, Button } from '@object-ui/components';
1616
import { toast } from 'sonner';
1717
import { useActionRunner, useGlobalUndo, useMutationInvalidationBridge, notifyDataChanged, FilterScopeProvider } from '@object-ui/react';
18+
import { getRecordDisplayName } from '@object-ui/core';
1819
import { useObjectTranslation, useObjectLabel } from '@object-ui/i18n';
1920
import type { AppAccessVerdict, ConnectionState } from '@object-ui/data-objectstack';
2021
import { useAuth, useWorkspaceAdminStatus } from '@object-ui/auth';
@@ -807,17 +808,37 @@ export function AppContent({ extraRoutes, extraRoutesNoApp }: AppContentProps =
807808
: currentObjectDef;
808809
const isChildFormTask = !!formObjectParam && formObjectParam !== currentObjectDef?.name;
809810

810-
const handleCrudSuccess = useCallback(() => {
811+
// objectstack-ai/objectstack#26172 — a record save names the record it wrote
812+
// (`ux-conventions.write-feedback`): `Task "Quarterly review" created`. The
813+
// name is the saved record's display name from the ONE ADR-0079 resolver,
814+
// read through the object being saved. The resolver's floor (`Record #<id>`,
815+
// or the empty fallback for an id-less row) is not a name, so a record with
816+
// nothing nameable keeps the object-label toast it always had.
817+
const recordSaveMessage = useCallback((saved: any): string => {
811818
const label = formObjectDef ? objectLabel(formObjectDef as any) : t('common.record', { defaultValue: 'Record' });
819+
const resolved = saved && typeof saved === 'object'
820+
? getRecordDisplayName(formObjectDef, saved, { fallback: '' })
821+
: '';
822+
const id = saved?.id ?? saved?._id;
823+
const title = resolved && !(id !== null && id !== undefined && resolved === `Record #${id}`)
824+
? resolved
825+
: undefined;
826+
if (editingRecord) {
827+
return title
828+
? t('form.updatedNamed', { object: label, title, defaultValue: `${label} "${title}" updated` })
829+
: t('form.updateSuccess', { object: label, defaultValue: `${label} updated successfully` });
830+
}
831+
return title
832+
? t('form.createdNamed', { object: label, title, defaultValue: `${label} "${title}" created` })
833+
: t('form.createSuccess', { object: label, defaultValue: `${label} created successfully` });
834+
}, [editingRecord, formObjectDef, objectLabel, t]);
835+
836+
const handleCrudSuccess = useCallback((saved?: any) => {
812837
executeAction({
813838
type: 'crud_success',
814-
params: {
815-
message: editingRecord
816-
? t('form.updateSuccess', { object: label, defaultValue: `${label} updated successfully` })
817-
: t('form.createSuccess', { object: label, defaultValue: `${label} created successfully` }),
818-
},
839+
params: { message: recordSaveMessage(saved) },
819840
});
820-
}, [executeAction, editingRecord, formObjectDef, objectLabel, t]);
841+
}, [executeAction, recordSaveMessage]);
821842

822843
const handleDialogCancel = useCallback(() => {
823844
executeAction({ type: 'dialog_cancel' });
@@ -837,14 +858,11 @@ export function AppContent({ extraRoutes, extraRoutesNoApp }: AppContentProps =
837858
// invalidation bus (#2269): the dataSource write already emitted a
838859
// MutationEvent that the bridge fans out — no manual notify needed.
839860
if (isChildFormTask) {
840-
const label = formObjectDef ? objectLabel(formObjectDef as any) : t('common.record', { defaultValue: 'Record' });
841-
toast.success(editingRecord
842-
? t('form.updateSuccess', { object: label, defaultValue: `${label} updated successfully` })
843-
: t('form.createSuccess', { object: label, defaultValue: `${label} created successfully` }));
861+
toast.success(recordSaveMessage(saved));
844862
closeRecordForm();
845863
return;
846864
}
847-
handleCrudSuccess();
865+
handleCrudSuccess(saved);
848866
if (editingRecord || !currentObjectDef) return;
849867
const target = resolvePostCreateTarget({
850868
objectName: currentObjectDef.name,
@@ -878,7 +896,7 @@ export function AppContent({ extraRoutes, extraRoutesNoApp }: AppContentProps =
878896
return;
879897
}
880898
navigate(target.url, { replace: true });
881-
}, [handleCrudSuccess, isChildFormTask, formObjectDef, editingRecord, currentObjectDef, appSegment, location.pathname, navigate, closeRecordForm, objectLabel, objectPluralLabel, t]);
899+
}, [handleCrudSuccess, recordSaveMessage, isChildFormTask, editingRecord, currentObjectDef, appSegment, location.pathname, navigate, closeRecordForm, objectPluralLabel]);
882900

883901
// Track recent items on route change. The hook matches `appName` against the
884902
// pathname's app segment, so it takes the ROUTE SEGMENT: handed
Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
/**
2+
* ObjectUI
3+
* Copyright (c) 2024-present ObjectStack Inc.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
/**
10+
* objectstack-ai/objectstack#26172 — the record create and update success
11+
* toasts name the saved record (`ux-conventions.write-feedback`), e.g.
12+
* `Task "Quarterly review" created`, by its ADR-0079 display name. A record
13+
* with nothing nameable keeps the object-label toast (`Task created
14+
* successfully`).
15+
*
16+
* Measured from a real `AppContent` mount inside a real i18next instance (the
17+
* `AppContent.createLandingOriginPlural-11733` harness): the global record-form
18+
* modal is a stand-in whose `onSuccess` is called with the saved record. Two
19+
* paths raise the toast. The main path hands it to the action runner as a
20+
* `crud_success` message, read here off the runner's `execute`; the child-form
21+
* task (a subtable child opened over its parent's record page) toasts it
22+
* directly, read here off sonner's `toast.success`.
23+
*
24+
* Direction, written before the run: with the toast put back to the object
25+
* label alone, every "names the record" cell RED (`Task created
26+
* successfully`); every "no name" cell GREEN on both sides.
27+
*/
28+
29+
import '@testing-library/jest-dom/vitest';
30+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
31+
import React from 'react';
32+
import { render, cleanup, waitFor } from '@testing-library/react';
33+
import { MemoryRouter, Routes, Route } from 'react-router-dom';
34+
import { toast } from 'sonner';
35+
import { createI18n, I18nProvider } from '@object-ui/i18n';
36+
37+
/** The part of the modal's schema this file reads. */
38+
interface ModalSchemaProbe {
39+
objectName?: string;
40+
onSuccess: (saved: Record<string, unknown>) => Promise<void> | void;
41+
}
42+
const { modalSchemas, execute } = vi.hoisted(() => ({
43+
modalSchemas: [] as ModalSchemaProbe[],
44+
execute: vi.fn(),
45+
}));
46+
47+
/** The global record-form modal: its schema carries the `onSuccess` under test. */
48+
vi.mock('@object-ui/plugin-form', async (importOriginal) => ({
49+
...(await importOriginal<Record<string, unknown>>()),
50+
ModalForm: ({ schema }: { schema: ModalSchemaProbe }) => {
51+
modalSchemas.push(schema);
52+
return <div data-testid="modal-form" />;
53+
},
54+
}));
55+
56+
vi.mock('@object-ui/plugin-designer', async (importOriginal) => ({
57+
...(await importOriginal<typeof import('@object-ui/plugin-designer')>()),
58+
CreateAppPage: () => <div data-testid="create-app-page" />,
59+
EditAppPage: () => <div data-testid="edit-app-page" />,
60+
DashboardDesignPage: () => <div data-testid="dashboard-design-page" />,
61+
}));
62+
63+
vi.mock('../../layout/ConsoleLayout', () => ({
64+
ConsoleLayout: ({ children }: { children?: React.ReactNode }) => (
65+
<div data-testid="console-layout">{children}</div>
66+
),
67+
}));
68+
vi.mock('../../chrome/CommandPalette', () => ({ CommandPalette: () => null }));
69+
vi.mock('../../chrome/KeyboardShortcutsDialog', () => ({ KeyboardShortcutsDialog: () => null }));
70+
vi.mock('../../chrome/OnboardingWalkthrough', () => ({ OnboardingWalkthrough: () => null }));
71+
vi.mock('../../views/ObjectView', () => ({ ObjectView: () => <div data-testid="object-view" /> }));
72+
vi.mock('../../views/RecordDetailView', () => ({ RecordDetailView: () => <div data-testid="record-page" /> }));
73+
74+
vi.mock('@object-ui/auth', async (importOriginal) => ({
75+
...(await importOriginal<Record<string, unknown>>()),
76+
useAuth: () => ({
77+
user: { id: 'u1', name: 'Ada', email: 'ada@example.com', role: 'user' },
78+
getAuthConfig: async () => ({ features: {} }),
79+
activeOrganization: null,
80+
}),
81+
useWorkspaceAdminStatus: () => ({ isAdmin: true, isResolved: true }),
82+
}));
83+
84+
const dataSourceStub = {
85+
onConnectionStateChange: () => () => {},
86+
getConnectionState: () => 'connected',
87+
};
88+
vi.mock('../../providers/AdapterProvider', async (importOriginal) => ({
89+
...(await importOriginal<Record<string, unknown>>()),
90+
useAdapter: () => dataSourceStub,
91+
}));
92+
93+
const OBJECTS = [
94+
// Declares its name field: a saved task is named by `subject`.
95+
{
96+
name: 'showcase_task',
97+
label: 'Task',
98+
nameField: 'subject',
99+
fields: { subject: { type: 'text', label: 'Subject' }, done: { type: 'boolean', label: 'Done' } },
100+
},
101+
// The parent the child-form task opens over.
102+
{ name: 'showcase_project', label: 'Project', fields: { name: { type: 'text', label: 'Name' } } },
103+
];
104+
105+
const APPS = [{ name: 'crm', label: 'CRM', isDefault: true, navigation: [] }];
106+
107+
// One value for the whole run: the route's effects key on these members.
108+
const METADATA = {
109+
apps: APPS,
110+
objects: OBJECTS,
111+
loading: false,
112+
ensureType: undefined,
113+
error: null,
114+
refresh: async () => {},
115+
};
116+
vi.mock('../../providers/MetadataProvider', async (importOriginal) => ({
117+
...(await importOriginal<Record<string, unknown>>()),
118+
useMetadata: () => METADATA,
119+
}));
120+
121+
const ACTION_RUNNER = { execute, runner: { registerHandler: () => {}, getContext: () => ({}) } };
122+
vi.mock('@object-ui/react', async (importOriginal) => ({
123+
...(await importOriginal<Record<string, unknown>>()),
124+
useActionRunner: () => ACTION_RUNNER,
125+
useGlobalUndo: () => {},
126+
useMutationInvalidationBridge: () => {},
127+
}));
128+
129+
import { AppContent } from '../AppContent';
130+
131+
type Lang = 'en' | 'zh-CN';
132+
133+
/** The catalog as the console loads it: `zh-CN` translates the task's label. */
134+
const BUNDLE: Record<Lang, Record<string, unknown>> = {
135+
en: {},
136+
'zh-CN': { showcase: { objects: { showcase_task: { label: '任务' } } } },
137+
};
138+
139+
const MAIN_CREATE = '/apps/crm/showcase_task?form=new';
140+
const MAIN_EDIT = '/apps/crm/showcase_task?form=t1';
141+
const CHILD_CREATE = '/apps/crm/showcase_project/record/p1?form=new&formObject=showcase_task';
142+
const CHILD_EDIT = '/apps/crm/showcase_project/record/p1?form=t1&formObject=showcase_task';
143+
144+
let toastSuccess: ReturnType<typeof vi.spyOn>;
145+
beforeEach(() => {
146+
modalSchemas.length = 0;
147+
execute.mockClear();
148+
toastSuccess = vi.spyOn(toast, 'success');
149+
});
150+
afterEach(() => {
151+
toastSuccess.mockRestore();
152+
cleanup();
153+
});
154+
155+
/** Mount at `url`, save `saved` through the record-form modal, and wait for its toast. */
156+
async function saveFrom(url: string, saved: Record<string, unknown>, lang: Lang = 'en'): Promise<void> {
157+
const i18n = createI18n({ defaultLanguage: lang, detectBrowserLanguage: false });
158+
expect(i18n.language).toBe(lang);
159+
i18n.addResourceBundle(lang, 'translation', BUNDLE[lang], true, true);
160+
render(
161+
<I18nProvider instance={i18n}>
162+
<MemoryRouter initialEntries={[url]}>
163+
<Routes>
164+
<Route path="/apps/:appName/*" element={<AppContent />} />
165+
</Routes>
166+
</MemoryRouter>
167+
</I18nProvider>,
168+
);
169+
await waitFor(() => expect(modalSchemas.length).toBeGreaterThan(0));
170+
const schema = modalSchemas[modalSchemas.length - 1];
171+
expect(schema.objectName).toBe('showcase_task');
172+
// Not inside `act`, as in the harness this file copies: the save settles at once.
173+
await schema.onSuccess(saved);
174+
}
175+
176+
/** The message the main path handed the runner as its `crud_success`. */
177+
function crudSuccessMessage(): string | undefined {
178+
const call = execute.mock.calls.find(([action]) => action?.type === 'crud_success');
179+
return call?.[0]?.params?.message;
180+
}
181+
182+
/** The message the child-form task toasted directly. */
183+
function childToastMessage(): string | undefined {
184+
return toastSuccess.mock.calls[0]?.[0] as string | undefined;
185+
}
186+
187+
describe('AppContent — the record save toast names the saved record (objectstack-ai/objectstack#26172)', () => {
188+
describe('main path (crud_success)', () => {
189+
it('create: names the record by its declared name field', async () => {
190+
await saveFrom(MAIN_CREATE, { id: 't1', subject: 'Quarterly review' });
191+
expect(crudSuccessMessage()).toBe('Task "Quarterly review" created');
192+
});
193+
194+
it('update: names the record by its declared name field', async () => {
195+
await saveFrom(MAIN_EDIT, { id: 't1', subject: 'Quarterly review' });
196+
expect(crudSuccessMessage()).toBe('Task "Quarterly review" updated');
197+
});
198+
199+
it('create, no name: the object-label toast', async () => {
200+
await saveFrom(MAIN_CREATE, { id: 't2', done: false });
201+
expect(crudSuccessMessage()).toBe('Task created successfully');
202+
});
203+
204+
it('update, no name: the object-label toast', async () => {
205+
await saveFrom(MAIN_EDIT, { id: 't1', done: true });
206+
expect(crudSuccessMessage()).toBe('Task updated successfully');
207+
});
208+
209+
it('zh-CN: the pack string, with the translated object label', async () => {
210+
await saveFrom(MAIN_CREATE, { id: 't1', subject: '季度复盘' }, 'zh-CN');
211+
expect(crudSuccessMessage()).toBe('任务“季度复盘”已创建');
212+
});
213+
});
214+
215+
describe('child-form task (direct toast)', () => {
216+
it('create: names the record', async () => {
217+
await saveFrom(CHILD_CREATE, { id: 't1', subject: 'Quarterly review' });
218+
expect(childToastMessage()).toBe('Task "Quarterly review" created');
219+
// The child task keeps the parent page as it is: no crud_success.
220+
expect(crudSuccessMessage()).toBeUndefined();
221+
});
222+
223+
it('update: names the record', async () => {
224+
await saveFrom(CHILD_EDIT, { id: 't1', subject: 'Quarterly review' });
225+
expect(childToastMessage()).toBe('Task "Quarterly review" updated');
226+
});
227+
228+
it('create, no name: the object-label toast', async () => {
229+
await saveFrom(CHILD_CREATE, { id: 't2', done: false });
230+
expect(childToastMessage()).toBe('Task created successfully');
231+
});
232+
233+
it('update, no name: the object-label toast', async () => {
234+
await saveFrom(CHILD_EDIT, { id: 't1', done: true });
235+
expect(childToastMessage()).toBe('Task updated successfully');
236+
});
237+
});
238+
});

‎ui/packages/i18n/src/__tests__/de-quote-pairing-3876.test.ts‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -351,7 +351,10 @@ describe('objectui#3876 — de pack closes „ with “ and not with a straight
351351
// 94 once objectui#13355 added `connectAgent.apiKey.confirmTitle`, the
352352
// API-key confirm that names the key („{{name}}“) — one interpolated span,
353353
// runtime data.
354-
expect(okSpans, 'correctly paired spans').toBe(94);
354+
// 96 once objectstack-ai/objectstack#26172 added `form.createdNamed` and
355+
// `.updatedNamed`, the record-save toasts that name the record
356+
// („{{title}}“) — one interpolated span each, runtime data.
357+
expect(okSpans, 'correctly paired spans').toBe(96);
355358
});
356359

357360
it('keeps the count identity that replaces the card’s count(„) === count(“)', () => {
@@ -413,8 +416,10 @@ describe('objectui#3876 — de pack closes „ with “ and not with a straight
413416
// `detail.deleteRowConfirmation`, one more matched pair. 93 / 93 / 0 after
414417
// objectui#13330 added `console.objectView.managedUsers.inviteLine`, one
415418
// more matched pair. 94 / 94 / 0 after objectui#13355 added
416-
// `connectAgent.apiKey.confirmTitle`, one more matched pair.
417-
expect({ open, close, rdq }).toEqual({ open: 94, close: 94, rdq: 0 });
419+
// `connectAgent.apiKey.confirmTitle`, one more matched pair. 96 / 96 / 0
420+
// after objectstack-ai/objectstack#26172 added `form.createdNamed` and
421+
// `.updatedNamed`, one matched pair each.
422+
expect({ open, close, rdq }).toEqual({ open: 96, close: 96, rdq: 0 });
418423
// The durable shape: every „ closed by a “, every surplus “ an English
419424
// opener answered by a ”. Survived translating the two English values.
420425
expect(close).toBe(open + rdq);

‎ui/packages/i18n/src/__tests__/residue-namespaces-3546.test.tsx‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -383,10 +383,12 @@ describe('objectui#3546 slice seven — the ratchet residue', () => {
383383
}
384384
// PageView uses it twice on the same button (title + aria-label) and
385385
// RelatedList twice in two different affordances — the counts are pinned so a
386-
// silent drop of one of them is visible.
386+
// silent drop of one of them is visible. AppContent's two record-save toast
387+
// sites share one message builder since objectstack-ai/objectstack#26172,
388+
// so its fallback label is written once.
387389
expect(sourceOf(PAGE_VIEW).split("t('common.editInStudio', { defaultValue: 'Edit in studio' })").length - 1).toBe(2);
388390
expect(sourceOf(RELATED_LIST).split("t('detail.add', { defaultValue: 'Add' })").length - 1).toBe(2);
389-
expect(sourceOf(APP_CONTENT).split("t('common.record', { defaultValue: 'Record' })").length - 1).toBe(2);
391+
expect(sourceOf(APP_CONTENT).split("t('common.record', { defaultValue: 'Record' })").length - 1).toBe(1);
390392
});
391393

392394
it('dashboard.loading matches useSafeTranslate positional fallback, not a defaultValue', () => {

‎ui/packages/i18n/src/locales/ar.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,8 @@ const ar = {
231231
update: "تحديث",
232232
createSuccess: "تم إنشاء {{object}}",
233233
updateSuccess: "تم تحديث {{object}}",
234+
createdNamed: "تم إنشاء {{object}} \"{{title}}\"",
235+
updatedNamed: "تم تحديث {{object}} \"{{title}}\"",
234236
created: "تم الإنشاء",
235237
saved: "تم الحفظ",
236238
savedNamed: "تم حفظ {{title}}",

‎ui/packages/i18n/src/locales/de.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,8 @@ const de = {
217217
update: "Aktualisieren",
218218
createSuccess: "{{object}} erfolgreich erstellt",
219219
updateSuccess: "{{object}} erfolgreich aktualisiert",
220+
createdNamed: "{{object}} „{{title}}“ erstellt",
221+
updatedNamed: "{{object}} „{{title}}“ aktualisiert",
220222
created: "Erstellt",
221223
saved: "Gespeichert",
222224
savedNamed: "{{title}} gespeichert",

0 commit comments

Comments
 (0)