Skip to content

Commit 48bffe3

Browse files
fix(studio): the dataset inspector stores no guessed measure currency (#27208)
<!-- claude-projects-attribution: {"github_login":"os-warren"} --> _Requested by **Warren** · [project thread](https://claude.ai/code/project/chan_01U5RdnAAmkSQmZbqh6FtqZ8?thread=cmsg_01U5RdnAAmkSQmZbqh6FtqZ8Kb1Q1LciCB4G4onVaiMJWK)_ Fixes #27156 Clause-②: no Metadata-protocol: none ## 维护者速读 - **改了什么:** Studio 数据集检查器里选「货币」格式但不选币种时,不再往度量上写 `currency: 'USD'`;货币字段上的度量默认「继承」,保存的度量没有 `currency` 键。 - **为什么改:** 度量自带的 `currency` 优先于字段固定币种和组织默认币种,猜出来的 USD 让人民币组织的仪表盘显示 `$`(裁决 6464202770,Q2 = X:任何地方都不猜币种)。 - **风险与回滚:** 只影响检查器的写入与读回;已存的 `currency: 'USD'` 原样保留、不迁移。回滚 = revert 本 PR。 - **先看:** `DatasetDefaultInspector.tsx` 的 `buildMeasureFormat` 与 `MeasureFormatField`;新测试 `DatasetDefaultInspector.currencyNoGuess-27156.test.tsx`。 - **两分钟验证:** Studio 打开一个数据集,给 sum(金额) 度量选「货币」不选币种并保存,度量定义里没有 `currency` 键,币种框显示「继承 —— 字段或组织的默认币种」。 - **需要你做的:** 无,席位落地。 ## What changes - `buildMeasureFormat('currency', …)` with an empty currency returns `currency: undefined` instead of `'USD'`, and the currency picker no longer pre-selects USD (it read `measure.currency || 'USD'`). - The save path drops the key: a new module-private `writeMeasureFormat` rebuilds the measure without `format` / `currency` when the picker leaves them unset (the `writeRowField` rule), so the held draft and the saved body both carry no `currency` key, rather than `currency: undefined`. - Over a `currency`-type source field the picker gains an "Inherit — field or organization default" row (`''`), which is the default. Over any other field it offers no inherit row and shows the placeholder "Choose a currency". - `parseMeasureFormat` takes a `monetaryField` flag: a code-less, non-percent numeral format over a `currency` field reads back as Currency · inherit. - The kind the author picks is held in component state while the stored format is still the one that pick wrote, so "Currency" does not snap back to "Number" before a code is chosen over a plain number field. - `@object-ui/i18n` studio namespace: `engine.inspector.dataset.currencyInherit` and `engine.inspector.dataset.currencyChoose` (en, zh). Patch changeset for `@object-ui/i18n`. ## Why Maintainer ruling 6464202770 on #27119 (Q2 = X): no surface guesses a currency. The analytics chain resolves a monetary measure's currency as explicit measure `currency`, then the source field's fixed currency, then the tenant default; a stored guess outranks both. ## Tests New `DatasetDefaultInspector.currencyNoGuess-27156.test.tsx` (6 cases, the inspector mounted with a stateful draft holder): - empty pick over a `currency` field: the measure has no `currency` property (`not.toHaveProperty`, so `currency: undefined` would fail too), `format: '0,0'`, the picker reads inherit; - CONTROL: picking CNY stores `currency: 'CNY'`; - a loaded `currency: 'USD'` measure: no patch on mount, the picker shows USD, a decimals change keeps `currency: 'USD'`; - over a number field: no `currency` key, kind stays Currency, the picker shows "Choose a currency" and offers no inherit row; - load: a code-less format over a `currency` field reads Currency · inherit; CONTROL: the same format over a number field reads Number. Ablation (committed fix, `node scripts/ablation-replace.mjs` with anchor `currency: currency || undefined };` replaced by `currency: currency || 'USD' };`): 2 failed / 4 passed — "stores no currency key when Currency is picked with no code" and the number-field case go red; restore verified (blob `10ecdd83` equals HEAD, `git diff HEAD` empty). ## Acceptance notes - **Stored currencies are left as authored.** A measure that already carries `currency: 'USD'` (including one this inspector wrote earlier) is explicit on the wire: no migration, no rewrite on load, and it round-trips through edits unchanged (pinned above). - **Mechanism assumption measured, partly disproved.** The order assumed the empty state reads as "inherit" for every measure. The analytics service treats a measure as monetary only when it declares a currency or aggregates a `currency`-type field; any other measure never receives a code. So "inherit" is offered only over a `currency` field; over other fields the empty state asks for a code, following the ruling's intent (no guess) rather than offering an inherit that resolves to nothing. - **Read-back change over `currency` fields.** A code-less numeral format over a `currency` field used to read back as "Number"; it now reads as Currency · inherit. The runtime already rendered those measures with a currency, so the new reading matches what dashboards show. A consequence: choosing "Number" over a `currency` field reads back as Currency after a reload, because the runtime attaches a currency to it either way. - **The sample under the picker** shows a plain number for an inherit measure: the inspector does not know the field's fixed currency or the tenant default. Noted, not changed. - **USD scan of the write path:** after this diff the only `'USD'` left in `views/metadata-admin` (non-test) is the `CURRENCY_OPTIONS` row itself. The `formatKind.currency` label still shows `$1,234.50` as an example; it is display text and writes nothing. - Serial: open PR #27163 edits this file's imports and filter section, not `buildMeasureFormat`; `origin/main` was merged before the gates. ## Verification Final head `77475026`. The last commit only drops an `export` keyword; the derived gates, the ui scripts suite and the two inspector test files were re-run on it, the wider suites ran on `dcfac927`. <details><summary>Local runs</summary> - On `77475026`: `node scripts/pm/dispatch-gates.mjs --commands` (list unchanged), all 34 families exit 0, `--ran` reconciliation 34 derived, 34 run, 0 NOT-MEASURED; `pnpm --dir ui exec vitest run scripts/` plus `DatasetDefaultInspector.currencyNoGuess-27156.test.tsx` and `DatasetDefaultInspector.test.tsx` (verify lock): 177 files passed, 2 skipped; 5040 tests passed, 2 skipped. - On `dcfac927`: `pnpm --dir ui exec vitest run packages/app-shell/src/views/metadata-admin/inspectors/ packages/i18n/` (verify lock): 286 files passed, 4116 tests passed, 14 skipped. - `pnpm --filter @object-ui/app-shell --filter @object-ui/i18n run type-check`: both `type-check: Done`, exit 0. - `pnpm --dir ui run check:new-line-citations`: 0 new citations, exit 0. - `eslint --no-inline-config` on the four changed files: 0 errors (pre-existing warnings only). - `dispatch-gates --tier`: no path-derived mandate; 233 changed lines. </details> ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. (not touched) - [x] 2. Every name I added uses the casing for its kind. - [x] 3. No formatter ran; `pnpm lint` passes. (changed files linted locally; repo-wide run is CI's) - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. - [x] 6. `packages/spec` touched: its generated artifacts are regenerated and committed. (not touched) - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English. (the 维护者速读 block is the dispatch order's Chinese summary) - [x] The *Before Submitting* list in CONTRIBUTING.md is done. --- _Generated by [Claude Code](https://claude.ai/code/session_01JQwxbakr3SQTyXX1NuZkt3)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 835b2d1 commit 48bffe3

5 files changed

Lines changed: 223 additions & 10 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/i18n': patch
3+
---
4+
5+
fix(studio): the dataset inspector stores no guessed measure currency
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
Picking the "Currency" display format for a dataset measure with no currency chosen used to store `currency: 'USD'`, and the currency picker pre-selected USD. A measure's own `currency` outranks the source field's fixed currency and the tenant default, so a measure saved that way showed `$` on a tenant whose default is another currency. The inspector now guesses nothing:
11+
12+
- **Over a `currency` field** the picker opens on "Inherit — field or organization default" and the saved measure carries no `currency` key, so it resolves the field's fixed currency, then the tenant default. A numeral format with no code over such a field reads back as Currency · inherit.
13+
- **Over any other field** there is nothing to inherit, so the picker offers no inherit row and asks for a code; the format kind stays Currency until one is picked.
14+
- **A stored `currency`**, `'USD'` included, is explicit authoring and is left as written: no migration, no rewrite on load.
15+
16+
`@object-ui/i18n` gains `engine.inspector.dataset.currencyInherit` and `engine.inspector.dataset.currencyChoose` in the studio namespace (en, zh).
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* objectstack#27156 — the dataset inspector never guesses a measure currency.
5+
*
6+
* Picking the "Currency" display format used to store `currency: 'USD'` when no
7+
* code was chosen, and the picker pre-selected USD. A measure's own `currency`
8+
* outranks both the source field's fixed currency and the tenant default in the
9+
* analytics chain, so a CNY tenant's dashboard showed `$` with no error. The
10+
* ruling (no surface guesses a currency) makes the empty pick the default and
11+
* stores no `currency` key at all, so the measure inherits.
12+
*
13+
* - empty pick over a `currency` field ⇒ the measure carries no `currency`
14+
* key (not `undefined`, not `''`), and the picker reads "inherit";
15+
* - CONTROL: a picked CNY is stored as `currency: 'CNY'`;
16+
* - a stored `currency: 'USD'` is explicit authoring and round-trips as is;
17+
* - a measure over a plain number field has nothing to inherit: no "inherit"
18+
* row, the picker asks for a code, and the kind stays "Currency" meanwhile;
19+
* - on load, a code-less numeral format over a `currency` field reads back as
20+
* "Currency · inherit" — CONTROL: the same format over a number field reads
21+
* back as "Number".
22+
*
23+
* Ablation: restoring `currency || 'USD'` in `buildMeasureFormat` turns the
24+
* first case red. Run recorded on the PR.
25+
*/
26+
27+
import '@testing-library/jest-dom/vitest';
28+
import * as React from 'react';
29+
import { afterEach, describe, expect, it, vi } from 'vitest';
30+
import { cleanup, render, screen, within } from '@testing-library/react';
31+
import userEvent from '@testing-library/user-event';
32+
33+
vi.mock('./useDatasetFields', () => ({
34+
useObjectOptions: () => ({ options: [], loading: false }),
35+
useDatasetFieldCatalog: () => ({
36+
relationships: [],
37+
fieldOptions: [
38+
{ value: 'amount', label: 'Amount', type: 'currency' },
39+
{ value: 'quantity', label: 'Quantity', type: 'number' },
40+
],
41+
loading: false,
42+
}),
43+
useDatasetUsage: () => ({ reports: 0, dashboards: 0, loading: false }),
44+
fieldTypeToDimensionType: (t: string) => (t === 'date' ? 'date' : 'string'),
45+
}));
46+
47+
import { DatasetDefaultInspector } from './DatasetDefaultInspector';
48+
49+
afterEach(cleanup);
50+
51+
type Draft = { measures: Array<Record<string, unknown>> } & Record<string, unknown>;
52+
53+
const draftWith = (measure: Record<string, unknown>): Draft => ({
54+
name: 'sales',
55+
label: 'Sales',
56+
object: 'opportunity',
57+
dimensions: [{ name: 'stage', field: 'stage', type: 'string' }],
58+
measures: [{ name: 'revenue', aggregate: 'sum', ...measure }],
59+
});
60+
61+
/** Holds the draft the way the designer does, so every pick re-renders from what was written. */
62+
function mount(initial: Draft) {
63+
const held = { current: initial };
64+
const onPatch = vi.fn();
65+
function Harness() {
66+
const [draft, setDraft] = React.useState<Draft>(initial);
67+
return (
68+
<DatasetDefaultInspector
69+
type="dataset"
70+
name="sales"
71+
locale="en-US"
72+
draft={draft}
73+
readOnly={false}
74+
onPatch={(patch: Record<string, unknown>) => {
75+
onPatch(patch);
76+
setDraft((d) => (held.current = { ...d, ...patch } as Draft));
77+
}}
78+
/>
79+
);
80+
}
81+
render(<Harness />);
82+
return { measure: () => held.current.measures[0]!, onPatch };
83+
}
84+
85+
const combo = (name: string) => screen.getByRole('combobox', { name });
86+
87+
async function pick(name: string, option: string | RegExp) {
88+
await userEvent.click(combo(name));
89+
await userEvent.click(await screen.findByRole('option', { name: option }));
90+
}
91+
92+
describe('dataset inspector stores no guessed measure currency (objectstack#27156)', () => {
93+
it('stores no currency key when Currency is picked with no code', async () => {
94+
const { measure } = mount(draftWith({ field: 'amount' }));
95+
await pick('Display format', /^Currency/);
96+
expect(measure()).not.toHaveProperty('currency');
97+
expect(measure().format).toBe('0,0');
98+
expect(combo('Display format')).toHaveTextContent(/^Currency/);
99+
expect(combo('Currency')).toHaveTextContent('Inherit — field or organization default');
100+
});
101+
102+
it('CONTROL: stores a picked CNY', async () => {
103+
const { measure } = mount(draftWith({ field: 'amount' }));
104+
await pick('Display format', /^Currency/);
105+
await pick('Currency', 'CNY (¥)');
106+
expect(measure()).toMatchObject({ format: '0,0', currency: 'CNY' });
107+
});
108+
109+
it('round-trips a stored currency USD unchanged', async () => {
110+
const { measure, onPatch } = mount(draftWith({ field: 'amount', format: '0,0.00', currency: 'USD' }));
111+
expect(onPatch).not.toHaveBeenCalled();
112+
expect(combo('Currency')).toHaveTextContent('USD ($)');
113+
await pick('Decimals', '0');
114+
expect(measure()).toMatchObject({ format: '0,0', currency: 'USD' });
115+
});
116+
117+
it('asks for a code over a plain number field and keeps the Currency kind meanwhile', async () => {
118+
const { measure } = mount(draftWith({ field: 'quantity' }));
119+
await pick('Display format', /^Currency/);
120+
expect(measure()).not.toHaveProperty('currency');
121+
expect(combo('Display format')).toHaveTextContent(/^Currency/);
122+
expect(combo('Currency')).toHaveTextContent('Choose a currency');
123+
await userEvent.click(combo('Currency'));
124+
const listbox = await screen.findByRole('listbox');
125+
expect(within(listbox).queryByRole('option', { name: /^Inherit/ })).toBeNull();
126+
});
127+
128+
it('reads a code-less format over a currency field back as Currency · inherit', () => {
129+
mount(draftWith({ field: 'amount', format: '0,0.00' }));
130+
expect(combo('Display format')).toHaveTextContent(/^Currency/);
131+
expect(combo('Currency')).toHaveTextContent('Inherit — field or organization default');
132+
});
133+
134+
it('CONTROL: reads the same format over a number field back as Number', () => {
135+
mount(draftWith({ field: 'quantity', format: '0,0.00' }));
136+
expect(combo('Display format')).toHaveTextContent(/^Number/);
137+
expect(screen.queryByRole('combobox', { name: 'Currency' })).toBeNull();
138+
});
139+
});

‎ui/packages/app-shell/src/views/metadata-admin/inspectors/DatasetDefaultInspector.tsx‎

Lines changed: 64 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,21 @@ export function writeRowField<T extends { field?: string }>(row: T, field: strin
166166
return (isBlankField(field) ? rest : { ...rest, field }) as T;
167167
}
168168

169+
/**
170+
* Write a measure's display format: a key the picker leaves unset is removed
171+
* rather than stored as `undefined`, the same rule as {@link writeRowField}, so
172+
* the held draft and the body the save sends say the same thing — a measure on
173+
* "inherit" carries no `currency` key at all.
174+
*/
175+
function writeMeasureFormat<T extends { format?: string; currency?: string }>(row: T, next: { format?: string; currency?: string }): T {
176+
const { format: _format, currency: _currency, ...rest } = row;
177+
return {
178+
...rest,
179+
...(next.format ? { format: next.format } : {}),
180+
...(next.currency ? { currency: next.currency } : {}),
181+
} as T;
182+
}
183+
169184
function SectionHeader({ title, count, onAdd, addLabel }: { title: string; count: number; onAdd?: () => void; addLabel: string }) {
170185
return (
171186
<div className="flex items-center justify-between">
@@ -197,23 +212,39 @@ function Advanced({ children, locale }: { children: React.ReactNode; locale: Sup
197212
);
198213
}
199214

200-
/** Best-effort parse of a stored measure format into the picker's {kind, decimals}. */
201-
function parseMeasureFormat(format?: string, currency?: string): { kind: string; decimals: number } {
215+
/**
216+
* Best-effort parse of a stored measure format into the picker's {kind, decimals}.
217+
*
218+
* `monetaryField` — the measure aggregates a `currency`-type field. Such a
219+
* measure is monetary at runtime whether or not it names a `currency` (the
220+
* analytics service resolves one from the field's fixed currency, then the
221+
* tenant default), so a numeral format with no code reads back as "Currency ·
222+
* inherit" — the state the picker wrote — rather than as a plain number.
223+
*/
224+
function parseMeasureFormat(format?: string, currency?: string, monetaryField = false): { kind: string; decimals: number } {
202225
const f = (format ?? '').trim();
203226
const m = f.match(/\.(0+)/);
204227
const decimals = m ? Math.min(m[1].length, 2) : 0;
205228
if (currency || /[$£€¥₹]/.test(f)) return { kind: 'currency', decimals };
229+
if (monetaryField && f && !f.includes('%')) return { kind: 'currency', decimals };
206230
if (f.includes('%')) return { kind: 'percent', decimals };
207231
if (f) return { kind: 'number', decimals };
208232
return { kind: 'raw', decimals: 0 };
209233
}
210234

211-
/** Generate {format, currency} from the picker selection. */
235+
/**
236+
* Generate {format, currency} from the picker selection.
237+
*
238+
* An empty currency stays empty: no surface guesses a code. A measure with no
239+
* `currency` inherits through the existing chain — the source field's fixed
240+
* currency, then the tenant default — where a guessed `'USD'` would outrank
241+
* both and show `$` on a CNY tenant's dashboard.
242+
*/
212243
function buildMeasureFormat(kind: string, decimals: number, currency: string): { format?: string; currency?: string } {
213244
const dp = decimals > 0 ? '.' + '0'.repeat(decimals) : '';
214245
switch (kind) {
215246
case 'number': return { format: `0,0${dp}`, currency: undefined };
216-
case 'currency': return { format: `0,0${dp}`, currency: currency || 'USD' };
247+
case 'currency': return { format: `0,0${dp}`, currency: currency || undefined };
217248
case 'percent': return { format: `0${dp}%`, currency: undefined };
218249
default: return { format: undefined, currency: undefined };
219250
}
@@ -224,10 +255,27 @@ function buildMeasureFormat(kind: string, decimals: number, currency: string): {
224255
* ⇄ the spec's `format`/`currency` strings and shows a live sample so a business
225256
* user never has to hand-write a numeral pattern.
226257
*/
227-
function MeasureFormatField({ measure, onPatch, disabled, locale }: { measure: Measure; onPatch: (p: Partial<Measure>) => void; disabled?: boolean; locale: SupportedLocale }) {
228-
const { kind, decimals } = parseMeasureFormat(measure.format, measure.currency);
229-
const currency = measure.currency || 'USD';
230-
const apply = (k: string, d: number, c: string) => onPatch(buildMeasureFormat(k, d, c));
258+
function MeasureFormatField({ measure, monetaryField, onPatch, disabled, locale }: { measure: Measure; monetaryField: boolean; onPatch: (p: { format?: string; currency?: string }) => void; disabled?: boolean; locale: SupportedLocale }) {
259+
const parsed = parseMeasureFormat(measure.format, measure.currency, monetaryField);
260+
// "Currency" with no code is not distinguishable on the wire from "Number"
261+
// for a measure over a plain number field, so the kind the author just picked
262+
// is held for as long as the stored format is still the one that pick wrote —
263+
// otherwise the select would snap back to "Number" before a code is chosen.
264+
const [held, setHeld] = React.useState<{ kind: string; format?: string; currency?: string } | null>(null);
265+
const kind = held && held.format === measure.format && held.currency === measure.currency ? held.kind : parsed.kind;
266+
const { decimals } = parsed;
267+
const currency = measure.currency ?? '';
268+
const apply = (k: string, d: number, c: string) => {
269+
const next = buildMeasureFormat(k, d, c);
270+
setHeld({ kind: k, ...next });
271+
onPatch(next);
272+
};
273+
// Only a measure over a `currency` field has a chain to inherit from; any
274+
// other measure without a code renders as a plain number, so it is offered
275+
// no "inherit" row and asks for a code instead.
276+
const currencyOptions = monetaryField
277+
? [{ value: '', label: t('engine.inspector.dataset.currencyInherit', locale) }, ...CURRENCY_OPTIONS]
278+
: CURRENCY_OPTIONS;
231279
// The sample is a PREVIEW of authored formatting, so it has to be rendered
232280
// through the same channel as the surfaces it previews (objectui#4575): a
233281
// German session picking "Number · 1 decimal" is shown `1.234,5`, because
@@ -247,7 +295,7 @@ function MeasureFormatField({ measure, onPatch, disabled, locale }: { measure: M
247295
)}
248296
</div>
249297
{kind === 'currency' && (
250-
<InspectorSelectField label={t('engine.inspector.dataset.currency', locale)} value={currency} options={CURRENCY_OPTIONS} onCommit={(v) => apply(kind, decimals, v)} disabled={disabled} />
298+
<InspectorSelectField label={t('engine.inspector.dataset.currency', locale)} value={currency} options={currencyOptions} placeholder={t('engine.inspector.dataset.currencyChoose', locale)} onCommit={(v) => apply(kind, decimals, v)} disabled={disabled} />
251299
)}
252300
{kind !== 'raw' && (
253301
<p className="text-[10px] text-muted-foreground">
@@ -673,7 +721,13 @@ export function DatasetDefaultInspector({ draft, onPatch, readOnly, name, locale
673721
{(() => { const rel = missingRelationship(m.field, include); return rel ? <RelWarning rel={rel} disabled={readOnly} locale={locale} onAdd={() => onPatch({ include: appendArray(include, rel) })} /> : null; })()}
674722
<Advanced locale={locale}>
675723
<InspectorTextField label={tr('engine.inspector.dataset.labelOptional')} value={m.label ?? ''} onCommit={(v) => patchMeasure(i, { label: v || undefined })} placeholder={m.name || tr('engine.inspector.dataset.displayLabel')} disabled={readOnly} />
676-
<MeasureFormatField measure={m} onPatch={(pp) => patchMeasure(i, pp)} disabled={readOnly} locale={locale} />
724+
<MeasureFormatField
725+
measure={m}
726+
monetaryField={fieldOptions.find((o) => o.value === m.field)?.type === 'currency'}
727+
onPatch={(fmt) => onPatch({ measures: measures.map((mm, idx) => (idx === i ? writeMeasureFormat(mm, fmt) : mm)) })}
728+
disabled={readOnly}
729+
locale={locale}
730+
/>
677731
<FilterConditionField
678732
label={tr('engine.inspector.dataset.measureFilter')}
679733
help={tr('engine.inspector.dataset.measureFilterHelp')}

‎ui/packages/i18n/src/namespaces/studio/en.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1172,6 +1172,8 @@ const STUDIO_EN: Record<string, string> = {
11721172
'engine.inspector.dataset.displayFormat': 'Display format',
11731173
'engine.inspector.dataset.decimals': 'Decimals',
11741174
'engine.inspector.dataset.currency': 'Currency',
1175+
'engine.inspector.dataset.currencyInherit': 'Inherit — field or organization default',
1176+
'engine.inspector.dataset.currencyChoose': 'Choose a currency',
11751177
'engine.inspector.dataset.sample': 'Sample: {sample}',
11761178
'engine.inspector.dataset.relWarning': "Relationship {rel} isn't in Included relationships.",
11771179
'engine.inspector.dataset.relWarningAdd': 'Add it',

‎ui/packages/i18n/src/namespaces/studio/zh.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1046,6 +1046,8 @@ const STUDIO_ZH: Record<string, string> = {
10461046
'engine.inspector.dataset.displayFormat': '显示格式',
10471047
'engine.inspector.dataset.decimals': '小数位数',
10481048
'engine.inspector.dataset.currency': '货币',
1049+
'engine.inspector.dataset.currencyInherit': '继承 —— 字段或组织的默认币种',
1050+
'engine.inspector.dataset.currencyChoose': '选择币种',
10491051
'engine.inspector.dataset.sample': '示例:{sample}',
10501052
'engine.inspector.dataset.relWarning': '关联 {rel} 不在「包含的关联」中。',
10511053
'engine.inspector.dataset.relWarningAdd': '添加它',

0 commit comments

Comments
 (0)