Skip to content

Commit 9a63ecb

Browse files
fix(console): the shared-record page shows a lookup as "Linked record", never its stored id (#31346)
<!-- claude-projects-attribution: {"github_login":"os-project-manager"} --> _Requested by **Leehom** · [project thread](https://claude.ai/code/project/chan_01TenzsRopZj4jMbvjX8uz1X?thread=cmsg_01TenzsRopZj4jMbvjX8uz1XLP7yPUeEpHsHyGhzfziP4q)_ Fixes #31326 Clause-②: yes (widening) Metadata-protocol: none ## 维护者速读 - 改了什么:分享链接页 `/s/TOKEN` 上,有值的查找字段与主从字段不再显示被关联记录的存储 ID,改为显示本地化的中性占位「已关联记录」/ "Linked record";空值仍显示为空。是否为查找字段,按 resolve 应答 `fields` 投影里已下发的 `type` 判断,不看值的形状。 - 为什么改:匿名访客看到一串内部 ID,读起来像页面坏了(#27200 字母 C 裁决)。显示目标记录名称是选项 B,仍挂在 #27200,本 PR 不碰。 - 风险与回滚:只改 console 一页的展示层,外加 `@object-ui/i18n` 每个语言包一个新键 `sharedRecord.linkedRecord`;不改 resolve 接口、不改服务端、不多发请求、不读目标记录。旧服务端不返回 `fields` 时没有类型可读,页面与之前一样。回滚即 revert。 - 先看:`ui/apps/console/src/pages/SharedRecordFields.tsx`(`RECORD_REFERENCE_TYPES` 与行渲染的分支)、`SharedRecordPage-27121.test.tsx` 末尾新增的 `objectstack#31326` describe。 - 两分钟验证:`pnpm --dir ui exec vitest run apps/console/src/pages/SharedRecordPage-27121.test.tsx`,预期 13 个用例全绿,其中查找、主从、多值查找三例断言整个 DOM 里找不到存储 ID。 - 需要你做的:无,席位落地。 ## What changes - **`SharedRecordFields.tsx`** (`@object-ui/console`): a row whose projected field `type` is `lookup` or `master_detail` (`RECORD_REFERENCE_TYPES`) no longer renders its value. A filled one (an id, or a non-empty array of ids for a multiple lookup) renders `t('sharedRecord.linkedRecord')` in the muted foreground; an empty one (null, undefined, empty string, empty array) renders the page's existing empty face. The decision reads the `type` the resolve answer's `fields` projection already serves, never the value's shape. The branch sits before the no-fetch cell renderers, so nothing for these rows reads the target record, the data source or the meta door. The module header now states this rule in place of "the id is what shows". - **`@object-ui/i18n`**: every built-in pack gains `sharedRecord.linkedRecord` (en "Linked record", zh-Hans 「已关联记录」, the other nine translated). `zh-Hant.ts` was produced by `pnpm --filter @object-ui/i18n gen:zh-hant`, and a rerun reproduces the committed file byte for byte. - **Changeset** `.changeset/31326-shared-lookup-placeholder.md`: `@object-ui/console` and `@object-ui/i18n` patch, with both declaration lines. - Not changed: `GET /api/v1/share-links/TOKEN/resolve`, `packages/types/src/share-link-resolve-answer.ts`, and any server code. ## Tests - `SharedRecordPage-27121.test.tsx`, the page's existing harness, rendered through the real route with the resolve answer stubbed the way both producers send it. - New top-level describe for objectstack#31326 (6 cases), fed an `opportunity` answer: a filled lookup (and the registry-injected `created_by` lookup) shows the placeholder and its id is absent from `document.body.innerHTML`, and only the resolve request is made; a filled master-detail likewise; a multiple lookup shows one placeholder and none of its ids; an empty lookup renders the empty face; under a zh `I18nProvider` the placeholder is the zh-Hans pack's value (guarded as a real zh row); CONTROL: text fields render their values as before. - Two #27121 cases that pinned the old behaviour now pin the new one: the lookup row reads "Linked record", and under a host data source the id and the target's name are absent from the DOM while `find`, `findOne` and `getObjectSchema` are never called. - Unchanged and green: an answer without `fields` (an older server) still shows the id, because no type is served to read. - Ablation on head `d33a03b2` through `scripts/ablation-replace.mjs`: the reference-type branch was replaced with a constant false, so the raw value renders again (anchor 1 to 0, blob `7bf595db` to `52d8493a`). Result: 6 of 13 red (the six cases that assert the placeholder or the id's absence); the empty-lookup preservation case and the control stay green. Restored: blob equal to HEAD (`7bf595db`) and `git diff HEAD` empty. The console tests read `SharedRecordFields.tsx` from source by relative import, so no build sat between the mutation and the run. ## Acceptance notes - **Which types.** The card names lookup and master-detail, and this PR covers exactly those two `type` values. A `user` field is stored the same way (a `sys_user` id) and, by reading the code path, still renders its id on this page; a `tree` field may too. Not measured on a live page; noted for the seat, not filed. - **An older server.** An answer without `fields` carries no `type`, so a lookup there still shows its id, as before #27121. Guessing from the value's shape was ruled out by the order. - **Option B** (the target's name) stays on #27200; nothing here reads or serves it. - **Declaration lines.** The claim opened at `Clause-②: no`; adding an `@object-ui/i18n` key is published surface, so this PR and its changeset declare `yes (widening)`. `Metadata-protocol: none`: nothing a metadata author writes changes. ## Verification Run on head `d33a03b2` (base `f5e23e2f`): <details><summary>Build, tests and type-check</summary> - `pnpm --workspace-concurrency=2 --filter '@object-ui/console^...' build` under the verify lock: VERDICT command-exit 0. - `pnpm --dir ui exec vitest run apps/console/src/pages/SharedRecordPage-27121.test.tsx apps/console/src/pages/SharedRecordPage-26767.test.tsx apps/console/src/pages/consolePagesLocale-secondPass-13460.test.tsx`: Test Files 3 passed (3), Tests 29 passed (29). - `pnpm --dir ui exec vitest run packages/i18n/ apps/console/src/pages/`: Test Files 250 passed, 1 failed (251); the one failure was `SettingsField.labelBinding-11690.test.tsx` timing out at 15 s while the gate loop ran beside it. That file is untouched by this diff, and rerun alone it passed 15 of 15. - `pnpm --filter @object-ui/console run type-check`: exit 0. `pnpm --filter @object-ui/i18n run type-check`: exit 0. - eslint on the two changed console files: exit 0. `pnpm --dir ui run check:new-line-citations`: 0 new citations, exit 0. </details> <details><summary>Derived gates (38)</summary> `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 38 families on this diff, and all 38 exited 0. `pnpm --dir ui exec vitest run scripts/` was SIGKILLed on its first run while the console suite ran beside it; rerun alone under the lock, it exited 0 (Test Files 170 passed, 2 skipped; Tests 4996 passed). `--ran` reconciliation with exit codes recorded: 38 derived, 38 run, 0 NOT-MEASURED, 0 UNRUN. Changed lines: 185, under the 3,000 threshold. </details> --- _Generated by [Claude Code](https://claude.ai/code/session_01FEkQ5nVH4357CHXi47gYLr)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 2ba7a9d commit 9a63ecb

14 files changed

Lines changed: 171 additions & 14 deletions

File tree

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@object-ui/console': patch
3+
'@object-ui/i18n': patch
4+
---
5+
6+
fix(console): the shared-record page shows a lookup as "Linked record", never its stored id
7+
8+
Clause-②: yes (widening)
9+
Metadata-protocol: none
10+
11+
- **`@object-ui/console`**: on a share link (`/s/TOKEN`), a lookup or master-detail field with a value now reads "Linked record" instead of the referenced record's stored id, which an anonymous visitor could not use and which read as a broken page. An empty one still reads as empty. Which field is a lookup comes from the `type` the resolve answer's `fields` projection already serves; the page sends no extra request and reads nothing about the referenced record. An answer from a server that predates the projection carries no type, so its rows render as before.
12+
- **`@object-ui/i18n`**: every pack gains `sharedRecord.linkedRecord` (en "Linked record", zh-Hans 「已关联记录」).

‎ui/apps/console/src/pages/SharedRecordFields.tsx‎

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,11 @@
2525
* string through the console's one date-display path, a number through its
2626
* one number-display path, a boolean as Yes / No (the detail view's
2727
* read-only word), and an array or object as short text — never raw JSON.
28-
* That includes a lookup: its value is the stored id, and the target's name
29-
* is not served to an anonymous visitor, so the id is what shows.
28+
* - A lookup or master-detail value is the stored id of ANOTHER record, and
29+
* neither that id nor the target's name is anything the visitor should read:
30+
* a filled one shows a neutral, localized "Linked record" and an empty one
31+
* the empty face. Which field is one is read from the type the projection
32+
* serves, never guessed from the value's shape; nothing reads the target.
3033
*
3134
* System fields are shown, not hidden: the record detail view's notion of a
3235
* framework-managed column (`isSystemManagedField`, `@object-ui/types`) only
@@ -112,6 +115,21 @@ const NO_FETCH_CELL_RENDERERS: ReadonlySet<React.FC<CellRendererProps>> = new Se
112115
CurrencyCellRenderer,
113116
]);
114117

