diff --git a/.changeset/26172-record-save-toast-names-record.md b/.changeset/26172-record-save-toast-names-record.md new file mode 100644 index 00000000000..de0e1737ac7 --- /dev/null +++ b/.changeset/26172-record-save-toast-names-record.md @@ -0,0 +1,16 @@ +--- +'@object-ui/app-shell': patch +'@object-ui/i18n': patch +--- + +fix(app-shell): the record create and update toasts name the saved record + +Clause-②: no (adds or rewords message keys; no key is removed, so `TranslationKeys` only widens; no exported runtime contract changes) +Metadata-protocol: none + +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. + +- **`@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. +- **`@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. + +Settings saves and flow-completion toasts are unchanged. diff --git a/scripts/sdui-manifest.sources/i18n.json b/scripts/sdui-manifest.sources/i18n.json index 9acdc472ef8..0c252a718c8 100644 --- a/scripts/sdui-manifest.sources/i18n.json +++ b/scripts/sdui-manifest.sources/i18n.json @@ -1 +1 @@ -{"package":"ui/packages/i18n","sources":31,"sha256":"7e7907af7823796538ad5a70e56fb172dc533ec3988a2318c8aeddf7069af14a","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"} +{"package":"ui/packages/i18n","sources":31,"sha256":"83894d7c91938618b320bda4f6664c1b39a0d5d6e7c3238e8e5eac5c51e1ec39","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"} diff --git a/ui/packages/app-shell/src/console/AppContent.tsx b/ui/packages/app-shell/src/console/AppContent.tsx index f4e6da25105..66183e3229a 100644 --- a/ui/packages/app-shell/src/console/AppContent.tsx +++ b/ui/packages/app-shell/src/console/AppContent.tsx @@ -15,6 +15,7 @@ import { ModalForm } from '@object-ui/plugin-form'; import { Empty, EmptyTitle, EmptyDescription, Button } from '@object-ui/components'; import { toast } from 'sonner'; import { useActionRunner, useGlobalUndo, useMutationInvalidationBridge, notifyDataChanged, FilterScopeProvider } from '@object-ui/react'; +import { getRecordDisplayName } from '@object-ui/core'; import { useObjectTranslation, useObjectLabel } from '@object-ui/i18n'; import type { AppAccessVerdict, ConnectionState } from '@object-ui/data-objectstack'; import { useAuth, useWorkspaceAdminStatus } from '@object-ui/auth'; @@ -807,17 +808,37 @@ export function AppContent({ extraRoutes, extraRoutesNoApp }: AppContentProps = : currentObjectDef; const isChildFormTask = !!formObjectParam && formObjectParam !== currentObjectDef?.name; - const handleCrudSuccess = useCallback(() => { + // objectstack-ai/objectstack#26172 — a record save names the record it wrote + // (`ux-conventions.write-feedback`): `Task "Quarterly review" created`. The + // name is the saved record's display name from the ONE ADR-0079 resolver, + // read through the object being saved. The resolver's floor (`Record #`, + // or the empty fallback for an id-less row) is not a name, so a record with + // nothing nameable keeps the object-label toast it always had. + const recordSaveMessage = useCallback((saved: any): string => { const label = formObjectDef ? objectLabel(formObjectDef as any) : t('common.record', { defaultValue: 'Record' }); + const resolved = saved && typeof saved === 'object' + ? getRecordDisplayName(formObjectDef, saved, { fallback: '' }) + : ''; + const id = saved?.id ?? saved?._id; + const title = resolved && !(id !== null && id !== undefined && resolved === `Record #${id}`) + ? resolved + : undefined; + if (editingRecord) { + return title + ? t('form.updatedNamed', { object: label, title, defaultValue: `${label} "${title}" updated` }) + : t('form.updateSuccess', { object: label, defaultValue: `${label} updated successfully` }); + } + return title + ? t('form.createdNamed', { object: label, title, defaultValue: `${label} "${title}" created` }) + : t('form.createSuccess', { object: label, defaultValue: `${label} created successfully` }); + }, [editingRecord, formObjectDef, objectLabel, t]); + + const handleCrudSuccess = useCallback((saved?: any) => { executeAction({ type: 'crud_success', - params: { - message: editingRecord - ? t('form.updateSuccess', { object: label, defaultValue: `${label} updated successfully` }) - : t('form.createSuccess', { object: label, defaultValue: `${label} created successfully` }), - }, + params: { message: recordSaveMessage(saved) }, }); - }, [executeAction, editingRecord, formObjectDef, objectLabel, t]); + }, [executeAction, recordSaveMessage]); const handleDialogCancel = useCallback(() => { executeAction({ type: 'dialog_cancel' }); @@ -837,14 +858,11 @@ export function AppContent({ extraRoutes, extraRoutesNoApp }: AppContentProps = // invalidation bus (#2269): the dataSource write already emitted a // MutationEvent that the bridge fans out — no manual notify needed. if (isChildFormTask) { - const label = formObjectDef ? objectLabel(formObjectDef as any) : t('common.record', { defaultValue: 'Record' }); - toast.success(editingRecord - ? t('form.updateSuccess', { object: label, defaultValue: `${label} updated successfully` }) - : t('form.createSuccess', { object: label, defaultValue: `${label} created successfully` })); + toast.success(recordSaveMessage(saved)); closeRecordForm(); return; } - handleCrudSuccess(); + handleCrudSuccess(saved); if (editingRecord || !currentObjectDef) return; const target = resolvePostCreateTarget({ objectName: currentObjectDef.name, @@ -878,7 +896,7 @@ export function AppContent({ extraRoutes, extraRoutesNoApp }: AppContentProps = return; } navigate(target.url, { replace: true }); - }, [handleCrudSuccess, isChildFormTask, formObjectDef, editingRecord, currentObjectDef, appSegment, location.pathname, navigate, closeRecordForm, objectLabel, objectPluralLabel, t]); + }, [handleCrudSuccess, recordSaveMessage, isChildFormTask, editingRecord, currentObjectDef, appSegment, location.pathname, navigate, closeRecordForm, objectPluralLabel]); // Track recent items on route change. The hook matches `appName` against the // pathname's app segment, so it takes the ROUTE SEGMENT: handed diff --git a/ui/packages/app-shell/src/console/__tests__/AppContent.recordSaveToastNamesRecord-26172.test.tsx b/ui/packages/app-shell/src/console/__tests__/AppContent.recordSaveToastNamesRecord-26172.test.tsx new file mode 100644 index 00000000000..8a9b6fad44f --- /dev/null +++ b/ui/packages/app-shell/src/console/__tests__/AppContent.recordSaveToastNamesRecord-26172.test.tsx @@ -0,0 +1,238 @@ +/** + * ObjectUI + * Copyright (c) 2024-present ObjectStack Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * objectstack-ai/objectstack#26172 — the record create and update success + * toasts name the saved record (`ux-conventions.write-feedback`), e.g. + * `Task "Quarterly review" created`, by its ADR-0079 display name. A record + * with nothing nameable keeps the object-label toast (`Task created + * successfully`). + * + * Measured from a real `AppContent` mount inside a real i18next instance (the + * `AppContent.createLandingOriginPlural-11733` harness): the global record-form + * modal is a stand-in whose `onSuccess` is called with the saved record. Two + * paths raise the toast. The main path hands it to the action runner as a + * `crud_success` message, read here off the runner's `execute`; the child-form + * task (a subtable child opened over its parent's record page) toasts it + * directly, read here off sonner's `toast.success`. + * + * Direction, written before the run: with the toast put back to the object + * label alone, every "names the record" cell RED (`Task created + * successfully`); every "no name" cell GREEN on both sides. + */ + +import '@testing-library/jest-dom/vitest'; +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import React from 'react'; +import { render, cleanup, waitFor } from '@testing-library/react'; +import { MemoryRouter, Routes, Route } from 'react-router-dom'; +import { toast } from 'sonner'; +import { createI18n, I18nProvider } from '@object-ui/i18n'; + +/** The part of the modal's schema this file reads. */ +interface ModalSchemaProbe { + objectName?: string; + onSuccess: (saved: Record) => Promise | void; +} +const { modalSchemas, execute } = vi.hoisted(() => ({ + modalSchemas: [] as ModalSchemaProbe[], + execute: vi.fn(), +})); + +/** The global record-form modal: its schema carries the `onSuccess` under test. */ +vi.mock('@object-ui/plugin-form', async (importOriginal) => ({ + ...(await importOriginal>()), + ModalForm: ({ schema }: { schema: ModalSchemaProbe }) => { + modalSchemas.push(schema); + return
; + }, +})); + +vi.mock('@object-ui/plugin-designer', async (importOriginal) => ({ + ...(await importOriginal()), + CreateAppPage: () =>
, + EditAppPage: () =>
, + DashboardDesignPage: () =>
, +})); + +vi.mock('../../layout/ConsoleLayout', () => ({ + ConsoleLayout: ({ children }: { children?: React.ReactNode }) => ( +
{children}
+ ), +})); +vi.mock('../../chrome/CommandPalette', () => ({ CommandPalette: () => null })); +vi.mock('../../chrome/KeyboardShortcutsDialog', () => ({ KeyboardShortcutsDialog: () => null })); +vi.mock('../../chrome/OnboardingWalkthrough', () => ({ OnboardingWalkthrough: () => null })); +vi.mock('../../views/ObjectView', () => ({ ObjectView: () =>
})); +vi.mock('../../views/RecordDetailView', () => ({ RecordDetailView: () =>
})); + +vi.mock('@object-ui/auth', async (importOriginal) => ({ + ...(await importOriginal>()), + useAuth: () => ({ + user: { id: 'u1', name: 'Ada', email: 'ada@example.com', role: 'user' }, + getAuthConfig: async () => ({ features: {} }), + activeOrganization: null, + }), + useWorkspaceAdminStatus: () => ({ isAdmin: true, isResolved: true }), +})); + +const dataSourceStub = { + onConnectionStateChange: () => () => {}, + getConnectionState: () => 'connected', +}; +vi.mock('../../providers/AdapterProvider', async (importOriginal) => ({ + ...(await importOriginal>()), + useAdapter: () => dataSourceStub, +})); + +const OBJECTS = [ + // Declares its name field: a saved task is named by `subject`. + { + name: 'showcase_task', + label: 'Task', + nameField: 'subject', + fields: { subject: { type: 'text', label: 'Subject' }, done: { type: 'boolean', label: 'Done' } }, + }, + // The parent the child-form task opens over. + { name: 'showcase_project', label: 'Project', fields: { name: { type: 'text', label: 'Name' } } }, +]; + +const APPS = [{ name: 'crm', label: 'CRM', isDefault: true, navigation: [] }]; + +// One value for the whole run: the route's effects key on these members. +const METADATA = { + apps: APPS, + objects: OBJECTS, + loading: false, + ensureType: undefined, + error: null, + refresh: async () => {}, +}; +vi.mock('../../providers/MetadataProvider', async (importOriginal) => ({ + ...(await importOriginal>()), + useMetadata: () => METADATA, +})); + +const ACTION_RUNNER = { execute, runner: { registerHandler: () => {}, getContext: () => ({}) } }; +vi.mock('@object-ui/react', async (importOriginal) => ({ + ...(await importOriginal>()), + useActionRunner: () => ACTION_RUNNER, + useGlobalUndo: () => {}, + useMutationInvalidationBridge: () => {}, +})); + +import { AppContent } from '../AppContent'; + +type Lang = 'en' | 'zh-CN'; + +/** The catalog as the console loads it: `zh-CN` translates the task's label. */ +const BUNDLE: Record> = { + en: {}, + 'zh-CN': { showcase: { objects: { showcase_task: { label: '任务' } } } }, +}; + +const MAIN_CREATE = '/apps/crm/showcase_task?form=new'; +const MAIN_EDIT = '/apps/crm/showcase_task?form=t1'; +const CHILD_CREATE = '/apps/crm/showcase_project/record/p1?form=new&formObject=showcase_task'; +const CHILD_EDIT = '/apps/crm/showcase_project/record/p1?form=t1&formObject=showcase_task'; + +let toastSuccess: ReturnType; +beforeEach(() => { + modalSchemas.length = 0; + execute.mockClear(); + toastSuccess = vi.spyOn(toast, 'success'); +}); +afterEach(() => { + toastSuccess.mockRestore(); + cleanup(); +}); + +/** Mount at `url`, save `saved` through the record-form modal, and wait for its toast. */ +async function saveFrom(url: string, saved: Record, lang: Lang = 'en'): Promise { + const i18n = createI18n({ defaultLanguage: lang, detectBrowserLanguage: false }); + expect(i18n.language).toBe(lang); + i18n.addResourceBundle(lang, 'translation', BUNDLE[lang], true, true); + render( + + + + } /> + + + , + ); + await waitFor(() => expect(modalSchemas.length).toBeGreaterThan(0)); + const schema = modalSchemas[modalSchemas.length - 1]; + expect(schema.objectName).toBe('showcase_task'); + // Not inside `act`, as in the harness this file copies: the save settles at once. + await schema.onSuccess(saved); +} + +/** The message the main path handed the runner as its `crud_success`. */ +function crudSuccessMessage(): string | undefined { + const call = execute.mock.calls.find(([action]) => action?.type === 'crud_success'); + return call?.[0]?.params?.message; +} + +/** The message the child-form task toasted directly. */ +function childToastMessage(): string | undefined { + return toastSuccess.mock.calls[0]?.[0] as string | undefined; +} + +describe('AppContent — the record save toast names the saved record (objectstack-ai/objectstack#26172)', () => { + describe('main path (crud_success)', () => { + it('create: names the record by its declared name field', async () => { + await saveFrom(MAIN_CREATE, { id: 't1', subject: 'Quarterly review' }); + expect(crudSuccessMessage()).toBe('Task "Quarterly review" created'); + }); + + it('update: names the record by its declared name field', async () => { + await saveFrom(MAIN_EDIT, { id: 't1', subject: 'Quarterly review' }); + expect(crudSuccessMessage()).toBe('Task "Quarterly review" updated'); + }); + + it('create, no name: the object-label toast', async () => { + await saveFrom(MAIN_CREATE, { id: 't2', done: false }); + expect(crudSuccessMessage()).toBe('Task created successfully'); + }); + + it('update, no name: the object-label toast', async () => { + await saveFrom(MAIN_EDIT, { id: 't1', done: true }); + expect(crudSuccessMessage()).toBe('Task updated successfully'); + }); + + it('zh-CN: the pack string, with the translated object label', async () => { + await saveFrom(MAIN_CREATE, { id: 't1', subject: '季度复盘' }, 'zh-CN'); + expect(crudSuccessMessage()).toBe('任务“季度复盘”已创建'); + }); + }); + + describe('child-form task (direct toast)', () => { + it('create: names the record', async () => { + await saveFrom(CHILD_CREATE, { id: 't1', subject: 'Quarterly review' }); + expect(childToastMessage()).toBe('Task "Quarterly review" created'); + // The child task keeps the parent page as it is: no crud_success. + expect(crudSuccessMessage()).toBeUndefined(); + }); + + it('update: names the record', async () => { + await saveFrom(CHILD_EDIT, { id: 't1', subject: 'Quarterly review' }); + expect(childToastMessage()).toBe('Task "Quarterly review" updated'); + }); + + it('create, no name: the object-label toast', async () => { + await saveFrom(CHILD_CREATE, { id: 't2', done: false }); + expect(childToastMessage()).toBe('Task created successfully'); + }); + + it('update, no name: the object-label toast', async () => { + await saveFrom(CHILD_EDIT, { id: 't1', done: true }); + expect(childToastMessage()).toBe('Task updated successfully'); + }); + }); +}); diff --git a/ui/packages/i18n/src/__tests__/de-quote-pairing-3876.test.ts b/ui/packages/i18n/src/__tests__/de-quote-pairing-3876.test.ts index de3091f7259..a7375a20bae 100644 --- a/ui/packages/i18n/src/__tests__/de-quote-pairing-3876.test.ts +++ b/ui/packages/i18n/src/__tests__/de-quote-pairing-3876.test.ts @@ -351,7 +351,10 @@ describe('objectui#3876 — de pack closes „ with “ and not with a straight // 94 once objectui#13355 added `connectAgent.apiKey.confirmTitle`, the // API-key confirm that names the key („{{name}}“) — one interpolated span, // runtime data. - expect(okSpans, 'correctly paired spans').toBe(94); + // 96 once objectstack-ai/objectstack#26172 added `form.createdNamed` and + // `.updatedNamed`, the record-save toasts that name the record + // („{{title}}“) — one interpolated span each, runtime data. + expect(okSpans, 'correctly paired spans').toBe(96); }); 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 // `detail.deleteRowConfirmation`, one more matched pair. 93 / 93 / 0 after // objectui#13330 added `console.objectView.managedUsers.inviteLine`, one // more matched pair. 94 / 94 / 0 after objectui#13355 added - // `connectAgent.apiKey.confirmTitle`, one more matched pair. - expect({ open, close, rdq }).toEqual({ open: 94, close: 94, rdq: 0 }); + // `connectAgent.apiKey.confirmTitle`, one more matched pair. 96 / 96 / 0 + // after objectstack-ai/objectstack#26172 added `form.createdNamed` and + // `.updatedNamed`, one matched pair each. + expect({ open, close, rdq }).toEqual({ open: 96, close: 96, 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); diff --git a/ui/packages/i18n/src/__tests__/residue-namespaces-3546.test.tsx b/ui/packages/i18n/src/__tests__/residue-namespaces-3546.test.tsx index 3467f8d9979..04a2ecf4b76 100644 --- a/ui/packages/i18n/src/__tests__/residue-namespaces-3546.test.tsx +++ b/ui/packages/i18n/src/__tests__/residue-namespaces-3546.test.tsx @@ -383,10 +383,12 @@ describe('objectui#3546 slice seven — the ratchet residue', () => { } // PageView uses it twice on the same button (title + aria-label) and // RelatedList twice in two different affordances — the counts are pinned so a - // silent drop of one of them is visible. + // silent drop of one of them is visible. AppContent's two record-save toast + // sites share one message builder since objectstack-ai/objectstack#26172, + // so its fallback label is written once. expect(sourceOf(PAGE_VIEW).split("t('common.editInStudio', { defaultValue: 'Edit in studio' })").length - 1).toBe(2); expect(sourceOf(RELATED_LIST).split("t('detail.add', { defaultValue: 'Add' })").length - 1).toBe(2); - expect(sourceOf(APP_CONTENT).split("t('common.record', { defaultValue: 'Record' })").length - 1).toBe(2); + expect(sourceOf(APP_CONTENT).split("t('common.record', { defaultValue: 'Record' })").length - 1).toBe(1); }); it('dashboard.loading matches useSafeTranslate positional fallback, not a defaultValue', () => { diff --git a/ui/packages/i18n/src/locales/ar.ts b/ui/packages/i18n/src/locales/ar.ts index bad113a3a44..e7fe620e843 100644 --- a/ui/packages/i18n/src/locales/ar.ts +++ b/ui/packages/i18n/src/locales/ar.ts @@ -231,6 +231,8 @@ const ar = { update: "تحديث", createSuccess: "تم إنشاء {{object}}", updateSuccess: "تم تحديث {{object}}", + createdNamed: "تم إنشاء {{object}} \"{{title}}\"", + updatedNamed: "تم تحديث {{object}} \"{{title}}\"", created: "تم الإنشاء", saved: "تم الحفظ", savedNamed: "تم حفظ {{title}}", diff --git a/ui/packages/i18n/src/locales/de.ts b/ui/packages/i18n/src/locales/de.ts index 9a8b2124614..4ce472a35ed 100644 --- a/ui/packages/i18n/src/locales/de.ts +++ b/ui/packages/i18n/src/locales/de.ts @@ -217,6 +217,8 @@ const de = { update: "Aktualisieren", createSuccess: "{{object}} erfolgreich erstellt", updateSuccess: "{{object}} erfolgreich aktualisiert", + createdNamed: "{{object}} „{{title}}“ erstellt", + updatedNamed: "{{object}} „{{title}}“ aktualisiert", created: "Erstellt", saved: "Gespeichert", savedNamed: "{{title}} gespeichert", diff --git a/ui/packages/i18n/src/locales/en.ts b/ui/packages/i18n/src/locales/en.ts index 3cee03521a2..16f55d84818 100644 --- a/ui/packages/i18n/src/locales/en.ts +++ b/ui/packages/i18n/src/locales/en.ts @@ -322,6 +322,12 @@ const en = { update: 'Update', createSuccess: '{{object}} created successfully', updateSuccess: '{{object}} updated successfully', + // The console's record-save toasts when the saved record has a display + // name (ADR-0079): `{{object}}` is the object label, `{{title}}` the + // record's name. A record with no name keeps `createSuccess` / + // `updateSuccess` above. + createdNamed: '{{object}} "{{title}}" created', + updatedNamed: '{{object}} "{{title}}" updated', // plugin-form's own feedback chrome (objectui#11039): the success toast // a form raises when the author declared no `successMessage` (`created` // after a create, `saved` after an edit), the master-detail form's edit diff --git a/ui/packages/i18n/src/locales/es.ts b/ui/packages/i18n/src/locales/es.ts index 555b8075582..f41b17f9574 100644 --- a/ui/packages/i18n/src/locales/es.ts +++ b/ui/packages/i18n/src/locales/es.ts @@ -224,6 +224,8 @@ const es = { update: "Actualizar", createSuccess: "{{object}} creado", updateSuccess: "{{object}} actualizado", + createdNamed: "{{object}} \"{{title}}\" creado", + updatedNamed: "{{object}} \"{{title}}\" actualizado", created: "Creado", saved: "Guardado", savedNamed: "{{title}} guardado", diff --git a/ui/packages/i18n/src/locales/fr.ts b/ui/packages/i18n/src/locales/fr.ts index f7139e172f7..c33746f956c 100644 --- a/ui/packages/i18n/src/locales/fr.ts +++ b/ui/packages/i18n/src/locales/fr.ts @@ -219,6 +219,8 @@ const fr = { update: "Mettre à jour", createSuccess: "{{object}} créé", updateSuccess: "{{object}} mis à jour", + createdNamed: "{{object}} \"{{title}}\" créé", + updatedNamed: "{{object}} \"{{title}}\" mis à jour", created: "Créé", saved: "Enregistré", savedNamed: "{{title}} enregistré", diff --git a/ui/packages/i18n/src/locales/ja.ts b/ui/packages/i18n/src/locales/ja.ts index 02aa0737346..ab506d3d835 100644 --- a/ui/packages/i18n/src/locales/ja.ts +++ b/ui/packages/i18n/src/locales/ja.ts @@ -217,6 +217,8 @@ const ja = { update: "更新", createSuccess: "{{object}}が作成されました", updateSuccess: "{{object}}が更新されました", + createdNamed: "{{object}}「{{title}}」を作成しました", + updatedNamed: "{{object}}「{{title}}」を更新しました", created: "作成しました", saved: "保存しました", savedNamed: "{{title}}を保存しました", diff --git a/ui/packages/i18n/src/locales/ko.ts b/ui/packages/i18n/src/locales/ko.ts index 823d2375349..cc986a057fa 100644 --- a/ui/packages/i18n/src/locales/ko.ts +++ b/ui/packages/i18n/src/locales/ko.ts @@ -217,6 +217,8 @@ const ko = { update: "업데이트", createSuccess: "{{object}} 생성됨", updateSuccess: "{{object}} 업데이트됨", + createdNamed: "{{object}} \"{{title}}\" 생성됨", + updatedNamed: "{{object}} \"{{title}}\" 업데이트됨", created: "생성됨", saved: "저장됨", savedNamed: "{{title}} 저장됨", diff --git a/ui/packages/i18n/src/locales/pt.ts b/ui/packages/i18n/src/locales/pt.ts index 962b4e2de3e..078ff6f1af5 100644 --- a/ui/packages/i18n/src/locales/pt.ts +++ b/ui/packages/i18n/src/locales/pt.ts @@ -219,6 +219,8 @@ const pt = { update: "Atualizar", createSuccess: "{{object}} criado", updateSuccess: "{{object}} atualizado", + createdNamed: "{{object}} \"{{title}}\" criado", + updatedNamed: "{{object}} \"{{title}}\" atualizado", created: "Criado", saved: "Salvo", savedNamed: "{{title}} salvo", diff --git a/ui/packages/i18n/src/locales/ru.ts b/ui/packages/i18n/src/locales/ru.ts index ee60b5ae180..e4f4ed7bed8 100644 --- a/ui/packages/i18n/src/locales/ru.ts +++ b/ui/packages/i18n/src/locales/ru.ts @@ -224,6 +224,8 @@ const ru = { update: "Обновить", createSuccess: "{{object}} создан", updateSuccess: "{{object}} обновлён", + createdNamed: "{{object}} \"{{title}}\" создан", + updatedNamed: "{{object}} \"{{title}}\" обновлён", created: "Создано", saved: "Сохранено", savedNamed: "{{title}} сохранён", diff --git a/ui/packages/i18n/src/locales/zh.ts b/ui/packages/i18n/src/locales/zh.ts index 0f8a6cd30fa..9efa75e303f 100644 --- a/ui/packages/i18n/src/locales/zh.ts +++ b/ui/packages/i18n/src/locales/zh.ts @@ -222,6 +222,8 @@ const zh = { update: '更新', createSuccess: '{{object}}创建成功', updateSuccess: '{{object}}更新成功', + createdNamed: '{{object}}“{{title}}”已创建', + updatedNamed: '{{object}}“{{title}}”已更新', // objectui#11039 — plugin-form feedback chrome; see the `en` pack. created: '已创建', saved: '已保存',