118+
/**
119+
* The field types whose value is the stored id of another record, as the
120+
* projection serves `type`. The value never reaches the page as text.
121+
*/
122+
const RECORD_REFERENCE_TYPES: ReadonlySet<string> = new Set(['lookup', 'master_detail']);
123+
124+
function isRecordReference(field: SharedFieldDisplay | undefined): boolean {
125+
return !!field?.type && RECORD_REFERENCE_TYPES.has(field.type);
126+
}
127+
128+
/** A value with nothing in it: no id was stored, so there is no link to name. */
129+
function isEmptyValue(value: unknown): boolean {
130+
return value === null || value === undefined || value === '' || (Array.isArray(value) && value.length === 0);
131+
}
132+
115133
/** The detail view's renderer for `field`'s type, when it is one this page may use. */
116134
function noFetchRendererOf(field: SharedFieldDisplay | undefined): React.FC<CellRendererProps> | undefined {
117135
if (!field?.type) return undefined;
@@ -146,7 +164,15 @@ export function SharedRecordFields({ record, fields }: SharedRecordFieldsProps)
146164
<div key={key} className="grid grid-cols-1 gap-1 py-2 sm:grid-cols-[minmax(8rem,1fr)_2fr] sm:gap-4">
147165
<dt className="text-xs font-medium text-muted-foreground">{field?.label || humanizeLabel(key)}</dt>
148166
<dd className="min-w-0 whitespace-pre-wrap break-words text-sm text-foreground">
149-
{Renderer ? (
167+
{isRecordReference(field) ? (
168+
isEmptyValue(record[key]) ? (
169+
EMPTY
170+
) : (
171+
<span className="text-muted-foreground">
172+
{t('sharedRecord.linkedRecord', { defaultValue: 'Linked record' })}
173+
</span>
174+
)
175+
) : Renderer ? (
150176
<Renderer
151177
value={record[key]}
152178
field={{ name: key, ...field } as unknown as FieldMetadata}

‎ui/apps/console/src/pages/SharedRecordPage-27121.test.tsx‎

Lines changed: 119 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44
* objectstack#27121 — the shared-record page reads the resolve answer's
55
* `fields` projection (objectstack#26991): each row carries the field's
66
* authored label, a picklist value its authored option label, a currency
7-
* value its currency through `resolveFieldCurrency`, and a lookup its stored
8-
* id. A key with no `fields` entry, and an answer from a server that predates
9-
* the projection, keep the humanized label and the JSON-shape value.
7+
* value its currency through `resolveFieldCurrency`, and a filled lookup a
8+
* neutral "Linked record" in place of its stored id (objectstack#31326). A key
9+
* with no `fields` entry, and an answer from a server that predates the
10+
* projection, keep the humanized label and the JSON-shape value.
1011
*
1112
* The stub answers `/share-links/:token/resolve` the way both producers do:
1213
* enveloped, with the redacted fields already stripped from `record` and
@@ -20,7 +21,8 @@ import { afterEach, describe, expect, it, vi } from 'vitest';
2021
import { cleanup, render, screen, within } from '@testing-library/react';
2122
import { MemoryRouter, Route, Routes } from 'react-router-dom';
2223
import { formatCurrency } from '@object-ui/fields';
23-
import { resolveFieldCurrency } from '@object-ui/i18n';
24+
import { I18nProvider, resolveFieldCurrency } from '@object-ui/i18n';
25+
import { builtInLocales } from '@object-ui/i18n/locales';
2426
import { SchemaRendererProvider } from '@object-ui/react';
2527
import SharedRecordPage from './SharedRecordPage';
2628

@@ -72,7 +74,11 @@ const json = (status: number, body: unknown) =>
7274
new Response(JSON.stringify(body), { status, headers: { 'Content-Type': 'application/json' } });
7375

7476
/** Stub the resolve route; answer `fields` only when given. Returns every requested path. */
75-
function shareServer(record: Record<string, unknown>, fields?: Record<string, unknown>): string[] {
77+
function shareServer(
78+
record: Record<string, unknown>,
79+
fields?: Record<string, unknown>,
80+
objectName = 'account',
81+
): string[] {
7682
const requested: string[] = [];
7783
vi.stubGlobal(
7884
'fetch',
@@ -88,7 +94,7 @@ function shareServer(record: Record<string, unknown>, fields?: Record<string, un
8894
link: {
8995
id: 'sl_1',
9096
token: TOKEN,
91-
object_name: 'account',
97+
object_name: objectName,
9298
record_id: String(record.id),
9399
permission: 'view',
94100
audience: 'anyone',
@@ -104,16 +110,27 @@ function shareServer(record: Record<string, unknown>, fields?: Record<string, un
104110
return requested;
105111
}
106112

107-
function renderLink(dataSource?: Record<string, unknown>) {
113+
function renderLink(dataSource?: Record<string, unknown>, language?: 'zh') {
108114
const routed = (
109115
<MemoryRouter initialEntries={[`/s/${TOKEN}`]}>
110116
<Routes>
111117
<Route path="/s/:token" element={<SharedRecordPage />} />
112118
</Routes>
113119
</MemoryRouter>
114120
);
121+
const withData = dataSource ? (
122+
<SchemaRendererProvider dataSource={dataSource as never}>{routed}</SchemaRendererProvider>
123+
) : (
124+
routed
125+
);
115126
return render(
116-
dataSource ? <SchemaRendererProvider dataSource={dataSource as never}>{routed}</SchemaRendererProvider> : routed,
127+
language ? (
128+
<I18nProvider config={{ defaultLanguage: language, detectBrowserLanguage: false }} persistLanguage={false}>
129+
{withData}
130+
</I18nProvider>
131+
) : (
132+
withData
133+
),
117134
);
118135
}
119136

@@ -180,12 +197,12 @@ describe('SharedRecordPage — rows read the resolve answer’s fields projectio
180197
expect(cellOf('Open Pipeline').textContent).not.toMatch(/CN¥|¥|CNY/);
181198
});
182199

183-
it('a lookup shows its stored id under its authored label, and nothing but resolve is fetched', async () => {
200+
it('a lookup shows a neutral placeholder under its authored label, and nothing but resolve is fetched', async () => {
184201
const requested = shareServer(ACCOUNT, FIELDS);
185202
renderLink();
186203

187204
await screen.findByText('Shared account');
188-
expect(cellOf('Parent Account').textContent).toBe('acc_parent_9');
205+
expect(cellOf('Parent Account').textContent).toBe('Linked record');
189206
// One request, to resolve: no lookup-name read, no /meta, no /data.
190207
expect(requested).toEqual([RESOLVE_PATH]);
191208
});
@@ -205,7 +222,10 @@ describe('SharedRecordPage — rows read the resolve answer’s fields projectio
205222
await screen.findByText('Shared account');
206223
// Let any effect a renderer scheduled settle before reading the spies.
207224
await new Promise((resolve) => setTimeout(resolve, 50));
208-
expect(cellOf('Parent Account').textContent).toBe('acc_parent_9');
225+
expect(cellOf('Parent Account').textContent).toBe('Linked record');
226+
// Neither the stored id nor the target's name reaches the page.
227+
expect(document.body.innerHTML).not.toContain('acc_parent_9');
228+
expect(document.body.textContent).not.toContain('Globex');
209229
expect(dataSource.find).not.toHaveBeenCalled();
210230
expect(dataSource.findOne).not.toHaveBeenCalled();
211231
expect(dataSource.getObjectSchema).not.toHaveBeenCalled();
@@ -236,3 +256,91 @@ describe('SharedRecordPage — rows read the resolve answer’s fields projectio
236256
expect(requested).toEqual([RESOLVE_PATH]);
237257
});
238258
});
259+
260+
/**
261+
* objectstack#31326 — a lookup or master-detail value is the stored id of
262+
* another record. The page shows a neutral, localized "Linked record" for a
263+
* filled one and the empty face for an empty one; the id never reaches the
264+
* DOM. Which field is one is read from the `type` the projection serves.
265+
*/
266+
describe('SharedRecordPage — a lookup shows a neutral placeholder, never its stored id (objectstack#31326)', () => {
267+
/** An `opportunity` as resolve hands it over: one filled and one empty lookup, a master-detail, a multi lookup. */
268+
const OPPORTUNITY = {
269+
id: 'opp_7',
270+
name: 'Renewal 2027',
271+
account_id: 'acc_9f3e1c',
272+
partner_id: null,
273+
plan_id: 'plan_mdx_42',
274+
contact_ids: ['con_a11', 'con_b22'],
275+
created_by: 'usr_creator_5',
276+
};
277+
const OPPORTUNITY_FIELDS = {
278+
id: { label: 'Record ID', type: 'text' },
279+
name: { label: 'Opportunity Name', type: 'text' },
280+
account_id: { label: 'Account', type: 'lookup', reference: 'account', displayField: 'name' },
281+
partner_id: { label: 'Partner', type: 'lookup', reference: 'account', displayField: 'name' },
282+
plan_id: { label: 'Plan', type: 'master_detail', reference: 'plan', displayField: 'title' },
283+
contact_ids: { label: 'Contacts', type: 'lookup', reference: 'contact', multiple: true },
284+
created_by: { label: 'Created By', type: 'lookup', reference: 'sys_user' },
285+
};
286+
const STORED_IDS = ['acc_9f3e1c', 'plan_mdx_42', 'con_a11', 'con_b22', 'usr_creator_5'];
287+
288+
it('a filled lookup shows the placeholder, and its id appears nowhere in the DOM', async () => {
289+
const requested = shareServer(OPPORTUNITY, OPPORTUNITY_FIELDS, 'opportunity');
290+
renderLink();
291+
292+
await screen.findByText('Shared opportunity');
293+
expect(cellOf('Account').textContent).toBe('Linked record');
294+
expect(cellOf('Created By').textContent).toBe('Linked record');
295+
expect(document.body.innerHTML).not.toContain('acc_9f3e1c');
296+
expect(document.body.innerHTML).not.toContain('usr_creator_5');
297+
expect(requested).toEqual([RESOLVE_PATH]);
298+
});
299+
300+
it('a filled master-detail shows the placeholder, and its id appears nowhere in the DOM', async () => {
301+
shareServer(OPPORTUNITY, OPPORTUNITY_FIELDS, 'opportunity');
302+
renderLink();
303+
304+
await screen.findByText('Shared opportunity');
305+
expect(cellOf('Plan').textContent).toBe('Linked record');
306+
expect(document.body.innerHTML).not.toContain('plan_mdx_42');
307+
});
308+
309+
it('a multiple lookup shows the placeholder, and none of its ids', async () => {
310+
shareServer(OPPORTUNITY, OPPORTUNITY_FIELDS, 'opportunity');
311+
renderLink();
312+
313+
await screen.findByText('Shared opportunity');
314+
expect(cellOf('Contacts').textContent).toBe('Linked record');
315+
for (const id of STORED_IDS) expect(document.body.innerHTML).not.toContain(id);
316+
});
317+
318+
it('an empty lookup renders empty, as before', async () => {
319+
shareServer(OPPORTUNITY, OPPORTUNITY_FIELDS, 'opportunity');
320+
renderLink();
321+
322+
await screen.findByText('Shared opportunity');
323+
expect(cellOf('Partner').textContent).toBe('—');
324+
});
325+
326+
it('the placeholder speaks the session language', async () => {
327+
const zh = builtInLocales['zh-Hans'].sharedRecord.linkedRecord;
328+
// Guard: a real zh row, not an English copy.
329+
expect(zh).not.toBe(builtInLocales.en.sharedRecord.linkedRecord);
330+
shareServer(OPPORTUNITY, OPPORTUNITY_FIELDS, 'opportunity');
331+
renderLink(undefined, 'zh');
332+
333+
await screen.findByText(builtInLocales['zh-Hans'].sharedRecord.sharedObject.replace('{{object}}', 'opportunity'));
334+
expect(cellOf('Account').textContent).toBe(zh);
335+
expect(document.body.innerHTML).not.toContain('acc_9f3e1c');
336+
});
337+
338+
it('a text field renders its value as before (control)', async () => {
339+
shareServer(OPPORTUNITY, OPPORTUNITY_FIELDS, 'opportunity');
340+
renderLink();
341+
342+
await screen.findByText('Shared opportunity');
343+
expect(cellOf('Opportunity Name').textContent).toBe('Renewal 2027');
344+
expect(cellOf('Record ID').textContent).toBe('opp_7');
345+
});
346+
});

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4092,6 +4092,7 @@ const ar = {
40924092
noMessages: 'لا تحتوي هذه المحادثة على رسائل بعد.',
40934093
sharedObject: '{{object}} مشترك',
40944094
redactedFields: 'أخفى المالك بعض الحقول: {{fields}}',
4095+
linkedRecord: 'سجل مرتبط',
40954096
},
40964097
publicForm: {
40974098
submit: "إرسال",

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3787,6 +3787,7 @@ const de = {
37873787
noMessages: 'Diese Unterhaltung enthält noch keine Nachrichten.',
37883788
sharedObject: 'Geteilt: {{object}}',
37893789
redactedFields: 'Einige Felder hat der Eigentümer ausgeblendet: {{fields}}',
3790+
linkedRecord: 'Verknüpfter Datensatz',
37903791
},
37913792
publicForm: {
37923793
submit: "Absenden",

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4592,6 +4592,7 @@ const en = {
45924592
noMessages: 'This conversation has no messages yet.',
45934593
sharedObject: 'Shared {{object}}',
45944594
redactedFields: 'Some fields are hidden by the owner: {{fields}}',
4595+
linkedRecord: 'Linked record',
45954596
},
45964597
publicForm: {
45974598
submit: 'Submit',

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3864,6 +3864,7 @@ const es = {
38643864
noMessages: 'Esta conversación aún no tiene mensajes.',
38653865
sharedObject: '{{object}} compartido',
38663866
redactedFields: 'El propietario ocultó algunos campos: {{fields}}',
3867+
linkedRecord: 'Registro vinculado',
38673868
},
38683869
publicForm: {
38693870
submit: "Enviar",

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3860,6 +3860,7 @@ const fr = {
38603860
noMessages: 'Cette conversation ne contient encore aucun message.',
38613861
sharedObject: '{{object}} partagé',
38623862
redactedFields: 'Certains champs sont masqués par le propriétaire : {{fields}}',
3863+
linkedRecord: 'Enregistrement lié',
38633864
},
38643865
publicForm: {
38653866
submit: "Envoyer",

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3789,6 +3789,7 @@ const ja = {
37893789
noMessages: 'この会話にはまだメッセージがありません。',
37903790
sharedObject: '共有された {{object}}',
37913791
redactedFields: '一部の項目は所有者によって非表示にされています: {{fields}}',
3792+
linkedRecord: '関連レコード',
37923793
},
37933794
publicForm: {
37943795
submit: "送信",

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3786,6 +3786,7 @@ const ko = {
37863786
noMessages: '이 대화에는 아직 메시지가 없습니다.',
37873787
sharedObject: '공유된 {{object}}',
37883788
redactedFields: '일부 필드는 소유자가 숨겼습니다: {{fields}}',
3789+
linkedRecord: '연결된 레코드',
37893790
},
37903791
publicForm: {
37913792
submit: "제출",

0 commit comments

Comments
 (0)