@object-ui/i18n 17.6.0 → 17.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4320 -0
- package/README.md +192 -13
- package/dist/builtinAggregateLabels.d.ts +40 -0
- package/dist/builtinAggregateLabels.js +23 -0
- package/dist/currency.d.ts +27 -3
- package/dist/currency.js +28 -4
- package/dist/fallbackInterpolation.d.ts +79 -0
- package/dist/fallbackInterpolation.js +124 -0
- package/dist/i18n.d.ts +27 -1
- package/dist/i18n.js +99 -16
- package/dist/index.d.ts +7 -13
- package/dist/index.js +41 -17
- package/dist/locales/ar.d.ts +946 -113
- package/dist/locales/ar.js +1095 -161
- package/dist/locales/de.d.ts +618 -113
- package/dist/locales/de.js +715 -134
- package/dist/locales/en.d.ts +632 -127
- package/dist/locales/en.js +1044 -162
- package/dist/locales/es.d.ts +700 -113
- package/dist/locales/es.js +794 -130
- package/dist/locales/fr.d.ts +700 -113
- package/dist/locales/fr.js +795 -132
- package/dist/locales/index.d.ts +6990 -1169
- package/dist/locales/index.js +48 -9
- package/dist/locales/ja.d.ts +618 -113
- package/dist/locales/ja.js +708 -128
- package/dist/locales/ko.d.ts +618 -113
- package/dist/locales/ko.js +712 -132
- package/dist/locales/pt.d.ts +700 -113
- package/dist/locales/pt.js +794 -130
- package/dist/locales/registry.d.ts +102 -0
- package/dist/locales/registry.js +220 -0
- package/dist/locales/rtl.d.ts +24 -0
- package/dist/locales/rtl.js +26 -0
- package/dist/locales/ru.d.ts +782 -113
- package/dist/locales/ru.js +910 -152
- package/dist/locales/zh.d.ts +635 -130
- package/dist/locales/zh.js +725 -131
- package/dist/pickLocalized.d.ts +30 -0
- package/dist/pickLocalized.js +39 -0
- package/dist/provider.d.ts +101 -12
- package/dist/provider.js +335 -50
- package/dist/translateFn.d.ts +29 -0
- package/dist/translateFn.js +8 -0
- package/dist/useDisplayLocale.d.ts +11 -0
- package/dist/useDisplayLocale.js +11 -0
- package/dist/useObjectLabel.d.ts +40 -18
- package/dist/useObjectLabel.js +46 -14
- package/dist/useSafeTranslation.js +40 -42
- package/dist/utils/formatting.d.ts +16 -0
- package/dist/utils/formatting.js +23 -3
- package/dist/utils/spec-formatters.d.ts +13 -23
- package/dist/utils/spec-formatters.js +41 -2
- package/dist/utils/spec-translations.d.ts +16 -3
- package/dist/utils/spec-translations.js +52 -13
- package/package.json +12 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,4325 @@
|
|
|
1
1
|
# @object-ui/i18n
|
|
2
2
|
|
|
3
|
+
## 17.7.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 6cc910b: A field-backed action param reaches its record picker, and a param whose backing
|
|
8
|
+
field cannot be read is refused instead of rendered as an empty text box.
|
|
9
|
+
|
|
10
|
+
An action declaring a `params` entry backed by a `lookup` field rendered as a bare
|
|
11
|
+
text input: no options, no typeahead, and no request for the referenced object on
|
|
12
|
+
the wire at all, because no picker was ever built. With the param `required`, the
|
|
13
|
+
action could not be launched from the UI — while the same field rendered a working
|
|
14
|
+
picker on a record form in the same build, off the same metadata.
|
|
15
|
+
|
|
16
|
+
`resolveActionParams()` resolves a field-backed param against the `objects` list
|
|
17
|
+
its caller holds, and two of the console's own callers cannot hold the right one:
|
|
18
|
+
`ConsoleShell`'s root action runtime — the provider that exists so a `type: 'flow'`
|
|
19
|
+
`action:button` works outside the four object views — passes no objects at all, and
|
|
20
|
+
`DeclaredActionsBar` passes exactly one (none when driven by an `actions` prop).
|
|
21
|
+
With the owner object absent the param took the resolver's last-resort shape, and
|
|
22
|
+
for a field-backed param that shape is `text`: it declares no inline `type`, so
|
|
23
|
+
there is nothing else to fall back to. The object binding was never missing from
|
|
24
|
+
the client — this seam never asked the metadata store for it.
|
|
25
|
+
|
|
26
|
+
- `useConsoleActionRuntime` now resolves field-backed params against the caller's
|
|
27
|
+
objects UNIONED with the console metadata store's, caller first (`withKnownObjects`),
|
|
28
|
+
after awaiting the object type so "this field does not exist" is an answer rather
|
|
29
|
+
than a boot race. A draft/preview overlay the caller carries still wins.
|
|
30
|
+
- The degradation is no longer anonymous. `resolveActionParams()` stamps
|
|
31
|
+
`unresolvedField: '<object>.<field>'` on a param whose backing field it could not
|
|
32
|
+
find and says so in dev. This is the half that made the defect invisible for a
|
|
33
|
+
whole version: by the time `paramToField()` sees such a param it IS a `text`
|
|
34
|
+
param, so `paramDegradesWithoutTarget()` answered false, no "no reference target"
|
|
35
|
+
warning fired, and even the "paste a record id" placeholder and help text did not
|
|
36
|
+
apply.
|
|
37
|
+
- `ActionParamDialog` refuses such a param: it renders an alert naming the
|
|
38
|
+
`<object>.<field>` pair in place of the control, logs one line per dialog opening
|
|
39
|
+
in every build, and disables Confirm so the action cannot be launched with a value
|
|
40
|
+
the dialog had no contract to collect.
|
|
41
|
+
|
|
42
|
+
New localized string `actionDialog.unresolvedParam` in all ten locale bundles. The
|
|
43
|
+
`<object>.<field>` locator renders as its own node rather than an interpolation, so
|
|
44
|
+
an identifier is never re-ordered by a translation.
|
|
45
|
+
- e686f4d: A record form can no longer be saved while an upload is still in flight (objectui#10166).
|
|
46
|
+
|
|
47
|
+
A `file` / `image` value only becomes its fileId once the presigned upload settles. Until
|
|
48
|
+
now a record form had no notion of upload state at all, so a Save pressed during that
|
|
49
|
+
window wrote the record WITHOUT the attachment — and reported success. The user picked the
|
|
50
|
+
file, saw it listed and saved; there was no error, no warning, and the record looked saved.
|
|
51
|
+
Whoever noticed did so later, looking at a record that should have a file and does not.
|
|
52
|
+
|
|
53
|
+
`onUploadingChange` (ADR-0059) already carried the signal, and had exactly one consumer,
|
|
54
|
+
`ActionParamDialog`. It could not have a second: that prop is per-widget, and a record form
|
|
55
|
+
hands a `fields` array to the `form` node renderer and never touches a widget, so there is
|
|
56
|
+
no point in the chain where it can attach a callback — and its upload controls can sit
|
|
57
|
+
inside a section, a tab or a line-items subform.
|
|
58
|
+
|
|
59
|
+
`@object-ui/fields` therefore publishes the AGGREGATION beside the prop: `useUploadingScope`
|
|
60
|
+
(the host's "is anything below me uploading") and `UploadingScopeProvider`.
|
|
61
|
+
`useUploadingSignal` — also exported now, for widgets authored outside this repo — feeds
|
|
62
|
+
both sinks from the one call it already made, so the per-widget prop and the scope cannot
|
|
63
|
+
disagree, and a host that mounts no provider is unaffected. A widget that unmounts
|
|
64
|
+
mid-upload releases its slot, so a collapsing section cannot wedge Save shut.
|
|
65
|
+
|
|
66
|
+
Nesting CHAINS rather than shadows: an inner scope gates its own Save AND reports itself to
|
|
67
|
+
the scope above. The direction is forced by `MasterDetailForm`, whose Save persists parent
|
|
68
|
+
and children in one batch while its rows are edited by nested `ObjectForm`s — a gate that
|
|
69
|
+
saw only the parent's uploads would refuse nothing while a child's attachment was in flight
|
|
70
|
+
and would still read as coverage.
|
|
71
|
+
|
|
72
|
+
Every submit owner in `@object-ui/plugin-form` is gated: `ObjectForm`, `ModalForm`,
|
|
73
|
+
`DrawerForm`, `SplitForm`, `TabbedForm`, `WizardForm`, `MasterDetailForm`, and
|
|
74
|
+
`EmbeddableForm` through the `ObjectForm` it hosts. While an upload is in flight each
|
|
75
|
+
refuses the submit (which is also the keyboard-submit guard), labels Save "Uploading…", and
|
|
76
|
+
renders the reason as a sentence — `form.uploadInFlight`, new in all ten locale packs. The
|
|
77
|
+
hosts that own their Save button — `ModalForm`, `DrawerForm`, `MasterDetailForm`, and
|
|
78
|
+
`WizardForm`'s final step — disable it as well; the flat `ObjectForm`, `SplitForm` and
|
|
79
|
+
`TabbedForm` paths submit through the `form` node renderer in `@object-ui/components`, which
|
|
80
|
+
exposes no per-button disable, so there the refusal plus the label and the notice are what
|
|
81
|
+
the user meets.
|
|
82
|
+
|
|
83
|
+
`WizardForm` is gated on its FINAL commit only. Moving between steps writes nothing, so
|
|
84
|
+
`Next` is deliberately untouched — but note that leaving a step unmounts its widgets, so an
|
|
85
|
+
upload in flight is released by the unmount and its value never reaches the record. That
|
|
86
|
+
loss predates this change and is not addressed by it.
|
|
87
|
+
- 5e67837: Translations that `I18nProvider` loads after mount now reach the readers already
|
|
88
|
+
on screen (objectui#10382).
|
|
89
|
+
|
|
90
|
+
**The defect.** Both of the provider's asynchronous loads end in
|
|
91
|
+
`addResourceBundle`: the app bundle from `loadLanguage`, and the built-in
|
|
92
|
+
catalogue for the language the instance booted in. Each was followed by a "force
|
|
93
|
+
re-render" that set the provider's `language` state to the value it already held.
|
|
94
|
+
React bails out of a same-value update, so nothing re-rendered. A reader that drew
|
|
95
|
+
the fallback before the bundle arrived (an object label read through
|
|
96
|
+
`useObjectLabel`, for example) kept drawing the English source label until
|
|
97
|
+
something unrelated re-rendered it, with no error. The console wires
|
|
98
|
+
`loadLanguage` exactly this way, so whether authored translations appeared on
|
|
99
|
+
first load depended on timing.
|
|
100
|
+
|
|
101
|
+
**What changed.**
|
|
102
|
+
|
|
103
|
+
- `useObjectTranslation`, and every hook built on it (`useObjectLabel`,
|
|
104
|
+
`useSafeTranslation`, `useDisplayLocale`), now subscribes each reader to the
|
|
105
|
+
i18next store's `added` event through react-i18next's `bindI18nStore` option,
|
|
106
|
+
passed per call. Every store write hands readers a new `t`, so a reader that
|
|
107
|
+
memoises on `t`, or on a resolver built from it, recomputes too.
|
|
108
|
+
- The two same-value `setLanguage` calls are gone. The one after the app bundle
|
|
109
|
+
also set the context's `language` back to the boot language when the user had
|
|
110
|
+
switched while the bundle was in flight. The context now stays on the language
|
|
111
|
+
i18next is on.
|
|
112
|
+
- The built-in catalogue is written only when the merge adds a key. That effect
|
|
113
|
+
re-runs on every language change, and an unconditional write would re-render
|
|
114
|
+
every reader for nothing.
|
|
115
|
+
- A switch through the context's `changeLanguage` writes its bundles silently,
|
|
116
|
+
because the `languageChanged` that follows re-renders every reader once, in
|
|
117
|
+
the new language.
|
|
118
|
+
|
|
119
|
+
**Behaviour you may notice.**
|
|
120
|
+
|
|
121
|
+
- On first load, a reader re-renders once when the app bundle lands, and once
|
|
122
|
+
more if the built-in catalogue was not resident yet. Work keyed on `t` or on a
|
|
123
|
+
label resolver runs again at that moment. For example, a chart that resolves
|
|
124
|
+
group labels while it fetches will fetch again, so that its labels come from
|
|
125
|
+
the new bundle.
|
|
126
|
+
- A language switch still re-renders each reader once.
|
|
127
|
+
- Host code that calls react-i18next's own `useTranslation` directly, rather
|
|
128
|
+
than `useObjectTranslation`, is not subscribed. It keeps react-i18next's
|
|
129
|
+
default, which re-renders on `languageChanged` only.
|
|
130
|
+
|
|
131
|
+
No export is added or removed. Marked `minor` rather than `patch` because the
|
|
132
|
+
first-load behaviour is observable.
|
|
133
|
+
- ea9d17f: A currency field in `dynamic` mode shows the tenant's currency, not its
|
|
134
|
+
`currencyConfig.defaultCurrency` (objectui#10422).
|
|
135
|
+
|
|
136
|
+
`@objectstack/spec` declares two currency modes: `fixed` (a single currency) and
|
|
137
|
+
`dynamic` (user selectable). Its field guidance says a field without a fixed
|
|
138
|
+
currency uses the tenant default at runtime. `resolveFieldCurrency` read
|
|
139
|
+
`currencyConfig.defaultCurrency` in either mode, so in a USD tenant a field
|
|
140
|
+
declaring `currencyConfig: { currencyMode: 'dynamic', defaultCurrency: 'EUR' }`
|
|
141
|
+
showed `€3,456` where it should show `$3,456`. An empty `currencyConfig`, which
|
|
142
|
+
the spec parses to a dynamic config with `defaultCurrency: 'CNY'`, showed
|
|
143
|
+
`CN¥`.
|
|
144
|
+
|
|
145
|
+
The resolver now reads `currencyConfig.defaultCurrency` only when
|
|
146
|
+
`currencyMode` is `'fixed'`. Its precedence is: the explicit `currency`, then a
|
|
147
|
+
fixed `currencyConfig.defaultCurrency`, then the legacy top-level
|
|
148
|
+
`defaultCurrency`, then the tenant default. With none of these, it returns
|
|
149
|
+
`undefined` and the amount shows as a plain number.
|
|
150
|
+
|
|
151
|
+
**Behaviour change.** Every face that resolves a currency through
|
|
152
|
+
`resolveFieldCurrency` changes together: the list cell, the field widget, the
|
|
153
|
+
metric tile, the detail summary chip, the gantt tooltip, and the grid's
|
|
154
|
+
configured cells and summary footer. On each of them, a field in `dynamic` mode
|
|
155
|
+
now shows the tenant currency. So does a `currencyConfig` that names no
|
|
156
|
+
`currencyMode`, because the spec's default mode is `dynamic`. To keep a field on
|
|
157
|
+
one currency, declare `currencyMode: 'fixed'`. A fixed field renders as before.
|
|
158
|
+
|
|
159
|
+
The parameter type of `resolveFieldCurrency` gains an optional
|
|
160
|
+
`currencyConfig.currencyMode` member (`'dynamic' | 'fixed'`), which is the
|
|
161
|
+
spec's enum.
|
|
162
|
+
- 4357a27: Show the environment admin a storage-capacity banner from the tenant runtime's own
|
|
163
|
+
storage verdict (objectui#10439, the objectui half of cloud#2135).
|
|
164
|
+
|
|
165
|
+
`GET /api/v1/usage/storage` already served the verdict the upload and bulk-import
|
|
166
|
+
guardrail refuses with (`warn` from 80%, `blocked` at 100%) and the two figures
|
|
167
|
+
`usedMb` / `limitMb`. Nothing in the console rendered them, so an environment could
|
|
168
|
+
fill up with no warning and learn it was full only when an upload failed.
|
|
169
|
+
|
|
170
|
+
- **`warn`**: a banner with the used and limit figures, for example "850 MB of
|
|
171
|
+
1,024 MB used".
|
|
172
|
+
- **`blocked`**: the banner says storage is full and uploads and imports are paused,
|
|
173
|
+
that existing data is untouched, and links to the control plane to upgrade. The link
|
|
174
|
+
is left out on a runtime that names no upstream cloud.
|
|
175
|
+
- **Anything else** (`ok`, `unknown`, `unlimited`, an endpoint that cannot be read or
|
|
176
|
+
answers off-contract) renders nothing.
|
|
177
|
+
|
|
178
|
+
The banner reads the served verdict. It never compares the figures with each other or
|
|
179
|
+
with a threshold, so it shows what the guardrail enforces. It is mounted in
|
|
180
|
+
`ConsoleShell` beside the read-rate report and has the same audience: only a workspace
|
|
181
|
+
admin sees it, and only a workspace admin's session issues the request. The two
|
|
182
|
+
banners share one request to the endpoint. Neither banner is exported from the package
|
|
183
|
+
entry.
|
|
184
|
+
|
|
185
|
+
`@object-ui/i18n` gains the five `console.storageUsage.*` keys in all ten locale packs.
|
|
186
|
+
- 7343376: fix(types,plugin-designer)!: the Studio app wizard saves a document the platform accepts, and an edit keeps the stored `accentColor` (objectui#10867)
|
|
187
|
+
|
|
188
|
+
⚠️ **BREAKING (authoring)**, marked `minor` under this repository's version-alignment rule (a `major` in the fixed group would move all of it off the `@objectstack` major). Two published `@object-ui/types` members narrow: a navigation separator no longer takes a `label`, and `AppWizardDraft.layout` is removed. A TypeScript literal that writes either no longer compiles. `NavigationItem` is now a union type alias, not an interface. So code that reads `label` off an unnarrowed `NavigationItem` into a `string` slot (its type is now `string | undefined`; objectui#11299, in the same release, widens it to the spec's `I18nLabel` or `undefined`, see `.changeset/11299-types-nav-label-locale-map.md`) also stops compiling, as does code that spreads an entry-only key onto one, and an `interface` that `extends NavigationItem` or augments it (extend `NavigationEntryItem` instead). `objectui validate` now refuses a separator `label` too: the zod mirror's `NavigationItemSchema` refuses every key on a separator that the spec's separator does not declare. The four `appDesigner` layout keys also leave the published `@object-ui/i18n` packs and `DESIGNER_DEFAULT_TRANSLATIONS`, so an application that calls `t()` with one of them now renders the raw key (unless the call passes a `defaultValue`).
|
|
189
|
+
|
|
190
|
+
**Clause-②: yes (narrowing)** — the separator arm of `NavigationItem` loses `label`, and `AppWizardDraft` loses `layout`.
|
|
191
|
+
|
|
192
|
+
- **A separator carries only `type`, `id` and `order`.** `@objectstack/spec`'s separator branch declares exactly those keys, and its `AppSchema` refuses anything else. `NavigationItem` required a `label` on every item, so the wizard's "Add separator" wrote `{ id, type: 'separator', label: '' }`, and the console's create-app and edit-app saves were refused with `422 INVALID_METADATA` (`unrecognized_keys` `['label']` at `navigation.N`). `NavigationItem` is now a union of two arms, discriminated by `type`. `NavigationEntryItem` holds every other nav type and keeps its required `label`. `NavigationSeparatorItem` admits `type`, `id` and `order`, and every other entry key is `?: never` on it. Both arms are exported. Reading an entry-only key off an unnarrowed item still compiles, but its type now includes `undefined` (a `label` is `string | undefined`; objectui#11299, in the same release, makes it the spec's `I18nLabel` or `undefined`, see `.changeset/11299-types-nav-label-locale-map.md`), so passing it where a `string` is required does not. Narrow on `item.type === 'separator'` before relying on `label`. `menuItemToNavigationItem` maps a legacy separator to `{ id, type: 'separator' }` and drops its label. `spec-derived-unions.test.ts` no longer pins the separator `label` as a blocker. It asserts, at both spec tiers, that the separator arm admits the spec separator's keys and no others.
|
|
193
|
+
- **`@object-ui/plugin-designer`: the wizard and `NavigationDesigner` write a separator as `{ id, type }`.** `NavigationDesigner` no longer writes a `label` onto a new separator, and its label, icon and visibility patchers skip one.
|
|
194
|
+
- **`@object-ui/layout` narrows on the separator arm; nothing it renders changes.** `resolveNavItemLabel` answers `''` for every separator. A stored separator carrying a non-empty `label`, which `menuItemToNavigationItem` produced before this change, used to resolve to that label. No renderer asks it for a separator's label. The mobile bottom nav's leaf list, which already skipped separators, is now typed as entries.
|
|
195
|
+
- **`@object-ui/app-shell` narrows the same way; nothing it renders changes.** `useNavPins` registers a favorite only for an entry and leaves a separator as it is, and the Studio sidebar's navigation walk passes a separator through unchanged.
|
|
196
|
+
- **`@object-ui/plugin-designer`: `EditAppPage` keeps the stored branding.** The wizard maintains the logo, primary colour and favicon, and its `branding` replaced the stored block, so a stored `accentColor` was dropped on every edit. The console reads that key. The save now keeps every stored `branding` key the spec's `AppBrandingSchema` declares, read from that schema. The wizard's values win for the keys it maintains. A stored key the spec does not declare is still left out.
|
|
197
|
+
- **The wizard's Layout control is removed, with `AppWizardDraft.layout`.** The spec declares no app `layout`, no console surface reads one, and since objectui#10842 the save wrote none. The control persisted nothing. `EditAppPage` no longer reads a stored `layout` into the draft. The Basic Info step's description now reads "Name, title, and icon".
|
|
198
|
+
- **`@object-ui/i18n`:** the four `appDesigner` layout keys (`layout`, `layoutSidebar`, `layoutHeader` and `layoutEmpty`) are removed from all ten packs, and `appDesigner.stepBasicDesc` no longer names a layout.
|
|
199
|
+
|
|
200
|
+
**Migration:** write a separator as `{ id, type: 'separator' }`, with an optional `order`. Remove `layout` from any `AppWizardDraft` you build. Code that spreads an entry-only key (`label`, `visible`, `requiredPermissions` and the rest) onto a value typed `NavigationItem` narrows it first (`item.type !== 'separator'`) or types it `NavigationEntryItem`: the separator arm refuses those keys. `AppComponentSchema.layout`, the renderer node's own layout strategy, is a different member and is unchanged.
|
|
201
|
+
|
|
202
|
+
Pinned in `packages/types/src/__tests__/app-wizard-separator-layout-10867.test.ts` and `packages/plugin-designer/src/__tests__/AppWizard.specDocument-10867.test.tsx`.
|
|
203
|
+
- 244d516: A grouped grid over a data source that declares no `queryGroupHeaders` now
|
|
204
|
+
**refuses grouping** instead of grouping a page of rows (objectui#10881,
|
|
205
|
+
maintainer ruling F). Grouping is a property of the query (ruling A on
|
|
206
|
+
objectui#7189): a data source that cannot answer the group header query cannot
|
|
207
|
+
group, so the grid names the member it is missing rather than drawing counts it
|
|
208
|
+
cannot know.
|
|
209
|
+
|
|
210
|
+
- **`@object-ui/plugin-grid`** — a grouped `object-grid` that fetches its own
|
|
211
|
+
rows over such a source renders an error panel naming `queryGroupHeaders`
|
|
212
|
+
and issues no row query. It used to fetch one window and group it in the
|
|
213
|
+
browser, so every group count was a page slice and a group whose rows all
|
|
214
|
+
fell past the window was missing. Rows handed in WHOLE — a
|
|
215
|
+
`data: { provider: 'value', items }` block, or a `data` prop from a parent
|
|
216
|
+
view that declares no larger `rowCount` — are still grouped in the browser,
|
|
217
|
+
exactly. A host that hands rows in while declaring them one page of more
|
|
218
|
+
(the external-pagination props: `manualPagination`, `onPageChange` and a
|
|
219
|
+
`rowCount` above the rows it handed) is now refused as well, with a sentence
|
|
220
|
+
of its own: grouping needs every record. It used to have that page grouped as
|
|
221
|
+
if it were whole.
|
|
222
|
+
- **`@object-ui/plugin-list`** — `ListView` makes the same refusal, with the
|
|
223
|
+
same sentence, in place of a grouped grid over such a source, and fetches no
|
|
224
|
+
window for it. It used to fetch one window and hand it to the grid as
|
|
225
|
+
`data`, which the grid grouped as if the rows were whole. Rows handed to
|
|
226
|
+
`ListView` in whole are still handed to the grid.
|
|
227
|
+
- **`@object-ui/i18n`** — two new keys, `grid.grouping.needsHeaderQuery` and
|
|
228
|
+
`grid.grouping.needsWholeRows`, in all ten locale packs.
|
|
229
|
+
|
|
230
|
+
The `Partial` marker described by the pending
|
|
231
|
+
`7189-grouped-grid-partial-disclosure` entry is retired everywhere before any
|
|
232
|
+
release carried it: its three `grid.grouping.partial*` strings, `GroupRow`'s
|
|
233
|
+
`partialLabel` / `partialTitle` props and the notice above the group list are
|
|
234
|
+
gone.
|
|
235
|
+
|
|
236
|
+
**Breaking semantics (declared `minor` per this repo's version policy):** a
|
|
237
|
+
grouped grid that fetches its own rows over a data source without
|
|
238
|
+
`queryGroupHeaders` — `ApiDataSource`, `ValueDataSource`, or a host adapter —
|
|
239
|
+
grouped the page it fetched in the last release, and now renders an error panel
|
|
240
|
+
instead. So does a grouped grid view in `ListView` over such a source. So does
|
|
241
|
+
a grouped grid handed rows by a host that declares them one page of more
|
|
242
|
+
(`manualPagination`, `onPageChange` and a `rowCount` above the rows handed),
|
|
243
|
+
whatever its data source. To keep grouping, either:
|
|
244
|
+
|
|
245
|
+
- implement `queryGroupHeaders` on the data source (the ObjectStack adapter
|
|
246
|
+
does), and let the grid fetch its own rows, or
|
|
247
|
+
- hand the rows in whole (`data: { provider: 'value', items }`, or a `data`
|
|
248
|
+
prop with no `rowCount` above it).
|
|
249
|
+
|
|
250
|
+
Not changed here: while a toolbar search is active, `ListView` over a data
|
|
251
|
+
source that answers the header query still hands a grouped grid its window,
|
|
252
|
+
whose group counts are the window's, because the header query carries no
|
|
253
|
+
search (objectstack#20358).
|
|
254
|
+
- d0fba91: A region-tagged language code reaches the built-in catalogue of its base language (objectui#11326)
|
|
255
|
+
|
|
256
|
+
The built-in catalogues are keyed by base language (`zh`), while the platform
|
|
257
|
+
answers, the Console's language menu offers, and the provider stores full
|
|
258
|
+
BCP-47 tags (`zh-CN`). `isBuiltInLanguage` and `loadBuiltInLocale` looked a code
|
|
259
|
+
up exactly, so `loadBuiltInLocale('zh-CN')` resolved `null`. A user who picked
|
|
260
|
+
中文(中国) got every built-in string in English on each full page load, with
|
|
261
|
+
only the `en` catalogue fetched, while server-translated labels stayed Chinese.
|
|
262
|
+
|
|
263
|
+
**What changed at the contract.** Three exported registry functions now accept a
|
|
264
|
+
region-tagged code they used to refuse. A code is looked up exact tag first,
|
|
265
|
+
then its base language, through one normaliser. That is the order the cached
|
|
266
|
+
locale seed already used, and the seed now reuses the same function.
|
|
267
|
+
|
|
268
|
+
- `loadBuiltInLocale('zh-CN')` resolves the `zh` catalogue. It is fetched and
|
|
269
|
+
memoised under `zh`, so `zh` and `zh-CN` share one fetch, and
|
|
270
|
+
`getLoadedBuiltInLocales()` stays keyed by catalogue code.
|
|
271
|
+
- `isBuiltInLanguage('zh-CN')` is `true`, and `isBuiltInLocaleLoaded('zh-CN')`
|
|
272
|
+
answers for the `zh` catalogue.
|
|
273
|
+
- A code with no catalogue for the tag or for its base (`xx-YY`, `tlh`) is
|
|
274
|
+
refused exactly as before.
|
|
275
|
+
- A catalogue keyed by a full tag would win over its base language. None ships
|
|
276
|
+
today.
|
|
277
|
+
|
|
278
|
+
**What a host observes.**
|
|
279
|
+
|
|
280
|
+
- A stored `zh-CN` is kept, and kept as written. `I18nProvider` boots in
|
|
281
|
+
`zh-CN` and renders from the `zh` catalogue, and its `language`, the
|
|
282
|
+
document's `lang` attribute and the stored value all stay `zh-CN`. A host with no
|
|
283
|
+
`loadLanguage` loader used to purge a stored `zh-CN` on boot. It no longer
|
|
284
|
+
does.
|
|
285
|
+
- `preloadBootstrapLocale()` fetches the `zh` catalogue for a stored `zh-CN`, so
|
|
286
|
+
the first paint is already Chinese.
|
|
287
|
+
- `changeLanguage('zh-CN')` fetches the `zh` catalogue before it switches.
|
|
288
|
+
- A cached locale seed of `zh-CN` now boots in `zh-CN` instead of being
|
|
289
|
+
rewritten to `zh`, because the `zh` catalogue serves the exact tag. The
|
|
290
|
+
strings are the same. The provider's `language` and the document's `lang`
|
|
291
|
+
attribute read `zh-CN`, the tag the signed-in user's `sys_user.locale` applies a moment
|
|
292
|
+
later.
|
|
293
|
+
- c476be0: The action success toast is composed from the action's `outcomeMessages`, then its `successMessage`, then the runner's default text. A `message` in the server's answer is no longer shown (objectui#11344).
|
|
294
|
+
|
|
295
|
+
`@objectstack/spec` 17.6.0 declares `ActionSchema.outcomeMessages`, the success copy for each handler outcome. Its keys are the snake_case `outcome` values that a `type: 'api'` or `type: 'script'` handler returns in its success payload. This follows ruling A on objectstack-ai/cloud#2315: the server returns facts, and the console writes the sentence in the user's locale.
|
|
296
|
+
|
|
297
|
+
- `@object-ui/core`: the `ActionRunner` success toast now picks its text in this order:
|
|
298
|
+
1. the `outcomeMessages` entry named by the top-level `outcome` of the handler's answer;
|
|
299
|
+
2. `successMessage`;
|
|
300
|
+
3. the runner's default text, in the language of the translator set with `setTranslator`.
|
|
301
|
+
|
|
302
|
+
Rungs 1 and 2 fill in `${result.*}` tokens from the handler's answer. That is the same scope `onSuccess.navigate` reads, and the values go in as text, not percent-encoded. **Behaviour change:** `result.data.message` used to take precedence over both. The runner no longer reads it at all. To show the server's sentence, write `${result.message}` as the copy. `ActionDef` now declares `outcomeMessages`.
|
|
303
|
+
- `@object-ui/react`: `useActionTextLocalizer` resolves each `outcomeMessages` entry that a named action declares. It looks in `_actions.NAME.outcomeMessages.OUTCOME`, then `globalActions.NAME.outcomeMessages.OUTCOME`, then falls back to the authored text. On a named action, an inline `I18nLabel` map on an entry, or on `successMessage`, is collapsed to the active language first. `${result.*}` tokens pass through unchanged, and the runner fills them in after the action runs.
|
|
304
|
+
- `@object-ui/i18n`: `useObjectLabel()` adds `actionOutcome(objectName, actionName, outcome, fallback)`, the resolver for that address.
|
|
305
|
+
- `@object-ui/components`: `action:button`, `action:icon`, `action:group` and `action:menu` now forward `outcomeMessages` to the runner. Before, a registered action rendered through `action:bar` lost the map one hop before the toast. The `action:button` and `action:icon` registrations do not publish it as an input, because their own spec rows do not declare it at 17.6.0.
|
|
306
|
+
- `@object-ui/types`: `UIActionSchema` declares `outcomeMessages`, derived from the spec.
|
|
307
|
+
|
|
308
|
+
`@object-ui/core` and `@object-ui/types` raise their `@objectstack/spec` floor from `^17.5.0` to `^17.6.0`, because their published types now read `ActionSchema.outcomeMessages`, a member the spec first declares in 17.6.0.
|
|
309
|
+
- 64dae8e: Six user-visible fixes across the maker surface, the assistant rail and the
|
|
310
|
+
dataset captions.
|
|
311
|
+
|
|
312
|
+
**The maker's start chips now promise only what ADR-0112 v1 builds
|
|
313
|
+
(cloud#1984).** Two of the five asked for automation the first version has no
|
|
314
|
+
flows or actions for — the ticket chip said 「状态流转」, the inventory chip said
|
|
315
|
+
「低库存预警」 — and the measured behaviour was not a refusal but a silent
|
|
316
|
+
degrade: a status kanban and a low-stock view. The chip promised an alert and
|
|
317
|
+
delivered a page. All five are reworded in all ten packs (and in the call-site
|
|
318
|
+
`defaultValue` fallbacks, which are a second copy of the same strings) to ask
|
|
319
|
+
for objects, fields, views, pages, dashboards and sample data, keeping each a
|
|
320
|
+
real business scenario — the ticket chip now asks for a status field and a board
|
|
321
|
+
grouped by it, the inventory chip for a view that filters below the reorder
|
|
322
|
+
point. A note beside the keys says to revert when v2 re-adds flows.
|
|
323
|
+
|
|
324
|
+
**Five newer AI tools get their step labels (objectui#7481).** A zh conversation
|
|
325
|
+
read `✓ Get authoring rules 已完成` between 「读取元数据结构」 and 「列出对象」:
|
|
326
|
+
`get_authoring_rules` (cloud#1837), plus `load_tools`, `open_record`,
|
|
327
|
+
`test_flow` and `toggle_flow`, are registered by the cloud AI runtime but are
|
|
328
|
+
newer than the pinned spec's tool registry, so they had no `chatbot.tool.*`
|
|
329
|
+
entry in any pack and fell through to the English title-caser.
|
|
330
|
+
|
|
331
|
+
**The assistant rail follows the thread when you send (objectui#7480).** The
|
|
332
|
+
rail and the full-page maker are the same component; what differs is width. A
|
|
333
|
+
reply that still ends on screen in the wide column runs two or three times
|
|
334
|
+
taller in a ~360px rail, so `StickToBottom`'s lock is escaped by the time the
|
|
335
|
+
user types and the new bubble, the tool steps and the streaming answer all land
|
|
336
|
+
below the fold. Every send path now re-arms the lock — including the plan-card
|
|
337
|
+
"Build it" and 确认修改 approvals, whose own code comments already named this
|
|
338
|
+
miss. Message APPENDS deliberately do not, so a user reading back through the
|
|
339
|
+
thread mid-answer is never yanked to the bottom.
|
|
340
|
+
|
|
341
|
+
**Console toasts move off the assistant composer (objectui#7482).** 「客户更新
|
|
342
|
+
成功」 sat on the ChatDock composer's send button and stayed there. One defect,
|
|
343
|
+
two symptoms: `apps/console` pinned the toaster to `bottom-right` — an override
|
|
344
|
+
that predates ADR-0057 P3a — so a toast both covered the button and, because
|
|
345
|
+
sonner pauses a toast's dismiss timer while the pointer is inside the toaster
|
|
346
|
+
region, never got to run its 4s timer with a pointer resting on the composer
|
|
347
|
+
underneath. The override is gone; the console takes `ConsoleToaster`'s own
|
|
348
|
+
documented top-right anchor, and the 4s success duration is now pinned.
|
|
349
|
+
|
|
350
|
+
**Built-in aggregate captions follow the locale everywhere (objectui#7534).**
|
|
351
|
+
objectui#7258 taught `buildChartSeries()` to resolve a server-minted default
|
|
352
|
+
measure through the locale map, so a chart legend read `计数` while the table
|
|
353
|
+
beneath it, the KPI caption, the pivot header and the dataset preview still
|
|
354
|
+
printed the server's hard-coded English `Count`. `buildDatasetFieldHelpers()`
|
|
355
|
+
takes the same optional `builtinAggregateLabels`, resolving through the one
|
|
356
|
+
`resolveMeasureLabel` order, and the five call sites pass it. Omitting the
|
|
357
|
+
argument reproduces the previous output byte for byte, and an author-declared
|
|
358
|
+
measure still keeps its own label verbatim (objectui#4106).
|
|
359
|
+
|
|
360
|
+
**The activity feed stops asking for an object the environment does not have
|
|
361
|
+
(objectui#7476).** A tenant environment has no `sys_activity`, so every page
|
|
362
|
+
load issued a request that 404'd. Everything downstream was already correct —
|
|
363
|
+
the adapter memoizes the missing collection, its logger demotes the failure, the
|
|
364
|
+
feed retires as an ANSWER and the panel renders its earned empty state — so what
|
|
365
|
+
is left is the request itself, and `data-objectstack` states the rule for it:
|
|
366
|
+
the cure for a doomed request is not issuing it. New `useObjectPresence` reads
|
|
367
|
+
the object registry the shell loads for the nav anyway; only a registry that has
|
|
368
|
+
ANSWERED and lists other objects without this one skips the read. Every
|
|
369
|
+
uncertainty — no provider, empty registry, still loading, errored — reads as
|
|
370
|
+
before, because a wrong skip would cost a real deployment its feed.
|
|
371
|
+
- d3995fe: Retire 25 confirmed-dead locale keys from all ten packs — 250 translated strings
|
|
372
|
+
with no reader anywhere in the repo (objectui#4730's key-level trim round;
|
|
373
|
+
`calendar.agenda` included).
|
|
374
|
+
|
|
375
|
+
Every key was confirmed individually, not swept from a tool's output. The
|
|
376
|
+
inventory comes from `scripts/check-i18n-dead-keys.mjs`, which stays report-only
|
|
377
|
+
by design, and each candidate then had to clear the objectui#4658 evidence
|
|
378
|
+
standard on its own: zero `t()` call sites, zero textual footprint anywhere
|
|
379
|
+
outside the packs, and a read of its plausible consumer confirming no i18n
|
|
380
|
+
wiring reaches it. Five namespaces held nothing but retired leaves and went with
|
|
381
|
+
them — `map`, `cellRender`, `rowAction`, `recordDetail`, and `home.stats`.
|
|
382
|
+
|
|
383
|
+
The retirements fall into three shapes:
|
|
384
|
+
|
|
385
|
+
- **Superseded twin vocabularies.** `cellRender.*` and `rowAction.*` duplicated
|
|
386
|
+
a `grid.*` vocabulary that won. `RowActionMenu.tsx` is fully i18n-wired and
|
|
387
|
+
reads `grid.openMenu` / `grid.edit` / `grid.delete`; `ObjectGrid.tsx` reads
|
|
388
|
+
`grid.empty` / `grid.yes` / `grid.no` / `grid.systemFields`. The twins had no
|
|
389
|
+
reader on either side.
|
|
390
|
+
- **Labels that outlived their control.** `calendar.agenda` labelled a view mode
|
|
391
|
+
`b55a34647` retired from `CalendarViewMode` (now `'month' | 'week' | 'day'`).
|
|
392
|
+
`home.quickActions.createApp*`, `layout.systemNav.createApp`,
|
|
393
|
+
`actionDialog.defaultActionTitle` / `.ok` and `grid.bulk.selectPlaceholder`
|
|
394
|
+
sit in namespaces whose consumers are live and wired but demonstrably read
|
|
395
|
+
other siblings.
|
|
396
|
+
- **Surfaces that left the product.** `map.*` is the strongest form:
|
|
397
|
+
`@object-ui/plugin-map` declares no `@object-ui/i18n` dependency and contains
|
|
398
|
+
no `t()` call at all, so it cannot consume a locale string. `home.stats.*` and
|
|
399
|
+
`recordDetail.viewersTooltip` name surfaces nothing renders.
|
|
400
|
+
|
|
401
|
+
`packages/i18n/src/__tests__/dead-key-batch-retired-4730.test.ts` pins the
|
|
402
|
+
retirement, following the convention of the five retirement pins already in that
|
|
403
|
+
directory. It is load-bearing rather than decorative: every i18n gate in this
|
|
404
|
+
repo runs call site to key, so a dead key coming **back** into the packs is
|
|
405
|
+
invisible to all of them, and this pin is the only thing watching that direction.
|
|
406
|
+
|
|
407
|
+
**Deliberately NOT deleted, and pinned as live.** Seven `console.*` bootstrap
|
|
408
|
+
strings that this same sweep reported CONFIRMED-dead are in fact **live**, and
|
|
409
|
+
were pulled back out of the batch. `LoadingScreen.tsx` is bootstrap-critical UI
|
|
410
|
+
that must render before i18n loads — precisely when the server is unreachable —
|
|
411
|
+
so it deliberately does not call `t()`. It imports the packs directly and reads
|
|
412
|
+
them as plain object properties (`strings.loadingSteps.connecting`). That
|
|
413
|
+
consumer is invisible to both legs of the sweep: there is no call for the AST
|
|
414
|
+
pass to classify, and the full dotted key is never spelled in source because the
|
|
415
|
+
namespace segment is bound to a local variable. The new pin asserts those keys
|
|
416
|
+
stay, so the next sweep round cannot repeat the mistake.
|
|
417
|
+
- 06b82b8: An action param that declares the spec's `carryOver` is shown read-only and submitted verbatim (objectui#6246)
|
|
418
|
+
|
|
419
|
+
`@objectstack/spec`'s `ActionParamSchema.carryOver` says a param's value is
|
|
420
|
+
carried through the action dialog rather than collected: seeded from the row
|
|
421
|
+
(`defaultFromRow: true` is required beside it), shown as a non-editable summary,
|
|
422
|
+
and submitted unchanged. The console dropped the key: `resolveActionParam()`
|
|
423
|
+
builds its output key by key and never copied it, so the dialog rendered every
|
|
424
|
+
such param as an ordinary editable field. The permission-set Clone action
|
|
425
|
+
declares it on its five JSON permission facets, so each of them — row-level
|
|
426
|
+
security among them — was a prefilled JSON textarea, and a hand edit that was
|
|
427
|
+
still valid JSON produced a clone granting more than its base.
|
|
428
|
+
|
|
429
|
+
- `@object-ui/core`: `ActionParamDef` gains `carryOver?: boolean`.
|
|
430
|
+
- `@object-ui/app-shell`: the resolver copies `carryOver` on all three of its
|
|
431
|
+
branches; `ActionParamDialog` renders a declared carry-over as a collapsed
|
|
432
|
+
read-only summary and builds no field widget for it at all; and
|
|
433
|
+
`serializeParamValues` leaves a carry-over value untouched, even on an upload
|
|
434
|
+
field. The action designer's dialog preview draws a carry-over param as the
|
|
435
|
+
same read-only line instead of its field's input.
|
|
436
|
+
- `@object-ui/i18n`: one new key, `actionDialog.carryOverHint`, in all ten packs.
|
|
437
|
+
|
|
438
|
+
Params that do not declare the key render and submit exactly as before.
|
|
439
|
+
- b362c1b: Setup gains a **Packaged automation** page — the operational surface for the flows an
|
|
440
|
+
installed package ships (ADR-0126 §7.4, objectui#6301). Reached the way every other
|
|
441
|
+
framework-contributed Setup surface is: the page registers the component-registry ref
|
|
442
|
+
`automation:packaged`, so app navigation names the ref and `ComponentNavView` resolves it
|
|
443
|
+
at `/apps/<app>/component/automation/packaged`. No bespoke route is added — a second way
|
|
444
|
+
in would be a URL the app metadata does not know about.
|
|
445
|
+
|
|
446
|
+
Per packaged flow the page does exactly two things:
|
|
447
|
+
|
|
448
|
+
- **on/off for this scope** — reads the activation state the engine reports
|
|
449
|
+
(`GET /api/v1/automation/_status`, backed by the ADR-0126 §7.2 `sys_metadata_activation`
|
|
450
|
+
ledger) and flips it through `POST /api/v1/automation/<name>/toggle`;
|
|
451
|
+
- **clone** — `POST /api/v1/automation/<name>/clone` with a mandatory new machine name and
|
|
452
|
+
label (§7.1). The carried-over definition is never offered as editable form fields; the
|
|
453
|
+
copy is edited in Studio like any other flow.
|
|
454
|
+
|
|
455
|
+
Authoring stays in Studio. The list is scoped to packaged flows by the server's own
|
|
456
|
+
three-clause provenance test (`isCodeArtifactBody`, ADR-0029 D9.6) rather than the
|
|
457
|
+
`_packageId`-only shortcut, which classifies a tenant overlay bound to a package as
|
|
458
|
+
packaged — the cloud#970 misread, and here it would put a tenant's own flow behind an
|
|
459
|
+
install-wide switch.
|
|
460
|
+
|
|
461
|
+
**Server refusals reach the operator verbatim** — no client-side softening or rewording.
|
|
462
|
+
Three shapes are relayed as sent: the §5 posture gate (403 `PERMISSION_DENIED`, whose
|
|
463
|
+
message names the tenancy posture *and* the sanctioned clone path), the §7.3 subflow guard
|
|
464
|
+
(409 `DELETE_RESTRICTED`, which names the packaged callers that would break mid-run — a
|
|
465
|
+
list nothing on the client could reconstruct), and the §7.1 clone name conflict (409).
|
|
466
|
+
|
|
467
|
+
⛔ **No drift or ancestry surface** (§9): no diff-vs-base, no "customized" badge, no
|
|
468
|
+
base-moved notice, no link from a clone back to its source. Cloned-without-disabled and
|
|
469
|
+
disabled-without-clone are ordinary states, shown plainly. Tests pin the absence, including
|
|
470
|
+
the case where a response carries a `clonedFrom` key anyway — the platform does not track
|
|
471
|
+
that lineage, so a page that displayed it would be displaying something it invented.
|
|
472
|
+
|
|
473
|
+
`@object-ui/i18n` gains the `packagedAutomation` key group — 24 keys in `en` and real
|
|
474
|
+
translations in all nine other packs, matching each pack's existing vocabulary for "flow"
|
|
475
|
+
and "package" (zh 流程/软件包, ja フロー/パッケージ, ko 플로우/패키지, de Flow/Paket,
|
|
476
|
+
fr Flux/Package, es Flujo/Paquete, pt Fluxo/Pacote, ru Поток/Пакет, ar التدفق/الحزمة) and
|
|
477
|
+
each one's quotation convention. The group deliberately carries no drift or ancestry
|
|
478
|
+
wording, and no server refusal text: those arrive as server-authored prose and are
|
|
479
|
+
rendered verbatim.
|
|
480
|
+
- 9602dc8: Setup › **Packaged automation** gains a packaged **actions** section beside its flows one —
|
|
481
|
+
the surface half of ADR-0126 §8 item 2 (objectui#6412; the engine, ledger and both dispatch
|
|
482
|
+
doors landed in objectstack#12348). The maintainer's pull, verbatim and untranslated:
|
|
483
|
+
「动作 可能是需要开关的,因为有的 action 我不想启用。」
|
|
484
|
+
|
|
485
|
+
Per packaged action the section does exactly **one** thing: **on/off for this scope**. That
|
|
486
|
+
is all the `sys_metadata_activation` ledger knows about an action, and the section claims
|
|
487
|
+
nothing more.
|
|
488
|
+
|
|
489
|
+
- **State** comes from the ledger's `metadata_type: 'action'` rows, read through the generic
|
|
490
|
+
data API list the object itself sanctions for exactly this
|
|
491
|
+
(`apiMethods: ['get', 'list']` — *"Reads stay open so operability surfaces can answer
|
|
492
|
+
'what is disabled here?'"*). **Absence of a row means active**, so a stock boot shows
|
|
493
|
+
everything armed.
|
|
494
|
+
- **Discovery** mirrors the runtime's own `collectActionDeclarations`: object-embedded
|
|
495
|
+
`actions[]` from `GET /meta/object` **plus** standalone items from `GET /meta/action`, with
|
|
496
|
+
the object-embedded declaration winning a `<object>:<action>` key clash. Listing only the
|
|
497
|
+
first source would leave an administrator with no off-switch for a packaged standalone
|
|
498
|
+
action.
|
|
499
|
+
- **Flips** invoke the L6 write door `POST /actions/_activation/:object/:action` with exactly
|
|
500
|
+
the one key its body declares (`{ enabled }`); `global` is the object segment for an
|
|
501
|
+
object-less action, the same spelling both dispatch doors take.
|
|
502
|
+
|
|
503
|
+
**⛔ No clone for actions.** The flows section keeps its own (§7.1); amendment ruling 3
|
|
504
|
+
charters the *switch* for actions and nothing else, and §8 keeps the clone half pre-chartered
|
|
505
|
+
until real pull appears. A clone control here would advertise machinery that does not exist —
|
|
506
|
+
which is also why the server's §5 refusal for actions recommends the platform operator and an
|
|
507
|
+
ordinary sibling action where the flow refusal recommends a clone.
|
|
508
|
+
|
|
509
|
+
**⛔ No drift or ancestry surface** (§9): no "customized" badge, no diff-vs-base, no
|
|
510
|
+
base-moved notice. The platform tracks no such lineage, so a surface showing it would be
|
|
511
|
+
showing something it had to invent. The absence is pinned against a response that smuggles
|
|
512
|
+
`clonedFrom` / `baseVersion` in, so it is enforced at the renderer and not merely by the wire.
|
|
513
|
+
|
|
514
|
+
Server refusals reach the operator **verbatim** — no client-side softening, no retry loop.
|
|
515
|
+
Three shapes are reachable in tests, each transcribed character-for-character from the
|
|
516
|
+
runtime's own message builders: the §5 posture gate (403 `PERMISSION_DENIED`, naming the
|
|
517
|
+
posture *and* the sanctioned path), the ambiguous-name refusal (409 `RESOURCE_CONFLICT`,
|
|
518
|
+
naming the objects a machine name collides across — a list nothing on the client could
|
|
519
|
+
reconstruct) and the no-ledger outage (503 `SERVICE_UNAVAILABLE`).
|
|
520
|
+
|
|
521
|
+
One further refusal is the section's own and it points the same way: a `hasMore` on the
|
|
522
|
+
ledger read is treated as a **load failure** rather than rendered. A dropped row reads as
|
|
523
|
+
"active", so a partial ledger would show a switched-off action as armed — the one direction
|
|
524
|
+
this section must not fail in.
|
|
525
|
+
|
|
526
|
+
The flows section is unchanged in behaviour. It gains a heading beside the new one, and the
|
|
527
|
+
page subtitle now says "Flows and actions" — that string moved in all ten packs together.
|
|
528
|
+
Nine new `packagedAutomation.*` keys land in `en` **and in all nine other packs** as real
|
|
529
|
+
translations; the two toggle-failure keys are artifact-neutral by wording and are reused
|
|
530
|
+
rather than duplicated.
|
|
531
|
+
- 969ba84: Renderers for the `app:launcher` and `nav:menu` page blocks.
|
|
532
|
+
Phase 1 of the 2026-08-26 maintainer ruling on objectstack#12183 — the two
|
|
533
|
+
`PageComponentType` members that are purely metadata-driven, so nothing had to
|
|
534
|
+
ship before their renderers could. Phase 2 (`global:search` /
|
|
535
|
+
`global:notifications`) landed in `f99932a42` and set the pattern this
|
|
536
|
+
follows.
|
|
537
|
+
|
|
538
|
+
A page that declared either member drew a dashed box. The two symptoms were not
|
|
539
|
+
the same, which is worth recording because it decides what "fixed" looks like
|
|
540
|
+
for each:
|
|
541
|
+
|
|
542
|
+
- `nav:menu` is in `PALETTE_PLACEHOLDER_BLOCKS`, registered eagerly, so it drew
|
|
543
|
+
the literal "Component Placeholder" scaffold in every host.
|
|
544
|
+
- `app:launcher` is only in `PROTOCOL_COMPONENTS`, registered when a host opts
|
|
545
|
+
in via `registerPlaceholders()` — which just `apps/console` does. So it drew
|
|
546
|
+
the scaffold in the console and `SchemaRenderer`'s red OBJUI-001 "Unknown
|
|
547
|
+
component type" panel everywhere else.
|
|
548
|
+
|
|
549
|
+
Neither block adds a data layer — each mounts plumbing that was already live,
|
|
550
|
+
and neither issues a request or touches an adapter:
|
|
551
|
+
|
|
552
|
+
- `app:launcher` reads the metadata app registry (`useMetadata().apps`, which
|
|
553
|
+
`MetadataProvider` fetches eagerly) through the shared `filterActiveApps`
|
|
554
|
+
predicate, and draws it with `HomeAppsStrip` — the console's own launcher
|
|
555
|
+
grid — so an authored launcher and the Home launcher cannot drift into two
|
|
556
|
+
looks for one thing.
|
|
557
|
+
- `nav:menu` reads the active app's navigation tree from that same registry and
|
|
558
|
+
renders it as page content, taking every derived fact from `@object-ui/layout`:
|
|
559
|
+
hrefs from `resolveHref`, labels from `resolveNavItemLabel`, the active row
|
|
560
|
+
from `resolveActiveNavItem`, and the item-level guards (`visible`,
|
|
561
|
+
`requiredPermissions`, `requiresObject` / `requiresService`) in the order
|
|
562
|
+
`NavigationItemRenderer` applies them, wired to the same console providers
|
|
563
|
+
`UnifiedSidebar` wires them to. `action` items dispatch through
|
|
564
|
+
`useNavActionDispatch`, so framework#4509's "renders but dead-clicks" shape is
|
|
565
|
+
not reintroduced.
|
|
566
|
+
|
|
567
|
+
`nav:menu` does not mount `NavigationRenderer` itself: that renders through
|
|
568
|
+
`SidebarMenuButton`, whose `useSidebar()` throws outside the shell's
|
|
569
|
+
`SidebarProvider`, and a page block has to render standalone. `@object-ui/layout`
|
|
570
|
+
therefore exports `resolveNavItemLabel`, which was module-private — an additive
|
|
571
|
+
export with no behaviour change, so the sidebar and an authored menu cannot show
|
|
572
|
+
one nav entry under two names.
|
|
573
|
+
|
|
574
|
+
Both registrations publish **no** `inputs`: `ComponentPropsMap` declares an empty
|
|
575
|
+
shape for each, and both use `skipFallback: true` so neither claims the bare
|
|
576
|
+
`launcher` / `menu` keys. This does not change the Studio page palette —
|
|
577
|
+
`app:launcher` remains recorded there as a shell singleton, which is a palette
|
|
578
|
+
decision independent of whether a declared type renders.
|
|
579
|
+
|
|
580
|
+
Three new strings — the launcher's and the menu's accessible names, and the
|
|
581
|
+
menu's empty state — are declared under `console.nav` in `en.ts` and its nine
|
|
582
|
+
sibling packs. An inline `defaultValue` alone is not a fix: it renders English
|
|
583
|
+
at one call site and leaves the string untranslatable everywhere
|
|
584
|
+
(objectui#3517).
|
|
585
|
+
- 220c18d: Dashboard/analytics widgets get a self-explaining DEFAULT empty state, stated
|
|
586
|
+
once for the surface (objectui#7063).
|
|
587
|
+
|
|
588
|
+
Maintainer ruling 2026-08-31 (hotcrm#1212, following hotcrm#1203): a widget that
|
|
589
|
+
renders a bare row-placeholder on an empty result is the PLATFORM's defect and
|
|
590
|
+
must be fixed uniformly — apps must not compensate widget by widget
|
|
591
|
+
(objectstack#13848). The measured scenario is a fresh flagship-demo install:
|
|
592
|
+
eleven populated tiles and one reading exactly `暂无数据行` mid-page, which reads
|
|
593
|
+
as "the dashboard failed to load" even though the widget, its declaration and
|
|
594
|
+
its (not yet produced) data are all legitimate.
|
|
595
|
+
|
|
596
|
+
- New `WidgetEmptyState` is the seam the three dashboard surfaces now share.
|
|
597
|
+
There was no shared placeholder to fix: `DatasetWidget` wrote
|
|
598
|
+
`dashboard.noRows`, while `ObjectDataTable` and `PivotTable` wrote
|
|
599
|
+
`dashboard.noDataAvailable` — three renders, two strings, no common code.
|
|
600
|
+
- The default now reads as a STATE, not a failure: `role="status"` (the empty
|
|
601
|
+
branches previously carried no role at all, while the failure branches beside
|
|
602
|
+
them are `role="alert"`), muted treatment with an inbox glyph rather than a
|
|
603
|
+
warning triangle, and a title plus an explanation where the placeholder was a
|
|
604
|
+
single terse fragment.
|
|
605
|
+
- It names WHAT is empty with zero authored copy — the widget's data source,
|
|
606
|
+
which is the half the reader cannot already see (the tile's title is rendered
|
|
607
|
+
by the card header directly above). That is `widget.dataset` on the dataset
|
|
608
|
+
path and `schema.objectName` on the object-bound table/pivot; `PivotTable`
|
|
609
|
+
takes it as a new optional `sourceLabel` prop, which `ObjectPivotTable`
|
|
610
|
+
forwards.
|
|
611
|
+
- Copy is platform i18n: `dashboard.empty.title` / `.message` / `.sourceLabel`
|
|
612
|
+
added to `en` and all nine sibling packs. No inline `defaultValue` and no
|
|
613
|
+
interpolation — the source renders as a labelled value, so no separator is
|
|
614
|
+
concatenated in code and every pack spells its own punctuation.
|
|
615
|
+
|
|
616
|
+
No new authoring obligation and no new spec key. Note that the `emptyState`
|
|
617
|
+
override the card assumes for this surface does not exist: `emptyState` is a
|
|
618
|
+
LIST-view contract, and `@objectstack/spec`'s `DashboardWidgetSchema` declares no
|
|
619
|
+
such key — so there is nothing here for an author to override, and adding one
|
|
620
|
+
would be a contract question rather than a rider.
|
|
621
|
+
- 8952395: A grouped grid now says, where the group counts are, that it grouped a **page**
|
|
622
|
+
(objectui#7189).
|
|
623
|
+
|
|
624
|
+
`useGroupedData` buckets the rows the browser already holds and computes every
|
|
625
|
+
per-group aggregate from that same array, so both the set of groups and every
|
|
626
|
+
number in a group header are properties of the fetched page, not of the query.
|
|
627
|
+
That is a correct implementation of client-side grouping and is **unchanged**
|
|
628
|
+
here — what was missing is any statement that client-side grouping is what you
|
|
629
|
+
are looking at. Measured on a 186-record store distributed 86 / 61 / 31 / 7 / 1
|
|
630
|
+
across five business units with a 100-row page: with contiguous rows the grid
|
|
631
|
+
rendered **two** group headers (`86`, `14`) and three units were absent from
|
|
632
|
+
the screen entirely; with interleaved rows all five resolved but every count
|
|
633
|
+
was a page slice (31 / 31 / 30 / 1 / 7). Nothing on screen said either.
|
|
634
|
+
|
|
635
|
+
The paging footer is not a statement about what was grouped, and it
|
|
636
|
+
demonstrably did not prevent the wrong reading — a wrong number invites a
|
|
637
|
+
second look, an absent row invites none. So the disclosure goes where the
|
|
638
|
+
authoritative-looking number is:
|
|
639
|
+
|
|
640
|
+
- a short `Partial` marker beside **every** group count, at every nesting
|
|
641
|
+
depth, carrying the full sentence as its `title` and its accessible name;
|
|
642
|
+
- one line directly above the group list, inside the grouped region rather
|
|
643
|
+
than in the footer: *"Grouped over the first 100 of 186 records. Group counts
|
|
644
|
+
are page-scoped, and a group whose records all fall beyond the loaded rows is
|
|
645
|
+
missing here."*
|
|
646
|
+
|
|
647
|
+
The trigger is the strongest thing the component can actually know, and the
|
|
648
|
+
wording never outruns it. With a real match total to compare against
|
|
649
|
+
(`resolvedTotalMatching` — the one derived value the pager and both bulk-bar
|
|
650
|
+
sites already read, reached either from the grid's own fetch or from a host's
|
|
651
|
+
`rowCount`) it states the fact with both numbers. With no total but a window
|
|
652
|
+
that came back full it may only say *"more may match"* — the same inference
|
|
653
|
+
`plugin-list`'s own footer draws when no total is known. Rows handed in inline
|
|
654
|
+
are not a page and are never marked, and **a grouped grid whose result set fits
|
|
655
|
+
in one page shows nothing at all**: the marker is conditional, which is what
|
|
656
|
+
makes it worth reading.
|
|
657
|
+
|
|
658
|
+
Server-side grouping — the durable fix — is deliberately NOT part of this. It
|
|
659
|
+
is an API-surface decision still open on objectui#5560, and nothing here builds
|
|
660
|
+
toward it or changes the fetch.
|
|
661
|
+
|
|
662
|
+
`@object-ui/i18n` carries the three new `grid.grouping.*` strings across all
|
|
663
|
+
ten locale packs; `GroupRow` gains two optional props (`partialLabel`,
|
|
664
|
+
`partialTitle`) and is otherwise unchanged. No metadata schema key was added:
|
|
665
|
+
the condition is derived from data the grid already has.
|
|
666
|
+
|
|
667
|
+
⚠️ **Dated note, 2026-09-28 — grid grouping is now server-side where the data
|
|
668
|
+
source can answer the group header query — objectui#7189, PR objectui#10878.**
|
|
669
|
+
The paragraphs above describe this change as it was written. Later in this same
|
|
670
|
+
release three of their sentences stopped describing the grid: that
|
|
671
|
+
`useGroupedData` buckets the rows the browser already holds, so the set of
|
|
672
|
+
groups and every header number are properties of the fetched page; that
|
|
673
|
+
client-side grouping is **unchanged** here; and that server-side grouping is not
|
|
674
|
+
part of this, is still open on objectui#5560, and that nothing here builds
|
|
675
|
+
toward it or changes the fetch. Maintainer ruling A on objectui#7189 settled that
|
|
676
|
+
question. On a data source that declares `queryGroupHeaders`, a grouped grid
|
|
677
|
+
that fetches its own rows now takes its groups, every group count and every
|
|
678
|
+
per-group aggregation from the query, and pages each group's rows on the
|
|
679
|
+
server; its counts are the query's own, so the `Partial` marker and the notice
|
|
680
|
+
described above never render there. On a data source without
|
|
681
|
+
`queryGroupHeaders`, the grid still groups the page it fetched in the browser,
|
|
682
|
+
as described above, and the marker and the notice are kept there. The rest of
|
|
683
|
+
this entry is kept as the reading of this change; the
|
|
684
|
+
`7189-server-side-grid-grouping` entry states what a grouped grid does now.
|
|
685
|
+
|
|
686
|
+
⚠️ **Dated note, 2026-09-28 — the `Partial` marker is retired before release —
|
|
687
|
+
objectui#10881.** Later in this same release the marker and the notice described
|
|
688
|
+
above were retired, with the three `grid.grouping.partial*` strings and
|
|
689
|
+
`GroupRow`'s `partialLabel` / `partialTitle` props that carried them (maintainer
|
|
690
|
+
ruling F). A grouped grid that fetches its own rows over a data source that
|
|
691
|
+
declares no `queryGroupHeaders` no longer groups the page it fetched: it
|
|
692
|
+
refuses grouping with an error naming that member. So the last sentences of the
|
|
693
|
+
note above — that on such a source the grid still groups the page it fetched,
|
|
694
|
+
and that the marker and the notice are kept there — no longer describe the
|
|
695
|
+
grid. The rest of this entry is kept as the reading of this change; the
|
|
696
|
+
`10881-grouping-needs-header-query` entry states what ships.
|
|
697
|
+
- 0348bc9: **BREAKING:** `@object-ui/plugin-detail` no longer exports seven components that
|
|
698
|
+
nothing registered and nothing mounted (objectui#7192, objectui#7175). This
|
|
699
|
+
narrows the package's published surface. It is declared `minor`, not `major`,
|
|
700
|
+
because this fixed release group follows the `@objectstack` major (AGENTS.md
|
|
701
|
+
§9); the breaking change is stated here instead.
|
|
702
|
+
|
|
703
|
+
Removed, with every type exported beside them:
|
|
704
|
+
|
|
705
|
+
- `CommentInput`, with `CommentInputProps`
|
|
706
|
+
- `DiffView`, with `DiffViewProps`, `DiffFieldType`, `DiffMode` and `DiffLine`
|
|
707
|
+
- `InlineCreateRelated`, with `InlineCreateRelatedProps`,
|
|
708
|
+
`RelatedFieldDefinition` and `RelatedRecordOption`
|
|
709
|
+
- `MentionAutocomplete` and the helper `createMentionFromSuggestion`, with
|
|
710
|
+
`MentionAutocompleteProps` and `MentionSuggestionItem`
|
|
711
|
+
- `PointInTimeRestore`, with `PointInTimeRestoreProps` and `RevisionEntry`
|
|
712
|
+
- `RecordNavigationEnhanced`, with `RecordNavigationEnhancedProps`
|
|
713
|
+
- `RelationshipGraph`, with `RelationshipGraphProps` and `GraphNode`
|
|
714
|
+
|
|
715
|
+
Why they went: none of the seven was passed to `ComponentRegistry.register`, so
|
|
716
|
+
metadata had no type string to name any of them, and none was mounted anywhere
|
|
717
|
+
in this repository. The downstream readings agree: `cloud` references none of
|
|
718
|
+
the seven names (objectstack#14187), and `hotcrm` depends on no `@object-ui/*`
|
|
719
|
+
package at all. The maintainer ruled to delete them; the ruling is relayed on
|
|
720
|
+
objectui#7192.
|
|
721
|
+
|
|
722
|
+
Not affected: `RichTextCommentInput` and its `MentionSuggestion` type (the
|
|
723
|
+
composer `RecordActivityTimeline` mounts), `RecordComments` (`DetailView`
|
|
724
|
+
mounts it) and `extractMentions` all stay exported.
|
|
725
|
+
|
|
726
|
+
`@object-ui/i18n`: the `detail.*` keys that only these components read are
|
|
727
|
+
removed from all ten locale packs, and the matching rows from
|
|
728
|
+
`DETAIL_DEFAULT_TRANSLATIONS`:
|
|
729
|
+
|
|
730
|
+
- `PointInTimeRestore`'s ten, added by objectui#7163: `revisionHistory`,
|
|
731
|
+
`noRevisions`, `revisionFieldsChanged`, `revisionFieldsChangedOne`,
|
|
732
|
+
`revisionPreview`, `revisionSnapshot`, `restoreConfirm`, `restoring`,
|
|
733
|
+
`confirmRestore`, `restoreToPoint`
|
|
734
|
+
- `emptyValue`, an older key whose last reader was `PointInTimeRestore`
|
|
735
|
+
- `DiffView`'s five: `unifiedDiff`, `sideBySideDiff`, `noChanges`,
|
|
736
|
+
`previousVersion`, `currentVersion`
|
|
737
|
+
- `RecordNavigationEnhanced`'s six: `firstRecord`, `previousRecordKey`,
|
|
738
|
+
`nextRecordKey`, `lastRecord`, `searchWhileNavigating`, `searchRecords`
|
|
739
|
+
|
|
740
|
+
Keys these components shared with surfaces that stay (`detail.recordOf`,
|
|
741
|
+
`detail.noRecords`, `detail.cancel`, `detail.activityEmptyValue`, the
|
|
742
|
+
relative-time keys) are kept. A host that read one of the removed keys itself
|
|
743
|
+
now gets the raw key back.
|
|
744
|
+
|
|
745
|
+
Other entries in this same release describe work on `PointInTimeRestore` and
|
|
746
|
+
`DiffView` (their translation and display-locale fixes). That work shipped in
|
|
747
|
+
components this entry removes.
|
|
748
|
+
|
|
749
|
+
**Migration:** there is no replacement. If you import one of these components,
|
|
750
|
+
copy its source file (`packages/plugin-detail/src/`, the file named after the
|
|
751
|
+
component) from a release tag that still ships it, for example
|
|
752
|
+
`@object-ui/plugin-detail@17.5.0`, into your own code. If you need one of them
|
|
753
|
+
back in the package, open an issue that says who uses it and where.
|
|
754
|
+
- a4514e8: Retire `useObjectLabel().viewDescription()` and the `_views.<view>.description`
|
|
755
|
+
catalog convention it resolved (objectui#7219, maintainer ruling 2026-09-02,
|
|
756
|
+
option B — enforce-or-remove).
|
|
757
|
+
|
|
758
|
+
**Breaking for translation bundles, deliberately — and this text is the notice.**
|
|
759
|
+
Out-of-repo translation bundles that authored
|
|
760
|
+
`<ns>.objects.<objectName>._views.<viewName>.description` cannot be seen from
|
|
761
|
+
this repo, so there is no census to point at and no migration script to run:
|
|
762
|
+
that key now resolves nowhere, and an entry left under it is simply ignored.
|
|
763
|
+
Nothing throws, and nothing else on that node changes.
|
|
764
|
+
|
|
765
|
+
**What replaces it.** A list view's description has exactly ONE channel: the
|
|
766
|
+
`I18nLabel` value authored on the view entry itself — a string, or an inline
|
|
767
|
+
locale map:
|
|
768
|
+
|
|
769
|
+
```ts
|
|
770
|
+
listViews: {
|
|
771
|
+
by_unit: {
|
|
772
|
+
label: 'By business unit',
|
|
773
|
+
description: { en: 'Open work only.', 'zh-CN': '仅未完成的工作。' },
|
|
774
|
+
},
|
|
775
|
+
}
|
|
776
|
+
```
|
|
777
|
+
|
|
778
|
+
`ObjectView` relays that value to the renderer and `plugin-list`'s `ListView`
|
|
779
|
+
resolves it against the display locale (objectui#7199, shipped before this
|
|
780
|
+
change), so the authored channel already works end to end. **Migration:** move
|
|
781
|
+
the sentence out of the translation bundle and onto the view entry as a locale
|
|
782
|
+
map.
|
|
783
|
+
|
|
784
|
+
**Why removed rather than wired in.** The member was declared and resolved but
|
|
785
|
+
had zero callers and zero in-repo bundle usage — an entry authored under the
|
|
786
|
+
catalog key reached no screen. Wiring it in would have put two vocabularies on
|
|
787
|
+
one concept (`I18nLabel` on the entry, and the catalog key) and required a
|
|
788
|
+
precedence rule between them, which is the ambiguity rather than the fix.
|
|
789
|
+
|
|
790
|
+
The two sibling members on the same node are **unaffected**: `viewLabel` and
|
|
791
|
+
`viewEmptyState` still resolve `_views.<view>.label` and
|
|
792
|
+
`_views.<view>.emptyState.{title,message}`, and the shared `viewSuffixes` key
|
|
793
|
+
builder they use is unchanged — only the `'description'` tail is gone. Pin tests
|
|
794
|
+
in `@object-ui/i18n` and `@object-ui/app-shell` were retargeted onto those two
|
|
795
|
+
survivors plus a case that authors the catalog `description` and asserts the
|
|
796
|
+
authored value is what a consumer resolves.
|
|
797
|
+
- 47547d0: Localize the server's built-in aggregate measure titles on dataset charts
|
|
798
|
+
(objectui#7258 — consumer half of the objectstack#14492 contract; maintainer
|
|
799
|
+
ruling B, 2026-09-02).
|
|
800
|
+
|
|
801
|
+
A dataset-bound chart's aggregate axis / legend title read the analytics
|
|
802
|
+
service's hard-coded English `Count` on a zh console whose category labels were
|
|
803
|
+
already Chinese. The renderer was passing `fields[].label` through verbatim —
|
|
804
|
+
correctly, for an author-declared measure (objectui#4106) — and had no way to
|
|
805
|
+
tell the server's built-in default apart from an author's label.
|
|
806
|
+
|
|
807
|
+
The wire now can: `AnalyticsResult.fields[]` gains an OPTIONAL structural
|
|
808
|
+
discriminator, `builtinAggregate?: 'count' | 'sum' | 'avg' | 'min' | 'max' |
|
|
809
|
+
'count_distinct'`, populated only on the server-side built-in defaults
|
|
810
|
+
(objectstack#14492). This change is the consumer side of that contract:
|
|
811
|
+
|
|
812
|
+
- `@object-ui/core`: `buildChartSeries` now accepts `ChartMeasureField[]` —
|
|
813
|
+
`ChartResultField` plus the optional `builtinAggregate` carrier
|
|
814
|
+
(`BuiltinAggregateCarrier`), declared beside the renderer shape rather than
|
|
815
|
+
on it because the spec this release is built against does not carry the key
|
|
816
|
+
yet; new `BUILTIN_AGGREGATES` / `BuiltinAggregate` / `isBuiltinAggregate` /
|
|
817
|
+
`resolveMeasureLabel`; `ChartSeriesOptions.builtinAggregateLabels` carries
|
|
818
|
+
the locale strings in (core stays React-free and i18n-free — the same
|
|
819
|
+
division as `nullCategoryLabel`). A field carrying a recognised
|
|
820
|
+
discriminator resolves through that map; every other field keeps its wire
|
|
821
|
+
`label` verbatim — never by matching the label's text or the field's name
|
|
822
|
+
(the rejected option A).
|
|
823
|
+
- `@object-ui/i18n`: `builtinAggregateLabels(tt)` resolves the six strings
|
|
824
|
+
through the existing `report.aggregate.*` keys (zh already carried 计数 /
|
|
825
|
+
求和 / 平均 / …; all ten packs are pinned to cover the vocabulary).
|
|
826
|
+
- `plugin-charts` (`ObjectChart`), `plugin-dashboard` (`DatasetWidget`),
|
|
827
|
+
`plugin-report` (`DatasetReportRenderer`): pass the resolved map to
|
|
828
|
+
`buildChartSeries`.
|
|
829
|
+
|
|
830
|
+
Before: 合作中 / 已流失 / 潜在 under an axis titled `Count`. After: the same
|
|
831
|
+
chart titled `计数`; an `en` session still reads `Count`; an author-labelled
|
|
832
|
+
measure (`Tasks`) and a measure literally named `count` without the
|
|
833
|
+
discriminator are byte-for-byte unchanged. Until the upstream field is
|
|
834
|
+
populated the wire carries no discriminator and every chart renders exactly as
|
|
835
|
+
before.
|
|
836
|
+
- 20cb8db: `ObjectTimeline` refuses an undeclared date axis instead of inventing one
|
|
837
|
+
(objectui#7459).
|
|
838
|
+
|
|
839
|
+
Steps ① and ② of the three-step sequence the maintainer ruled on 2026-09-01
|
|
840
|
+
(总监批 #28). House posture, on record with that ruling:
|
|
841
|
+
日期轴永不虚构 — a date axis is never fabricated.
|
|
842
|
+
|
|
843
|
+
`ObjectTimeline` resolved its date axis through five declared bindings and then
|
|
844
|
+
closed the chain with a sixth rung that was a bare literal field name nobody
|
|
845
|
+
has ever declared. A name therefore ALWAYS resolved: for a view that declared
|
|
846
|
+
no axis, every record read a key its object does not carry, every event landed
|
|
847
|
+
in the "No date" bucket, and the screen read as a timeline that had been built
|
|
848
|
+
and simply had nothing in it.
|
|
849
|
+
|
|
850
|
+
Two changes, shipped together because neither is observable alone:
|
|
851
|
+
|
|
852
|
+
- **The renderer now refuses.** An object-bound timeline with no declared date
|
|
853
|
+
axis renders a diagnostic naming every binding it accepts —
|
|
854
|
+
`timeline.startDateField`, `timeline.dateField`, `mapping.date`, and the two
|
|
855
|
+
deprecated flat spellings — instead of a chart. The twin of `ObjectGantt`'s
|
|
856
|
+
"Gantt configuration required" screen, in the shape `5f4514f7b` settled.
|
|
857
|
+
- **The invented sixth rung is gone**, which is the only thing that makes the
|
|
858
|
+
refusal reachable. Added while the floor stood, it would have been dead code;
|
|
859
|
+
retired without the refusal, it would have produced exactly the silent
|
|
860
|
+
"No date" outcome the ruling rejects.
|
|
861
|
+
|
|
862
|
+
**What changes for an author.** A view that declares a date axis is completely
|
|
863
|
+
unaffected — all five declared spellings resolve exactly as before, and a
|
|
864
|
+
timeline authored from literal `items` is never refused, since its items carry
|
|
865
|
+
their own dates and no field name is read for them. A view that declared no
|
|
866
|
+
axis anywhere, and was rendering an empty-looking timeline, now says so.
|
|
867
|
+
|
|
868
|
+
⚠️ Both premises were RE-MEASURED on the current tree before anything was
|
|
869
|
+
edited, rather than taken from the card: the renderer had no absent-axis
|
|
870
|
+
refusal (against a live control term that fires in the same file), and the
|
|
871
|
+
floor was still present and still spelled as reported. The pairing itself is
|
|
872
|
+
pinned — the refusal cases go red the moment the floor returns, including one
|
|
873
|
+
whose records carry a column that happens to be named `date`, where a returned
|
|
874
|
+
floor renders a convincing timeline rather than an empty one.
|
|
875
|
+
|
|
876
|
+
Refusal is distinguished from "renders an empty timeline" by asserting the
|
|
877
|
+
canvas is ABSENT, not merely event-free. The component's success surface is now
|
|
878
|
+
named (`data-testid="timeline-canvas"`) so that distinction can be measured;
|
|
879
|
+
every other terminal state of the component already named itself.
|
|
880
|
+
|
|
881
|
+
Step ③ of the ruling — the `'created_at'` floors on the two plugin faces —
|
|
882
|
+
stays for a later change (it landed as `04a67b9dc`) and is deliberately NOT in this change.
|
|
883
|
+
- 77b2a18: Load locale catalogues on demand — the ten packs leave the eager closure (objectui#7479)
|
|
884
|
+
|
|
885
|
+
`@object-ui/i18n`'s entry re-exported all ten catalogues and `builtInLocales`
|
|
886
|
+
named every one of them in a single object literal, so every import of the
|
|
887
|
+
package made all ten statically reachable and nothing could tree-shake them.
|
|
888
|
+
The console fetched **456,196 gzipped bytes** of translation data before first
|
|
889
|
+
paint, for a viewer who reads one catalogue — and every new translation key was
|
|
890
|
+
paid for by every visitor in every language.
|
|
891
|
+
|
|
892
|
+
**What changed at the contract.** `en` stays statically re-exported from the
|
|
893
|
+
entry; the other nine are fetched per locale through the new
|
|
894
|
+
`loadBuiltInLocale()`. `en` is not an exemption, it is what makes the split
|
|
895
|
+
safe: it is `fallbackLng`, it is the source of the `TranslationKeys` type, and
|
|
896
|
+
it is the synchronous dictionary `@object-ui/app-shell`'s splash renders from
|
|
897
|
+
before i18n is usable and on the server-down path. Removing it would have meant
|
|
898
|
+
raw keys on screen in nine languages, or English literals in the splash — the
|
|
899
|
+
"delete or untranslate the copy" route this work was explicitly not allowed to
|
|
900
|
+
take.
|
|
901
|
+
|
|
902
|
+
New on the entry, all of them payload-free:
|
|
903
|
+
|
|
904
|
+
- `loadBuiltInLocale(code)` — fetch one catalogue; `null` for a code this
|
|
905
|
+
package does not ship. Memoised, and a failed fetch stays retryable.
|
|
906
|
+
- `getLoadedBuiltInLocales()` / `isBuiltInLocaleLoaded(code)` — what is resident
|
|
907
|
+
right now, synchronously.
|
|
908
|
+
- `BUILT_IN_LANGUAGE_CODES` / `isBuiltInLanguage(code)` — enumerate all ten
|
|
909
|
+
without fetching any of them.
|
|
910
|
+
- `preloadBootstrapLocale(options)` / `resolveBootstrapLanguage(options)` —
|
|
911
|
+
resolve the boot language and fetch its catalogue BEFORE the first render.
|
|
912
|
+
- `pickInitialLanguage(config)` — the language `createI18n` will boot in.
|
|
913
|
+
|
|
914
|
+
`I18nProvider` fetches the catalogue for whatever language it boots into, and
|
|
915
|
+
`changeLanguage()` awaits the new catalogue before switching, so a switcher
|
|
916
|
+
needs no new wiring.
|
|
917
|
+
|
|
918
|
+
**BREAKING for an importer of a specific pack.** `zh`, `ja`, `ko`, `de`, `fr`,
|
|
919
|
+
`es`, `pt`, `ru`, `ar` and `builtInLocales` are no longer exported from
|
|
920
|
+
`@object-ui/i18n`. They are exported from the new `@object-ui/i18n/locales`
|
|
921
|
+
subpath, which is the explicit all-ten door — a specifier whose cost is visible
|
|
922
|
+
at the import site. ⚠️ Never reach for it on a page-load path. (Marked `minor`
|
|
923
|
+
rather than `major` per this repository's version-alignment rule; the breaking
|
|
924
|
+
semantics are stated here.)
|
|
925
|
+
|
|
926
|
+
**What a page load costs now.** Two full console builds in one container, same
|
|
927
|
+
instrument, `origin/main` `d8b4739d4` against this change:
|
|
928
|
+
|
|
929
|
+
| reading | control | after | delta |
|
|
930
|
+
|-----------------------------|----------:|----------:|---------:|
|
|
931
|
+
| eager closure, gzipped | 3,575,370 | 3,164,817 | −410,553 |
|
|
932
|
+
| eager chunks / total | 50/518 | 51/528 | |
|
|
933
|
+
| catalogues in that closure | 10 | 1 | −9 |
|
|
934
|
+
| `vendor-objectstack` | 1,236,315 | 1,236,315 | 0 |
|
|
935
|
+
| `ui-components` | 394,726 | 394,718 | −8 |
|
|
936
|
+
| `framework` | 80,414 | 80,430 | +16 |
|
|
937
|
+
|
|
938
|
+
The last three rows are the control that makes the first one readable: the bytes
|
|
939
|
+
did not move to a roomier chunk, they left the eager closure. The nine deferred
|
|
940
|
+
catalogues weigh 405.5 KB gzipped and the browser fetches exactly one of them,
|
|
941
|
+
only when the viewer's locale is not `en`.
|
|
942
|
+
|
|
943
|
+
**Gates.** `PER_CHUNK_GZIP_CEILINGS['i18n-locales']` (465,000 over ten
|
|
944
|
+
catalogues) is replaced by `['i18n-locale-en']` at 50,000 over 40,415, and
|
|
945
|
+
`MAX_EAGER_CLOSURE_GZIP_BYTES` comes down from 3,597,000 to 3,210,000 — both
|
|
946
|
+
LOWERINGS that follow the measured drop, and the aggregate one is required: left
|
|
947
|
+
where it was, its headroom would have been 4.74x the regression this gate must
|
|
948
|
+
catch and the sensitivity half would have called the gauge blind. The
|
|
949
|
+
`i18n-locales` entry in `EXHAUSTED_HEADROOM_ALLOWANCES` is removed rather than
|
|
950
|
+
lowered — its chunk no longer exists, and the catalogue that remains clears the
|
|
951
|
+
floor on its own.
|
|
952
|
+
|
|
953
|
+
A new gate, `pnpm check:eager-locale-catalogues`, weighs COMPOSITION rather than
|
|
954
|
+
bytes: exactly one catalogue may be in the built eager closure, and it must be
|
|
955
|
+
the resident one. A byte ceiling cannot catch the catalogues returning one at a
|
|
956
|
+
time inside its headroom, and cannot tell "left the closure" from "moved to a
|
|
957
|
+
chunk with more room".
|
|
958
|
+
|
|
959
|
+
**Correction, 2026-10-01 (objectui#11326).** The list above says
|
|
960
|
+
`loadBuiltInLocale(code)` resolves `null` for a code this package does not ship,
|
|
961
|
+
and pairs `isBuiltInLanguage(code)` with the ten codes. That was true when this
|
|
962
|
+
change was written, and it no longer is: objectui#11326 looks a code up exact
|
|
963
|
+
tag first, then its base language, so `loadBuiltInLocale('zh-CN')` resolves the
|
|
964
|
+
`zh` catalogue and `isBuiltInLanguage('zh-CN')` is `true`. A code with no
|
|
965
|
+
catalogue for the tag or for its base still resolves `null`, and
|
|
966
|
+
`BUILT_IN_LANGUAGE_CODES` still lists the ten.
|
|
967
|
+
- c2f0f48: feat(console): a "Language" item on the profile page, writing the signed-in user's own
|
|
968
|
+
`sys_user.locale` (objectui#7501).
|
|
969
|
+
|
|
970
|
+
The platform made that column user-writable on 2026-09-03 and nothing in the product let
|
|
971
|
+
anyone set it — the profile page edited `name` and the avatar and carried no language
|
|
972
|
+
control at all, which is the `{name, image}` whitelist the ruling widened. Two people in
|
|
973
|
+
one deployment were already receiving their notifications in different languages with no
|
|
974
|
+
way for either of them to choose.
|
|
975
|
+
|
|
976
|
+
The new card reads and writes the user's own `sys_user` row through the data adapter, and
|
|
977
|
+
it is deliberately a card of its own rather than another field in the Personal Information
|
|
978
|
+
form: that form is written by `useAuth().updateUser`, which posts to better-auth's
|
|
979
|
+
`/update-user`, and `locale` is not a better-auth `additionalFields` entry, so that
|
|
980
|
+
endpoint cannot carry the column. Its feedback vocabulary is the form's — the same success
|
|
981
|
+
and failure `Alert`s, the same disabled-while-saving submit.
|
|
982
|
+
|
|
983
|
+
What it offers is the i18n provider's own `offerableLanguages` (the deployment's published
|
|
984
|
+
locales intersected with what this renderer can resolve) — the same set `LocaleSwitcher`
|
|
985
|
+
renders, so no second language list is introduced. Each entry is named in its own language.
|
|
986
|
+
A stored tag the deployment no longer publishes is shown rather than dropped, so the
|
|
987
|
+
control never displays a value that is not the account's.
|
|
988
|
+
|
|
989
|
+
Clearing is a first-class option: "Use the deployment default" writes `null`, which is the
|
|
990
|
+
documented meaning of an unset column and the only way back once a tag has been stored.
|
|
991
|
+
|
|
992
|
+
A refused tag (`400 VALIDATION_FAILED` with a per-field `locale` entry) is rendered on the
|
|
993
|
+
item, with `aria-invalid` and `aria-describedby` pointing at it, rather than as an
|
|
994
|
+
undirected alert. When the deployment's write route is not this user's — the field/object
|
|
995
|
+
permission answer for `sys_user.locale` — the control is read-only and says why instead of
|
|
996
|
+
offering a button that would answer 403, and it does not render at all while the row is
|
|
997
|
+
unread or if the read is refused.
|
|
998
|
+
|
|
999
|
+
**This does not change the interface language.** The UI language is a per-device
|
|
1000
|
+
preference the i18n provider keeps in `localStorage` and the globe menu switches;
|
|
1001
|
+
`sys_user.locale` is a per-user server column read per recipient at delivery time. The two
|
|
1002
|
+
stayed separate here on purpose; whether they should be joined is a product decision that
|
|
1003
|
+
has not been made.
|
|
1004
|
+
|
|
1005
|
+
`@object-ui/i18n` gains the seven `profile.language.*` keys, in all ten packs.
|
|
1006
|
+
- 23b9958: `RecipientPickerField` gains a picker mode for the `field` sharing recipient
|
|
1007
|
+
(objectui#7613; maintainer ruling objectstack#14103, executor objectstack#15072).
|
|
1008
|
+
|
|
1009
|
+
`sys_sharing_rule.recipient_type` carries a sixth value, `field` — the
|
|
1010
|
+
record-relative recipient. The recipients are whoever a user-valued column of the
|
|
1011
|
+
SHARED object names on each matched record, so `recipient_id` stores a field
|
|
1012
|
+
NAME (`assignees`, `owner_manager`) and not a record id. The widget's
|
|
1013
|
+
`TYPE_TO_OBJECT` table had no entry for that kind, so it fell through to the
|
|
1014
|
+
plain text input documented for unknown types: functional (the stored value was
|
|
1015
|
+
already the right one), but the admin had to know and type the machine name of
|
|
1016
|
+
the column by hand, with no list to choose from and nothing saying whether the
|
|
1017
|
+
name existed.
|
|
1018
|
+
|
|
1019
|
+
When `recipient_type` is `field`, the widget now reads the shared object from
|
|
1020
|
+
the sibling `object_name` field — the same dependency the `filter-condition`
|
|
1021
|
+
widget already reads — loads that object's schema through
|
|
1022
|
+
`dataSource.getObjectSchema`, and offers its **user-valued** columns, storing the
|
|
1023
|
+
column's name.
|
|
1024
|
+
|
|
1025
|
+
**The offered set is exactly the set the evaluator honours**, and that agreement
|
|
1026
|
+
is the point of the change rather than a detail of it. The sharing evaluator
|
|
1027
|
+
reads a column as users when it is the `user` type, or a `lookup` /
|
|
1028
|
+
`master_detail` whose `reference` is `sys_user`; anything else it treats as
|
|
1029
|
+
"grants nobody" and warns about once per rule. A picker offering a wider set
|
|
1030
|
+
would let an admin save a rule that looks configured and authorises nobody —
|
|
1031
|
+
worse than a hand-typed name, because it has a credible appearance. `hidden` is
|
|
1032
|
+
deliberately not filtered (the evaluator honours a hidden user column), and
|
|
1033
|
+
`reference_to` is deliberately not read (the protocol refuses that spelling by
|
|
1034
|
+
name, and so does the evaluator).
|
|
1035
|
+
|
|
1036
|
+
Three things the mode says out loud rather than leaving to be inferred: it asks
|
|
1037
|
+
for the shared object before offering anything when `object_name` is still
|
|
1038
|
+
unset; an object with no user columns says so instead of rendering a
|
|
1039
|
+
search-flavoured "No matches"; and a stored name that is not on the offered list
|
|
1040
|
+
— a column deleted or retyped since the rule was saved — stays visible and is
|
|
1041
|
+
marked, because the evaluator grants nobody for it and a control that merely
|
|
1042
|
+
looked empty would hide a rule that still names it.
|
|
1043
|
+
|
|
1044
|
+
Unknown recipient types keep degrading to the plain text input, and so does
|
|
1045
|
+
`field` itself when the data source cannot enumerate an object's columns: a
|
|
1046
|
+
hand-typed name is worse than a list, and better than a list that can never
|
|
1047
|
+
fill.
|
|
1048
|
+
|
|
1049
|
+
Three copy keys are added across all ten locale packs
|
|
1050
|
+
(`fields.recipient.selectField`, `noUserFields`, `fieldNotUserTyped`). The
|
|
1051
|
+
"select an object first" gate deliberately reuses the criteria builder's
|
|
1052
|
+
existing sentence rather than adding a twin: it is the same sentence in the same
|
|
1053
|
+
role on the same form, gating on the same sibling field.
|
|
1054
|
+
- 91f9276: **The maker's edit-mode starter offers sample data, not an automation v1 cannot
|
|
1055
|
+
build (objectui#7709).** Bound to an existing app (`?package=`), the maker's
|
|
1056
|
+
empty state offered four starters: add a field, add an object, add a dashboard,
|
|
1057
|
+
and 「加一个自动化 —— 审批、状态流转或通知」. Approval, status flow and
|
|
1058
|
+
notification are all refused by ADR-0112 v1 (cloud#1956 / PR #1970), and the
|
|
1059
|
+
measured behaviour on the sibling chips was not a refusal but a silent degrade
|
|
1060
|
+
into a view — so the product recommended an automation and would have handed
|
|
1061
|
+
back a page.
|
|
1062
|
+
|
|
1063
|
+
Rewording it was not available: asking for a field, a view or a dashboard
|
|
1064
|
+
duplicates one of the three chips beside it. The fourth chip is now
|
|
1065
|
+
`addSampleData` —「给现有对象补一批贴近真实的示例数据,好拿去演示。」 — in all
|
|
1066
|
+
ten packs and in the call-site `defaultValue` fallback, which is a second copy
|
|
1067
|
+
of the same string. The three surviving chips all add STRUCTURE; what an app
|
|
1068
|
+
that already has objects most often lacks is DATA, and `seed` is on v1's
|
|
1069
|
+
authoring whitelist. A note beside the keys in every pack and at the call site
|
|
1070
|
+
says this chip's automation wording comes back when ADR-0112 v2 re-adds flows
|
|
1071
|
+
and actions, and the retired sentence for each pack is kept in the guard suite
|
|
1072
|
+
so v2 has it verbatim.
|
|
1073
|
+
- a915064: `listImportMappings` no longer renders a refused door as "no mapping is registered"
|
|
1074
|
+
(objectui#7741).
|
|
1075
|
+
|
|
1076
|
+
`ObjectStackAdapter.listImportMappings` degrades every failure to an empty list, and the
|
|
1077
|
+
import wizard hides its saved-mapping selector on an empty list. So "the server served
|
|
1078
|
+
zero mappings" and "the server refused, or broke" produced the identical UI on every
|
|
1079
|
+
deployment — the feature simply absent — with a `console.warn` as the only
|
|
1080
|
+
discriminator, in the browser console, with nothing pointing at it. That silence did not
|
|
1081
|
+
merely hide a fault: it produced a confident WRONG diagnosis in a careful reporter
|
|
1082
|
+
(objectstack#14026 was filed, routed and worked by two seats against a wizard that had
|
|
1083
|
+
been correct since `@object-ui/data-objectstack@17.1.0`).
|
|
1084
|
+
|
|
1085
|
+
**The empty-list return is unchanged.** `listImportMappings` still answers
|
|
1086
|
+
`Promise<any[]>` and still never throws, on every arm including the loud ones — this is
|
|
1087
|
+
a channel added ALONGSIDE that contract, not a change to it.
|
|
1088
|
+
|
|
1089
|
+
- **New: `ObjectStackAdapter.onMetadataReadWarning(cb)`** — a subscribe/unsubscribe
|
|
1090
|
+
channel, sibling in shape to `onWriteWarning` and `onSaveAdvisory`. It fires when a
|
|
1091
|
+
metadata read failed in a way that is NOT the supported "this deployment does not
|
|
1092
|
+
serve that kind" shape, carrying `MetadataReadWarningEvent`: which read it was, the
|
|
1093
|
+
object, whether the server `refused` this caller or the answer was `unreadable`, and
|
|
1094
|
+
the server's own ADR-0112 code, HTTP status and message.
|
|
1095
|
+
- **New: `classifyImportMappingsFailure(err)`** and `ImportMappingsFailureKind`, exported
|
|
1096
|
+
so a consumer can apply the same verdict. It reads the ERROR — the ADR-0112 `code`
|
|
1097
|
+
first, the status only where no code was declared — and never "is the result an empty
|
|
1098
|
+
array", which is what both conditions produce and so can never tell them apart.
|
|
1099
|
+
- **The older-server case stays quiet.** A deployment that does not serve the `mapping`
|
|
1100
|
+
kind (404/501 with no route, `ROUTE_NOT_FOUND`, `NOT_IMPLEMENTED`, or the metadata list
|
|
1101
|
+
door's 400 `INVALID_REQUEST`) still degrades to an empty list with no selector and no
|
|
1102
|
+
event. That is a real, supported deployment shape and it must not become a visible
|
|
1103
|
+
fault.
|
|
1104
|
+
- **The console now says so.** `AdapterProvider` subscribes to the new channel and
|
|
1105
|
+
renders a warning toast naming the object, the remedy and the server's own words, so a
|
|
1106
|
+
user without devtools open can tell "there are none" from "we could not find out".
|
|
1107
|
+
Three new `console.importMappings*` keys ship in all ten locale packs.
|
|
1108
|
+
|
|
1109
|
+
This applies framework objectstack#13906 decision 1 option A — *a thing that could not be READ is
|
|
1110
|
+
not a thing that is ABSENT* — at this seam. It is an already-adopted discrimination, not
|
|
1111
|
+
a new principle.
|
|
1112
|
+
- af9e957: fix(types,components,plugin-form,console,core): a faulted `visibleWhen` refuses the submit, naming the field and the rule; a blank field rule is refused; blank gates are diagnosed (objectui#8069)
|
|
1113
|
+
|
|
1114
|
+
⚠️ **User-visible, and a narrowing.** A form whose field `visibleWhen` cannot be
|
|
1115
|
+
evaluated — a typo in a column name, a syntax error, an unbound root — used to
|
|
1116
|
+
render the field (fail-open) and submit as if the rule had said "show". It now
|
|
1117
|
+
still renders the field, and **refuses the submit** with a message that names
|
|
1118
|
+
the field and the rule (`form.visibleWhenFaulted`). This is ADR-0137 D2 as
|
|
1119
|
+
ruled for objectui#8069 (Q1 = B, one judge per rule): no server evaluates a
|
|
1120
|
+
field's `visibleWhen`, so its fail-open render direction (D3) was a silent grant
|
|
1121
|
+
— a field the working rule would have hidden, drawn, edited and written. The
|
|
1122
|
+
refusal applies on the record form renderer (`form.tsx`, every `ObjectForm`
|
|
1123
|
+
layout), the console's `/forms/:name` and `/f/:slug` page, and the wizard's
|
|
1124
|
+
cross-step gate at final submit.
|
|
1125
|
+
|
|
1126
|
+
- `requiredWhen` / `readonlyWhen` are **unchanged on the client**: the server
|
|
1127
|
+
evaluates both and refuses a faulted one itself (ADR-0137 D2), and the form
|
|
1128
|
+
renderer shows that field-attributed refusal beside the input.
|
|
1129
|
+
- **Accepted residuals:** a `visibleWhen` reading `previous` cannot be
|
|
1130
|
+
evaluated on a CREATE form, so such a form is refused on every submit; and
|
|
1131
|
+
the wizard's cross-step gate binds no `previous` in either mode, so the same
|
|
1132
|
+
rule is refused at the final submit of an EDIT wizard too.
|
|
1133
|
+
- A **blank** field rule (`''`, whitespace, an envelope whose `source` is
|
|
1134
|
+
blank) is a fault, not "no rule" (ADR-0137 D2). A STORED blank `visibleWhen`
|
|
1135
|
+
is refused at submit on the same three paths; a stored blank `requiredWhen` /
|
|
1136
|
+
`readonlyWhen` is the server's to refuse.
|
|
1137
|
+
|
|
1138
|
+
⚠️ **Narrowing (`@object-ui/types`): `FormFieldSchema` refuses a blank field
|
|
1139
|
+
rule at parse.** `visibleWhen`, `readonlyWhen` and `requiredWhen` on a form
|
|
1140
|
+
field now refuse a predicate that is blank after trimming, with the spec's own
|
|
1141
|
+
sentence (`EVALUATED_EXPRESSION_SOURCE_REQUIRED`) — ADR-0137 D1, the same
|
|
1142
|
+
refusal `@objectstack/spec` makes on `FieldSchema`. The accepted SHAPE is
|
|
1143
|
+
unchanged (a string or `{ dialect?, source }`, `ExpressionWireSchema`'s own
|
|
1144
|
+
arms): only the blank value is taken out. A blank GATE — `BaseSchema`'s
|
|
1145
|
+
`visible` / `hidden` / `disabled`, a form field's view-level `visibleOn`, an
|
|
1146
|
+
option's `visibleWhen` — still parses and is still read as "no gate".
|
|
1147
|
+
|
|
1148
|
+
**Added (`@object-ui/core`):** `resolveFieldRuleState` returns `faults` beside
|
|
1149
|
+
its three verdicts — the per-rule fault report the submit paths read, filled
|
|
1150
|
+
from the same evaluation — and its type, `FieldRuleFaults`, is exported.
|
|
1151
|
+
**Added (`@object-ui/i18n`):** the `form.visibleWhenFaulted` key in all ten
|
|
1152
|
+
packs.
|
|
1153
|
+
|
|
1154
|
+
**Diagnosed, no verdict changed (ADR-0137 D4):** a blank CEL gate reaching
|
|
1155
|
+
`ExpressionEvaluator.evaluateCondition`, and a blank gate folded to "no gate" by
|
|
1156
|
+
`hasDeclaredPredicate`, now each report once through the same `[blank]` channel
|
|
1157
|
+
field-rule faults use. Both verdicts (objectui#3850 / #3960) are unchanged,
|
|
1158
|
+
`throwOnError` included.
|
|
1159
|
+
- 0758bd8: `ObjectChart` refuses an object-bound chart that declares no category axis (objectui#8168).
|
|
1160
|
+
|
|
1161
|
+
An object-bound chart that named no field to group by used to be composed anyway.
|
|
1162
|
+
`runAggregate` passed `schema.aggregate` to `ds.aggregate(objectName, { field,
|
|
1163
|
+
function, groupBy, filter })` with no guard on `groupBy`, and the `ds.find` leg
|
|
1164
|
+
handed the same bag to `aggregateRecords`, which buckets every record on
|
|
1165
|
+
`record[groupBy] ?? 'Unknown'`. So one path asked a driver to group by `undefined`
|
|
1166
|
+
and the other collapsed the whole object into a single `'Unknown'` bar — and which
|
|
1167
|
+
of those a reader saw was decided by the data source, not by the renderer. The only
|
|
1168
|
+
loud states this component had were a fetch `error` (`chart-error`) and a generic
|
|
1169
|
+
"No data yet"; neither is a statement about an absent binding.
|
|
1170
|
+
|
|
1171
|
+
`ObjectCalendar`, `ObjectGantt` and `ObjectTimeline` each already refuse a view that
|
|
1172
|
+
declares no axis. This is the fourth, following `ObjectTimeline`'s shape
|
|
1173
|
+
(objectui#7459): a `role="alert"` box, `data-testid="chart-missing-category-axis"`,
|
|
1174
|
+
naming the bindings the author can declare — `aggregate.groupBy`, `xAxisKey`,
|
|
1175
|
+
`xAxis.field` — rendered from the resolver's own vocabulary so the message cannot
|
|
1176
|
+
drift from what the resolver reads.
|
|
1177
|
+
|
|
1178
|
+
**Breaking, deliberately — and this repo ships breaking as `minor`.** A chart that
|
|
1179
|
+
previously rendered an `'Unknown'`-bucketed bar (or whatever the driver did with
|
|
1180
|
+
`groupBy: undefined`) now renders the refusal instead. That is the intent: the
|
|
1181
|
+
picture it drew was not a picture of the data.
|
|
1182
|
+
|
|
1183
|
+
It keys on the CATEGORY alone. A measure may legitimately be absent — `count` takes
|
|
1184
|
+
no field — so refusing on an absent measure would refuse `count` grouped by a
|
|
1185
|
+
declared category, a chart that renders correctly. Four shapes are deliberately
|
|
1186
|
+
untouched: an ADR-0021 `dataset` chart (which may declare no dimension), a chart
|
|
1187
|
+
carrying authored `data` or a `bind` scope (no field name is read to fetch those
|
|
1188
|
+
rows), a spec-shape `xAxis: { field }` with no `xAxisKey` (resolved through
|
|
1189
|
+
`normalizeChartSchema`, this package's one translation of the author-facing shape),
|
|
1190
|
+
and every schema the five in-repo producers compose today — all five floor their own
|
|
1191
|
+
category, so none of them can reach the refusal.
|
|
1192
|
+
|
|
1193
|
+
This does **not** retire the six `'name'` / `'value'` floors at the three relay faces;
|
|
1194
|
+
that is the remainder of objectui#7547 and is mechanical only once this screen exists.
|
|
1195
|
+
|
|
1196
|
+
New key `chart.unconfigured.noCategoryAxis` in all ten locale packs.
|
|
1197
|
+
- a695f50: `@object-ui/i18n` now publishes `TranslateFn` — i18next's `t` narrowed to
|
|
1198
|
+
`(key: string, options?: Record<string, unknown>) => string` — as the one
|
|
1199
|
+
authority for that name (objectui#8261, on the maintainer's ruling on
|
|
1200
|
+
objectui#8165, option A):
|
|
1201
|
+
|
|
1202
|
+
```ts
|
|
1203
|
+
import type { TranslateFn } from '@object-ui/i18n';
|
|
1204
|
+
```
|
|
1205
|
+
|
|
1206
|
+
`@object-ui/app-shell` (`writeWarningToast`, which its sibling toast modules
|
|
1207
|
+
re-export) and
|
|
1208
|
+
`@object-ui/fields` (`file-size-guard`) each declared their own copy of this
|
|
1209
|
+
type. Both copies were byte-identical to the new declaration, and both modules
|
|
1210
|
+
now re-export it from `@object-ui/i18n` instead of declaring it, so every
|
|
1211
|
+
existing import of `TranslateFn` from those modules resolves to the same type
|
|
1212
|
+
it did before — no call site's contract moves. Neither package publishes the
|
|
1213
|
+
name from its own entry; `@object-ui/i18n` is the one place to import it from.
|
|
1214
|
+
|
|
1215
|
+
The `TranslateFn` entry in the one-authority gate's `KNOWN_COLLISIONS` baseline
|
|
1216
|
+
(`scripts/__tests__/one-authority-per-exported-name-6273.test.ts`) is removed:
|
|
1217
|
+
the name now has one declaration.
|
|
1218
|
+
- 55ba3ff: A `lookup` reference that resolved to nothing now gets **one** answer instead of
|
|
1219
|
+
two opposite ones (objectui#8695), carrying objectui#8434's ruling to the second
|
|
1220
|
+
renderer that had the same defect.
|
|
1221
|
+
|
|
1222
|
+
`LookupCellRenderer` split a single epistemic state — *a reference this screen
|
|
1223
|
+
did not resolve* — by the **shape of the string**. `isLikelyOpaqueId` sent
|
|
1224
|
+
opaque-looking ids to a muted `—` and sent everything else to confident bare
|
|
1225
|
+
text. Measured with `reference_to: 'sys_user'`, `'Ada Lovelace'` rendered
|
|
1226
|
+
`<span class="block max-w-full truncate" title="Ada Lovelace">Ada Lovelace</span>`
|
|
1227
|
+
— **byte-identical** to what a `text` cell prints for the same string — while
|
|
1228
|
+
`'01HQZX9K2M4N6P8R'` rendered a muted `—`. So the name-shaped case stated a
|
|
1229
|
+
confident fact the screen did not have (a dirty row read exactly like a clean
|
|
1230
|
+
one), and the opaque case destroyed the raw id, which objectui#8434's triage
|
|
1231
|
+
named as "the only clue for diagnosing existing dirty rows". Two opposite
|
|
1232
|
+
failures, one state.
|
|
1233
|
+
|
|
1234
|
+
Both now render the affordance objectui#8434 settled on: the raw value kept
|
|
1235
|
+
**visible**, beside a muted marker glyph and a stated sentence, keyed as
|
|
1236
|
+
`detail.unresolvedLookupReference` in all ten locale packs. `master_detail` and
|
|
1237
|
+
`tree` route through the same renderer and get the same answer, as does the
|
|
1238
|
+
multi-value chip shape — including the `+N` overflow chip's `title`, which now
|
|
1239
|
+
lists the values it hides instead of a row of dashes.
|
|
1240
|
+
|
|
1241
|
+
**The sentence is epistemic, not ontological, and it is a sibling of the `user`
|
|
1242
|
+
one rather than the same key.** At least six causes reach this arm and the
|
|
1243
|
+
renderer distinguishes none of them: never fetched, in flight, the resolver
|
|
1244
|
+
threw, the resolver answered with no record, it answered with a record no
|
|
1245
|
+
display field could name, and — for array entries after the first — never asked.
|
|
1246
|
+
`useLookupName` returns `string | undefined`, dropping the `pending`/`err`/`ok`
|
|
1247
|
+
discriminator its own cache holds. Several of those also cover a record the
|
|
1248
|
+
viewer may simply not be allowed to read, and "cannot read" versus "does not
|
|
1249
|
+
exist" is a boundary this renderer cannot see across. So it states only what is
|
|
1250
|
+
true of all of them: this screen did not resolve it. The `user` pack value ends
|
|
1251
|
+
"was not resolved to a user", which is false on a lookup pointing at any other
|
|
1252
|
+
object, hence a separate key.
|
|
1253
|
+
|
|
1254
|
+
**Why `minor` and not `patch`.** The test this lane applies is whether EXISTING
|
|
1255
|
+
STORED DATA renders differently, and here some of it is not broken data.
|
|
1256
|
+
`LookupCellRenderer` auto-resolves only the FIRST primitive of an array
|
|
1257
|
+
(`primaryPrimitiveId`, and `resolveLabel` returns a name only when
|
|
1258
|
+
`val === primaryPrimitiveId`) — a documented cheapness policy, not a defect. So
|
|
1259
|
+
on a multi-value `lookup` holding perfectly clean ids, entries 2..n were never
|
|
1260
|
+
asked and now wear the unresolved marker where they previously printed a bare
|
|
1261
|
+
id or a dash. That is a visible change to a working screen over correct data,
|
|
1262
|
+
which is the line between the two levels; the name-shaped and opaque-shaped
|
|
1263
|
+
single-value cases would each have been `patch` on their own, since both were
|
|
1264
|
+
already wrong. `@object-ui/i18n` is `minor` for the ordinary reason: a key is
|
|
1265
|
+
ADDED — `detail.unresolvedLookupReference`, across all ten packs (the `user`
|
|
1266
|
+
sentence `detail.unresolvedReference` already shipped with objectui#8434).
|
|
1267
|
+
|
|
1268
|
+
**Nothing else moves.** A reference resolved by an expanded record, by the
|
|
1269
|
+
author's `options`, or by the fetch-on-demand resolver renders exactly as before,
|
|
1270
|
+
unmarked; an empty cell keeps `EmptyValue`; the affordance sits inside
|
|
1271
|
+
`ReferencedRecordLink`, so an unresolved reference is still navigable
|
|
1272
|
+
(objectui#4336). Display only — no query, sort, export or save path reads a cell
|
|
1273
|
+
renderer's output. `isLikelyOpaqueId` stays exported (removing it would be a
|
|
1274
|
+
breaking change) but no longer decides how anything is drawn.
|
|
1275
|
+
- ef52001: Retire 23 measured-dead locale keys from all ten packs — two whole families and
|
|
1276
|
+
eleven individual leaves (objectui#8754; director seat summon #22, 2026-09-12,
|
|
1277
|
+
maintainer verbatim 「其他同意」).
|
|
1278
|
+
|
|
1279
|
+
**Breaking, deliberately.** These keys no longer exist in any of the ten built-in
|
|
1280
|
+
packs. This repository has no reader for any of them, but `@object-ui/i18n` is a
|
|
1281
|
+
published package: an application outside this repository that calls `t()` with
|
|
1282
|
+
one of the keys below now renders the raw key name instead of a sentence, with no
|
|
1283
|
+
error thrown (i18next's missing-key fallback). That risk is accepted rather than
|
|
1284
|
+
overlooked — the standing maintainer ruling is immediate retirement with no staged
|
|
1285
|
+
window and no double-spelling grace, on the grounds that 「已发布零消费的能力不因
|
|
1286
|
+
沉没成本获得豁免」.
|
|
1287
|
+
|
|
1288
|
+
Marked `minor` rather than `major` because this repository's fixed release group
|
|
1289
|
+
tracks `@objectstack`'s major line; the breaking semantics are stated here instead.
|
|
1290
|
+
|
|
1291
|
+
**Migration.** If you call one of these keys, supply the string yourself — pass
|
|
1292
|
+
`defaultValue` to your `t()` call, or add the key to your own application's
|
|
1293
|
+
resource bundle. For the two families there is a better answer than re-adding the
|
|
1294
|
+
key, because a live surface already exists and the retired family was never wired
|
|
1295
|
+
to it:
|
|
1296
|
+
|
|
1297
|
+
- `approvals.*` — the shipping approvals UI renders through **`approvalsInbox.*`**,
|
|
1298
|
+
a different namespace that is fully populated in all ten packs. Point at that.
|
|
1299
|
+
- `marketplace.pricing.*` — nothing renders pricing labels in this repo at all.
|
|
1300
|
+
There is no replacement key; author your own.
|
|
1301
|
+
|
|
1302
|
+
**The two families, retired whole** (the unit of this retirement is the family:
|
|
1303
|
+
retiring half a dead family leaves the other half looking live by contrast, and
|
|
1304
|
+
the namespace roots are removed, not merely emptied):
|
|
1305
|
+
|
|
1306
|
+
- `approvals.approve`, `approvals.approveSuccess`, `approvals.comment`,
|
|
1307
|
+
`approvals.reject`, `approvals.rejectConfirm`, `approvals.rejectSuccess`
|
|
1308
|
+
- `marketplace.pricing.contact-sales`, `marketplace.pricing.free`,
|
|
1309
|
+
`marketplace.pricing.freemium`, `marketplace.pricing.paid`,
|
|
1310
|
+
`marketplace.pricing.subscription`, `marketplace.pricing.usage-based`
|
|
1311
|
+
|
|
1312
|
+
**The eleven individual leaves.** Each was held out of an earlier sweep by a
|
|
1313
|
+
LONGER live sibling that merely contained it, until PR objectui#8753 put a
|
|
1314
|
+
key-boundary requirement on the text probe. The sibling that actually renders is
|
|
1315
|
+
named beside each, and every one of them is unaffected by this change:
|
|
1316
|
+
|
|
1317
|
+
- `appDesigner.noNavItems` — renders as `appDesigner.noNavItemsHint`
|
|
1318
|
+
- `appDesigner.separator` — renders as `appDesigner.separatorLabel`
|
|
1319
|
+
- `console.objectView.groupBy` — renders as `console.objectView.groupByField`
|
|
1320
|
+
- `console.objectView.toolbar` — the live sibling is `…toolbarEnabledCount`
|
|
1321
|
+
- `dashboard.removeWidget` — the live string is the designer table's own
|
|
1322
|
+
`engine.inspector.dashboard.removeWidget`, a different corpus
|
|
1323
|
+
- `form.addItem` — likewise the designer table's `engine.form.addItem`
|
|
1324
|
+
- `home.recent` — renders as `home.recentApps.title`
|
|
1325
|
+
- `home.starred` — renders as `home.starredApps.title`
|
|
1326
|
+
- `home.subtitle` — the live sibling is `home.build.subtitle`
|
|
1327
|
+
- `sidebar.help` — renders as `sidebar.helpTooltip`
|
|
1328
|
+
- `workspace.create` — renders as `workspace.createTitle` / `…createButton`
|
|
1329
|
+
|
|
1330
|
+
**How the set was admitted.** Not from the card's body: `pnpm check:i18n-dead-keys`
|
|
1331
|
+
was re-run on this change's own base after the objectui#9222 instrument repair,
|
|
1332
|
+
and every key above is CONFIRMED by that run, or is NEEDS-REVIEW whose only hit is
|
|
1333
|
+
non-liveness (a changelog line; a pin test). The retirement is pinned negatively by
|
|
1334
|
+
`packages/i18n/src/__tests__/dead-pack-keys-retired-8754.test.ts`, because every
|
|
1335
|
+
other i18n gate in this repo runs call site → key and none of them can see a key
|
|
1336
|
+
coming back.
|
|
1337
|
+
- 804831c: A render-time filter refusal now renders a named "this view's filter is malformed" state instead of throwing out of render (objectui#9050).
|
|
1338
|
+
|
|
1339
|
+
`convertFiltersToAST` refuses eleven authored shapes with a `FilterOperatorError`, and
|
|
1340
|
+
`toFilterNode` delegates to it from four call sites that are not on the wire path:
|
|
1341
|
+
`RelatedList`, `LineItemsPanel` and `ObjectGrid` read it from a render-time `useMemo`
|
|
1342
|
+
(where a throw is a render error, with no `classifyLoadError` to turn it into "the
|
|
1343
|
+
filter is malformed"), and `ObjectGrid`'s deprecated `defaultFilters` leg reads it
|
|
1344
|
+
inside the load effect, which caught the refusal but reported it under "Error loading
|
|
1345
|
+
grid" over the converter's English paragraph.
|
|
1346
|
+
|
|
1347
|
+
What changes:
|
|
1348
|
+
|
|
1349
|
+
- `@object-ui/core` gains `toFilterNodeSafely`, whose result is a UNION — `{ ok: true,
|
|
1350
|
+
node }` or `{ ok: false, refusal }`. The refusal is deliberately NOT representable as
|
|
1351
|
+
`undefined`: `undefined` means "no filter", i.e. every row, which is the silently
|
|
1352
|
+
unconstrained query objectui#9001 closed. A caller must narrow before it can build a
|
|
1353
|
+
query.
|
|
1354
|
+
- `FilterOperatorError` now carries `operator` and `field` as data, and
|
|
1355
|
+
`filterRefusalSubject` picks the token a diagnostic names. The eleven messages
|
|
1356
|
+
deliberately share no idiom, so recovering that token by pattern would be a twelfth
|
|
1357
|
+
dialect. `operator` is the spelling the AUTHOR wrote, never a canonical form
|
|
1358
|
+
substituted for it; it is absent on exactly the two arms that judge a comparand
|
|
1359
|
+
written with no operator in it, where `field` is the only handle.
|
|
1360
|
+
- The three render-time readers keep the refusal as a value, send nothing, and render a
|
|
1361
|
+
state that names the operator. The subtree around them is unaffected — on the
|
|
1362
|
+
schema-rendered path a `SchemaErrorBoundary` already contained the throw, so what is
|
|
1363
|
+
new there is the DIAGNOSIS in place of a generic "Component failed to render"; mounted
|
|
1364
|
+
directly, which every one of these components supports through its package entry,
|
|
1365
|
+
there was no boundary at all and the throw reached the host.
|
|
1366
|
+
- `view.malformedFilter` is added to all ten locale packs and to the three affected
|
|
1367
|
+
`createSafeTranslation` tables.
|
|
1368
|
+
|
|
1369
|
+
⛔ Not changed: which shapes convert. The converter's accept set is byte-identical —
|
|
1370
|
+
this is a delivery change, not an acceptance one. Where the protocol's own parse accepts
|
|
1371
|
+
an input this layer refuses, that gap is the protocol's to close and is filed against
|
|
1372
|
+
`@objectstack/spec` rather than papered over here.
|
|
1373
|
+
|
|
1374
|
+
⚠️ **Dated note, 2026-09-27 — one more refusal carries no operator — objectui#9164.**
|
|
1375
|
+
Later in this same release `convertFiltersToAST` also refuses an empty operator map that
|
|
1376
|
+
is all a filter says (`{ a: {} }`). Its `FilterOperatorError` carries `field` and no
|
|
1377
|
+
`operator`, like the two comparand arms, so `filterRefusalSubject` names the field and
|
|
1378
|
+
the three render-time readers show the malformed-filter state for it. The counts above
|
|
1379
|
+
(eleven shapes, two arms without an operator) are this change's reading. The rest of this
|
|
1380
|
+
entry is kept as the reading of this change; the objectui#9164 entry states the new
|
|
1381
|
+
refusal.
|
|
1382
|
+
- 2bf34f7: The console's "app not available" screen now says what it measured, and the by-name app
|
|
1383
|
+
probe stopped folding four answers into one (objectui#9262).
|
|
1384
|
+
|
|
1385
|
+
**BREAKING for consumers of the published `AppAccessVerdict` type.** `probeAppAccess`
|
|
1386
|
+
widens by two members ruled by the maintainer: `granted | denied | unknown` becomes
|
|
1387
|
+
`granted | denied | not_found | unreachable | unknown`. A consumer that exhaustively
|
|
1388
|
+
switches on the verdict gains two cases. The meaning of `unknown` also narrows — it no
|
|
1389
|
+
longer means "an absent app, an unreachable server, or an adapter that cannot ask", it
|
|
1390
|
+
means only that nothing was asked (a host DataSource without this probe). Code that read
|
|
1391
|
+
`unknown` as a positive absence must move to `not_found`.
|
|
1392
|
+
|
|
1393
|
+
**What users see.** The sentence `This app is not available yet — it may still be
|
|
1394
|
+
publishing. Try again in a moment.` is retired from all ten locale packs. It was asserted
|
|
1395
|
+
for seven distinct causes, only one of which is a publish, and it is the screen an author
|
|
1396
|
+
lands on after any of them — the same defect objectui#4252 split `denied` off this screen
|
|
1397
|
+
for. In its place: `This app can't be opened` for a measured absence, `Couldn't reach the
|
|
1398
|
+
server` for a probe that got no answer, the unchanged denial screen for a permission
|
|
1399
|
+
refusal, and a neutral `App not available` when nothing was measured. All but the denial
|
|
1400
|
+
keep the Retry button, because all but the denial can change on their own. A genuine
|
|
1401
|
+
post-publish lag reads as an absence, and the one forced metadata refresh that gives that
|
|
1402
|
+
lag its chance to resolve is unchanged.
|
|
1403
|
+
|
|
1404
|
+
**Measured before the branches were written.** On a real server the by-name route answers
|
|
1405
|
+
`200` with the declared envelope MINUS its `item` for a name that does not exist — not the
|
|
1406
|
+
`404` this code assumed — so `probeAppAccess` returned `granted` for every typo, every
|
|
1407
|
+
never-created app and every unpublished draft. `granted` fell through to the same screen as
|
|
1408
|
+
`unknown`, which is why nothing showed. Absence is therefore read from the envelope as well
|
|
1409
|
+
as from the `404` code; a `not_found` branch keyed on the status alone would have been dead
|
|
1410
|
+
for the commonest case.
|
|
1411
|
+
- 15b33ae: Build history rows now state their item count in each language's own grammar
|
|
1412
|
+
(objectui#9266).
|
|
1413
|
+
|
|
1414
|
+
The commit timeline composed the count by concatenation — the row rendered the
|
|
1415
|
+
number itself and then asked the locale pack for a bare unit word to sit beside
|
|
1416
|
+
it. A pack that never sees the number cannot agree with it, so, measured through
|
|
1417
|
+
a real render at one item, six of the ten packs read `1 Elemente`,
|
|
1418
|
+
`1 éléments`, `1 elementos`, `1 itens`, `1 элементов` and `1 عناصر`.
|
|
1419
|
+
|
|
1420
|
+
`preview.history.items` now carries the count itself, one interpolated value per
|
|
1421
|
+
pack, and the call site passes `count` instead of printing the digit beside the
|
|
1422
|
+
translation. Each pack states the quantity in an idiom whose grammar does not
|
|
1423
|
+
bend on the number: `de` / `fr` / `es` / `pt` / `ru` / `ar` take the label-colon
|
|
1424
|
+
form (`Elemente: 3`, `Элементов: 3`), which is what
|
|
1425
|
+
`fields.textarea.charactersRemaining` already uses for this same reason.
|
|
1426
|
+
|
|
1427
|
+
`en`, `zh`, `ja` and `ko` render exactly what they rendered before, at every
|
|
1428
|
+
count — the three CJK packs because their grammar marks no plural, and `en`
|
|
1429
|
+
because its value already carried the count-invariant `(s)` marker. No copy that
|
|
1430
|
+
was correct has been changed.
|
|
1431
|
+
|
|
1432
|
+
A real `_one` / `_other` plural family was measured and not adopted: identical
|
|
1433
|
+
key sets across the ten packs leave `ru` without `_few` and `ar` without
|
|
1434
|
+
`_two` / `_many`, so those categories fall through to the base key and `ru` would
|
|
1435
|
+
still read `2 элементов` — wrong at the counts a build history shows most often.
|
|
1436
|
+
|
|
1437
|
+
The count is kept. Dropping the number (the route taken for `kanban.columns`,
|
|
1438
|
+
where the lanes are visible anyway) would delete information here rather than
|
|
1439
|
+
de-duplicate it.
|
|
1440
|
+
- 641fb55: The `FilterBuilder` dropdown speaks the protocol's operator ids (objectui#9306).
|
|
1441
|
+
|
|
1442
|
+
`defaultOperators` now emits the twenty members of `@objectstack/spec`'s
|
|
1443
|
+
`VIEW_FILTER_OPERATORS`, spelled as the spec spells them (`not_equals`,
|
|
1444
|
+
`greater_than_or_equal`, `is_null`, `icontains`, …), plus the two opt-in
|
|
1445
|
+
existence ids `exists` / `notExists`, which the protocol has no member for and
|
|
1446
|
+
which stay unfolded (objectui#9559 ruling B). The camelCase ids the dropdown used
|
|
1447
|
+
to emit (`notEquals`, `greaterOrEqual`, `isNull`, …) are the spec's deprecated
|
|
1448
|
+
alias form (objectui#7993); `containsCaseInsensitive` is the one former id the
|
|
1449
|
+
spec's alias table has no row for, and the builder reads it itself (below).
|
|
1450
|
+
|
|
1451
|
+
**Stored filters keep loading.** The builder folds a stored spelling at its read
|
|
1452
|
+
boundary, through the spec's `normalizeFilterOperator` plus one local row the
|
|
1453
|
+
spec's alias table lacks (`containsCaseInsensitive` → `icontains`,
|
|
1454
|
+
objectstack-ai/objectstack#20092), and the author's next edit writes the
|
|
1455
|
+
canonical id back. Opening a stored filter writes nothing. No row changes the
|
|
1456
|
+
predicate it stores: the sharing-rule criteria, the dataset filter, the saved-view
|
|
1457
|
+
fold, the live grid and the override recovery pass were each measured over all
|
|
1458
|
+
22 former ids against the ids they became, and store the same predicate. The only
|
|
1459
|
+
cells that differ are `icontains` on the three consumers that never offered the
|
|
1460
|
+
case-insensitive contains (dataset filter, saved-view fold, live grid), where
|
|
1461
|
+
the old id produced no storable filter at all and the new one does.
|
|
1462
|
+
|
|
1463
|
+
**Breaking, stated here because the group never takes a `major`:**
|
|
1464
|
+
|
|
1465
|
+
- `@object-ui/components`: the published `FilterBuilderOperator` type NARROWS
|
|
1466
|
+
from the camelCase union to the spec's `ViewFilterOperator` plus `'exists' |
|
|
1467
|
+
'notExists'` — a `'greaterOrEqual'` literal typed against it no longer
|
|
1468
|
+
compiles. `FILTER_BUILDER_OPERATORS` and `VALUELESS_FILTER_BUILDER_OPERATORS`
|
|
1469
|
+
hold the canonical ids, and a host's `onChange` receives them. New export:
|
|
1470
|
+
`normalizeFilterBuilderOperator`, the builder's read-side fold.
|
|
1471
|
+
- `@object-ui/components`: `icontains` ("Contains (ignore case)") is no longer
|
|
1472
|
+
opt-in. Its old reason — only the Mongo criteria dialect could carry a
|
|
1473
|
+
case-insensitive contains — stopped being true when `VIEW_FILTER_OPERATORS`
|
|
1474
|
+
gained `icontains` (spec 17.1.0), and `OPT_IN_OPERATORS`' own docblock recorded
|
|
1475
|
+
deleting the entry as the planned outcome. It is offered on the text bucket to
|
|
1476
|
+
every consumer; `contains` and `icontains` stay two operators (objectui#7379).
|
|
1477
|
+
- `@object-ui/i18n`: the `filterBuilder.operators.*` keys are re-keyed to the
|
|
1478
|
+
canonical ids in all ten packs (`filterBuilder.operators.is_null`, …); every
|
|
1479
|
+
translated value is unchanged. A host that overrides one of these keys must
|
|
1480
|
+
re-key its override.
|
|
1481
|
+
- `@object-ui/fields`: `FILTER_CONDITION_EXTRA_OPERATORS` is `['exists',
|
|
1482
|
+
'notExists']` — the case-insensitive contains needs no grant any more.
|
|
1483
|
+
`FilterConditionField` still writes `$icontains` for it, and its builder rows
|
|
1484
|
+
(`kvToCondition`) carry the canonical ids.
|
|
1485
|
+
- `@object-ui/plugin-view`: `toFilterGroup` emits the canonical ids.
|
|
1486
|
+
|
|
1487
|
+
Also in `@object-ui/app-shell`: the dataset inspector's bridge maps `icontains`
|
|
1488
|
+
to `$icontains`; the drill-down "is null" chip reads the re-keyed label; and the
|
|
1489
|
+
view-override recovery pass folds a row's operator before its value-less check,
|
|
1490
|
+
so a stored `{ operator: 'isEmpty', value: '' }` row is kept rather than dropped
|
|
1491
|
+
as unfinished.
|
|
1492
|
+
- c698a81: Render the environment admin's read-rate report from the usage endpoint's `readRate`
|
|
1493
|
+
reading (objectui#9954; maintainer ruling on cloud#2333, batch #164 item 3).
|
|
1494
|
+
|
|
1495
|
+
The tenant runtime's `GET /api/v1/usage/storage` gained one optional nested key,
|
|
1496
|
+
`readRate`, carrying the control plane's verdict (`state`), the ratio it measured
|
|
1497
|
+
(`readsPerWrite`) and the line that verdict was taken against (`ratioThreshold`). The
|
|
1498
|
+
data half landed on the cloud side; nothing in this repo consumed it, so a measured
|
|
1499
|
+
anomaly reached nobody. This is the rendering half.
|
|
1500
|
+
|
|
1501
|
+
**New:** `useReadRateReading` (a hook beside `useAiUsage`) and `ReadRateBanner` (a
|
|
1502
|
+
layout surface beside `ImpersonationBanner`, mounted in `ConsoleShell` so every console
|
|
1503
|
+
route including `/home` carries it). Neither is exported from the package entry; a host
|
|
1504
|
+
reaches the banner only through `ConsoleShell`, which mounts it.
|
|
1505
|
+
|
|
1506
|
+
Three properties of the contract shape the implementation, and each is pinned by a test:
|
|
1507
|
+
|
|
1508
|
+
- **An absent `readRate` is not "fine".** It means the control plane reported NO
|
|
1509
|
+
reading. The hook reports it as `unmeasured`, which is a different value from a
|
|
1510
|
+
measured `ok` and from an unreadable endpoint. All three render nothing, and the code
|
|
1511
|
+
keeps all three apart — "why does my environment show no banner" has more than one
|
|
1512
|
+
answer and one of them is *nobody has measured it*.
|
|
1513
|
+
- **An absent `readsPerWrite` is the worst case, not a missing number.** It means the
|
|
1514
|
+
environment made no writes at all, so the ratio has no upper bound. It gets its own
|
|
1515
|
+
title, its own sentence and the heavier tone — never a dash, and never a hidden
|
|
1516
|
+
banner.
|
|
1517
|
+
- **The threshold is data.** It is rendered from `ratioThreshold` on the wire; this repo
|
|
1518
|
+
holds no copy of the line, and the verdict is never re-derived from the ratio.
|
|
1519
|
+
|
|
1520
|
+
It is a **report**: no gate, no throttle, no upgrade call to action, and the copy says in
|
|
1521
|
+
as many words that nothing is limited or blocked. It is shown only to a workspace admin,
|
|
1522
|
+
who is also the only session that issues the request.
|
|
1523
|
+
|
|
1524
|
+
`@object-ui/i18n` gains the four `console.readRate.*` keys in all ten locale packs.
|
|
1525
|
+
- 77f846a: The approval panel identifies the pending approver by name, not by a truncated raw id.
|
|
1526
|
+
|
|
1527
|
+
A record waiting on a position rendered its approver as `positi…ager` — the
|
|
1528
|
+
engine reference `position:sales_manager`, 22 characters, past the identity
|
|
1529
|
+
formatter's 14-character truncation arm and middle-truncated to fit its chip. The
|
|
1530
|
+
step names beside it were human prose; the one line answering *who is holding
|
|
1531
|
+
this record* was an internal identifier, and not even a complete one. The same
|
|
1532
|
+
reference reached the admin-override confirm dialog un-truncated, so a paragraph
|
|
1533
|
+
of plain governance prose ended `— position:sales_manager`.
|
|
1534
|
+
|
|
1535
|
+
Both surfaces now resolve the reference before rendering, in three tiers, most
|
|
1536
|
+
authoritative first. The server's own `pending_approver_names` wins whenever it
|
|
1537
|
+
answers, and a backend that resolves its own slate costs the record page no extra
|
|
1538
|
+
request. Otherwise the console reads the directory row the spec's approver
|
|
1539
|
+
binding names — `sys_position.label` gives `Sales Manager` / `销售经理` — and,
|
|
1540
|
+
for a position, who fills the seat (`Sales Manager · Zhang Wei, Li Na`). With no
|
|
1541
|
+
adapter and no row, the machine name still prettifies into prose rather than
|
|
1542
|
+
truncating. The raw reference stays on hover, which is where an internal
|
|
1543
|
+
identifier belongs.
|
|
1544
|
+
|
|
1545
|
+
An unstaffed position is surfaced rather than hidden: `销售经理(暂无在岗人员)`
|
|
1546
|
+
is actionable where `positi…ager` is not, and it is the motivating rescue case
|
|
1547
|
+
for the admin-override path. Staffing is deliberately tri-state — a
|
|
1548
|
+
`sys_user_position` read the viewer is not permitted to make leaves the seat's
|
|
1549
|
+
staffing UNKNOWN and says nothing, because "I could not look" is a different
|
|
1550
|
+
claim from "nobody holds it" and only one of them is safe to print on a
|
|
1551
|
+
governance surface.
|
|
1552
|
+
|
|
1553
|
+
Two locale keys are added across all ten packs: `approvalsInbox.approverUnstaffed`
|
|
1554
|
+
and `approvalsInbox.approverNameSeparator`. The separator is a translated
|
|
1555
|
+
punctuation key rather than `Intl.ListFormat`, which was measured on this tree
|
|
1556
|
+
joining `['张伟','李娜']` into `张伟李娜` for `zh` — two names run together with
|
|
1557
|
+
no separator, reading as one person's name.
|
|
1558
|
+
|
|
1559
|
+
The directory-backed kinds and their value columns are read from
|
|
1560
|
+
`@objectstack/spec`'s `APPROVER_VALUE_SOURCES` rather than restated, so a new
|
|
1561
|
+
approver type is covered the day the spec publishes it. Id-valued kinds
|
|
1562
|
+
(`user` / `team` / `department`) keep the existing middle-truncation: a row id
|
|
1563
|
+
has no prose to recover, and that arm is objectui#3461's answer, not this card's
|
|
1564
|
+
defect.
|
|
1565
|
+
- c40f3b8: A screen flow's resume result reaches the user — on both outcomes.
|
|
1566
|
+
|
|
1567
|
+
A dogfood walkthrough reported that a refused `resume` and a successful one
|
|
1568
|
+
"render identically: the dialog closes and the page is unchanged", leaving no
|
|
1569
|
+
gesture that distinguishes "created" from "rejected". Re-measured against `main`
|
|
1570
|
+
before any change, one half of that was already fixed — `interpretFlowResponse`
|
|
1571
|
+
reads the ADR-0112 envelope, and `FlowRunner`'s `toast.error` has carried its
|
|
1572
|
+
prose since the `400 FLOW_FAILED` classification landed in `17.6.0`, five minors
|
|
1573
|
+
after the version the report was measured on. There was no interpreter bug and
|
|
1574
|
+
no un-consolidated fourth call site. Three gaps in the RUNNER's disposition were
|
|
1575
|
+
real, and they are what changed:
|
|
1576
|
+
|
|
1577
|
+
- **A terminal failure no longer closes the dialog.** The reason it closed is
|
|
1578
|
+
unchanged and is not reversed: on a `FLOW_FAILED` the engine has already
|
|
1579
|
+
consumed the suspension, so a resubmit can only reach "No suspended run" and
|
|
1580
|
+
must not be offered. Closing was one way to withhold that dead retry and the
|
|
1581
|
+
expensive one — the user had just typed a form they could no longer see, and
|
|
1582
|
+
the engine's sentence names a value that left the screen with it. The dialog
|
|
1583
|
+
now stays open with the submit affordance withdrawn: the flat footer swaps
|
|
1584
|
+
Submit for Close, and an `object-form` step drops its Save (which also stops a
|
|
1585
|
+
second click from duplicating the record it had already persisted).
|
|
1586
|
+
- **The refusal has a second, non-expiring carrier.** The toast stays — it is
|
|
1587
|
+
viewport-fixed, so it still reaches a user scrolled past a tall step's header
|
|
1588
|
+
— and an inline destructive `Alert` (`role="alert"`) now holds the same
|
|
1589
|
+
sentence inside the dialog, beside the values that produced it. A retryable
|
|
1590
|
+
refusal (`INVALID_SCREEN_INPUT`, transport, 5xx) keeps Submit live as before,
|
|
1591
|
+
and its banner clears as soon as the user starts editing.
|
|
1592
|
+
- **A successful run invalidates what the flow WROTE, not just what the user is
|
|
1593
|
+
looking at.** Both hosts answered `onComplete` with
|
|
1594
|
+
`notifyDataChanged({ objectName: <this page's object> })`, so a flow that
|
|
1595
|
+
created a quote from an Opportunity page never told the related list that
|
|
1596
|
+
would now contain it — the record did not appear until a manual reload. The
|
|
1597
|
+
runner cannot know which objects a flow touched, so it emits
|
|
1598
|
+
`{ objectName: '*' }`: the same scope, for the same stated reason, that the
|
|
1599
|
+
record page's manual ⟳ already uses. Everything mounted refetches in place
|
|
1600
|
+
over the invalidation bus, with no remount.
|
|
1601
|
+
|
|
1602
|
+
The runner's copy now goes through `@object-ui/i18n` instead of being hardcoded
|
|
1603
|
+
English: a new `flowRunner` namespace (`title`, `submitting`, `saveAndContinue`,
|
|
1604
|
+
`nextStep`, `completed`) in all ten packs, plus reuse of
|
|
1605
|
+
`common.{loading,cancel,close,submit}` and `wizard.missingRequired`. The
|
|
1606
|
+
server's own refusal sentence is still passed through untranslated — it is prose
|
|
1607
|
+
the automation engine composed for a human, not copy with a key.
|
|
1608
|
+
- 6a576c9: Announce inbox messages when they arrive — an in-app toast while the tab is visible, a
|
|
1609
|
+
desktop notification while it is hidden (objectui#7011).
|
|
1610
|
+
|
|
1611
|
+
The inbox was completely silent about arrivals. `sharedUserFeeds` polls
|
|
1612
|
+
`sys_inbox_message` every 10s, the rows landed in the store, the bell badge counted
|
|
1613
|
+
them — and a user not staring at the bell learned nothing, so approvals and @-mentions
|
|
1614
|
+
were routinely missed. Three candidate popup paths existed and none was connected to the
|
|
1615
|
+
inbox: the feed had no diff logic, the console's sonner bridge only serves notifications
|
|
1616
|
+
that explicitly declare `displayType: 'toast'`, and there was no `new Notification(` call
|
|
1617
|
+
anywhere in `packages/` or `apps/`.
|
|
1618
|
+
|
|
1619
|
+
**Presentation layer only.** The transport is untouched: the same two reads, the same
|
|
1620
|
+
10s / 60s cadence, the same backoff, and no push channel. The accepted consequence is
|
|
1621
|
+
that a backgrounded tab can be up to a minute late — speeding the poll up to shave that
|
|
1622
|
+
would trade a server-wide cost for one surface's latency.
|
|
1623
|
+
|
|
1624
|
+
What arrives:
|
|
1625
|
+
|
|
1626
|
+
- **`useInboxArrivalNotifier`**, mounted from `useInboxBell` — the one wiring of the
|
|
1627
|
+
shared feed onto a bell — so the header bell and the `global:notifications` page block
|
|
1628
|
+
announce by the same rules and through the same `markRead`.
|
|
1629
|
+
- **`inboxArrivals`**, the pure diff: a session-scoped seen set, `(topic, title)`
|
|
1630
|
+
collapse reused from the inbox's own `groupNotifications`, and a bounded memory.
|
|
1631
|
+
- **`desktopNotifications`**, the single door to the browser Notification API.
|
|
1632
|
+
- **Two switches** in the account menu's Preferences section, stored per user in
|
|
1633
|
+
localStorage: in-app alerts (on by default) and desktop notifications (off).
|
|
1634
|
+
|
|
1635
|
+
Four rules decide when nothing happens, and they matter more than the positive case — an
|
|
1636
|
+
announcer that pops for everything is worse than the silence it replaces, because users
|
|
1637
|
+
switch it off and then miss the approvals too:
|
|
1638
|
+
|
|
1639
|
+
- the FIRST answered read primes the seen set and announces nothing, so historical unread
|
|
1640
|
+
at login or after a refresh updates the badge only;
|
|
1641
|
+
- several rows in one cycle announce once, collapsed by `(topic, title)`;
|
|
1642
|
+
- a row that already carries a read receipt never announces;
|
|
1643
|
+
- a hidden tab gets the desktop notification and no toast; a visible tab gets the toast
|
|
1644
|
+
and no desktop notification.
|
|
1645
|
+
|
|
1646
|
+
**`Notification.requestPermission()` is called from the settings toggle's change handler
|
|
1647
|
+
and from nowhere else** — never on mount, on a feed refresh, or on a first message. A
|
|
1648
|
+
browser answers that prompt once and `denied` is permanent for the origin, so a
|
|
1649
|
+
load-time request spends the channel for every user who reflexively blocks, and no later
|
|
1650
|
+
release can undo it. A browser that has not granted permission behaves exactly as it did
|
|
1651
|
+
before this change: completely silent.
|
|
1652
|
+
|
|
1653
|
+
Seven `notifications.*` keys are added to all ten locale packs.
|
|
1654
|
+
- fffa30d: `listViews` no longer renders a refused metadata read as "this object has no saved views"
|
|
1655
|
+
(objectui#8151).
|
|
1656
|
+
|
|
1657
|
+
`ObjectStackAdapter.listViews` degrades every failure to an empty list, and every consumer
|
|
1658
|
+
reads only the return. So "the server served zero saved views" and "the server refused, or
|
|
1659
|
+
broke" produced the identical UI — with a `console.warn` as the only discriminator, in the
|
|
1660
|
+
browser console, with nothing pointing at it. It is the same defect objectui#7741 removed
|
|
1661
|
+
from `listImportMappings` one method over, and the user-visible cost is the higher one: an
|
|
1662
|
+
empty `listViews` is an object's **view switcher**, so a user whose token lapsed
|
|
1663
|
+
mid-session could be shown an object that appears to have no saved views at all —
|
|
1664
|
+
including views they created themselves.
|
|
1665
|
+
|
|
1666
|
+
**The empty-list return is unchanged.** `listViews` still answers `Promise<any[]>` and
|
|
1667
|
+
still never throws, on every arm including the loud ones — this is a channel added
|
|
1668
|
+
ALONGSIDE that contract, not a change to it. `listImportMappings` is likewise unchanged,
|
|
1669
|
+
down to its wording.
|
|
1670
|
+
|
|
1671
|
+
- **`ObjectStackAdapter.listViews` now emits on `onMetadataReadWarning`** — the channel
|
|
1672
|
+
objectui#7741 added — when the read failed in a way that is not the supported "this host
|
|
1673
|
+
mounted no metadata door" shape. The event carries the object, whether the server
|
|
1674
|
+
`refused` this caller or the answer was `unreadable`, and the server's own ADR-0112
|
|
1675
|
+
code, HTTP status and message.
|
|
1676
|
+
- **New: `classifyViewsFailure(err)`.** A SEPARATE reading, deliberately not a second
|
|
1677
|
+
caller of `classifyImportMappingsFailure`: `view`'s quiet set is strictly smaller. The
|
|
1678
|
+
arm the mapping classifier is built around — 400 `INVALID_REQUEST`, the metadata list
|
|
1679
|
+
door's "this deployment carries no such kind" — is unreachable for `view`, which is in
|
|
1680
|
+
the platform's static spelling contract, so reading it as kind-absence would swallow a
|
|
1681
|
+
real refusal. On `view`, only a host with no `/meta` door at all stays quiet.
|
|
1682
|
+
- **`MetadataReadWarningEvent`'s `operation` and `kind` gain their second members**
|
|
1683
|
+
(`'listViews'` / `'view'`). This is the additive, reviewed widening the single-member
|
|
1684
|
+
unions were designed for, and it worked as designed: the consumer that renders these
|
|
1685
|
+
events had a `switch` naming one operation, so the widening turned "a views failure is
|
|
1686
|
+
toasted as an import-mapping failure" into a compile error rather than a runtime lie.
|
|
1687
|
+
- **New: `MetadataReadFailureKind`**, the neutral spelling of the three verdicts.
|
|
1688
|
+
`ImportMappingsFailureKind` is now an alias of it — identical members, so existing
|
|
1689
|
+
consumers are unaffected in both directions.
|
|
1690
|
+
- **The console says which list it was.** `metadataReadWarningToast` picks its title and
|
|
1691
|
+
its remedy by `operation`, so a failed view read reads *"Saved views for {{object}}
|
|
1692
|
+
could not be loaded … not because this object has no saved views"*. Three new
|
|
1693
|
+
`console.savedViews*` keys ship in all ten locale packs; the `console.importMappings*`
|
|
1694
|
+
copy is untouched.
|
|
1695
|
+
|
|
1696
|
+
This applies framework objectstack#13906 decision 1 option A — *a thing that could not be READ is not
|
|
1697
|
+
a thing that is ABSENT* — at the second seam that needed it.
|
|
1698
|
+
- 20e317c: Marketplace-less runtimes now say so instead of erroring: `OS_CLOUD_URL=off` is a
|
|
1699
|
+
first-class disabled state, and the load-failure hint describes the control plane
|
|
1700
|
+
the runtime was actually pointed at (objectui#5504).
|
|
1701
|
+
|
|
1702
|
+
`apps/objectos-ee/deploy/.env.example` ships `OS_CLOUD_URL=off` as its factory
|
|
1703
|
+
default, so a stock self-hosted stack has no marketplace at all. The Console still
|
|
1704
|
+
recommended one: Home led with "Start with a template" and "Browse App
|
|
1705
|
+
Marketplace", and the click landed on a red **Failed to load marketplace / Not
|
|
1706
|
+
found** card whose hint claimed this runtime "points at the public ObjectStack
|
|
1707
|
+
cloud by default" and advised setting `OS_CLOUD_URL`. Both claims were false for
|
|
1708
|
+
exactly the deployment reading them — the operator had not left the default, and
|
|
1709
|
+
the advice pointed back at the template that told them to set `off`. "Marketplace
|
|
1710
|
+
disabled by configuration" is a configuration conclusion, not a load failure.
|
|
1711
|
+
|
|
1712
|
+
- `isMarketplaceEnabled()` (`runtime-config`) reads the server's own
|
|
1713
|
+
`features.marketplace`, which `RuntimeConfigPlugin` derives per request from the
|
|
1714
|
+
serving app's route table (objectstack#8356). It is never inferred from the shape
|
|
1715
|
+
of a failed request: a control plane that is merely DOWN leaves the flag `true`,
|
|
1716
|
+
so an outage still renders as an outage. Unknown fails OPEN.
|
|
1717
|
+
- The marketplace page renders an informational "App Marketplace is turned off"
|
|
1718
|
+
state — muted, not `destructive` — and issues no request it knows will 404.
|
|
1719
|
+
- Home's "Start with a template" cover greys out with a visible localized reason,
|
|
1720
|
+
and the "Browse App Marketplace" shortcut is withheld, exactly as they already
|
|
1721
|
+
are for the `manage_metadata` capability gate.
|
|
1722
|
+
- `marketplace.load.failedHint` is replaced by `failedHintConfigured` (naming the
|
|
1723
|
+
configured control plane) and `failedHintSameOrigin`. The "points at the public
|
|
1724
|
+
cloud by default" sentence is gone: it was rendered unconditionally, including on
|
|
1725
|
+
every runtime whose operator had overridden `OS_CLOUD_URL`.
|
|
1726
|
+
|
|
1727
|
+
All ten locale packs carry the new keys.
|
|
1728
|
+
- 19cbf10: fix(app-shell): the report inspector no longer destroys a localized title or label
|
|
1729
|
+
|
|
1730
|
+
`ReportChartSchema.title` and `ReportSchema.label` are `I18nLabel` — a plain
|
|
1731
|
+
string **or** an inline per-locale map. The report inspector narrowed both to
|
|
1732
|
+
the string arm when reading and wrote the typed string back over the whole
|
|
1733
|
+
value, so against a stored map the two halves failed in opposite directions:
|
|
1734
|
+
Studio painted an empty box over a report that has a title, and the retype that
|
|
1735
|
+
empty box invited replaced the map with a bare string, losing every other
|
|
1736
|
+
locale. Committing the empty box untouched deleted the stored map outright.
|
|
1737
|
+
|
|
1738
|
+
Reads now go through `pickLocalized` and writes through `setLocalized`, so an
|
|
1739
|
+
edit lands in the active locale's entry and every other locale survives. An
|
|
1740
|
+
author shown a display fallback from another locale can neither overwrite nor
|
|
1741
|
+
delete the locale it was borrowed from.
|
|
1742
|
+
|
|
1743
|
+
**Clearing is ruled, not inherited.** Clearing the box removes only the active
|
|
1744
|
+
locale's entry; when that was the last entry the key is dropped entirely, which
|
|
1745
|
+
is byte-for-byte what clearing a plain-string value already did.
|
|
1746
|
+
|
|
1747
|
+
**What an author who does nothing differently now gets:** a report whose title
|
|
1748
|
+
or label is a plain string behaves exactly as before — same box, same committed
|
|
1749
|
+
value, including on clear. A report whose title or label is a locale map now
|
|
1750
|
+
shows their own locale's string instead of a blank box, and editing it keeps
|
|
1751
|
+
every other language instead of deleting it.
|
|
1752
|
+
|
|
1753
|
+
`@object-ui/i18n` gains `clearLocalized`, the clear arm of the same
|
|
1754
|
+
single-locale editor `setLocalized` serves. Additive: no existing export
|
|
1755
|
+
changed shape.
|
|
1756
|
+
- 8033ad1: The built-in record-detail tab labels and the last two English landmark names now speak the session locale (objectui#4645).
|
|
1757
|
+
|
|
1758
|
+
`buildDefaultPageSchema` synthesizes the record-detail tab strip with plain
|
|
1759
|
+
English tokens on the nodes (`Details`, `Related`, `Attachments`, `Activity`,
|
|
1760
|
+
`History`, `Approvals`), and three of its own comments said those tokens
|
|
1761
|
+
"localize through the tab strip's `KNOWN_LABEL_DICT`". That dict shipped
|
|
1762
|
+
exactly two arms — `zh-CN` and `zh-TW` — so the claim held for Chinese and
|
|
1763
|
+
silently failed for the eight other shipped packs: a ja-JP or es-ES record
|
|
1764
|
+
detail rendered `Details / Related / Attachments` inside otherwise fully
|
|
1765
|
+
localized chrome, measured in a real browser on `@objectstack/console` 17.0.0
|
|
1766
|
+
GA and re-measured red on current `main`.
|
|
1767
|
+
|
|
1768
|
+
Every one of those strings was already in all ten packs (`detail.details`,
|
|
1769
|
+
`detail.related`, `detail.activity`, `detail.history`, `detail.attachments`,
|
|
1770
|
+
`detail.approvalsPanelTitle`); the strip simply never asked. It asks now,
|
|
1771
|
+
BEHIND the exact-locale dict, which stays first because `zh-TW` has no pack of
|
|
1772
|
+
its own — i18next resolves it to the Simplified `zh` resource, so the dict is
|
|
1773
|
+
the only source of the Traditional forms — and because it carries tokens
|
|
1774
|
+
(`Notes`, `Files`, `Tasks`, `Events`, object names) that no pack does. `en` and
|
|
1775
|
+
`zh` render byte-identically to before; the other eight locales change from the
|
|
1776
|
+
English token to their pack value. `page:accordion` reads the same lookup, so
|
|
1777
|
+
the two renderers cannot answer differently for one token.
|
|
1778
|
+
|
|
1779
|
+
Alongside it, the two `aria-label`s that were English in *every* locale —
|
|
1780
|
+
`HeaderHighlight`'s `Record highlights` and `RecordActivityTimeline`'s
|
|
1781
|
+
`Discussion` — no longer read English. `HeaderHighlight`'s now routes through
|
|
1782
|
+
the bundle. Its section carries no visible label, so the `aria-label` is the
|
|
1783
|
+
landmark as far as assistive tech is concerned (the argument objectui#4024 made
|
|
1784
|
+
for the dialog `Close` label, and objectui#5956 made for `record:path`'s own
|
|
1785
|
+
container name). `RecordActivityTimeline`'s section always had a visible
|
|
1786
|
+
heading, so it no longer has a fixed `Discussion` name. Its landmark is named by
|
|
1787
|
+
that heading's title: `detail.activity`, or an authored `titleLabel` such as
|
|
1788
|
+
`record:chatter`'s `detail.discussion` (objectui#9998). `detail.activity`
|
|
1789
|
+
already existed in all ten packs; `detail.highlightsLabel` is the single new
|
|
1790
|
+
key, added to all ten and mirrored byte-identically into
|
|
1791
|
+
`DETAIL_DEFAULT_TRANSLATIONS`.
|
|
1792
|
+
- f157423: Studio workbench and AI tool cards speak the author's language (objectui#7254)
|
|
1793
|
+
|
|
1794
|
+
- The Interfaces breadcrumb, canvas caption and navigation rail show the
|
|
1795
|
+
metadata label plus a translated kind; the internal `type · name` pair moves
|
|
1796
|
+
to the tooltip. An unlabelled nav leaf now falls back to its object name
|
|
1797
|
+
instead of rendering an empty row.
|
|
1798
|
+
- The Studio top-bar package switcher reads the package's human name from
|
|
1799
|
+
either position the packages endpoint serves it in, instead of degrading a
|
|
1800
|
+
registry-shaped entry to its reverse-domain id.
|
|
1801
|
+
- The dashboard property panel is localized: the spec's authoring form is
|
|
1802
|
+
overlaid through the platform's own `metadataForms.<type>` convention, so
|
|
1803
|
+
section headings, field labels, hints and the `header` composite's sub-fields
|
|
1804
|
+
render in Chinese (developer vocabulary such as "Tailwind units" is replaced
|
|
1805
|
+
with something an author can act on, not transliterated).
|
|
1806
|
+
- AI tool cards: tool titles resolve through `chatbot.tool.<name>` (all thirty
|
|
1807
|
+
platform-provided tools, ten locale packs), the header status badge is
|
|
1808
|
+
localized, and the plan count strip is a real plural family instead of an
|
|
1809
|
+
English `+ "s"` concatenation.
|
|
1810
|
+
- The tool card's header badge and its body badge now come from one producer:
|
|
1811
|
+
a proposal that has been confirmed, built or published no longer keeps a
|
|
1812
|
+
header reading "Awaiting Approval".
|
|
1813
|
+
- 7a90afd: Studio's `新建对象` asks for the record-sharing baseline, and an unauthored one is reported before Publish rather than by it.
|
|
1814
|
+
|
|
1815
|
+
Creating an object through Studio collected exactly two things — display name and
|
|
1816
|
+
identifier — and saved a draft that declared no `sharingModel`. The draft saved
|
|
1817
|
+
happily, the form designer worked, and the object was then refused at 发布 →
|
|
1818
|
+
全部发布 by `security-owd-unset`: a required decision the surface never asked
|
|
1819
|
+
for, delivered by failing, as English ADR prose in a toast that then vanished on
|
|
1820
|
+
a timer. The one actionable word in it named a control three clicks away that
|
|
1821
|
+
nothing routed to.
|
|
1822
|
+
|
|
1823
|
+
The publish gate is correct and is unchanged — an org-wide default has to be an
|
|
1824
|
+
authored decision, not an accident. What changes is when the console asks and
|
|
1825
|
+
when it answers:
|
|
1826
|
+
|
|
1827
|
+
- **The create dialog asks.** A third field collects the baseline, pre-selected
|
|
1828
|
+
to `private` and glossed with the Settings tab's own strings, so a new object
|
|
1829
|
+
is publishable by construction. `buildObjectSkeleton` now takes the value as a
|
|
1830
|
+
required parameter — a future create path cannot omit the baseline without
|
|
1831
|
+
failing to type-check. `controlled_by_parent` is deliberately not offered at
|
|
1832
|
+
creation: it derives access from a master relation a brand-new object does not
|
|
1833
|
+
have yet, so offering it would trade one publish refusal for another.
|
|
1834
|
+
- **The review sheet reports it.** The pending-changes panel now runs the
|
|
1835
|
+
framework's own `validateSecurityPosture` over the pending object drafts and
|
|
1836
|
+
names any blocking finding, with its fix-it hint, next to the Publish button.
|
|
1837
|
+
It mirrors the producer's rule rather than re-deriving it, and it reports
|
|
1838
|
+
without blocking — the server door stays the authority.
|
|
1839
|
+
- **The Settings tab stops calling an unset baseline safe.** It described unset
|
|
1840
|
+
as "defaults to Private", which answers what the runtime does and not whether
|
|
1841
|
+
the object can ship. It now reads as the publish-blocking problem it is,
|
|
1842
|
+
styled like the external-wider warning beside it.
|
|
1843
|
+
- fb96ecb: `WidgetConfigPanel` reads an inline-locale-map title, and a save no longer destroys the other locales.
|
|
1844
|
+
|
|
1845
|
+
The dashboard widget config panel carried a private `resolveLabel` documented as
|
|
1846
|
+
resolving an `I18nLabel` while reading `defaultValue || key` — the key-reference
|
|
1847
|
+
form `@objectstack/spec` retired at 17.0.0-rc.6 (objectstack#5055). The inline
|
|
1848
|
+
per-locale map `I18nLabelSchema` actually admits has neither limb, so
|
|
1849
|
+
`{ en: 'Revenue', zh: '收入' }` resolved to `''`. It was the fourth private copy
|
|
1850
|
+
of that resolver; objectui#4032 swept the other three out of `DashboardRenderer`,
|
|
1851
|
+
`MetricWidget` and `MetricCard`.
|
|
1852
|
+
|
|
1853
|
+
This was not a display bug. The resolved value seeds the panel's editable draft,
|
|
1854
|
+
so a widget whose stored title was a map opened with an **empty** Title field and
|
|
1855
|
+
the next save wrote `''` over the author's map — on the ordinary path, not an
|
|
1856
|
+
exotic one: open the widget, change anything, save.
|
|
1857
|
+
|
|
1858
|
+
Both halves are fixed, per the maintainer's 2026-08-20 ruling on objectui#5301:
|
|
1859
|
+
|
|
1860
|
+
- **Reading** goes through `pickLocalized(value, language)`, so the panel shows
|
|
1861
|
+
the active locale like every sibling surface post-objectui#4032.
|
|
1862
|
+
- **Writing** replaces only the active locale's entry and carries every other
|
|
1863
|
+
locale across. A title the author never touched round-trips the stored object
|
|
1864
|
+
itself through an unrelated config edit; an edited one merges into the entry
|
|
1865
|
+
that was displayed. The live-update callback (`onFieldChange`) forwards the
|
|
1866
|
+
merged map for the same reason — hosts feed it back into the widget the panel
|
|
1867
|
+
re-opens from, so a bare string there dropped the map before a save ever ran.
|
|
1868
|
+
|
|
1869
|
+
`@object-ui/i18n` gains `setLocalized(value, language, next)`, the write-side
|
|
1870
|
+
inverse of `pickLocalized`, so the rule is stated once instead of re-derived per
|
|
1871
|
+
panel. It follows `pickLocalized`'s first three limbs — exact tag, base language,
|
|
1872
|
+
region-qualified sibling — and deliberately stops there: the `default` / `en` /
|
|
1873
|
+
first-value limbs are display fallbacks that hand back *another* locale's string,
|
|
1874
|
+
and writing to one would let an author editing in `fr` overwrite English. With no
|
|
1875
|
+
entry for the active locale the edit adds one. The pairing
|
|
1876
|
+
`pickLocalized(setLocalized(map, lang, s), lang) === s` is pinned, because a
|
|
1877
|
+
write that lands where the read does not look is how a "saved" string disappears.
|
|
1878
|
+
|
|
1879
|
+
A full multi-locale editing UI remains out of scope (objectui#4163).
|
|
1880
|
+
|
|
1881
|
+
### Patch Changes
|
|
1882
|
+
|
|
1883
|
+
- a507334: Full-page search results now read correctly in Russian and Arabic at every
|
|
1884
|
+
count (objectui#10024).
|
|
1885
|
+
|
|
1886
|
+
The results line picks `search.resultsCount` at exactly one result and
|
|
1887
|
+
`search.resultsCountPlural` at every other count. Two slots cover English; they do
|
|
1888
|
+
not cover Russian, which needs three noun forms for whole numbers, or Arabic, which
|
|
1889
|
+
needs more. Russian's second slot held the form for 5 to 20, so two results read
|
|
1890
|
+
`2 результатов` (the language needs `2 результата`) and twenty-one read
|
|
1891
|
+
`21 результатов` (it needs `21 результат`). Arabic's two slots were the same string
|
|
1892
|
+
byte for byte, so the switch gave Arabic nothing at 2 or at 3 to 10.
|
|
1893
|
+
|
|
1894
|
+
In both packs the count-not-one half is now a count label that reads correctly at
|
|
1895
|
+
any number: `Результатов по запросу "…": N` and `عدد نتائج البحث عن "…": N`. That is
|
|
1896
|
+
the device both packs already use where two slots are all there are — Russian's
|
|
1897
|
+
`collaboration.commentCount` (`Комментариев: N`) is the same shape on the same
|
|
1898
|
+
two-key convention. The singular half, the call site, the key set and the other
|
|
1899
|
+
eight packs are unchanged; `zh`, `ja` and `ko` keep one string in both halves
|
|
1900
|
+
because their languages have a single plural category.
|
|
1901
|
+
|
|
1902
|
+
⚠️ **Dated note, 2026-10-02 — superseded in this release — objectui#11445.**
|
|
1903
|
+
Later in this same release the `search.resultsCount` / `search.resultsCountPlural`
|
|
1904
|
+
switch gave way to a `search.resultsCount` count family in all ten packs:
|
|
1905
|
+
`ru` and `ar` now write a noun form for each CLDR category instead of the count
|
|
1906
|
+
label above (`2 результата`, `21 результат`), and `search.resultsCountPlural` left
|
|
1907
|
+
every pack. The rest of this entry is kept as the reading of this change.
|
|
1908
|
+
- afb2284: `ObjectCalendar`'s user-visible copy now reaches the locale packs. The component
|
|
1909
|
+
was already i18n-aware — it imports and calls both translation hooks — and a set
|
|
1910
|
+
of English sentences sat beside those calls as literals, so a non-English session
|
|
1911
|
+
read a half-translated calendar: the unscheduled label and the two write-failure
|
|
1912
|
+
toasts in its own language, and the loading screen, the error screen, the refusal
|
|
1913
|
+
screen, the pull-to-refresh affordance, the whole quick-create dialog and the
|
|
1914
|
+
record overlay's fallback title in English (objectui#10031).
|
|
1915
|
+
|
|
1916
|
+
Every one of those now reads a key. Two of them reuse keys the packs already
|
|
1917
|
+
carried (`common.cancel`, `common.create`), one reuses `calendar.newEvent`, and
|
|
1918
|
+
twelve are new under `calendar.`.
|
|
1919
|
+
|
|
1920
|
+
**The hook is chosen per site by one criterion, which the file already stated for
|
|
1921
|
+
its existing call:** `useSafeTranslate`'s `tt(key, fallback)` passes no options to
|
|
1922
|
+
i18next and therefore cannot fill a hole, so the two sentences that carry one —
|
|
1923
|
+
the error prefix and the quick-create date line — read their key through
|
|
1924
|
+
`useObjectTranslation`'s `t()` with an inline default, and every hole-free string
|
|
1925
|
+
uses `tt()`.
|
|
1926
|
+
|
|
1927
|
+
**⚠️ A key added to `en` is not an `en`-only change in this repo**, and that is
|
|
1928
|
+
worth stating because the cheap route looks available: the English fallback does
|
|
1929
|
+
carry the English text at the call site, but the key-set invariant is a
|
|
1930
|
+
pack-vs-pack one — every pack defines every `en` key — so it is answered by the
|
|
1931
|
+
locale packs, not by the call site. All ten packs therefore carry the twelve new
|
|
1932
|
+
keys, with real translations rather than the English value. The instrument that
|
|
1933
|
+
says so is `all-locales-key-parity.test.ts`; `pnpm check:i18n-drift` prints which
|
|
1934
|
+
of the two owns an addition.
|
|
1935
|
+
|
|
1936
|
+
**One rendered English string moved**: the loading placeholder's three ASCII full
|
|
1937
|
+
stops became the typographic ellipsis, because its sentence is now a pack value
|
|
1938
|
+
and `ellipsis-glyph-3878.test.ts` holds every pack value to U+2026. The refusal
|
|
1939
|
+
screen's copy is byte-identical to what it rendered before, which is what keeps
|
|
1940
|
+
the five suites that pin it green.
|
|
1941
|
+
- 3ac2de8: Localize the Studio front door's wordmark tooltip (objectui#10043).
|
|
1942
|
+
|
|
1943
|
+
The `/studio` landing's wordmark carried its `title` tooltip as a hard-coded literal
|
|
1944
|
+
in one language, outside i18n entirely — the only user-visible string on that surface
|
|
1945
|
+
that never reached a translation bundle, so every other locale read it in that one
|
|
1946
|
+
language. AGENTS.md commandment #-1 names titles in the categories it covers.
|
|
1947
|
+
|
|
1948
|
+
It now resolves through `useObjectTranslation` and a new `console.studio.backToHome`
|
|
1949
|
+
key, which is the mechanism the rest of this app's chrome already uses (the same route
|
|
1950
|
+
objectui#4024 took for the settings screen, whose chrome was hard-coded beside a keyed
|
|
1951
|
+
sibling). The key ships in all ten locale packs, and each pack's value is that pack's
|
|
1952
|
+
own existing wording for this affordance rather than a new translation — the sibling
|
|
1953
|
+
Home button one route away inside the same frame says the same thing, and the two must
|
|
1954
|
+
not name the same home two different ways.
|
|
1955
|
+
- 63f4f92: A signed-in user's language has one source of truth: `sys_user.locale`.
|
|
1956
|
+
|
|
1957
|
+
One word 「语言」 named two settings. The profile page's language card wrote the
|
|
1958
|
+
server column `sys_user.locale`; the console's globe menu switched a
|
|
1959
|
+
device-local UI language in `localStorage`. Neither writer could see the other,
|
|
1960
|
+
so a user who chose 日本語 on their profile went on reading an English console,
|
|
1961
|
+
with no refusal and no notice — and the profile card's own description said the
|
|
1962
|
+
two were separate, which was true and is what this change ends.
|
|
1963
|
+
|
|
1964
|
+
For a signed-in user the column now decides, and the wiring is what makes that
|
|
1965
|
+
hold rather than anyone's discipline:
|
|
1966
|
+
|
|
1967
|
+
- the globe writes `sys_user.locale` through the same adapter the profile card
|
|
1968
|
+
already used, so there is one writer path rather than two settings;
|
|
1969
|
+
- `@object-ui/app-shell` reads the column back once per shell, on mount and on
|
|
1970
|
+
every `sys_user` mutation the invalidation bus reports — which includes the
|
|
1971
|
+
profile card's save, so changing the language in either place changes it in
|
|
1972
|
+
both, in the same session and with no reload;
|
|
1973
|
+
- the device-local value is kept and demoted to a CACHE: it still decides before
|
|
1974
|
+
sign-in and for the first paint of a boot (the column cannot be read
|
|
1975
|
+
synchronously), and it is overwritten by the column as soon as the row is
|
|
1976
|
+
read. A stale device value can no longer outlive one round trip.
|
|
1977
|
+
|
|
1978
|
+
A failed write is reported to the user and the local switch stands until the
|
|
1979
|
+
next successful read, which then takes the UI back to what the column says —
|
|
1980
|
+
never a silent no-op. A signed-out visitor is unaffected: with no `sys_user`
|
|
1981
|
+
row nothing is read and nothing is written, and the globe is the device-local
|
|
1982
|
+
switch it has always been.
|
|
1983
|
+
|
|
1984
|
+
`@object-ui/i18n` is prose only here. Its precedence chain still adjudicates
|
|
1985
|
+
what it can see (device choice → tenant seed → browser → `en`), and its
|
|
1986
|
+
docblocks now say that a signed-in user's language is not one of those tiers:
|
|
1987
|
+
reading a `sys_user` row needs an authenticated data adapter, which a renderer
|
|
1988
|
+
package does not take as a dependency, so the host applies the column through
|
|
1989
|
+
the ordinary `changeLanguage`.
|
|
1990
|
+
- 8cedb0d: The two declared display-locale contracts now each name the caller they govern,
|
|
1991
|
+
and each points at the other (objectui#10098). This is documentation only: no
|
|
1992
|
+
module's behaviour moves.
|
|
1993
|
+
|
|
1994
|
+
- `DisplayNumberFormatOptions.locale` (`@object-ui/core`): leaving the tag
|
|
1995
|
+
`undefined` means the runtime default, and that rule is written for a non-React
|
|
1996
|
+
display caller with no tag in hand. For that caller the viewer's own environment
|
|
1997
|
+
is the honest locale for a user-facing display. A malformed tag lands in the
|
|
1998
|
+
same place through `formatDisplayNumber`'s retry.
|
|
1999
|
+
- `useDisplayLocale` (`@object-ui/i18n`): its concrete `'en'` last resort, kept for
|
|
2000
|
+
determinism, is the rule for a React renderer whose provider chain yields no tag
|
|
2001
|
+
at all, not a rule for every caller without a tag.
|
|
2002
|
+
- `formatNumberInDisplayLocale` (`@object-ui/plugin-report`): the note on its
|
|
2003
|
+
malformed-tag retry no longer calls the dropped-tag retry behind
|
|
2004
|
+
`formatDisplayNumber` a wrong answer. Each retry follows its own package's
|
|
2005
|
+
declared contract.
|
|
2006
|
+
|
|
2007
|
+
Read alone, either docblock used to look like a rule for every caller, and each
|
|
2008
|
+
contradicted the other. The new wording reaches each package's published type
|
|
2009
|
+
declarations, which is why this is declared as a patch rather than left
|
|
2010
|
+
undeclared.
|
|
2011
|
+
- b57107d: fix(auth,app-shell,console): a browser that changes hands no longer keeps the previous account's UI language
|
|
2012
|
+
|
|
2013
|
+
Both device language slots (`objectui-locale` and `objectui-locale-seed`) hold the
|
|
2014
|
+
language of whoever last signed in on the browser. A boot resolves its language from
|
|
2015
|
+
them before anyone is signed in. When the session then turns out to belong to someone
|
|
2016
|
+
else (an SSO redirect back into the console, or a sign-in in another window),
|
|
2017
|
+
`SessionUserScope.adopt` already purged both slots (objectui#5664). But the live
|
|
2018
|
+
language had already been derived from them, and nothing re-derived it. A new account
|
|
2019
|
+
with no `sys_user.locale` of its own therefore read the previous account's language
|
|
2020
|
+
for its whole first session.
|
|
2021
|
+
|
|
2022
|
+
- `@object-ui/auth` (new exports, hence minor): `getSessionOwnerChangeCount()` and
|
|
2023
|
+
`subscribeSessionOwnerChange(listener)` report that the change-of-owner purge ran
|
|
2024
|
+
in this page-load. They are held in memory, with no new storage key.
|
|
2025
|
+
`SessionUserScope.adopt` now returns `true` when it purged a previous owner.
|
|
2026
|
+
- `@object-ui/app-shell`: `useSignedInUserLocale` resets the UI language on an owner
|
|
2027
|
+
change, the same way a boot of the swept storage would resolve it. It then
|
|
2028
|
+
re-reads the new owner's `sys_user.locale`, which applies after the reset.
|
|
2029
|
+
- `@object-ui/console`: `LocalizationFetchProvider` and the boot-time seed fetch no
|
|
2030
|
+
longer write an answer that was requested before an owner change. Such an answer
|
|
2031
|
+
describes the previous owner.
|
|
2032
|
+
- `@object-ui/i18n` (docs only): the seed was documented as the "tenant's" locale.
|
|
2033
|
+
It is the server's resolved locale for the signed-in caller: their own
|
|
2034
|
+
`sys_user.locale`, else `Accept-Language`, else the deployment default. The docs
|
|
2035
|
+
now say so, and declare what a visitor who has not signed in reads: the last
|
|
2036
|
+
owner's language, until someone else signs in.
|
|
2037
|
+
- bb5d4ee: Two display-locale faces now follow the session's display locale (objectui#10232).
|
|
2038
|
+
|
|
2039
|
+
- `@object-ui/plugin-ai` is wired to `@object-ui/i18n` as a whole. The `nl-query`,
|
|
2040
|
+
`ai-form-assist` and `ai-recommendations` components read their strings from a new
|
|
2041
|
+
`ai.*` namespace, translated in all ten locale packs, instead of hard-coded English.
|
|
2042
|
+
The `nl-query` history date, which was formatted in the machine's locale, and the
|
|
2043
|
+
confidence and score percentages now format in the display locale
|
|
2044
|
+
(`useDisplayLocale()`). The package's manifest now names `@object-ui/i18n`, which also
|
|
2045
|
+
puts it inside the repo-wide machine-locale census.
|
|
2046
|
+
- `@object-ui/app-shell`: the Studio home page's "recently viewed" times and the Data
|
|
2047
|
+
pillar's last-saved time format in the display locale. They used to be handed
|
|
2048
|
+
`useMetadataLocale()`, which picks the designer's string table and is `en-US` for every
|
|
2049
|
+
language other than zh, so a de-DE session read US English. That hook's doc comment
|
|
2050
|
+
now says it must never be used as an `Intl` locale.
|
|
2051
|
+
- dc666f7: fix(i18n): a translation bundle with no field label is recognised as a spec payload (objectui#10235)
|
|
2052
|
+
|
|
2053
|
+
The console's `loadLanguage` asks `isSpecTranslationData` whether the payload it fetched is a
|
|
2054
|
+
`@objectstack/spec` `TranslationData`. On `true` it transforms the payload and namespaces it under
|
|
2055
|
+
`app`, where `useObjectLabel` and the screen-flow runner read it; on `false` it returns the payload
|
|
2056
|
+
untouched, for a mock or local-dev server that already speaks i18next namespaces. The predicate
|
|
2057
|
+
answered `true` only when some `objects` entry carried `fields`, so a genuine bundle that translated
|
|
2058
|
+
object labels, apps, pages, dashboards or flows — and no field label — took the untouched branch, sat
|
|
2059
|
+
at the root of the i18next resource tree, and was read by nothing: every screen drew the authored
|
|
2060
|
+
copy in every locale, with nothing reported.
|
|
2061
|
+
|
|
2062
|
+
`isSpecTranslationData` now answers `true` for a payload carrying any top-level group of the served
|
|
2063
|
+
translation document — `objects`, `apps`, `messages`, `globalActions`, `dashboards`, `datasets`,
|
|
2064
|
+
`pages`, `flows`, `settings`, `metadataForms` or `settingsCommon` — whose value is an object. The
|
|
2065
|
+
console's own tests walk that group list off `@objectstack/spec`'s `GetTranslationsResponseSchema`,
|
|
2066
|
+
so a group the spec adds and the predicate does not know fails a test instead of being dropped.
|
|
2067
|
+
|
|
2068
|
+
An already-namespaced i18next tree still takes the untouched branch: its top-level keys are namespace
|
|
2069
|
+
names (`common`, an app namespace such as `crm`, or the `app` the transform itself emits), none of
|
|
2070
|
+
which is a spec group, and no built-in locale pack namespace is spelled like one. A group-named key
|
|
2071
|
+
holding a string or an array is not treated as a group.
|
|
2072
|
+
|
|
2073
|
+
What a user sees change: in the console, an app whose translation bundle carries no field label now
|
|
2074
|
+
shows its translated object names, app and navigation labels, dashboard and page titles, and
|
|
2075
|
+
screen-flow wizard copy, where it used to show the authored copy.
|
|
2076
|
+
- 3335767: Russian now reads correctly at every count on the tab-count badge's accessible
|
|
2077
|
+
name and the activity-feed reaction chip, and Arabic on the badge and the comment
|
|
2078
|
+
thread's count and reaction tooltip (objectui#10242).
|
|
2079
|
+
|
|
2080
|
+
Each of these surfaces picks its `…One` key at exactly one and the plain key at
|
|
2081
|
+
every other count. Two slots cover English. They do not cover Russian, which needs
|
|
2082
|
+
three noun forms for whole numbers, or Arabic, which needs more. Where the plain key
|
|
2083
|
+
held a single noun form, it was wrong at some of the counts it serves. A Russian tab
|
|
2084
|
+
badge read `2 элементов` at two (the language needs `2 элемента`) and
|
|
2085
|
+
`21 элементов` at twenty-one (it needs `21 элемент`), and a Russian reaction chip
|
|
2086
|
+
read `2 реакций` at two. The Arabic badge, comment count and reaction tooltip used
|
|
2087
|
+
the plural form for three to ten, which is wrong at two and from eleven up.
|
|
2088
|
+
|
|
2089
|
+
The count-not-one half of these five values is now a count label that reads
|
|
2090
|
+
correctly at any number:
|
|
2091
|
+
|
|
2092
|
+
- `ru` `common.itemCount`: `Элементов: N`
|
|
2093
|
+
- `ru` `detail.reactionCount`: the emoji, then `Реакций: N`
|
|
2094
|
+
- `ar` `common.itemCount`: `عدد العناصر: N`
|
|
2095
|
+
- `ar` `collaboration.commentCount`: `عدد التعليقات: N`
|
|
2096
|
+
- `ar` `collaboration.reactionCount`: `عدد التفاعلات: N`
|
|
2097
|
+
|
|
2098
|
+
That is the device objectui#10024 used for `search.resultsCountPlural`, and the one
|
|
2099
|
+
Russian's `collaboration.commentCount` and `collaboration.reactionCount` already
|
|
2100
|
+
use. The singular halves, the key set, the call sites and the other eight packs are
|
|
2101
|
+
unchanged.
|
|
2102
|
+
|
|
2103
|
+
⚠️ **Dated note, 2026-10-02 — superseded in this release — objectui#11445.**
|
|
2104
|
+
Later in this same release the `…One` / plain-key switch on `common.itemCount`,
|
|
2105
|
+
`detail.reactionCount`, `collaboration.commentCount` and
|
|
2106
|
+
`collaboration.reactionCount` gave way to one count family per key in all ten
|
|
2107
|
+
packs: a numeric count picks the noun form its CLDR category needs (`ru`
|
|
2108
|
+
`2 элемента`, `21 элемент`), the `…One` siblings left every pack, and the count
|
|
2109
|
+
labels above remain only on the base key, which answers a call made without a
|
|
2110
|
+
count. The rest of this entry is kept as the reading of this change.
|
|
2111
|
+
- 1dbb993: fix(plugin-designer): the Navigation Designer has an entry for the spec's `doc` navigation item type
|
|
2112
|
+
|
|
2113
|
+
objectstack#19789 added a `doc` member to the spec's navigation item union (an
|
|
2114
|
+
item that targets a book and/or a doc). `NAV_TYPE_META` in `NavigationDesigner`
|
|
2115
|
+
is keyed by that spec-derived union, so objectui stopped compiling against
|
|
2116
|
+
`@objectstack/spec` built from objectstack `main`, and every row reads its badge,
|
|
2117
|
+
colour and icon from that map. The map now has a `doc` entry (a `BookOpen` icon,
|
|
2118
|
+
its own colour and the `appDesigner.navTypeDoc` label key). The key has an
|
|
2119
|
+
English fallback in the designer's defaults and a translation in all ten locale
|
|
2120
|
+
packs.
|
|
2121
|
+
|
|
2122
|
+
The map is typed `Record<NavigationItemType | 'doc', ...>` so it compiles both
|
|
2123
|
+
against the pinned `@objectstack/spec`, which predates `doc`, and against
|
|
2124
|
+
objectstack `main`. The `| 'doc'` goes away at the pin bump that ships `doc`.
|
|
2125
|
+
`doc` is not added to the quick-add buttons: an empty `doc` item fails the spec's
|
|
2126
|
+
book-or-doc requirement, and authoring one is objectui#10188.
|
|
2127
|
+
- 45362a3: Russian now reads correctly at every count on the list view's record-count bar,
|
|
2128
|
+
and Arabic on that bar, the record picker's count, the activity-feed reaction chip,
|
|
2129
|
+
the presence avatars' accessible name and overflow tooltip, and the full-page
|
|
2130
|
+
search count while browsing (objectui#10425).
|
|
2131
|
+
|
|
2132
|
+
Each of these surfaces picks its `…One` key at exactly one and the plain key at
|
|
2133
|
+
every other count. Where the plain key held a single noun form, it was wrong at
|
|
2134
|
+
some of the counts it serves. The Russian record-count bar read `2 записей` at two
|
|
2135
|
+
(the language needs `2 записи`) and `21 записей` at twenty-one (it needs
|
|
2136
|
+
`21 запись`). The Arabic values each held one noun form (the plural for three to
|
|
2137
|
+
ten, or in `lookup.recordCount` the singular), while Arabic needs different forms
|
|
2138
|
+
at two, at three to ten and from eleven up.
|
|
2139
|
+
|
|
2140
|
+
The count-not-one half of these seven values is now a count label that reads
|
|
2141
|
+
correctly at any number:
|
|
2142
|
+
|
|
2143
|
+
- `ru` `list.recordCount`: `Записей: N`
|
|
2144
|
+
- `ar` `list.recordCount`: `عدد السجلات: N`
|
|
2145
|
+
- `ar` `lookup.recordCount`: `عدد السجلات: N`
|
|
2146
|
+
- `ar` `detail.reactionCount`: the emoji, then `عدد التفاعلات: N`
|
|
2147
|
+
- `ar` `collaboration.presentUserCount`: `عدد المستخدمين المتواجدين: N`
|
|
2148
|
+
- `ar` `collaboration.moreUserCount`: `عدد المستخدمين الآخرين: N`
|
|
2149
|
+
- `ar` `search.itemsAvailable`: `عدد العناصر المتاحة: N`
|
|
2150
|
+
|
|
2151
|
+
That is the device objectui#10024 and objectui#10242 used, and the one Russian's
|
|
2152
|
+
`lookup.recordCount` already uses. The singular halves, the key set, the call sites
|
|
2153
|
+
and the other eight packs are unchanged.
|
|
2154
|
+
|
|
2155
|
+
⚠️ **Dated note, 2026-10-02 — superseded in this release — objectui#11445.**
|
|
2156
|
+
Later in this same release the `…One` / plain-key switch on `list.recordCount`,
|
|
2157
|
+
`lookup.recordCount`, `detail.reactionCount`, `collaboration.presentUserCount`,
|
|
2158
|
+
`collaboration.moreUserCount` and `search.itemsAvailable` gave way to one count
|
|
2159
|
+
family per key in all ten packs: a numeric count picks the noun form its CLDR
|
|
2160
|
+
category needs (`ru` `2 записи`, `ar` `3 سجلات`), the `…One` siblings left every
|
|
2161
|
+
pack, and the count labels above remain only on the base key, which answers a
|
|
2162
|
+
call made without a count. The rest of this entry is kept as the reading of this change.
|
|
2163
|
+
- 2a943bf: fix(app-shell): a cloud Re-seed / Purge refused with `ENVIRONMENT_KERNEL_UNAVAILABLE` now says why and stops offering the retry
|
|
2164
|
+
|
|
2165
|
+
`reseedSampleData` and `purgeSampleData` kept only the text of a refusal and
|
|
2166
|
+
dropped its error code. On a 500 that text is the control plane's withheld
|
|
2167
|
+
generic sentence, so the package page showed the same message for every server
|
|
2168
|
+
fault. That included a control plane with no environment kernel, where the two
|
|
2169
|
+
actions can never succeed (cloud#2072).
|
|
2170
|
+
|
|
2171
|
+
Both calls now return the code beside the text. When the code is
|
|
2172
|
+
`ENVIRONMENT_KERNEL_UNAVAILABLE`, the package page says that this control plane
|
|
2173
|
+
has no environment kernel and that the action must be run from the
|
|
2174
|
+
environment's own runtime, and it disables both cloud sample-data actions for
|
|
2175
|
+
the rest of the visit. Every other code, `INTERNAL_ERROR` included, keeps
|
|
2176
|
+
today's message and leaves the actions enabled. The HTTP status handling is
|
|
2177
|
+
unchanged.
|
|
2178
|
+
|
|
2179
|
+
`@object-ui/i18n` carries the new sentence as `marketplace.detail.sampleDataKernelUnavailable`
|
|
2180
|
+
in all ten locale packs.
|
|
2181
|
+
- 8740e86: fix(fields): the editable date-time faces no longer roll a stored impossible day into a real one
|
|
2182
|
+
|
|
2183
|
+
`toDateTimeInputValue` let a `Z`, offset or date-only spelling fall through to
|
|
2184
|
+
`new Date(...)`, which accepts a day of 01-31 for every month and rolls the
|
|
2185
|
+
surplus forward: a stored `2026-02-30T10:00:00Z` reached the `datetime-local`
|
|
2186
|
+
control as `2026-03-02T10:00`, and an edit could save that day back. The read
|
|
2187
|
+
faces already refuse such a value (objectui#10026, objectui#10301).
|
|
2188
|
+
|
|
2189
|
+
The adapter now asks the same judgement (`isRealCalendarDate`) of the day AS
|
|
2190
|
+
WRITTEN, before any conversion, in every spelling, and `fromDateTimeInputValue`
|
|
2191
|
+
never re-emits a nonexistent day as a rolled one. A `datetime-local` control can
|
|
2192
|
+
paint a nonexistent day only blank (measured in Chromium), so the editable
|
|
2193
|
+
`DateTimeField` and the sub-grid's editable `datetime` cell leave the control
|
|
2194
|
+
empty, mark it `aria-invalid`, and name the stored string beside it with the new
|
|
2195
|
+
`fields.dateTime.impossibleDay` sentence (all ten packs). Nothing is written until
|
|
2196
|
+
the user picks a new value; a real date-time and a zone-less value behave as before.
|
|
2197
|
+
- ac2d6f1: fix(fields): the editable date-only faces no longer blank a stored impossible day silently
|
|
2198
|
+
|
|
2199
|
+
`toDateInputValue` keeps a stored `2026-02-30` as written and never rolls it,
|
|
2200
|
+
but an `<input type="date">` sanitises a day that does not exist to an empty
|
|
2201
|
+
value (measured in Chromium). So `DateField` and the sub-grid's editable `date`
|
|
2202
|
+
cell showed an empty control with no marker for a value that was stored. That
|
|
2203
|
+
is a silent blank, which the direction of objectui#10026 rules out, and it is
|
|
2204
|
+
the date-only half of objectui#10474.
|
|
2205
|
+
|
|
2206
|
+
Both faces now judge the day as written with `isImpossibleStoredDay` (the check
|
|
2207
|
+
objectui#10474 added). For such a day they hand the control an empty value,
|
|
2208
|
+
mark it `aria-invalid`, and name the stored string beside it with the new
|
|
2209
|
+
date-only sentence `fields.date.impossibleDay`, in all ten packs. The control's
|
|
2210
|
+
`aria-describedby` points at that sentence. Nothing is written until the user
|
|
2211
|
+
picks a new day, and picking a real day clears the marker. Real days and empty
|
|
2212
|
+
values render as before.
|
|
2213
|
+
- 9fbbb17: BREAKING (`@object-ui/components`): `RefreshIndicator`'s `ariaLabel` prop is now required and has no default. It used to default to the English literal "Refreshing", so the progress bar on every view that passed no name was announced in English to screen-reader users in every locale (objectui#10580). A `RefreshIndicator` rendered without `ariaLabel` now fails to type-check.
|
|
2214
|
+
|
|
2215
|
+
(The bump is `minor` by this repo's release model: objectui's major follows the `@objectstack` family major, and its own breaking changes ship as `minor` with the breaking semantics stated here.)
|
|
2216
|
+
|
|
2217
|
+
Migration: pass the bar's accessible name from your own translation layer, for example `ariaLabel={t('grid.refreshing')}`. The component renders the string you pass as the bar's `aria-label` and does not translate it.
|
|
2218
|
+
|
|
2219
|
+
`ObjectGrid`, `ListView`, `ObjectChart` and `ObjectDataTable` now name their refresh bar in the active locale. The grid and the list read the `grid.refreshing` and `list.refreshing` keys their pull-to-refresh text already uses. The chart reads `chart.refreshing` and the dashboard data table reads `dashboard.refreshing`, both with no English literal behind them, so a host that renders either one with no i18next instance at all gets the key itself as the bar's name.
|
|
2220
|
+
|
|
2221
|
+
`@object-ui/i18n`: new keys `chart.refreshing` and `dashboard.refreshing` in every built-in locale pack.
|
|
2222
|
+
- f905090: The console's record-count footer, drawn under the list when a record is open
|
|
2223
|
+
beside it in split navigation, now reads `1 record` at one record instead of
|
|
2224
|
+
`1 records` (objectui#10636).
|
|
2225
|
+
|
|
2226
|
+
The footer rendered one key, `console.objectView.recordCount`, at every count.
|
|
2227
|
+
It now picks the new `console.objectView.recordCountOne` at exactly one record
|
|
2228
|
+
and `console.objectView.recordCount` at every other count, the same switch the
|
|
2229
|
+
list view's record-count bar uses. Every built-in pack words the pair the way it
|
|
2230
|
+
words the list bar's `list.recordCount` / `list.recordCountOne`, so the footer
|
|
2231
|
+
and the bar count the same records in the same words. In Japanese, Korean and
|
|
2232
|
+
Chinese, which have no separate singular, the two values are the same string.
|
|
2233
|
+
|
|
2234
|
+
Russian and Arabic also read correctly at other counts now. The count-not-one
|
|
2235
|
+
value held one noun form: Russian read `2 записей` at two and `21 записей` at
|
|
2236
|
+
twenty-one, and Arabic used the singular noun at every count. Both now use the
|
|
2237
|
+
count label their list bar already uses, `Записей: N` and `عدد السجلات: N`,
|
|
2238
|
+
which is right at any number. At one record they read `1 запись` and `1 سجل`.
|
|
2239
|
+
|
|
2240
|
+
⚠️ **Dated note, 2026-10-02 — superseded in this release — objectui#11445.**
|
|
2241
|
+
Later in this same release the `console.objectView.recordCountOne` /
|
|
2242
|
+
`console.objectView.recordCount` switch gave way to a
|
|
2243
|
+
`console.objectView.recordCount` count family in all ten packs, worded like
|
|
2244
|
+
`list.recordCount`'s: the footer calls the family key with `{ count }`,
|
|
2245
|
+
`console.objectView.recordCountOne` left every pack, and `ru` and `ar` read a
|
|
2246
|
+
noun form at each count (`2 записи`, `3 سجلات`) where the count label stood. The rest of this entry is kept as the reading of this change.
|
|
2247
|
+
- eb97ce6: fix(plugin-timeline,plugin-charts,i18n): a stored date-only day reads as that day in every viewer zone on the timeline, on a chart's date axis and in the published date helpers (objectui#10866, slice 2)
|
|
2248
|
+
|
|
2249
|
+
These readers parsed a `YYYY-MM-DD` value with the engine's own `Date` parse, which reads it as UTC midnight, so every viewer west of UTC saw the day before. Each now reads the value through `toDisplayDate` from `@object-ui/core`, which rebuilds a date-only value at local midnight of the day it names. A value with a time part keeps its instant.
|
|
2250
|
+
|
|
2251
|
+
- **Timeline.** The object timeline's date bucket, its sort and the item date the renderer prints read the value that way. West of UTC an item due today no longer sits under "Overdue", one due tomorrow no longer sits under "Today", and the `short` and `long` item faces print the stored day. The `iso` face prints a date-only value's day from local getters, and an instant's UTC day as before. A day its month does not have, such as `2026-02-30`, is no longer rolled into March: the item sits under "No date", sorts with the dateless items, and its `short` and `long` faces are the ones an unparsable value already had. An unparsable value now sorts with the dateless items too, and the `iso` face prints such a value as written where it used to throw. The gantt variant's axis headers, extent and bar positions are not changed here, so west of UTC its bar tooltip, which prints through the same item-date function, now names the stored day while the axis above it still reads the day before, until a later slice of objectui#10866.
|
|
2252
|
+
- **Charts.** A date-only category on the x axis reads `Sep 1` for `2026-09-01`, and `Sep 2026` on a month-grained axis, in every zone. When the display locale is a tag `Intl` refuses, the tick still prints the stored day: it reads the local-midnight value with local getters, because its `toISOString()` would name the day before east of UTC. An instant's fallback keeps its UTC day. A date-only category naming a day its month does not have falls back to the raw category, the face any other non-date category gets, instead of the rolled day.
|
|
2253
|
+
- **i18n helpers.** `formatDate`, `formatDateTime`, `formatRelativeTime` and `formatDateSpec` read a date-only string as the day it names: `formatDateTime` shows midnight of that day, and `formatRelativeTime` counts to the start of it. `formatDateSpec` applies its `timeZone` to an instant only, and formats a date-only value in the local zone, where the shared step's local midnight reads back as its day; a `timeZone` west of UTC no longer turns `2026-09-01` into August 31st for every viewer. All four return a day its month does not have as the raw string. `formatRelativeTime` now returns an unparsable value as its string, the face the other three already gave one, where it used to throw a `RangeError`.
|
|
2254
|
+
- ac15833: fix(data-objectstack, plugin-dashboard): a dataset tile the viewer may not read shows a localized "no access" state (objectui#10899)
|
|
2255
|
+
|
|
2256
|
+
When the analytics read admission refused a dashboard dataset query — `403`
|
|
2257
|
+
with ADR-0112 `PERMISSION_DENIED` — `queryDataset` had no branch for that code,
|
|
2258
|
+
so it threw the generic `Dataset query failed: 403 Forbidden — [Analytics]
|
|
2259
|
+
Access denied: …` string, and `DatasetWidget` printed it verbatim in a red
|
|
2260
|
+
alert. The list view over the same object already says 「无权访问」.
|
|
2261
|
+
|
|
2262
|
+
- `queryDataset` now throws a typed `AnalyticsForbiddenError` for that code,
|
|
2263
|
+
carrying `httpStatus: 403` and `code: 'PERMISSION_DENIED'` (plus the server's
|
|
2264
|
+
code and message for diagnostics). A code-less 403 — no ObjectStack route
|
|
2265
|
+
wrote it — keeps the generic error.
|
|
2266
|
+
- `DatasetWidget` classifies a failed query with the shared `classifyLoadError`
|
|
2267
|
+
and, for `forbidden`, renders a localized no-access state
|
|
2268
|
+
(`dashboard.widgetForbiddenTitle` / `dashboard.widgetForbiddenMessage`, new in
|
|
2269
|
+
all ten packs) instead of the exception text. Every other failure keeps the
|
|
2270
|
+
detailed alert.
|
|
2271
|
+
- ac15833: fix(app-shell, plugin-detail): a record comment whose write fails is never shown as sent (objectui#10899)
|
|
2272
|
+
|
|
2273
|
+
The record page's discussion writers appended the new comment (or reply) to the
|
|
2274
|
+
panel first and then fired the `sys_comment` create, swallowing its rejection.
|
|
2275
|
+
On a tenant with no `sys_comment` the write answered 404 while the panel showed
|
|
2276
|
+
the comment and a bumped count, with no error — and the comment was gone after a
|
|
2277
|
+
reload.
|
|
2278
|
+
|
|
2279
|
+
`RecordDetailView` now appends a comment or reply only after its write resolves.
|
|
2280
|
+
A failed write raises a localized error toast (`detail.commentFailed`, new in all
|
|
2281
|
+
ten packs) and rejects back to the composer. The composers — the activity
|
|
2282
|
+
timeline's comment box, the threaded reply input, and `RecordComments` — now
|
|
2283
|
+
treat a rejected `onAddComment` / `onAddReply` explicitly as "not written": they
|
|
2284
|
+
keep the draft for a retry instead of letting the rejection escape unhandled.
|
|
2285
|
+
- e2dffc9: fix(app-shell): a failed reaction write takes the reaction back and says so, instead of staying shown as applied (objectui#10899)
|
|
2286
|
+
|
|
2287
|
+
A reaction click on the record page's discussion shows at once and is stored in
|
|
2288
|
+
the background with a `sys_comment` update. That update was issued from inside
|
|
2289
|
+
the React state updater and its rejection was discarded, so a refused write
|
|
2290
|
+
left the reaction on screen as applied, with no message, until a reload. In a
|
|
2291
|
+
StrictMode development build the updater also ran twice, so one click sent two
|
|
2292
|
+
updates.
|
|
2293
|
+
|
|
2294
|
+
`RecordDetailView` now computes the toggle and issues the write from the click
|
|
2295
|
+
handler, one write per click. When a write is refused, the row shows the newest
|
|
2296
|
+
reaction set a write stored or can still store, and a localized error toast
|
|
2297
|
+
(`detail.reactionFailed`, new in all ten packs) says the reaction was not
|
|
2298
|
+
saved. A refused click that a later click's write also carries is kept, because
|
|
2299
|
+
each write stores the row's whole reaction set. The stored
|
|
2300
|
+
`{ emoji: userIds[] }` shape is unchanged.
|
|
2301
|
+
- 328abeb: Four Console surfaces that read English under a zh-CN session now read the session's
|
|
2302
|
+
language (objectui#10900). English stays the default.
|
|
2303
|
+
|
|
2304
|
+
- **The generic action success toast.** When an action declares no `successMessage`, the
|
|
2305
|
+
runner falls back to "Action completed successfully".
|
|
2306
|
+
That fallback now goes through a translator: `ActionRunner.setTranslator(translate)` is
|
|
2307
|
+
new in `@object-ui/core`, and `<ActionProvider>` and `useActionRunner` in
|
|
2308
|
+
`@object-ui/react` install the session's `t` on the runner they build. The pack key is
|
|
2309
|
+
`actions.completedSuccessfully`. An author's `successMessage` still reaches the toast
|
|
2310
|
+
untranslated, and a runner with no translator installed still toasts the English sentence.
|
|
2311
|
+
- **The social sign-in buttons on the login and sign-up pages.** `SocialSignInButtons`
|
|
2312
|
+
takes a new `buttonText` prop, a template whose `{provider}` is replaced with the
|
|
2313
|
+
provider's display name; unset, the buttons keep "Continue with {provider}" and
|
|
2314
|
+
"Sign up with {provider}". `LoginForm` and `RegisterForm` take a new `socialButton`
|
|
2315
|
+
label and pass it, with their existing `orText` label, to the buttons. `orText` was
|
|
2316
|
+
documented as the divider label but rendered nowhere; it now sets the divider under the
|
|
2317
|
+
buttons, and its documented default is corrected from "or" to "or continue with email",
|
|
2318
|
+
the text that divider has always shown. The console's login and sign-up pages and
|
|
2319
|
+
`@object-ui/app-shell`'s `DefaultLoginPage` / `DefaultRegisterPage` pass the new
|
|
2320
|
+
`auth.login.*` and `auth.register.*` `socialButton` / `orText` keys. The provider's display
|
|
2321
|
+
name is inserted as-is: the component's own label for the branded providers it knows,
|
|
2322
|
+
otherwise the name the server reports.
|
|
2323
|
+
- **Build Doctor.** The build conversation's Build Doctor button (its accessible name and
|
|
2324
|
+
both tooltips) and the title of the drawer it opens read `console.ai.buildDoctor`,
|
|
2325
|
+
`console.ai.buildDoctorTitle` and `console.ai.buildDoctorDisabledTitle`.
|
|
2326
|
+
- **Setup → marketplace.** On `system/marketplace` and the pages under it, the breadcrumb
|
|
2327
|
+
segment after System reads `console.breadcrumb.marketplace` instead of the humanized URL
|
|
2328
|
+
slug; other `system/*` segments are unchanged. The zh marketplace search placeholder
|
|
2329
|
+
reads 「按名称或标识搜索应用…」 instead of 「按名称或 manifest ID 搜索应用…」. The search
|
|
2330
|
+
itself is unchanged: it matches the display name, the identifier and the description.
|
|
2331
|
+
|
|
2332
|
+
All ten locale packs carry the nine new keys; no existing `en` value changes.
|
|
2333
|
+
|
|
2334
|
+
**Clause-②: yes** — besides the nine pack keys, the public surface widens by four optional
|
|
2335
|
+
members: `ActionRunner.setTranslator`, `SocialSignInButtonsProps.buttonText`,
|
|
2336
|
+
`LoginFormLabels.socialButton` and `RegisterFormLabels.socialButton`. Nothing is removed,
|
|
2337
|
+
renamed or narrowed, no accept set changes, and `@object-ui/react` exports nothing new.
|
|
2338
|
+
- 24d3e65: New SDUI widget `cloud:plan-status`: a "Current plan" badge for one plan card on the Cloud pricing page, shown when that card's plan is the organization's plan (objectui#10919).
|
|
2339
|
+
|
|
2340
|
+
**Clause-②: yes** — the accept set of `AnyComponentSchema`, and so of `safeValidateSchema` and `objectui validate`, widens by one `type` literal, `cloud:plan-status`, and `@object-ui/types/zod` exports one new schema, `CloudPlanStatusSchema`. Nothing that parsed before is refused now.
|
|
2341
|
+
|
|
2342
|
+
**Why.** The pricing page is static metadata, and nothing in a page's expression scope carries the organization's plan, so the page could not tell which of its cards the organization is already on. The plan is available only from the org-scoped `GET /cloud/environment-entitlements` summary.
|
|
2343
|
+
|
|
2344
|
+
**What changed, in observable terms.**
|
|
2345
|
+
|
|
2346
|
+
- `@object-ui/app-shell` registers `cloud:plan-status`. A page places one node on each plan card and names that card's plan code in `properties.plan`: `{ "type": "cloud:plan-status", "properties": { "plan": "free" } }`. The widget reads the summary through the hook the environment list already uses, and renders the badge when the summary's `plan` equals `properties.plan`. The comparison is exact, so `Free` does not match `free`.
|
|
2347
|
+
- The widget renders nothing on every other card, while the summary loads, when the request fails, and when the body is not the `{ success, data }` envelope. It never guesses a plan.
|
|
2348
|
+
- The node's `className` and `responsiveStyles` reach the badge. Each node reads the summary itself, so a page with three cards makes three requests.
|
|
2349
|
+
- The widget is registered under one key, `cloud:plan-status`. There is no bare `plan-status` fallback and no `app-shell:`-prefixed twin.
|
|
2350
|
+
- `@object-ui/types/zod` exports `CloudPlanStatusSchema`, a member of `AnyComponentSchema`. `properties` is required and must be exactly `{ plan }`, with `plan` a non-empty string. A missing bag, a missing or empty `plan`, and any other key in the bag are each refused at that path.
|
|
2351
|
+
- `body` and `children` are refused by name on this node, because the widget reads neither.
|
|
2352
|
+
- `plan` is not an enum: the plan catalog belongs to the control plane, and ObjectUI does not list its codes.
|
|
2353
|
+
- `@object-ui/i18n` adds `cloudPlanStatus.current` ("Current plan") to all ten locale packs.
|
|
2354
|
+
- `@object-ui/cli`: `objectui check` knows `cloud:plan-status` as a registered type.
|
|
2355
|
+
- 4b742f4: fix(components): an `element:number` that asks for an aggregate and names no object says so instead of painting a silent dash
|
|
2356
|
+
|
|
2357
|
+
Since `element:number` accepts its object from the node-level `dataSource`
|
|
2358
|
+
binding (objectui#10909), `object` is no longer a required input, and the
|
|
2359
|
+
manifest has no way to say "one of `object` and `dataSource.object`". A node
|
|
2360
|
+
that authors an `aggregate` with neither therefore passes the html tier with no
|
|
2361
|
+
diagnostic, and the renderer used to paint "—", which reads like a real empty
|
|
2362
|
+
value.
|
|
2363
|
+
|
|
2364
|
+
The renderer now draws a short muted notice in that case: "No object named: set
|
|
2365
|
+
object or dataSource.object." It is renderer chrome, so it reads the locale
|
|
2366
|
+
packs (`element.number.noObject`, added to all ten packs) and speaks the session
|
|
2367
|
+
language. It does not throw and it queries nothing.
|
|
2368
|
+
|
|
2369
|
+
Only authored absence draws it. A binding that names an object keeps its own
|
|
2370
|
+
panels while its `view` is resolving or after it failed to resolve, and a node
|
|
2371
|
+
with no `aggregate` still paints the dash exactly as before.
|
|
2372
|
+
- 6cd8f66: The console strings objectui#10900 left English under zh-CN now read the session's language (objectui#10969).
|
|
2373
|
+
|
|
2374
|
+
- **The action runner's other own text.** `ActionRunner` asks the translator installed through `setTranslator` (objectui#10900) for three more strings it writes itself: the error toast when the error that reached it carries no readable message (`actions.failed`), a parallel chain's error when no failed action reported one (`actions.parallelFailed`), and the label of an undoable success toast's Undo button (`actions.undo`). An author's `errorMessage` / `successMessage` and an action's own error stay untranslated. With no translator the text stays English.
|
|
2375
|
+
- **The Undo label now arrives from the runner.** For an undoable result the runner hands the toast handler `undo: { label }` where it used to hand `undo: {}`; `ToastHandler`'s type is unchanged (`label` was already optional). A handler that renders `options.undo.label` shows the translated label; one that ignores it keeps its own.
|
|
2376
|
+
- **The console app's `system/*` breadcrumbs.** In `AppHeader`, the segment after `System` for `settings`, `apps`, `profile`, `approvals`, `ai-approvals` and `audit-log` reads `console.breadcrumb.*` (as `marketplace` has since objectui#10900). In English, `ai-approvals` now reads `AI Approvals` instead of the humanized slug `Ai Approvals`; the other five read as before. A segment the header does not know keeps its humanized slug.
|
|
2377
|
+
- **The Build Doctor drawer's body.** Its description, loading and not-found states, summary line, verdict, and each section's title and hint read `console.ai.buildDoctorDrawer.*`. The report's own data (tool, artifact and status names, timeline text) stays verbatim.
|
|
2378
|
+
- **"manifest ID" leaves the marketplace search placeholder in every pack.** `en` now reads `Search apps by name or app ID…`, and the eight packs that translated the jargon literally follow. zh already read 「按名称或标识搜索应用…」 and is unchanged.
|
|
2379
|
+
|
|
2380
|
+
The new keys are in all ten packs.
|
|
2381
|
+
- e327c89: fix(console): a sign-up refused by the server's audience gate reads in the session's language (objectui#10998)
|
|
2382
|
+
|
|
2383
|
+
When an environment admits new accounts by invitation only (the server's default)
|
|
2384
|
+
or only from allowlisted email domains, the server refuses `/sign-up/email` with
|
|
2385
|
+
`403 SELF_REGISTRATION_CLOSED` or `403 EMAIL_DOMAIN_NOT_ALLOWED`. The console's
|
|
2386
|
+
register page mapped only the user-exists codes to pack text, so either refusal
|
|
2387
|
+
showed the server's English message in every language, and the invitation-only
|
|
2388
|
+
refusal also named the internal setting behind it.
|
|
2389
|
+
|
|
2390
|
+
- `@object-ui/console`: `RegisterPage` maps both codes to localized text. A code it
|
|
2391
|
+
does not map still shows the server's own message.
|
|
2392
|
+
- `@object-ui/i18n`: all ten locale packs carry the two new keys,
|
|
2393
|
+
`auth.register.errors.selfRegistrationClosed` and
|
|
2394
|
+
`auth.register.errors.emailDomainNotAllowed`. No existing value changes.
|
|
2395
|
+
- 2eaf5be: `WizardForm`'s own chrome now reads the locale packs (objectui#10999). The
|
|
2396
|
+
default Cancel, Back, Next, Submitting, Create and Update labels, the
|
|
2397
|
+
"Step x of y" counter, the step indicator's label for a step that declares no
|
|
2398
|
+
`label`, the indicator's accessible name, and the notice on a step with no
|
|
2399
|
+
fields were English literals, so a wizard in a zh session showed `Cancel`,
|
|
2400
|
+
`Step 1 of 3` and `Next` beside a Chinese UI. They now resolve through the
|
|
2401
|
+
wizard's existing `createSafeTranslation` hook, the way the rest of
|
|
2402
|
+
plugin-form's chrome does.
|
|
2403
|
+
|
|
2404
|
+
An authored `cancelText`, `prevText`, `nextText` or `submitText` still renders
|
|
2405
|
+
exactly as authored, in every locale: only the defaults are localized, and the
|
|
2406
|
+
four keys stay plain strings.
|
|
2407
|
+
|
|
2408
|
+
Five of the strings reuse keys the packs already carried: `common.cancel`,
|
|
2409
|
+
`common.next`, `form.create`, `form.update`, and `form.stepOf`, a key that
|
|
2410
|
+
`pnpm check:i18n-dead-keys` listed as read by nothing until now. Five are new
|
|
2411
|
+
under `wizard.` in all ten packs: `back`, `submitting`, `stepFallback`,
|
|
2412
|
+
`progressLabel` and `emptyStep`. The wizard gets its own `back` rather than
|
|
2413
|
+
`common.back` because zh words a wizard's step back ("previous step", the term
|
|
2414
|
+
`grid.import.back` and `grid.bulk.back` already use) differently from a page's
|
|
2415
|
+
back ("return").
|
|
2416
|
+
|
|
2417
|
+
The zh value of `form.stepOf` gains the spaces around its numbers that the zh
|
|
2418
|
+
pack's other position counters, such as `table.pageInfo` and
|
|
2419
|
+
`detail.recordOf`, already carry.
|
|
2420
|
+
|
|
2421
|
+
**One rendered English string changes**: the final button's in-flight label was
|
|
2422
|
+
typed with three ASCII full stops and is now `Submitting…` with the typographic
|
|
2423
|
+
ellipsis, because it is a pack value now and `ellipsis-glyph-3878.test.ts`
|
|
2424
|
+
holds every pack value to U+2026. Every other English default renders the same
|
|
2425
|
+
text as before.
|
|
2426
|
+
- bf7ab35: A record form whose fields are all locked because the user may not create (or
|
|
2427
|
+
edit) records of its object now says so (objectui#11000). The ADR-0092 D4 lock
|
|
2428
|
+
disables every field when the object's affordance for the form's mode is
|
|
2429
|
+
closed: the object's `managedBy` bucket keeps it closed, or the server's
|
|
2430
|
+
effective API operation set for the user lacks `create` (or `update` on an edit
|
|
2431
|
+
form). Until now nothing on the form said why, and a wizard's Next stayed
|
|
2432
|
+
enabled.
|
|
2433
|
+
|
|
2434
|
+
- Every form layout that draws the lock (the default form, `drawer`, `modal`,
|
|
2435
|
+
`tabbed`, `split` and `wizard`) renders one notice above the fields, such as
|
|
2436
|
+
"You don't have permission to create Project records. The fields are
|
|
2437
|
+
read-only." It names the object by its label, translated when the app's
|
|
2438
|
+
locale bundle translates it, and names `create` on a create form or `edit` on
|
|
2439
|
+
an edit form. It is announced as a status (`role="status"`).
|
|
2440
|
+
- The notice reads the same verdict the lock does, so it appears exactly when
|
|
2441
|
+
every field is locked for that reason. A field locked on its own (one the
|
|
2442
|
+
user may read but not edit, or one declared `readonly`) shows no notice.
|
|
2443
|
+
- A wizard does not walk a user who cannot submit through its steps: while the
|
|
2444
|
+
lock holds, Next and the final submit button are disabled, and the step
|
|
2445
|
+
indicator does not jump forward even with `allowSkip`. Cancel and Back stay
|
|
2446
|
+
usable, and every step is still shown. A user whose affordance is open sees
|
|
2447
|
+
no change.
|
|
2448
|
+
- Two keys are new in the `form` namespace of all ten packs:
|
|
2449
|
+
`noPermissionToCreate` and `noPermissionToEdit`, each with an `{{object}}`
|
|
2450
|
+
placeholder.
|
|
2451
|
+
|
|
2452
|
+
The lock itself is unchanged, and so is the Save button of the non-wizard
|
|
2453
|
+
layouts.
|
|
2454
|
+
- 51c2949: The form family's own feedback chrome now reads the locale packs (objectui#11039). The default success toast, the thank-you heading, the loading line, the load-failure heading and the default submit and cancel labels were English literals, so a zh session saw `Created`, `Thanks!`, `Loading form...` and `Error loading form` beside a Chinese UI. They now resolve through the i18n catalogue, the way `WizardForm`'s footer has since objectui#10999.
|
|
2455
|
+
|
|
2456
|
+
**What moved.**
|
|
2457
|
+
|
|
2458
|
+
- `@object-ui/plugin-form`, across the presentations `object-form` routes to:
|
|
2459
|
+
- The success toast when no `successMessage` is authored: `form.created` after a create, `form.saved` after an edit (the default arm and `WizardForm`), and `MasterDetailForm`'s built-in save toast, whose "… saved" after an authored `title` is `form.savedNamed`.
|
|
2460
|
+
- The note that rides that toast when a declared `navigateOnSuccess` was refused: `form.navigateRefused`. The submitter reads it, as the toast's description.
|
|
2461
|
+
- The thank-you heading when a `thank-you` submit behaviour declares no `title` (the default arm and `WizardForm`): `publicForm.thankYouTitle`.
|
|
2462
|
+
- The loading line of the default arm, `TabbedForm`, `SplitForm`, `WizardForm` and `DrawerForm`: `publicForm.loading`.
|
|
2463
|
+
- The heading of the load-failure panel in those five and `ModalForm`: `form.errorLoading`.
|
|
2464
|
+
- The default submit label, `form.create` or `form.update`, in the default arm, `TabbedForm`, `SplitForm`, `DrawerForm` and `ModalForm`; the default Cancel of `DrawerForm` and `ModalForm`, `common.cancel`; and `MasterDetailForm`'s default Save, Create and Cancel, `common.save`, `form.create` and `common.cancel`.
|
|
2465
|
+
- `@object-ui/console`: the form page (`/f/:slug` and `/forms/:name`) reads its loading line from `common.loading`, its success toast from `form.submitted`, and its default thank-you heading and message from `publicForm.thankYouTitle` and `publicForm.thankYouMessage`, through the `I18nProvider` that `main.tsx` mounts above every console route.
|
|
2466
|
+
- `@object-ui/i18n`: six keys are new under `form.` in all ten packs: `created`, `saved`, `savedNamed` (`{{title}}`), `submitted`, `errorLoading` and `navigateRefused`. The three `publicForm.` keys were already in all ten packs with nothing reading them (`pnpm check:i18n-dead-keys` listed them as confirmed dead), and say the same thing. The zh value of `publicForm.thankYouTitle` ends with a full-width exclamation mark instead of an ASCII one.
|
|
2467
|
+
|
|
2468
|
+
**Authored values still win.** An authored `successMessage`, `submitText` or `cancelText`, a plain string or a per-locale map, and a `thank-you` behaviour's own `title` and `message`, render as authored in every locale. Only the defaults are localized. No schema key is added, renamed or retyped.
|
|
2469
|
+
|
|
2470
|
+
**Three English strings change their rendered text**, because each is now the value of a key whose English differs from the old literal:
|
|
2471
|
+
|
|
2472
|
+
- the loading line, `Loading form...` with three ASCII full stops, is now `Loading form…` with the typographic ellipsis;
|
|
2473
|
+
- the thank-you heading, `Thanks!`, is now `Thank you!`, in the default arm, `WizardForm` and the form page;
|
|
2474
|
+
- the form page's default thank-you message, `Your submission has been received.`, is now `Your submission has been received successfully.`
|
|
2475
|
+
|
|
2476
|
+
Every other English default renders the same text as before. With no i18n provider mounted, plugin-form's forms still render English from their defaults tables, which `pnpm check:i18n-keys` holds byte-identical to the `en` pack.
|
|
2477
|
+
- 8732846: The console's undo confirmation toast reads the session's language (objectui#11056).
|
|
2478
|
+
|
|
2479
|
+
- **The toast after Undo.** Pressing the Undo button on an undoable success toast used to raise a hard-coded English "Change undone", even where the button itself was already translated (objectui#10969). Both console undo handlers, in `useConsoleActionRuntime` and in `RecordDetailView`, now read the new `actions.undone` key through the translator each already holds. Under zh-CN the toast reads 「已撤销更改」.
|
|
2480
|
+
- **English is unchanged.** The `en` value is `Change undone`, the same text as before.
|
|
2481
|
+
- **The new key is in all ten packs.** de, es, fr, ja, ko, pt and ru word it as their own `navigationSync.undone` without the word for navigation; ar does the same and gives the noun its article. zh reads 「已撤销更改」, with the 撤销 of `actions.undo` and the 更改 of the pack's 「保存更改」.
|
|
2482
|
+
|
|
2483
|
+
`AppContent`'s own Ctrl+Z handler, which toasts `Undo: ` or `Redo: ` followed by the operation's description, is not changed here.
|
|
2484
|
+
- 54997ff: fix(console,plugin-form,i18n): the public form page and the master-detail form chrome speak the user's language
|
|
2485
|
+
|
|
2486
|
+
objectui#11039 moved the form family's feedback chrome onto the locale packs and
|
|
2487
|
+
left two halves behind, both still English inside a Chinese UI.
|
|
2488
|
+
|
|
2489
|
+
The console form page (`/f/:slug`, `/forms/:name`) rendered its action chrome from
|
|
2490
|
+
literals: the submit button's `Submit`, its in-flight `Submitting…` and
|
|
2491
|
+
`Uploading…`, the `Redirecting…` line of a pending redirect, and the frame of the
|
|
2492
|
+
`Required: …` refusal. They now read `publicForm.submit`, `publicForm.submitting`,
|
|
2493
|
+
`fields.file.uploading` (the key every other form's in-flight Save label already
|
|
2494
|
+
reads), and two new keys, `publicForm.redirectPending` and
|
|
2495
|
+
`publicForm.requiredFields`. The refusal is one key with a `{{fields}}` hole and
|
|
2496
|
+
its labels are joined with the pack's own list separator, so the colon, its
|
|
2497
|
+
spacing and the word order belong to the locale.
|
|
2498
|
+
|
|
2499
|
+
`plugin-form` had the same literals in the master-detail form: `Loading columns…`,
|
|
2500
|
+
the `Subtotal` / `Tax (N%)` / `Total` stack, the row editor's `Line item — row N`
|
|
2501
|
+
title with its `Apply` and `Close`, the in-form collection's `Add`, and the Save
|
|
2502
|
+
button's `Saving…`; in `ModalForm`, the sr-only description of a master-detail
|
|
2503
|
+
dialog; and in `ObjectForm`, the hint under a field the caller may read but not
|
|
2504
|
+
write. They now read the pack (new keys under `form.masterDetail` and
|
|
2505
|
+
`form.deniedDescription`, plus the existing `detail.add`, `detail.saving` and
|
|
2506
|
+
`common.close`), in all ten packs.
|
|
2507
|
+
|
|
2508
|
+
Only defaults move. An authored value still wins everywhere it did: a collection
|
|
2509
|
+
`title` and `addLabel`, a dialog `description`, a field `description`, an authored
|
|
2510
|
+
field label inside the refusal. English output is byte-identical to the literals.
|
|
2511
|
+
- 1d6a23d: The console's global Undo and Redo toasts read the session's language (objectui#11080).
|
|
2512
|
+
|
|
2513
|
+
- **The Ctrl+Z / Ctrl+Shift+Z toasts.** `AppContent`'s `useGlobalUndo` handlers used to raise `Undo: ` or `Redo: ` in English before the operation's description, under every language. They now read two new keys, `actions.undoneOperation` and `actions.redoneOperation`, through the translator `AppContent` already holds. The operation's description is interpolated unchanged, so the pack owns the words and their order. Under zh-CN the toasts read 「撤销:…」 and 「重做:…」.
|
|
2514
|
+
- **English is unchanged.** The `en` values are `Undo: {{description}}` and `Redo: {{description}}`, the same text as before.
|
|
2515
|
+
- **The new keys are in all ten packs.** Each pack words them with its own `actions.undo` and its own word for Redo, followed by its usual colon: full-width in zh, and a space before the colon in fr.
|
|
2516
|
+
|
|
2517
|
+
The description of an operation whose action declared no `label` is still built in English by its producer. That half of objectui#11080 is not changed here.
|
|
2518
|
+
- cff8641: The dashboard's refresh button now speaks the session language (objectui#11097). `DashboardRenderer` and `DashboardGridLayout` hard-coded "Refresh All", "Refreshing…" and the accessible name "Refresh dashboard", so a zh-CN session read English on the button that every console dashboard shows since the console began wiring `onRefresh`. Both components now read the pack keys `dashboard.refreshAll`, `dashboard.refreshDashboard` and the existing `dashboard.refreshing`. The English wording is unchanged.
|
|
2519
|
+
|
|
2520
|
+
`@object-ui/i18n`: new keys `dashboard.refreshAll` and `dashboard.refreshDashboard` in every built-in locale pack.
|
|
2521
|
+
- 385ebc5: fix(plugin-form,fields,i18n): the record page's line-items panel and the line-items grid speak the user's language
|
|
2522
|
+
|
|
2523
|
+
Under a Chinese session the record page's `record:line_items` panel still
|
|
2524
|
+
rendered some of its chrome in English. Its default title read `Line Items` and
|
|
2525
|
+
its button read `Save` / `Saving…`. The line-items grid (`GridField`) read
|
|
2526
|
+
`Add line` on its Add button, `No items yet — click “Add” to begin.` in list
|
|
2527
|
+
mode, and `No items` when read-only.
|
|
2528
|
+
|
|
2529
|
+
They now read the locale packs. The button reuses `common.save` and
|
|
2530
|
+
`detail.saving`, and the empty text names `detail.add`. Four new keys are added
|
|
2531
|
+
to all ten packs: `form.lineItems.title`, `fields.grid.addLine`,
|
|
2532
|
+
`fields.grid.noItems` and `fields.grid.noItemsAddHint`.
|
|
2533
|
+
|
|
2534
|
+
Only the defaults move. An authored `title` or `add_label` still wins, and an
|
|
2535
|
+
authored `add_label` is also the label the empty text names. English output is
|
|
2536
|
+
byte-identical to the literals these replace, with or without an i18n provider.
|
|
2537
|
+
- a8c5509: fix(plugin-form,fields,i18n): the line-items panel, the grid field and the master-detail heading finish speaking the user's language
|
|
2538
|
+
|
|
2539
|
+
Under a Chinese session the rest of the line-items chrome still rendered in
|
|
2540
|
+
English. The record page's `record:line_items` panel read `Loading…`, `Save the
|
|
2541
|
+
record first to add line items.`, `This record’s line items have not been
|
|
2542
|
+
loaded.`, the `Failed to load line items` / `Failed to save line items`
|
|
2543
|
+
fallbacks and its no-`childObject` hint. The line-items grid (`GridField`) read
|
|
2544
|
+
its footer `Total`, its column chooser's `Columns` / `Optional columns`, the
|
|
2545
|
+
computed cell's `Computed` tooltip and its row actions. A master-detail
|
|
2546
|
+
collection with no `title` was headed `Line Items`.
|
|
2547
|
+
|
|
2548
|
+
They now read the locale packs. Reused keys: `common.loading`, `table.columns`,
|
|
2549
|
+
`form.masterDetail.total` (the footer), `view.dragToReorder` (the drag handle)
|
|
2550
|
+
and `form.lineItems.title` (the master-detail heading). New keys, in all ten
|
|
2551
|
+
packs: `form.lineItems.saveRecordFirst`, `form.lineItems.notLoaded`,
|
|
2552
|
+
`form.lineItems.loadFailed`, `form.lineItems.saveFailed`,
|
|
2553
|
+
`form.lineItems.noChildObject`, `fields.grid.optionalColumns`,
|
|
2554
|
+
`fields.grid.computed`, `fields.grid.openRow`, `fields.grid.duplicateRow` and
|
|
2555
|
+
`fields.grid.removeRow`.
|
|
2556
|
+
|
|
2557
|
+
Each row action now has one key, read by both its `aria-label` and its `title`.
|
|
2558
|
+
The English kept for each is the accessible name it already had:
|
|
2559
|
+
|
|
2560
|
+
- open the row in the full form: `Open row` (the tooltip was `Open full form`);
|
|
2561
|
+
- duplicate the row: `Duplicate row` (the tooltip was `Duplicate line`);
|
|
2562
|
+
- remove the row: `Remove row` (no tooltip, as before);
|
|
2563
|
+
- the drag handle: `Drag to reorder`.
|
|
2564
|
+
|
|
2565
|
+
Only the defaults move. An authored collection `title` still wins, and so does
|
|
2566
|
+
a failure message the server sent. Apart from the two tooltips above, English
|
|
2567
|
+
output is byte-identical to the literals these replace, with or without an i18n
|
|
2568
|
+
provider.
|
|
2569
|
+
- c2a8d23: fix(fields,plugin-form,i18n): the line-items grid's required-cell text and the master-detail form's config hints read the locale packs
|
|
2570
|
+
|
|
2571
|
+
Under a Chinese session two parts of the line-items family still rendered in
|
|
2572
|
+
English. The line-items grid (`GridField`) named a required, empty cell as the
|
|
2573
|
+
column label followed by ` is required`. That text is the plain cell's tooltip
|
|
2574
|
+
and the `error` the lookup and file cells take. A master-detail collection that
|
|
2575
|
+
could not be resolved showed one of three English configuration hints: no
|
|
2576
|
+
`childObject`, a child schema that failed to load, or no lookup or
|
|
2577
|
+
master_detail field linking the child to the parent.
|
|
2578
|
+
|
|
2579
|
+
They now read the locale packs. The grid cells reuse `validation.required`
|
|
2580
|
+
(`{{field}} is required`), with the column label in `{{field}}`: the sentence
|
|
2581
|
+
the form renderer already shows for a required field. The three hints read
|
|
2582
|
+
new keys in all ten packs: `form.masterDetail.noChildObject`,
|
|
2583
|
+
`form.masterDetail.schemaUnavailable` and
|
|
2584
|
+
`form.masterDetail.noRelationshipField`. Property names (`childObject`,
|
|
2585
|
+
`relationshipField`) and object names stay code elements and are never
|
|
2586
|
+
translated.
|
|
2587
|
+
|
|
2588
|
+
English output is byte-identical to the literals these replace, with or
|
|
2589
|
+
without an i18n provider.
|
|
2590
|
+
- 1263e40: fix(app-shell,plugin-detail): the record feed says "no permission" when its read is refused, instead of showing an empty list
|
|
2591
|
+
|
|
2592
|
+
This widens two published surfaces: `@object-ui/plugin-detail` gains the export `isRefusedFeedRead`, and `@object-ui/react`'s `DiscussionContextValue` gains the optional members `activityDenied` and `commentsDenied` (as do `RecordActivityTimelineProps` and `RecordChatterPanelProps`).
|
|
2593
|
+
|
|
2594
|
+
When the server refused a record's activity read (401, 403, or a permission
|
|
2595
|
+
envelope), the record page's discussion panel and the `record:activity` block
|
|
2596
|
+
rendered the empty state: "No comments yet" or "No activity recorded". A member
|
|
2597
|
+
who was not allowed to see the activity could not tell that apart from a record
|
|
2598
|
+
with no history. Every failed feed read ended in a catch that treated it as an
|
|
2599
|
+
empty answer.
|
|
2600
|
+
|
|
2601
|
+
Each feed read now keeps its verdict. A refused read shows a localized
|
|
2602
|
+
no-permission state in place of the empty state: a lock and "You don't have
|
|
2603
|
+
permission to view activity on this record." When the other read did answer, its
|
|
2604
|
+
rows stay on screen and the notice above them names the half that was withheld.
|
|
2605
|
+
A refused `sys_comment` read shows "You don't have permission to view comments on
|
|
2606
|
+
this record." in the same way.
|
|
2607
|
+
|
|
2608
|
+
- **What counts as refused** is one verdict, `isRefusedFeedRead`, exported from
|
|
2609
|
+
`@object-ui/plugin-detail`. Both surfaces use it. It is the `forbidden` and
|
|
2610
|
+
`unauthorized` kinds of the existing `classifyLoadError` read classifier, and
|
|
2611
|
+
it reads no status of its own.
|
|
2612
|
+
- **Unchanged:** a read that answers with zero rows, a 404 (`sys_activity` on a
|
|
2613
|
+
deployment without the audit plugin), and any other failure such as a 500 keep
|
|
2614
|
+
the empty state. The panel has no error state of its own.
|
|
2615
|
+
- **Not retried:** a refusal is recorded and shown; the read is not re-issued.
|
|
2616
|
+
- `DiscussionContext` (`@object-ui/react`) gains two optional members,
|
|
2617
|
+
`activityDenied` and `commentsDenied`, which the host that owns the fetch
|
|
2618
|
+
sets. `RecordActivityTimeline` and `RecordChatterPanel` take the same two
|
|
2619
|
+
optional props.
|
|
2620
|
+
- Two new `detail.*` strings, `activityAccessDenied` and `commentsAccessDenied`,
|
|
2621
|
+
are translated in all ten locale packs.
|
|
2622
|
+
- 9419df1: fix(app-shell): the save warning has a sentence of its own for a formula field the server ignored
|
|
2623
|
+
|
|
2624
|
+
objectstack `b2805465` gave the write path a fourth strip reason, `computed`: a
|
|
2625
|
+
value the caller sends for a `formula` field is stripped on every write path,
|
|
2626
|
+
because the server computes that field and has nowhere to store it. The save
|
|
2627
|
+
warning now words that reason as what it is — "Calculated by the server from a
|
|
2628
|
+
formula, so the value sent did not take effect: …" — through a new
|
|
2629
|
+
`detail.writeStrippedComputed` key in all ten locale packs, and never as a
|
|
2630
|
+
read-only lock, which would send the user after a permission problem that does
|
|
2631
|
+
not exist.
|
|
2632
|
+
|
|
2633
|
+
The reason table stays exhaustive over the spec's union, and one spelling now
|
|
2634
|
+
compiles against both the published `@objectstack/spec` 17.5.0 (which does not
|
|
2635
|
+
name the arm) and objectstack `main` (which does), which is what the
|
|
2636
|
+
`Spec Main Shape Gate` needs to go green again.
|
|
2637
|
+
|
|
2638
|
+
When the sentence reaches a user: the adapter judges a reported reason against
|
|
2639
|
+
the spec release this bundle is built with, and 17.5.0 does not declare
|
|
2640
|
+
`computed`, so until that release carries it a formula strip from a newer server
|
|
2641
|
+
is still reported as "Not applied by the server: …" — truthful, but without the
|
|
2642
|
+
cause. The new sentence is used from the spec release that declares the arm on.
|
|
2643
|
+
- 55d18c6: fix(i18n): every count plural family carries every plural form its language uses (objectui#11432)
|
|
2644
|
+
|
|
2645
|
+
The 14 i18next count plural families (`console.ai.usage.resetsWeeklyHours`,
|
|
2646
|
+
`detail.fileCount`, `perm.facet.objects` and the rest) held `_one`, sometimes
|
|
2647
|
+
`_other`, and a base key. i18next asks `Intl.PluralRules` for the one suffix a number
|
|
2648
|
+
needs, and where a pack lacked that slot the base key answered with its single
|
|
2649
|
+
string. So `ru` read "Сброс через 3 часов" on the AI usage indicator where Russian
|
|
2650
|
+
needs "часа", and `ar` served 0, 2, 3-10 and 11-99 from one "singular(plural)"
|
|
2651
|
+
marker.
|
|
2652
|
+
|
|
2653
|
+
Every pack now spells out every CLDR category its language selects: `_other` where
|
|
2654
|
+
the base stood in for it, `ru` `_few`/`_many`, `ar` `_zero`/`_two`/`_few`/`_many`,
|
|
2655
|
+
and `fr`/`es`/`pt` `_many` (exact millions, "1000000 de fichiers"). Existing slots
|
|
2656
|
+
whose form was wrong for their own category are corrected: `ar` `_other` (100-102…)
|
|
2657
|
+
now holds the singular, `ru` `_other` (fractions) the genitive singular, and the four
|
|
2658
|
+
`ru` `perm.facet.*` `_one` forms agree with the numeral like the new slots. English
|
|
2659
|
+
and the `de`/`zh`/`ja`/`ko` strings a user sees do not change.
|
|
2660
|
+
|
|
2661
|
+
`all-locales-key-parity.test.ts` now computes each family's required slots from
|
|
2662
|
+
`Intl.PluralRules` per pack, so a new family or a new pack that misses a category
|
|
2663
|
+
fails at PR time, and the parity rule admits a key `en` lacks only when it is such a
|
|
2664
|
+
slot.
|
|
2665
|
+
- c4ab6d0: The ten locale packs drop `appDesigner.fieldDesigner.validationRules` ("Validation Rules") and `appDesigner.fieldDesigner.addRule` ("Add Rule") (objectui#11434).
|
|
2666
|
+
|
|
2667
|
+
Both labelled an editor for `DesignerFieldDefinition.validationRules` that was never built: no component asked for either key, and `@object-ui/types` retires that member on both faces in this release. A host that looked either key up itself now gets the key back instead of a label; author its own label if it needs one.
|
|
2668
|
+
- 6158e4c: objectui now resolves `@objectstack/*` 17.6.0 (objectui#11438). One declared range moves: `@object-ui/types` raises its `@objectstack/spec` floor to `^17.6.0` in objectui#11227's own changeset, because its published types read `EmptyState` / `EmptyStateSchema`, which 17.5.0 does not export. Every other `@objectstack/*` range already admits 17.6.0 and stays as it was, and `check:spec-floors` names no other floor that has to rise. Everything below is a contract move 17.6.0 made, and it already reaches any consumer that resolves `@objectstack/spec ^17.6`.
|
|
2669
|
+
|
|
2670
|
+
- `@object-ui/components`: the `page:header` registration no longer publishes a `breadcrumb` input. 17.6.0 retires the key (objectstack#20758) and refuses it by name; the renderer has ignored it since objectui#11166.
|
|
2671
|
+
- `@object-ui/i18n`: a served translation document is recognised as a spec payload when it carries only the `picklists` group, which 17.6.0's `GetTranslationsResponseSchema` adds. Such a bundle used to be returned untransformed, so nothing read it. The group is now namespaced under `app` like every other group.
|
|
2672
|
+
- `@object-ui/console`: the bundle inlines the 17.6.0 packages, so its client-side validation answers as a 17.6.0 server does. The first screen is smaller: 17.6.0's spec root no longer carries the migration chain, so the eager closure is 250,096 gzipped bytes lighter (measured against a `main` build), and the bundle budget comes down by that amount.
|
|
2673
|
+
- 4a1adb7: Count labels read the right noun form in every language at every count: each label a word must agree with is now an i18next count family in all ten packs (objectui#11445).
|
|
2674
|
+
|
|
2675
|
+
**What was wrong.** objectui#11432 gave every count family every CLDR category its language selects. Two other spellings of the same defect sat outside that rule. Fifteen labels were code-selected pairs (`detail.attachmentCount` / `detail.attachmentCountPlural`, `common.itemCount` / `common.itemCountOne`, and thirteen more): the component chose between two keys on `=== 1`, which gives a language two forms, so Russian read `3 вложений` and Arabic `2 مرفقات`. Fifty-three labels were one string at every count, so Russian read `Согласовать 3 запросов?`, Arabic `منذ 5 دقيقة`, Spanish `1 seleccionados` and English `3 row modified`.
|
|
2676
|
+
|
|
2677
|
+
**What changes.** Each of those 68 keys is a family: `en` `_one` / `_other`, `ru` `_one` / `_few` / `_many` / `_other`, `ar` all six categories, and each other pack the categories its language selects. The base key keeps its value and answers a call made without a count.
|
|
2678
|
+
|
|
2679
|
+
**Removed keys.** The fifteen code-selected siblings leave all ten packs: `lookup.recordCountOne`, `common.itemCountOne`, `list.recordCountOne`, `detail.reactionCountOne`, `detail.attachmentCountPlural`, `detail.replyCountPlural`, `console.objectView.recordCountOne`, `search.resultsCountPlural`, `search.itemsAvailableOne`, `ai.formAssist.suggestionCountOne`, `ai.formAssist.appliedCountOne`, `collaboration.commentCountOne`, `collaboration.reactionCountOne`, `collaboration.presentUserCountOne` and `collaboration.moreUserCountOne`. A host that looked one up itself now gets the key back instead of a label: call the family key with `{ count }` instead (`t('detail.replyCount', { count: n })`), and i18next picks the form.
|
|
2680
|
+
|
|
2681
|
+
**Provider-less fallback.** `createSafeTranslation` now reads a count family's `_one` / `_other` row for a numeric `count` before the base row, in the order i18next reads the `en` pack, so a host with no `I18nProvider` keeps rendering "1 reply" / "3 replies". A string `count` still reads the base row, as it does in i18next.
|
|
2682
|
+
|
|
2683
|
+
**Supersedes, in this release.** The notes for objectui#9664, objectui#10024, objectui#10242, objectui#10425 and objectui#10636 describe the `…One` / `…Plural` switch these families replace; each now carries a dated note saying so. Their `ru` / `ar` count labels (`Записей: N`, `عدد السجلات: N`) stay only on the base key, which answers a call made without a count.
|
|
2684
|
+
|
|
2685
|
+
**Pinned.** `count-families-11445.test.ts` fails on any `…Plural` key in a pack and on any `{{count}}` value outside a family unless its count-neutral list names the key with the reason no word agrees with the number.
|
|
2686
|
+
- d0097af: The console's Create View dialog now creates chart views the platform accepts (objectui#11576).
|
|
2687
|
+
|
|
2688
|
+
The dialog wrote a chart view as `chart: { chartType, xAxisField, yAxisFields }`, the inline
|
|
2689
|
+
axes `@objectstack/spec` retired under ADR-0021. The spec's list chart block refuses those two
|
|
2690
|
+
keys by name and requires `dataset` and `values`, so the platform's view write door refused
|
|
2691
|
+
every chart view created from the console, from both "Save as view" and the view tab bar's
|
|
2692
|
+
add button. The dialog had already closed, so the user saw nothing happen.
|
|
2693
|
+
|
|
2694
|
+
The chart type now binds a semantic-layer dataset instead:
|
|
2695
|
+
|
|
2696
|
+
- the dialog offers the ADR-0021 datasets whose base `object` is the view's own object, and
|
|
2697
|
+
only those;
|
|
2698
|
+
- it then offers the chosen dataset's measures (`values`) and, when the dataset declares any,
|
|
2699
|
+
its dimensions (`dimensions`, optional);
|
|
2700
|
+
- an object that exposes no dataset shows the chart type as unavailable, with its own reason.
|
|
2701
|
+
|
|
2702
|
+
The payload is `chart: { chartType, dataset, values, dimensions? }`, with no second spelling.
|
|
2703
|
+
The dataset catalog reader the dashboard and report editors use now also carries each dataset's
|
|
2704
|
+
base `object`.
|
|
2705
|
+
|
|
2706
|
+
In `@object-ui/i18n`, the four `console.objectView` axis labels (`xAxisField`, `xAxisFieldHelp`,
|
|
2707
|
+
`yAxisField`, `yAxisFieldHelp`) leave all ten packs, and eight keys label the dataset, measure
|
|
2708
|
+
and dimension picks (`dataset`, `datasetHelp`, `chartMeasure`, `chartMeasureHelp`,
|
|
2709
|
+
`chartDimension`, `chartDimensionHelp`, `viewTypeUnavailableDataset`, `noDatasetMeasure`).
|
|
2710
|
+
- 432882b: The marketplace consent panel no longer promises confinement the runtime does not provide.
|
|
2711
|
+
|
|
2712
|
+
`PluginDisclosure` introduced a code-bearing package's structured permission set with "On install, this package will be granted:". A list of grants on a security panel is read as a confinement promise — the complement assumed denied — and on this platform it is not. The consented set is persisted (`sys_package_installation.granted_permissions`), re-confirmed on a widening upgrade, and registered on the runtime's `PluginPermissionEnforcer` at load; it is queried by nothing, because `SecurePluginContext` has zero production construction sites and the fs/network gates have no caller at all (measured on objectstack `9bd4344e4`; recorded upstream in objectstack `aaacf1d5c`).
|
|
2713
|
+
|
|
2714
|
+
`marketplace.disclosure.grantsIntro` now states a REQUEST — "This package requests:" — and a new `marketplace.disclosure.notEnforced` line beside the list says the set is recorded at install, re-confirmed if a later version asks for more, and not yet a runtime restriction. Both land in all ten locale packs; the `ja` value stays predicate-final so that pack's halfwidth-colon rule still decides it.
|
|
2715
|
+
|
|
2716
|
+
The trust-tier badge is deliberately untouched: it is the trust-tier half of the same panel, which objectstack `a9ee98992` settled separately.
|
|
2717
|
+
- b06e374: The Spanish pack renders `Done` as `Listo` at every one of the four sites that say it
|
|
2718
|
+
(objectui#3880, triage adjudication 2026-08-09). `grid.bulk.done` — the footer button that
|
|
2719
|
+
dismisses the bulk-action result dialog — read `Hecho` while `common.done`, `view.done` and
|
|
2720
|
+
`form.fullscreen.done` all read `Listo`, so the same English word rendered two ways in
|
|
2721
|
+
Spanish across dialogs a user meets in one session.
|
|
2722
|
+
|
|
2723
|
+
Adjudicated a typo rather than a deliberate contextual split, on three checks. All four
|
|
2724
|
+
keys hold the byte-identical `en` value `Done`, and all four call sites are the same
|
|
2725
|
+
control: a dialog-footer button whose click finishes or dismisses the surface
|
|
2726
|
+
(`BulkActionDialog` `onClose(result)`, `ManageViewsDialog` `onOpenChange(false)`,
|
|
2727
|
+
`fullscreen-editor` `commitFullscreen`, `InviteMemberDialog`'s invitation-created footer).
|
|
2728
|
+
The nine other packs each render all four identically (de `Fertig`, fr `Terminé`, pt
|
|
2729
|
+
`Concluído`, ru `Готово`, ja `完了`, ko `완료`, zh `完成`, ar `تم`), so no other translation
|
|
2730
|
+
pass had found a context worth splitting on. And the neighbouring `Hecho`/`Deshecho`
|
|
2731
|
+
pairing that could have justified it does not hold: `grid.bulk.undo` is the verb `Deshacer`,
|
|
2732
|
+
and `Deshecho: ` is `undonePrefix`, a result-line status rather than a button.
|
|
2733
|
+
|
|
2734
|
+
`Hecho` moved to the 3:1 majority `Listo`, which is the value objectui#3546 slice seven had
|
|
2735
|
+
already chosen for `common.done`. `packages/i18n/src/__tests__/residue-namespaces-3546.test.tsx`
|
|
2736
|
+
pinned the old outlier as a recorded example of deliberate divergence; that pin now asserts
|
|
2737
|
+
the four as one value instead, and its note keeps the history plus the `Pending`/zh row,
|
|
2738
|
+
which remains a genuine deliberate split.
|
|
2739
|
+
|
|
2740
|
+
No `en` value changes, so no other pack is asked to follow. This is the value half of
|
|
2741
|
+
objectui#3880 only — the card's 281/164 shared-string census stays on the card as
|
|
2742
|
+
documentation, and is explicitly not a gate: 164 of those groups diverge legitimately.
|
|
2743
|
+
- 978507b: **Behaviour change:** an action whose own declared `visible` gate hides it is no
|
|
2744
|
+
longer run by `autoTrigger`, and the refusal is reported instead of swallowed
|
|
2745
|
+
(objectui#4191).
|
|
2746
|
+
|
|
2747
|
+
Before, `action:button` registered its auto-trigger effect above its `visible`
|
|
2748
|
+
early return, so an action carrying both `autoTrigger: true` and a `visible`
|
|
2749
|
+
that evaluated false rendered nothing and executed anyway; `action:menu` did the
|
|
2750
|
+
same for overflow actions, deliberately, to stay in step. The declared nav deep
|
|
2751
|
+
link made that reachable from a URL: `ObjectView` armed the requested action
|
|
2752
|
+
after checking only its placement, so any list-page link naming an action its
|
|
2753
|
+
author had hidden there ran it.
|
|
2754
|
+
|
|
2755
|
+
The author's verdict now outranks the transport flag:
|
|
2756
|
+
|
|
2757
|
+
- `action:button` and `action:menu` refuse the auto-trigger of an action their
|
|
2758
|
+
own `visible` gate hides. Both go through the one shared hook, so which
|
|
2759
|
+
renderer received the action (decided by `action:bar`'s `maxVisible`, and so by
|
|
2760
|
+
the viewport) cannot change the outcome. The refusal shows a warning toast
|
|
2761
|
+
naming the action, from the new `actions.notAvailableHere` key (all ten locale
|
|
2762
|
+
packs), and writes a console diagnostic in development builds. An action whose
|
|
2763
|
+
predicate becomes true later in the same mount still runs, once.
|
|
2764
|
+
- The deep-link preparation step on the object list evaluates the same
|
|
2765
|
+
predicate before it marks the action `autoTrigger`. A hidden action is not
|
|
2766
|
+
armed and the deep-link parameter stays in the URL, so the one-shot intent is
|
|
2767
|
+
not spent on nothing, and the same notice is shown.
|
|
2768
|
+
|
|
2769
|
+
This was never an authorization boundary: confirm dialogs, parameter
|
|
2770
|
+
collection, entitlement checks and server-side permissions apply on every
|
|
2771
|
+
execute path, before and after this change. What the gate now protects is the
|
|
2772
|
+
author's rule about where an action may be offered.
|
|
2773
|
+
- 594704f: The `console.objectView.*` config-panel vocabulary is retired — 116 keys removed from each
|
|
2774
|
+
of the ten packs, 1160 translated strings that nothing read (objectui#4730, maintainer
|
|
2775
|
+
ruling 2026-08-19).
|
|
2776
|
+
|
|
2777
|
+
The namespace held 209 keys per pack. 116 of them labelled a view-configuration settings
|
|
2778
|
+
panel that does not exist: appearance and density toggles, accessibility attributes,
|
|
2779
|
+
conditional-formatting rules, row-action and inline-edit switches, quick-filter builders,
|
|
2780
|
+
an advanced-settings tier. `packages/app-shell/src/views/ViewConfigPanel.tsx` — the panel
|
|
2781
|
+
they were written for — was migrated off the legacy `buildViewConfigSchema` engine onto
|
|
2782
|
+
`ViewVariantInspector`, a spec-driven inspector whose field labels come from
|
|
2783
|
+
`@objectstack/spec` metadata rather than from this namespace. The panel was replaced; the
|
|
2784
|
+
keys were not cleaned up with it.
|
|
2785
|
+
|
|
2786
|
+
Removed under objectui#4658's three-legged evidence standard, re-measured on this branch's
|
|
2787
|
+
merge base rather than inherited from the card: zero `t()`/`tt()` call sites, zero textual
|
|
2788
|
+
occurrence of the dotted key anywhere in the repo outside the packs that define it, and a
|
|
2789
|
+
consumer spot-check confirming no i18n wiring. The 93 live keys stay — the create-view
|
|
2790
|
+
dialog fields, the view-type catalogue, `new`/`save`/`cancel`, the object-not-found copy,
|
|
2791
|
+
plus the 38 keys whose spelling still appears somewhere the AST pass cannot see, which are
|
|
2792
|
+
out of scope here.
|
|
2793
|
+
|
|
2794
|
+
Four of the retired keys name `ListViewSchema` properties that are still active —
|
|
2795
|
+
`rowActions`, `inlineEdit`, `hiddenFields`, `filterableFields`. They are retired anyway, by
|
|
2796
|
+
the ruling's own words: a live schema property is not a consumer of a locale string; only a
|
|
2797
|
+
labelled UI control is. If such a panel is ever specified, its keys are re-authored
|
|
2798
|
+
alongside it.
|
|
2799
|
+
|
|
2800
|
+
`packages/i18n/src/__tests__/objectView-config-keys-retired-4730.test.ts` pins the removal
|
|
2801
|
+
by name. Every i18n gate in this repo runs call site → key, so none of them can see a dead
|
|
2802
|
+
key come back: the parity gate is fully satisfied by 116 dead keys present in all ten packs,
|
|
2803
|
+
and the reverse sweep that found them (`scripts/check-i18n-dead-keys.mjs`) is report-only by
|
|
2804
|
+
design. The pin is the only thing that would notice.
|
|
2805
|
+
- 41b7ce3: **View configuration is explicitly org-wide, and its write path is now gated (objectstack#7494's
|
|
2806
|
+
ruling, maintainer 2026-08-12).** The `sort` / `hiddenFields` / `columnState` / `rowHeight` that a
|
|
2807
|
+
list toolbar persists were never per-user: they are one shared row on the view, so an ordinary user
|
|
2808
|
+
dragging a column or cycling density was re-styling that view for the entire organization. Nothing
|
|
2809
|
+
in the console said so, and nothing stopped it. A per-user scope stays parked (objectstack#7611,
|
|
2810
|
+
v18) and is deliberately not built here — which is precisely why the write has to be gated rather
|
|
2811
|
+
than narrowed: there is no second, private store for it to fall back to.
|
|
2812
|
+
|
|
2813
|
+
`ObjectStackAdapter.updateViewConfig` now refuses when the session's **reported** ADR-0066 capability
|
|
2814
|
+
set does not contain `manage_metadata`, throwing the new `ViewConfigPermissionDeniedError`
|
|
2815
|
+
(`VIEW_CONFIG_PERMISSION_DENIED`, with `isViewConfigPermissionDeniedError` and the
|
|
2816
|
+
`VIEW_CONFIG_CAPABILITY` constant alongside it). The gate is the **first** statement in the method —
|
|
2817
|
+
before `connect()`, before the payload is assembled — so a refused call puts nothing on the wire.
|
|
2818
|
+
It is on the write rather than on the toolbar button on purpose: withholding the affordance would
|
|
2819
|
+
leave the method still accepting the call from anything else holding the adapter, whereas a gate on
|
|
2820
|
+
the write is inherited by every caller, present and future.
|
|
2821
|
+
|
|
2822
|
+
`manage_metadata` is not a newly minted name. It is the capability this repo already treats as
|
|
2823
|
+
metadata-authoring authority — `HomePage`'s `AUTHORING_CAPABILITY`, the one the server itself
|
|
2824
|
+
refuses metadata writes without — and the gated write goes through `client.meta.saveItem`, the very
|
|
2825
|
+
same ADR-0005 metadata door, so this applies the authority the server is already applying instead of
|
|
2826
|
+
inventing a parallel one.
|
|
2827
|
+
|
|
2828
|
+
**Unknown fails open, by doctrine.** A capability set that was never reported (a backend predating
|
|
2829
|
+
ADR-0066, or no permission provider mounted) is not a denial: the server enforces regardless, so a
|
|
2830
|
+
client-side refusal on missing data cannot protect anything and can only break a permitted user. A
|
|
2831
|
+
*reported* empty grant gates strictly. Hosts push the session's capabilities in with the new
|
|
2832
|
+
`setSystemCapabilities`; `ObjectView` wires it from `usePermissions()`.
|
|
2833
|
+
|
|
2834
|
+
The refusal is also **said out loud**. `ObjectView`'s persist path previously swallowed every failure
|
|
2835
|
+
into `console.error`, which for a debounced toggle whose UI has already moved would have left the
|
|
2836
|
+
operator looking at a density they did not get; a denied write now raises a toast. And the "View
|
|
2837
|
+
settings" popover — where density and field visibility are actually changed — now states the scope
|
|
2838
|
+
before the operator acts: *"Grouping, color, density, and visible fields. Applies to everyone who
|
|
2839
|
+
uses this view."*, translated in all ten packs.
|
|
2840
|
+
- 95f8704: `record:path` now announces each stage's state, not just its label (WCAG 2.2 SC 1.4.1)
|
|
2841
|
+
|
|
2842
|
+
The lifecycle path distinguished travelled, upcoming and lost-terminal stages with
|
|
2843
|
+
colour plus a `✓`/`✗` glyph, and both glyphs are `aria-hidden` decoration.
|
|
2844
|
+
`aria-current="step"` marked the current stage and nothing else, so a screen-reader
|
|
2845
|
+
user heard a run of identically-announced items — and a rejected stage announced
|
|
2846
|
+
exactly like an ordinary stage the record had not reached yet.
|
|
2847
|
+
|
|
2848
|
+
Each stage now carries an accessible name composing its (already picklist-localized)
|
|
2849
|
+
label with its state, from five new `detail.pathStage*` keys translated in all ten
|
|
2850
|
+
locale packs. The glyphs stay decorative and the readout's `role="listitem"` /
|
|
2851
|
+
`aria-current` semantics are unchanged.
|
|
2852
|
+
|
|
2853
|
+
The name is composed into `aria-label` rather than visually-hidden text because
|
|
2854
|
+
`listitem` takes its name from the author only: text placed inside a stage computes
|
|
2855
|
+
to an empty accessible name, so the visually-hidden shape would have looked right in
|
|
2856
|
+
the markup and delivered nothing to the accessibility tree.
|
|
2857
|
+
- 8d3a529: `record:path` finishes localizing and de-colouring its accessible names — the two residues
|
|
2858
|
+
objectui#5916 named and deliberately left behind (objectui#5956, objectui#5957).
|
|
2859
|
+
|
|
2860
|
+
**The list's own label was English on a localized surface, and the other one named nothing.**
|
|
2861
|
+
Both the desktop and the mobile `role="list"` row did
|
|
2862
|
+
`aria-label={schema.aria?.label || 'Record path'}`, so a zh/ja/ar session heard `Record path`
|
|
2863
|
+
for the list while every stage inside it announced in the session locale — one control
|
|
2864
|
+
speaking two languages at once. The fallback is now `detail.pathLabel`, translated in all ten
|
|
2865
|
+
packs; the `schema.aria.label` author override still wins ahead of it.
|
|
2866
|
+
|
|
2867
|
+
The lost-terminal alt group was a different defect wearing the same clothes: its
|
|
2868
|
+
`aria-label="Alternative terminal stages"` sat on a bare `div`, which has the `generic` role,
|
|
2869
|
+
and browsers expose no accessible name on a generic element. That string reached nobody —
|
|
2870
|
+
inert, not merely untranslated — so translating it would have shipped copy to ten packs that
|
|
2871
|
+
no user can hear. It is removed rather than given a role that takes a name, on three
|
|
2872
|
+
measurements: nothing is lost (it was never announced), it would be redundant (every stage
|
|
2873
|
+
inside already announces `closed lost` in the session locale after objectui#5916, in the one
|
|
2874
|
+
place `role="list"` can carry it), and it would fork the two rows (the mobile row renders one
|
|
2875
|
+
flat list with no alt group, so a named group would make one control expose two structures by
|
|
2876
|
+
viewport).
|
|
2877
|
+
|
|
2878
|
+
**An unreached goal terminus was distinguished by hue alone.** `railClass` paints it
|
|
2879
|
+
`bg-emerald-500/30` where a plain upcoming stage gets `bg-muted` — the renderer's own note
|
|
2880
|
+
calls this "a faint emerald so the goal is legible" — while both announced the identical
|
|
2881
|
+
`{{stage}}, upcoming`. Two stages ahead of the record painted differently and read the same:
|
|
2882
|
+
the WCAG 2.2 SC 1.4.1 class objectui#5916 closed, on the one distinction it left behind, and
|
|
2883
|
+
reachable without authors opting in because `classify()` finds `won` through the `WON_TOKENS`
|
|
2884
|
+
heuristic as well as an explicit `terminal: 'won'`. New key `detail.pathStageWonUpcoming`
|
|
2885
|
+
(`{{stage}}, goal stage, not reached`), translated in all ten packs.
|
|
2886
|
+
|
|
2887
|
+
Scoped to the UNREACHED goal, which is a measurement of the stylesheet rather than a
|
|
2888
|
+
preference: a reached goal terminus paints `bg-primary` when current and `bg-emerald-500` when
|
|
2889
|
+
completed, byte-identical to any other current or completed stage. Naming it apart would hand
|
|
2890
|
+
a screen reader a distinction the screen does not make — the mirror image of the defect — so
|
|
2891
|
+
it is one new key, not a pair, and a test pins that decision so it cannot drift into a fourth
|
|
2892
|
+
state unnoticed.
|
|
2893
|
+
|
|
2894
|
+
Both new keys also land in `DETAIL_DEFAULT_TRANSLATIONS`, which
|
|
2895
|
+
`defaults-maps-mirror-en-pack` compares against the `en` pack key by key, so neither can fork
|
|
2896
|
+
between a provider-mounted console and a provider-less embed. No existing `en` value changes,
|
|
2897
|
+
so no pack is asked to follow an edit.
|
|
2898
|
+
- 5ac2e2c: The merged `Loading…` group now reads one way per language (objectui#5972).
|
|
2899
|
+
|
|
2900
|
+
objectui#3878 converged every pack on the typographic ellipsis, and in doing so **merged**
|
|
2901
|
+
the ASCII `Loading...` group into the U+2026 `Loading…` group. Nobody re-measured the
|
|
2902
|
+
wording afterwards. Re-derived on today's tree by flattening all ten packs and comparing
|
|
2903
|
+
exact values, the group is 10 keys — `lookup.loading`, `common.loading`,
|
|
2904
|
+
`fields.recipient.loading`, `grid.import.historyLoading`, `grid.bulk.loading`,
|
|
2905
|
+
`detail.loading`, `report.loading`, `dashboard.loading`, `auth.device.loading`,
|
|
2906
|
+
`approvalsInbox.loadingMore` — and `de` rendered them four ways, `ko` two and `ar` two,
|
|
2907
|
+
while en/zh/ja/fr/es/pt/ru were already unanimous.
|
|
2908
|
+
|
|
2909
|
+
Three packs move, translation copy only — no key is added or removed, no `en` value
|
|
2910
|
+
changes, and every value keeps its U+2026:
|
|
2911
|
+
|
|
2912
|
+
- **de** → `Wird geladen…` on `detail.loading`, `report.loading` (were `Laden…`) and
|
|
2913
|
+
`approvalsInbox.loadingMore` (was `Lädt…`). The passive is both the group majority and
|
|
2914
|
+
the pack's dominant register for in-flight states generally.
|
|
2915
|
+
- **ko** → `로딩 중…` on `fields.recipient.loading`, `grid.bulk.loading`,
|
|
2916
|
+
`grid.import.historyLoading` and `approvalsInbox.loadingMore` (were `불러오는 중…`).
|
|
2917
|
+
Majority, and it matches the pack's own pattern: `불러오는 중` is what `ko` uses when the
|
|
2918
|
+
string names the thing being loaded, the bare form is `로딩 중`.
|
|
2919
|
+
- **ar** → `جارٍ التحميل…` on `common.loading` and `detail.loading` (were `جاري التحميل…`).
|
|
2920
|
+
This one is an orthography normalization rather than a wording choice: `جارٍ` is the
|
|
2921
|
+
indefinite منقوص participle with tanwīn on the rāʾ, `جاري` the yāʾ-retaining form.
|
|
2922
|
+
|
|
2923
|
+
`de` `auth.device.loading` deliberately stays `Lade…`. It is the one member whose outlier
|
|
2924
|
+
spelling is coherent with its own screen: `DeviceAuthPage` renders that namespace's three
|
|
2925
|
+
in-flight states together and `de` writes all three in the same first-person voice
|
|
2926
|
+
(`Genehmige…`, `Ablehne…`, `Lade…`), the other two being outside this group. Converging it
|
|
2927
|
+
alone would manufacture a fresh same-screen inconsistency, so it is reported as a fork and
|
|
2928
|
+
pinned as a named exemption instead.
|
|
2929
|
+
|
|
2930
|
+
`packages/i18n/src/__tests__/ellipsis-glyph-3878.test.ts` gains the per-language
|
|
2931
|
+
uniformity pin beside the glyph rule that created the group. The pin derives the group
|
|
2932
|
+
from `en` and asserts its membership and per-language value counts **before** asserting
|
|
2933
|
+
uniformity, so it cannot pass by matching nothing.
|
|
2934
|
+
- b08b7eb: All ten locale packs now define `home.recentApps.itemType.report` and
|
|
2935
|
+
`home.recentApps.itemType.metadata` (objectui#6023). `RecentItem['type']` is a six-member
|
|
2936
|
+
union and `useTrackRouteAsRecent` writes both of these at runtime — `metadata` on any
|
|
2937
|
+
`/metadata/<type>/<name>` route visit, `report` on any `/report/<name>` visit — but the
|
|
2938
|
+
packs defined only four of the six, so the Recently Accessed and Starred cards labelled
|
|
2939
|
+
those two items from the call sites' inline `defaultValue` instead of from the pack.
|
|
2940
|
+
|
|
2941
|
+
Ten packs missing the same member is full parity, so no pack-vs-pack gate could see it,
|
|
2942
|
+
and the prefix rule only ever asked whether `home.recentApps.itemType` resolved, which it
|
|
2943
|
+
did. What made it quiet rather than loud is the `defaultValue`: English readers saw a
|
|
2944
|
+
plausible `Report` / `Metadata` (lowercase in the rail) rather than a raw key, and the
|
|
2945
|
+
other nine locales saw those English words — objectui#3517's mechanism for hiding a
|
|
2946
|
+
missing key for months.
|
|
2947
|
+
|
|
2948
|
+
The nine translations were taken from each pack's own existing rendering of the same word
|
|
2949
|
+
rather than composed: the singular `Report` that `appDesigner.navReport`,
|
|
2950
|
+
`appDesigner.navTypeReport` and `search.badgeReport` already carry (ar `تقرير`, de
|
|
2951
|
+
`Bericht`, es `Informe`, fr `Rapport`, ja `レポート`, ko `보고서`, pt `Relatório`, ru
|
|
2952
|
+
`Отчёт`, zh `报表`), and the `Metadata` that `layout.metadata.label` already carries (ar
|
|
2953
|
+
`البيانات الوصفية`, de `Metadaten`, es `Metadatos`, fr `Métadonnées`, ja `メタデータ`, ko
|
|
2954
|
+
`메타데이터`, pt `Metadados`, ru `Метаданные`, zh `元数据`). No new vocabulary was invented
|
|
2955
|
+
for any locale.
|
|
2956
|
+
|
|
2957
|
+
The two matching entries in `scripts/i18n-call-site-key-baseline.json`'s `missingMembers`
|
|
2958
|
+
are cleared, since that list is a ratchet: a baselined entry whose defect is gone fails the
|
|
2959
|
+
build too. The union itself is untouched — narrowing `RecentItem['type']` is the other
|
|
2960
|
+
resolution the gate accepts, and it would have been a lie about data both call sites
|
|
2961
|
+
demonstrably write.
|
|
2962
|
+
- 5fa06c4: The zh pack's `console.breadcrumb.reports` renders 报表, the noun the rest of the pack
|
|
2963
|
+
already uses for the report feature (objectui#6166).
|
|
2964
|
+
|
|
2965
|
+
**The authority for this edit is the maintainer ruling of 2026-08-25, not an occurrence
|
|
2966
|
+
count.** The card was filed explicitly as a native-speaker call and explicitly forbade
|
|
2967
|
+
resolving it by normalising to the majority: 报表 (a tabular/data report) and 报告 (a
|
|
2968
|
+
written/narrative report) are not interchangeable, so a pack that spells one key
|
|
2969
|
+
differently from its siblings is evidence of a majority and never, on its own, evidence of
|
|
2970
|
+
a mistake. The maintainer ruled that no deliberate narrative-report distinction was
|
|
2971
|
+
intended here and that the breadcrumb names the same report feature the rest of the pack
|
|
2972
|
+
calls 报表.
|
|
2973
|
+
|
|
2974
|
+
The render context corroborates the ruling and closes the confidence gap triage recorded
|
|
2975
|
+
when it declined to decide this itself. `console.breadcrumb.reports` labels the
|
|
2976
|
+
`routeType === 'report'` **list** route in app-shell's `AppHeader` — a structural sibling
|
|
2977
|
+
of the `dashboards`, `pages` and `system` segments beside it — and drilling through it
|
|
2978
|
+
appends a metadata report definition, the same feature named by
|
|
2979
|
+
`console.commandPalette.reports`, `console.nav.navReport`, `search.typeReports`,
|
|
2980
|
+
`search.badgeReport` and `search.reportNotFound`. Nothing narrative renders beneath it.
|
|
2981
|
+
|
|
2982
|
+
A comment at the key records that this was **ruled** rather than counted, and states the
|
|
2983
|
+
报表/报告 distinction it was ruled against, so the next reader measuring pack consistency
|
|
2984
|
+
neither re-files it nor quietly restores 报告 after reading the render context and
|
|
2985
|
+
disagreeing. That comment is half of the deliverable; the value change alone would leave
|
|
2986
|
+
the decision unrecorded, which is the failure mode the card was most concerned about.
|
|
2987
|
+
|
|
2988
|
+
**Scope: one key, one pack.** This is not a licence to normalise vocabulary across the ten
|
|
2989
|
+
packs — the card names that hazard explicitly and says it would need its own ruling, and
|
|
2990
|
+
this ruling grants nothing beyond the single key it names. No `en` value changes, so no
|
|
2991
|
+
other pack is asked to follow, and no other pack was touched.
|
|
2992
|
+
- 5d3a2d1: The capability picker localizes `manage_sharing` (objectui#6285). Before this, "Manage
|
|
2993
|
+
Sharing" was the one platform capability in `sys_permission_set`'s picker that rendered in
|
|
2994
|
+
English in every locale, beside seven siblings that translated — a user-visible missing
|
|
2995
|
+
translation, in all ten packs at once.
|
|
2996
|
+
|
|
2997
|
+
The cause was an unchecked copy. `CURATED_CAPABILITY_LABELS` in
|
|
2998
|
+
`CapabilityMultiSelectField.tsx` listed seven capability names under a doc comment claiming
|
|
2999
|
+
it mirrored `@objectstack/spec/security`'s `PLATFORM_CAPABILITIES`; the spec grew an eighth
|
|
3000
|
+
member and the list did not follow, so `manage_sharing` fell through to the English label
|
|
3001
|
+
the `sys_capability` registry serves. Nothing could catch it: the i18n gate reads that list
|
|
3002
|
+
as this key family's vocabulary and checks the members it names — all seven had keys — and
|
|
3003
|
+
no instrument compared the vocabulary to the array it was named after.
|
|
3004
|
+
|
|
3005
|
+
`capability.label.manage_sharing` is now authored in all ten packs and in the field widgets'
|
|
3006
|
+
provider-less defaults map, the list carries the member, and the prose claim is replaced by
|
|
3007
|
+
a check: `CapabilityMultiSelectField.specParity-6285.test.tsx` imports `PLATFORM_CAPABILITIES`
|
|
3008
|
+
and fails on any difference in either direction, reading the declaration through the i18n
|
|
3009
|
+
gate's own source reader so what it pins is exactly what that gate consumes. `labelFor` also
|
|
3010
|
+
gains a `defaultValue`, so a capability that arrives in a future spec bump before its
|
|
3011
|
+
translation is authored degrades to the registry's English label rather than rendering a raw
|
|
3012
|
+
i18n key at the user.
|
|
3013
|
+
- 00c665e: `appDesigner.fieldDesigner.formula` is retired — one row removed from each of the ten
|
|
3014
|
+
locale packs plus the designer defaults map, 11 lines, zero readers (objectui#6310).
|
|
3015
|
+
|
|
3016
|
+
objectui#6043 retired the Field Designer's formula-expression textarea, which was the
|
|
3017
|
+
key's only call site (`FieldDesigner.tsx`, the `{ name: 'formula', label:
|
|
3018
|
+
t('appDesigner.fieldDesigner.formula') }` field descriptor). The value outlived it in
|
|
3019
|
+
eleven places: `DESIGNER_DEFAULT_TRANSLATIONS` in
|
|
3020
|
+
`packages/plugin-designer/src/hooks/useDesignerTranslation.ts`, and the `appDesigner >
|
|
3021
|
+
fieldDesigner > formula` leaf of `packages/i18n/src/locales/{en,de,es,fr,pt,ru,ja,ko,zh,ar}.ts`.
|
|
3022
|
+
|
|
3023
|
+
Removed under objectui#4658's evidence standard, re-measured on this branch rather than
|
|
3024
|
+
inherited from the card: zero `t()`/`tt()` call sites, no dynamic template head that could
|
|
3025
|
+
reach it (`appDesigner.fieldDesigner.typeCategory.` is the namespace's only one), and its
|
|
3026
|
+
sole textual occurrence anywhere in the repo was the defaults-map row this change removes
|
|
3027
|
+
with it — so the key goes from NEEDS-REVIEW to no footprint at all.
|
|
3028
|
+
|
|
3029
|
+
The map and all ten packs move in one commit, which is what keeps
|
|
3030
|
+
`defaults-maps-mirror-en-pack` green: that gate fails a map row whose key the `en` pack
|
|
3031
|
+
lacks, and `all-locales-key-parity` fails a pack left behind.
|
|
3032
|
+
|
|
3033
|
+
Not touched: `designer.field.formula` (`'Formula (CEL)'`) in
|
|
3034
|
+
`packages/app-shell/src/views/metadata-admin/i18n.ts`, a different and live key belonging
|
|
3035
|
+
to metadata-admin's `ObjectFieldInspector` — the surface that still authors formula
|
|
3036
|
+
expressions.
|
|
3037
|
+
|
|
3038
|
+
`packages/i18n/src/__tests__/appDesigner-fieldDesigner-formula-retired-6310.test.ts` pins the
|
|
3039
|
+
removal by name, following the four prior retirements (objectui#4145, objectui#4392,
|
|
3040
|
+
objectui#4730, objectui#5504). Every i18n gate here runs call site → key, so none of them can
|
|
3041
|
+
see a dead key come BACK into the packs: the reverse sweep that found this one is report-only
|
|
3042
|
+
by design, `all-locales-key-parity` is fully satisfied by ten packs agreeing on a dead key, and
|
|
3043
|
+
`check:i18n-drift` only fires when a value changes. Reverse-verified rather than asserted —
|
|
3044
|
+
reviving the row in all ten packs turns exactly that one case red, naming each pack, while the
|
|
3045
|
+
parity gate and the defaults-map mirror stay green.
|
|
3046
|
+
- fff9645: `ar` spells the منقوص participle one way — `جارٍ`, pack-wide (objectui#6610).
|
|
3047
|
+
|
|
3048
|
+
The pack wrote the active participle of جرى two ways at once: `جارٍ` on 92
|
|
3049
|
+
values and `جاري` on 8. One word, two spellings, differing by a single code
|
|
3050
|
+
point — U+064D (tanwīn kasr, the yāʾ dropped) against U+064A (the yāʾ kept) —
|
|
3051
|
+
and same-screen visible: `LoadingScreen` renders `console.initializing` as the
|
|
3052
|
+
heading over the three `console.loadingSteps.*` and `console.actions.retrying`
|
|
3053
|
+
on the retry button, four values that all said `جاري`, inside a console that had
|
|
3054
|
+
already said `جارٍ` on `dashboard.loading`, `list.loading`,
|
|
3055
|
+
`detail.loadingAttachments` and `console.ai.*`.
|
|
3056
|
+
|
|
3057
|
+
The 92:8 majority is not why the 8 moved. An اسم منقوص declines three ways, so
|
|
3058
|
+
"the 8 are right in their own context" was a live reading and was falsified
|
|
3059
|
+
rather than out-voted: all eight opened their string as a fronted indefinite
|
|
3060
|
+
predicate over a delayed subject — nominative, indefinite, not annexed — and
|
|
3061
|
+
five had a word-for-word twin already spelled `جارٍ`, `grid.refreshing`
|
|
3062
|
+
(`جاري التحديث…`) against `list.refreshing` (`جارٍ التحديث…`) being the exact
|
|
3063
|
+
minimal pair. Meanwhile the three values where the yāʾ is grammatically required
|
|
3064
|
+
(accusative `جاريًا` after لا يزال / ما زال, definite `الجارية`) are correct and
|
|
3065
|
+
are left alone.
|
|
3066
|
+
|
|
3067
|
+
Eight values converged; no key added or removed and no `en` value touched.
|
|
3068
|
+
`ar-participle-orthography-6610.test.ts` makes it a pack-wide invariant rather
|
|
3069
|
+
than an eight-key edit — a ninth value arriving with the yāʾ now fails by key
|
|
3070
|
+
name — and pins the three legitimate yāʾ occurrences so a later sweep cannot
|
|
3071
|
+
flatten a correct distinction into a real grammatical error.
|
|
3072
|
+
- 9c3b7ce: The German pack's `auth.device.*` in-flight states move to the passive register
|
|
3073
|
+
(objectui#6611, triage ruling 2026-08-27, option 2): `approving` `Genehmige…` →
|
|
3074
|
+
`Wird genehmigt…`, `denying` `Ablehne…` → `Wird abgelehnt…`, `loading` `Lade…` →
|
|
3075
|
+
`Wird geladen…`.
|
|
3076
|
+
|
|
3077
|
+
Graded a bug, not a taste call: `Ablehne…` was not a grammatical German form.
|
|
3078
|
+
`ablehnen` is a separable-prefix verb, so the first-person singular is *ich lehne
|
|
3079
|
+
ab* — the pack shipped broken German on `DeviceAuthPage.tsx` to users today.
|
|
3080
|
+
Re-derived the ruling's premise against the tree before touching anything: the
|
|
3081
|
+
`de` pack's dominant in-flight register is measurably the passive — 40 of 61
|
|
3082
|
+
`en`-bare-gerund keys render `Wird …`, against 21 that don't (a Unicode-aware,
|
|
3083
|
+
position-aware scan, positive-controlled on `common.loading` → `Wird geladen…`),
|
|
3084
|
+
close to triage's "roughly 37" and the same conclusion either way. `loading` was
|
|
3085
|
+
the sole member of objectui#5972's merged `Loading…` group carved out by name in
|
|
3086
|
+
`LOADING_GROUP_FORKS` (`packages/i18n/src/__tests__/ellipsis-glyph-3878.test.ts`),
|
|
3087
|
+
because converging it alone while `approving`/`denying` stayed first-person would
|
|
3088
|
+
have manufactured a new same-screen inconsistency. Moving the whole namespace to
|
|
3089
|
+
the passive removes that fork rather than maintaining it, so the exemption row is
|
|
3090
|
+
deleted and the pin now asserts zero forks.
|
|
3091
|
+
|
|
3092
|
+
No key added or removed, no `en` value moves, no other locale touched.
|
|
3093
|
+
- dbd5194: The shell's `sys_activity.type` reading stops calling every unrecognised type an
|
|
3094
|
+
update (objectui#6730).
|
|
3095
|
+
|
|
3096
|
+
`mapActivityRows` in `hooks/sharedUserFeeds.ts` — the feed behind the AppHeader
|
|
3097
|
+
bell's Activity tab, Home's activity card and the exported `ActivityFeed` panel
|
|
3098
|
+
— carried the third hand-written reading of that column in this repo, and it
|
|
3099
|
+
bucketed every value outside `created` / `deleted` / `commented` / `mentioned`
|
|
3100
|
+
as `update`. That is not a missing decision; it is a wrong one stated out loud:
|
|
3101
|
+
a `scheduled` meeting, a `login`, a nightly `system` rollup and an author's
|
|
3102
|
+
`contract_countersigned` all rendered as "somebody updated this record".
|
|
3103
|
+
|
|
3104
|
+
- New `layout/activityItemType.ts` holds the whole reading — the table, the
|
|
3105
|
+
generic bucket, the `"NOW()"` timestamp fallback and the row constructor that
|
|
3106
|
+
applies all three — DOM-free, so what a row becomes is assertable directly.
|
|
3107
|
+
- `ActivityItem['type']` gains a fifth kind, `system`: the generic bucket, with
|
|
3108
|
+
its own icon, label and notification toggle. Following
|
|
3109
|
+
`UNMAPPED_ACTIVITY_FEED_TYPE`'s precedent, an unrecognised value renders
|
|
3110
|
+
through it and is named once on `console.warn` rather than being dropped —
|
|
3111
|
+
`sys_activity.type` is author-extensible (objectstack `88b9d749a`, direction 4), so
|
|
3112
|
+
an unmapped value is real activity nobody has ruled on, not a mistake.
|
|
3113
|
+
- The built-ins that had no honest presentation among the four existing kinds —
|
|
3114
|
+
`system`, `completed`, `scheduled`, `login`, `logout` — now land in that
|
|
3115
|
+
bucket instead of claiming `update`. `assigned` and `shared` stay `update`:
|
|
3116
|
+
both write to the record.
|
|
3117
|
+
|
|
3118
|
+
⛔ The two readings of this column are deliberately NOT converged.
|
|
3119
|
+
`activityRowToFeedItem` builds a `FeedItem`, and the vocabularies cross:
|
|
3120
|
+
`FeedItem` collapses create/update/delete into one `field_change` and drops
|
|
3121
|
+
`commented` / `mentioned` outright, so routing this surface through it would
|
|
3122
|
+
cost the bell every comment row and every create/delete distinction. What is
|
|
3123
|
+
shared is a pin, not an import — the new suite reads plugin-detail's real table
|
|
3124
|
+
(a devDependency; no runtime edge) and fails when the declared vocabulary grows
|
|
3125
|
+
an entry this side has not read, or when the two readings stop disagreeing in
|
|
3126
|
+
the three measured ways.
|
|
3127
|
+
- b392674: Field widgets say WHY they refused an edit in the reader's language
|
|
3128
|
+
(objectui#6755, maintainer ruling 2026-08-29).
|
|
3129
|
+
|
|
3130
|
+
Three sentences a person has to read to recover from a refusal were string
|
|
3131
|
+
literals in the widgets, inside a package whose locale channel 11 of its 55
|
|
3132
|
+
widgets already use: `ObjectField`'s `Invalid JSON`, and `LocationField`'s
|
|
3133
|
+
format and range refusals (objectui#6716 / #6714). So a zh / ja / ar user who
|
|
3134
|
+
mistyped a coordinate or a JSON blob was told why in English, in a form whose
|
|
3135
|
+
labels, gate hints and validation copy were all translated.
|
|
3136
|
+
|
|
3137
|
+
- All three now read from `useFieldTranslation` / `FIELD_DEFAULTS` under
|
|
3138
|
+
`fields.object.invalidJson`, `fields.location.refusedFormat` and
|
|
3139
|
+
`fields.location.refusedRange`, with entries in all ten locale packs — bound
|
|
3140
|
+
from now on by `check:i18n-drift`.
|
|
3141
|
+
- The `en` values are byte-identical to the literals they replace, so English
|
|
3142
|
+
and provider-less rendering are unchanged, and the refusal pins of
|
|
3143
|
+
objectui#6716 / #6715 and `plugin-form`'s two refusal suites are untouched.
|
|
3144
|
+
- `fields.location.refusedRange` keys the FRAME only: the interpolated
|
|
3145
|
+
`{{detail}}` is `LocationValueSchema`'s own complaint, because the widget must
|
|
3146
|
+
not restate the spec's bounds (a hand-copied range is a second contract).
|
|
3147
|
+
- Not in scope, and recorded rather than folded in: `LocationField`'s third
|
|
3148
|
+
refusal sentence — the residue arm objectui#6715 added after the ruling was
|
|
3149
|
+
written — is still a literal. The card behind `320374d2a` carries it.
|
|
3150
|
+
- 4f3a1e2: fix(plugin-timeline): refuse an unusable gantt date range with a diagnostic that names the offending value
|
|
3151
|
+
|
|
3152
|
+
A gantt whose date range cannot be used now renders a `role="alert"` diagnostic
|
|
3153
|
+
naming the value that made it unusable, instead of crashing or drawing a chart
|
|
3154
|
+
that is confidently wrong. Two input classes, which failed in opposite
|
|
3155
|
+
directions:
|
|
3156
|
+
|
|
3157
|
+
- A date that does not parse threw `RangeError: Invalid time value` mid-render —
|
|
3158
|
+
the same crash site objectui#6750 guarded for the empty list, on a different
|
|
3159
|
+
input. This covers a malformed value (`startDate: 'not-a-date'`), an absent
|
|
3160
|
+
one, and an unparseable `minDate` / `maxDate` pinned on the schema.
|
|
3161
|
+
- An inverted pinned range (`minDate` after `maxDate`) drew a bar at
|
|
3162
|
+
`left: 157.9%; width: -4.3%` under a header row with zero cells, with no
|
|
3163
|
+
error and no diagnostic.
|
|
3164
|
+
|
|
3165
|
+
Valid gantts, the empty-list sentinel from objectui#6750 and the degenerate
|
|
3166
|
+
`minDate === maxDate` axis are unchanged.
|
|
3167
|
+
- 320374d: Key `LocationField`'s THIRD refusal sentence — the residue arm — into the locale
|
|
3168
|
+
packs.
|
|
3169
|
+
|
|
3170
|
+
Typing a half that is only PARTLY a number (`12abc, 34`) is refused by
|
|
3171
|
+
`LocationField` with its own sentence, added by objectui#6715. objectui#6755 had
|
|
3172
|
+
ruled two weeks earlier that a widget's own refusal sentence goes through
|
|
3173
|
+
`useFieldTranslation` + `FIELD_DEFAULTS`, and named three sentences — but it was
|
|
3174
|
+
written on 2026-08-29 14:53 and this arm landed after, so it stayed a hard-coded
|
|
3175
|
+
English literal while its two siblings were keyed.
|
|
3176
|
+
|
|
3177
|
+
**The consequence was worse than one more English string.** All three refusal arms
|
|
3178
|
+
render through the SAME `<p>` and the same `refusalError` state, so after #6755
|
|
3179
|
+
landed that one line spoke the reader's language when the format or range arm fired
|
|
3180
|
+
and English when the residue arm did — objectui#4028's shape ("four Chinese labels
|
|
3181
|
+
around one English one") compressed into a single sentence position, which reads to
|
|
3182
|
+
a user as a bug rather than as a missing translation.
|
|
3183
|
+
|
|
3184
|
+
**Arity is answered explicitly, not defaulted.** Unlike the other two sentences, this
|
|
3185
|
+
one had grammatical number: `verb` was `is not a number` / `are not numbers`, chosen
|
|
3186
|
+
in TypeScript. Handing a pack an English verb form through a `{{hole}}` gives it a
|
|
3187
|
+
fragment it cannot inflect around — Arabic has a DUAL, and two halves is exactly that
|
|
3188
|
+
case. So the verb is not a hole. It lives inside two SIBLING keys picked at the call
|
|
3189
|
+
site, `fields.location.refusedResidue` and `fields.location.refusedResidueOne`,
|
|
3190
|
+
following this repo's own plural convention rather than i18next's `_one`/`_other`
|
|
3191
|
+
suffixes — the same shape `RecordPickerDialog` already uses in this very defaults map
|
|
3192
|
+
(`lookup.recordCount` / `lookup.recordCountOne`), and for the reason `ReactionPicker`
|
|
3193
|
+
states in source: zh/ja/ko have no separate singular form, would legitimately omit a
|
|
3194
|
+
`_one` half, and `all-locales-key-parity` reads that as a missing key. The `ar` pack
|
|
3195
|
+
now uses its dual (`ليسا رقمين`), which the old implementation could not have produced.
|
|
3196
|
+
|
|
3197
|
+
The English conjunction `' and '` and the coordinate NOUNS go the same way: each pack
|
|
3198
|
+
writes its own conjunction inside the two-half value, and `latitude` / `longitude`
|
|
3199
|
+
become `fields.location.latitude` / `fields.location.longitude`, keyed once each and
|
|
3200
|
+
interpolated into both arities so no locale holds two spellings of the same word. The
|
|
3201
|
+
only holes carrying untranslated data are `{{text}}` / `{{otherText}}` — the
|
|
3202
|
+
characters the person actually typed.
|
|
3203
|
+
|
|
3204
|
+
**No behaviour moves.** The English values are byte-identical to the literal they
|
|
3205
|
+
replace in both arities, verified by `check:i18n-drift` (0 en values changed, 4 added)
|
|
3206
|
+
and by objectui#6715's own `LocationField.strictNumeric.test.tsx` and `plugin-form`'s
|
|
3207
|
+
`ObjectForm.locationResidue.test.tsx` passing untouched. Provider-less rendering is
|
|
3208
|
+
unchanged, the refusal itself is unchanged, and the four new keys are bound from here
|
|
3209
|
+
on by `check:i18n-keys` and `all-locales-key-parity` like their three siblings.
|
|
3210
|
+
- 639114c: Carry `manage_org_presentation`, the ninth platform capability (objectui#7122).
|
|
3211
|
+
|
|
3212
|
+
`@objectstack/spec` 17.3.0 declares a ninth member of `PLATFORM_CAPABILITIES`
|
|
3213
|
+
and the capability picker's curated set carried eight, so
|
|
3214
|
+
`CapabilityMultiSelectField` fell back to the `sys_capability` registry's
|
|
3215
|
+
English label for it in every locale — the exact defect objectui#6285 filed
|
|
3216
|
+
when `manage_sharing` did the same thing.
|
|
3217
|
+
|
|
3218
|
+
The label is the spec artifact's own (`Manage Organization Presentation`), read
|
|
3219
|
+
off the installed build rather than invented, and it is authored everywhere the
|
|
3220
|
+
widget's docblock requires of any edit to that list: `useFieldTranslation.ts`
|
|
3221
|
+
and all ten locale packs. Each non-English string is composed from that pack's
|
|
3222
|
+
own established sibling vocabulary (`manage_org_users`,
|
|
3223
|
+
`manage_platform_settings`) rather than machine-translated; a native review pass
|
|
3224
|
+
is welcome on the nine, and nothing about the capability's behaviour depends on
|
|
3225
|
+
the wording.
|
|
3226
|
+
|
|
3227
|
+
The parity pin is unchanged and still fails on ANY difference in either
|
|
3228
|
+
direction, which is what made this visible before it reached a screen.
|
|
3229
|
+
- 1490691: `dashboard.noRows` and `dashboard.noDataAvailable` are retired — two rows removed from
|
|
3230
|
+
each of the ten locale packs, 20 entries, zero readers (objectui#7125).
|
|
3231
|
+
|
|
3232
|
+
objectui#7063 routed the three dashboard empty-state renders (`DatasetWidget`,
|
|
3233
|
+
`ObjectDataTable`, `PivotTable`) through one shared `WidgetEmptyState`, which resolves its
|
|
3234
|
+
own copy from the `dashboard.empty.*` family. The two keys the old per-widget placeholders
|
|
3235
|
+
used outlived their call sites in `packages/i18n/src/locales/{en,de,es,fr,pt,ru,ja,ko,zh,ar}.ts`.
|
|
3236
|
+
|
|
3237
|
+
Removed under objectui#4658's evidence standard, re-measured on this branch rather than
|
|
3238
|
+
inherited from the card: zero `t()`/`tt()` call sites for either fully qualified key, no
|
|
3239
|
+
dynamic `dashboard.` head a substitution could resolve onto them, and every surviving
|
|
3240
|
+
textual occurrence in `packages/` is a comment recording the consolidation. `pnpm
|
|
3241
|
+
check:i18n-keys` stays green across the deletion with the `en` pack at 2,962 keys (2,964
|
|
3242
|
+
before) and every in-scope call-site key still resolving.
|
|
3243
|
+
|
|
3244
|
+
Not touched: `table.noRows` (`'No rows to display'`) and `engine.form.noRows`
|
|
3245
|
+
(`packages/app-shell/src/views/metadata-admin/i18n.ts`, read at `widgets.tsx`) — two
|
|
3246
|
+
different, same-named keys in different namespaces. Nor the comments in
|
|
3247
|
+
`WidgetEmptyState.tsx`, `DatasetWidget.tsx`, `ObjectDataTable.tsx` and `PivotTable.tsx`
|
|
3248
|
+
that record WHY three widgets with three strings became one shared empty state; the packs'
|
|
3249
|
+
own comment keeps that rationale and now names the retirement instead of a row that is
|
|
3250
|
+
gone.
|
|
3251
|
+
|
|
3252
|
+
`packages/i18n/src/__tests__/dashboard-emptyState-keys-retired-7125.test.ts` pins the removal
|
|
3253
|
+
by name, following the five prior retirements (objectui#4145, objectui#4392, objectui#4730,
|
|
3254
|
+
objectui#5504, objectui#6310). Every i18n gate here runs call site → key, so none of them can
|
|
3255
|
+
see a dead key come BACK into the packs: the reverse sweep that found these is report-only by
|
|
3256
|
+
design, `all-locales-key-parity` is fully satisfied by ten packs agreeing on a dead key, and
|
|
3257
|
+
`check:i18n-drift` reported the deletion as `2 removed — those are all-locales-key-parity's`.
|
|
3258
|
+
- d8ec8d6: `ActivityTimeline` speaks the session locale — the other 18 literals
|
|
3259
|
+
(objectui#7149).
|
|
3260
|
+
|
|
3261
|
+
objectui#7142 gave this component its first `t()` call (the empty-state title)
|
|
3262
|
+
and filed the sweep that found the rest. Until now a zh activity tab read
|
|
3263
|
+
`"Activity(0)暂无活动记录"`: one translated string in a component that was
|
|
3264
|
+
otherwise entirely English.
|
|
3265
|
+
|
|
3266
|
+
All 18 now resolve from the ten packs, in three groups:
|
|
3267
|
+
|
|
3268
|
+
- **Relative timestamps and the card title** (`just now`, `{{count}}m/h/d ago`,
|
|
3269
|
+
`Activity`) — these render on *every* activity tab. All five were a pure
|
|
3270
|
+
lookup swap: the `en` pack value was already byte-identical to the literal,
|
|
3271
|
+
and the sibling `RecordActivityTimeline` already used the same keys.
|
|
3272
|
+
- **The `formatFieldChange` sentences** — assembled in code, so they needed new
|
|
3273
|
+
keys *with* interpolation holes rather than a lookup. Same reachability as the
|
|
3274
|
+
timestamps: they render for any entry whose optional `description` is absent.
|
|
3275
|
+
The quotes live inside each pack's value, so every locale punctuates its own
|
|
3276
|
+
way (de `„…“`, zh `“…”`, ja `「…」`, fr/ru `«…»`).
|
|
3277
|
+
- **The six filter chips and the chip group's accessible name** — reachable only
|
|
3278
|
+
through the published export, since no host in this repo passes `filterable`.
|
|
3279
|
+
|
|
3280
|
+
Ten new `detail.*` keys across all ten packs (no inline `defaultValue` —
|
|
3281
|
+
objectui#3517), mirrored byte-for-byte into `DETAIL_DEFAULT_TRANSLATIONS` so a
|
|
3282
|
+
provider-less host still reads English rather than a raw key.
|
|
3283
|
+
|
|
3284
|
+
One deliberate English copy change: the chip group's `aria-label` was
|
|
3285
|
+
`"Activity type filter"` and now resolves `detail.filterActivity`
|
|
3286
|
+
(`"Filter activity"`) — the key `RecordActivityTimeline` already uses for the
|
|
3287
|
+
accessible name of its own activity filter, so one control does not carry two
|
|
3288
|
+
names across two components.
|
|
3289
|
+
|
|
3290
|
+
Also drops the unused `Filter` import (a pre-existing eslint warning).
|
|
3291
|
+
- 866cd1d: `RecordComments` and `PointInTimeRestore` resolve their copy from the locale
|
|
3292
|
+
packs instead of hardcoded English (objectui#7163).
|
|
3293
|
+
|
|
3294
|
+
Both files carried their own `formatTimestamp` whose relative-time branches
|
|
3295
|
+
returned English literals, so a zh/ja/ar session read `5m ago` next to
|
|
3296
|
+
otherwise translated chrome — the defect objectui#7142/#7149 fixed one file
|
|
3297
|
+
over in `ActivityTimeline`.
|
|
3298
|
+
|
|
3299
|
+
- **`RecordComments`** was already wired to the packs (11 `t('detail.…')`
|
|
3300
|
+
references), so this is a pure lookup swap onto `detail.justNow` /
|
|
3301
|
+
`minutesAgo` / `hoursAgo` / `daysAgo` — keys already present in all ten packs,
|
|
3302
|
+
each `en` value byte-identical to the literal it replaces. **No new key, no
|
|
3303
|
+
copy change.**
|
|
3304
|
+
- **`PointInTimeRestore`** used no translation hook at all, so it is swept
|
|
3305
|
+
WHOLE rather than having only its timestamps converted: card title, empty
|
|
3306
|
+
state, field-count line, preview panel, snapshot heading, restore
|
|
3307
|
+
confirmation and all three buttons. Wiring one string into an otherwise
|
|
3308
|
+
untranslated component is what shipped objectui#7142's visibly half-done zh
|
|
3309
|
+
card; that is not repeated here.
|
|
3310
|
+
|
|
3311
|
+
Ten new `detail.*` keys land in all ten packs and in
|
|
3312
|
+
`DETAIL_DEFAULT_TRANSLATIONS`: `revisionHistory`, `noRevisions`,
|
|
3313
|
+
`revisionFieldsChanged`, `revisionFieldsChangedOne`, `revisionPreview`,
|
|
3314
|
+
`revisionSnapshot`, `restoreConfirm`, `restoring`, `confirmRestore`,
|
|
3315
|
+
`restoreToPoint`. `detail.cancel`, `detail.activityEmptyValue` and
|
|
3316
|
+
`detail.emptyValue` are **reused**, not forked.
|
|
3317
|
+
|
|
3318
|
+
The restore confirmation becomes one key with a `{{when}}` hole rather than a
|
|
3319
|
+
sentence assembled around a JSX expression, and the field-count line uses the
|
|
3320
|
+
repo's two-key plural convention selected by a static ternary over two literal
|
|
3321
|
+
keys — never `t(KEYS[n])`, which objectui#7149 measured as invisible to the
|
|
3322
|
+
i18n scanners.
|
|
3323
|
+
|
|
3324
|
+
One deliberate copy change: the snapshot panel's null placeholder was an EN
|
|
3325
|
+
DASH (`–`, U+2013) written inline and now resolves `detail.emptyValue`, which
|
|
3326
|
+
is an EM DASH (`—`, U+2014) in all ten packs — the glyph the rest of the detail
|
|
3327
|
+
package already uses for an empty value.
|
|
3328
|
+
- 20c04b2: A gantt timeline whose rows are malformed now refuses to draw, naming the row,
|
|
3329
|
+
instead of crashing the render (objectui#7164, maintainer ruling A+).
|
|
3330
|
+
|
|
3331
|
+
`TimelineRenderer`'s gantt branch used to read the authored rows twice — once
|
|
3332
|
+
defensively in `findUnusableGanttDate`, once bare in `calculateDateRange` — and
|
|
3333
|
+
every input in the gap threw a `TypeError` mid-render from ordinary JSON:
|
|
3334
|
+
`items: [null]`, a row whose `items` is `5` / `true` / `{}` / an array-like
|
|
3335
|
+
object, or `items` itself not an array. The three readers (the date scan, the
|
|
3336
|
+
range computation and the render loop) now consume ONE verdict from
|
|
3337
|
+
`classifyGanttRows`, and a malformed shape renders the existing `role="alert"`
|
|
3338
|
+
refusal through a new diagnostic key,
|
|
3339
|
+
`timeline.gantt.unusableRange.malformedRow` — "items[0] is null, which is not
|
|
3340
|
+
a row shape" — never the `malformedDate` copy, which named the wrong fault.
|
|
3341
|
+
The key lands in `en` and the nine sibling locale packs.
|
|
3342
|
+
|
|
3343
|
+
`@object-ui/types` (minor — the accept set narrows): `TimelineSchema.items` no
|
|
3344
|
+
longer declares `z.array(z.any())`. Every element must be an object, and a
|
|
3345
|
+
gantt row's own `items`, when present, must be an array, so `validate` refuses
|
|
3346
|
+
`items: [null]` and `items: [{ items: 5 }]` at authoring time — before they
|
|
3347
|
+
reach a renderer. Feed items (`vertical` / `horizontal`) carry no `items` key
|
|
3348
|
+
and parse exactly as before; every in-repo `type: 'timeline'` fixture parses
|
|
3349
|
+
green on both sides of the change. Rows with no bars (`items: []`, a row
|
|
3350
|
+
without `items`) stay the ordinary empty state and still draw.
|
|
3351
|
+
- 01c9023: `AiPendingActionsInbox` speaks the session locale — every string in it, not only its timestamps (objectui#7173).
|
|
3352
|
+
|
|
3353
|
+
The AI HITL approval inbox held its own relative-time helper returning hardcoded
|
|
3354
|
+
English (`'just now'`, `` `${min}m ago` ``), so a zh / ja / ar session read English
|
|
3355
|
+
relative times on every row. It is the fifth spelling of that helper in the repo,
|
|
3356
|
+
and the file had **no translation wiring at all** — the unwired-component shape,
|
|
3357
|
+
not the lookup-swap shape.
|
|
3358
|
+
|
|
3359
|
+
It is therefore swept whole. objectui#7142 wired one string into an otherwise
|
|
3360
|
+
untranslated component and shipped something visibly half-done, and objectui#7149
|
|
3361
|
+
is what finishing that afterwards cost; the triage ruling on this card (2026-09-01)
|
|
3362
|
+
carried that forward as *sweep the file whole or leave it*. Everything the user can
|
|
3363
|
+
read now resolves from the locale packs: the card heading and description, the three
|
|
3364
|
+
tabs, the refresh button, all five status badges, the six column headings, the empty
|
|
3365
|
+
state, the row and drawer buttons, all nine drawer field labels, the outcome banner
|
|
3366
|
+
and the whole reject-reason dialog.
|
|
3367
|
+
|
|
3368
|
+
**No new rows for the four relative-time branches.** `detail.justNow`,
|
|
3369
|
+
`detail.minutesAgo`, `detail.hoursAgo` and `detail.daysAgo` already existed,
|
|
3370
|
+
translated, in all ten packs, and cross-package key borrowing is this repo's settled
|
|
3371
|
+
convention rather than an open question — `ObjectGrid`, `ObjectKanban`, `ObjectTree`,
|
|
3372
|
+
`ListView`, `ObjectView`, `NavigationOverlay`, `RecordAttachmentsPanel`,
|
|
3373
|
+
`RecordDetailView` and `apps/console` all resolve `detail.*` from outside
|
|
3374
|
+
`plugin-detail`. One phrase on one kind of control should not get a second
|
|
3375
|
+
translation that can drift from the first.
|
|
3376
|
+
|
|
3377
|
+
The rest of the sweep needed copy no pack had, so `@object-ui/i18n` gains an
|
|
3378
|
+
`aiApprovals` namespace: 38 keys, translated in all ten packs. It is deliberately
|
|
3379
|
+
separate from `approvalsInbox`, which is the human approval-**process** inbox — a
|
|
3380
|
+
different surface and a different feature, so no rows are shared with it. Four
|
|
3381
|
+
generic verbs are reused rather than forked (`common.refresh`, `common.cancel`,
|
|
3382
|
+
`common.loading`, `common.ok`).
|
|
3383
|
+
|
|
3384
|
+
**⛔ The five relative-time helpers are not unified.** They differ in real behaviour
|
|
3385
|
+
— `Math.round` here against `Math.floor` in `plugin-detail`, thresholds 45s/30d
|
|
3386
|
+
against 60s/7d, different tails — so normalising them is a behaviour change wearing
|
|
3387
|
+
a refactor's clothes and needs its own card. This inbox's arithmetic is untouched,
|
|
3388
|
+
and three rows in the new suite exist only to pin it: 50s renders `1m ago` (a 60s
|
|
3389
|
+
threshold would still say "just now"), 90s renders `2m ago` (`Math.floor` gives
|
|
3390
|
+
`1m ago`), and 20d renders `20d ago` (a 7d threshold would already show a date).
|
|
3391
|
+
|
|
3392
|
+
Two assembled English sentences became single interpolated keys — the outcome banner
|
|
3393
|
+
(`Approve for {{id}}: {{message}}`) and the drawer subtitle
|
|
3394
|
+
(`Tool {{tool}} on {{object}}`). Their word order differs per locale, which fragments
|
|
3395
|
+
around a `<code>` element cannot express, so the two identifiers lose their monospace
|
|
3396
|
+
styling. That is the deliberate cost of making those sentences translatable.
|
|
3397
|
+
|
|
3398
|
+
Evidence: an `en`-only assertion cannot discriminate here, because each key's `en`
|
|
3399
|
+
value is byte-identical to the literal it replaced. The suite asserts in **zh and
|
|
3400
|
+
ar**, and the provider-less path separately, in its own file (`createI18n` installs
|
|
3401
|
+
itself as react-i18next's module-level global, so a provider-less render in a file
|
|
3402
|
+
that has already mounted a provider silently reads that pack instead of the defaults
|
|
3403
|
+
map). No inline `defaultValue` anywhere (objectui#3517).
|
|
3404
|
+
|
|
3405
|
+
Two consequences of the sweep, both landed here rather than left for CI to find:
|
|
3406
|
+
|
|
3407
|
+
`packages/app-shell/src/console/ai/__tests__/ConversationsSidebar.test.tsx` froze its
|
|
3408
|
+
`vi.mock('@object-ui/i18n', ...)` factory to a hand-written object. Its import graph
|
|
3409
|
+
reaches `plugin-chatbot`, which now resolves `createSafeTranslation` at module scope, so
|
|
3410
|
+
the frozen surface made that read `undefined` and the file died during COLLECTION — the
|
|
3411
|
+
objectui#6849 shape, which does not look like a test failure. It now spreads
|
|
3412
|
+
`importOriginal()` and overrides only `useObjectTranslation`. Measured, not guessed: of
|
|
3413
|
+
the 41 frozen `@object-ui/i18n` factories in the repo, running every one of them showed
|
|
3414
|
+
this to be the only file whose graph reaches the package.
|
|
3415
|
+
|
|
3416
|
+
The ten pack blocks are locale DATA, and locale data lands in the console's eager
|
|
3417
|
+
`framework` chunk, so `scripts/check-eager-closure-budget.mjs` raises that chunk's
|
|
3418
|
+
ceiling from 512,000 to 524,000 gzipped bytes and re-pins its baseline onto a fresh
|
|
3419
|
+
measurement (502,405 to 514,863). Attributed by three console builds: the merge parent
|
|
3420
|
+
reads 510,192, this branch with the ten `aiApprovals` blocks cut reads 510,192 again, and
|
|
3421
|
+
this branch reads 514,863 — so the whole 4,671-byte delta is the pack data and nothing
|
|
3422
|
+
else. Headroom is kept at the line's own convention (9,137 bytes, 0.10x the regression
|
|
3423
|
+
the gate must catch) rather than widened; most of the overage was pre-existing drift, with
|
|
3424
|
+
the merge parent already at 510,192 of the 512,000 allowed.
|
|
3425
|
+
- e8c553b: A scatter handed more than one series now refuses instead of drawing a false picture.
|
|
3426
|
+
|
|
3427
|
+
Scatter binds one measure: `series[0].dataKey` is the y axis, and every series was
|
|
3428
|
+
handed the same rows through that one axis. A second series therefore added a
|
|
3429
|
+
colour and a legend entry and nothing else — measured, two series over two rows
|
|
3430
|
+
painted four symbols at two positions, each drawn twice, and the second measure's
|
|
3431
|
+
values appeared nowhere on the plot. The data was valid and the picture was
|
|
3432
|
+
confidently wrong, which no existing refusal could see.
|
|
3433
|
+
|
|
3434
|
+
A `chartType: 'scatter'` with two or more `series` now renders the renderer's
|
|
3435
|
+
refusal shell under `data-chart-error="scatter-multi-series"`, stating that a
|
|
3436
|
+
scatter plots one measure, naming the fix (keep exactly one series) and listing
|
|
3437
|
+
the series keys it was handed. A single-series scatter is unchanged.
|
|
3438
|
+
|
|
3439
|
+
This refusal counts authored `series` only. `compareTo` on scatter is out of
|
|
3440
|
+
its scope: objectui#7402 ruled (b) that scatter joins pie / donut / funnel in
|
|
3441
|
+
excluding `compareTo` — `supportsCompareTo` and the dashboard widget path stop
|
|
3442
|
+
synthesising a comparison series for it, so no `…__comparison` overlay is ever
|
|
3443
|
+
built for a scatter and this guard is never reached by a compare-to document.
|
|
3444
|
+
That exclusion ships as a separate change; until it lands, a `compareTo`
|
|
3445
|
+
document still reaches the renderer as two series and refuses here today.
|
|
3446
|
+
|
|
3447
|
+
No multi-measure projection is built (maintainer ruling, 2026-09-02): nothing
|
|
3448
|
+
in-repo authors a two-series scatter, so that capability waits for a real caller.
|
|
3449
|
+
The refusal copy is `chart.scatterOneMeasure` in all ten locale packs.
|
|
3450
|
+
- 7c3df8f: A non-grid view's fetch now carries a platform row ceiling, and crossing it is
|
|
3451
|
+
never silent (objectui#7210, maintainer ruling a′, 2026-09-02).
|
|
3452
|
+
|
|
3453
|
+
Before this, `ObjectGantt`, `ObjectCalendar`, `ObjectMap` and `ObjectTree` each
|
|
3454
|
+
issued a `find` with **no `$top` at all**, so the request returned the entire
|
|
3455
|
+
filtered result set. At the 186 rows the card was filed from that is invisible;
|
|
3456
|
+
on an object with 100k scheduled rows it is the whole table into the browser,
|
|
3457
|
+
and nothing an author could write — `pagination.pageSize` included — could
|
|
3458
|
+
bound a request that never carried a cap to begin with.
|
|
3459
|
+
|
|
3460
|
+
**What changed.** Those four fetches now ask for `NON_GRID_ROW_CEILING_TOP`
|
|
3461
|
+
rows, draw at most `NON_GRID_ROW_CEILING` of them, and when the result set was
|
|
3462
|
+
larger they render a footnote naming both numbers, verbatim as it renders:
|
|
3463
|
+
*"Showing the first 2000 of 41234 records. Narrow the filter."* Below the
|
|
3464
|
+
ceiling nothing changes: the full set draws and no footnote appears.
|
|
3465
|
+
|
|
3466
|
+
The four view packages take a **minor**, not a patch: a result set above the
|
|
3467
|
+
ceiling is no longer drawn in full, which is a behaviour break whatever the
|
|
3468
|
+
fixed group does to the released version number.
|
|
3469
|
+
|
|
3470
|
+
**The ceiling is a platform constant, not an authorable key** — `2000`, exported
|
|
3471
|
+
from `@object-ui/react` as `NON_GRID_ROW_CEILING`. An authored `limit` or
|
|
3472
|
+
`dataSource: { limit }` still does not reach these queries, by the same ruling;
|
|
3473
|
+
three alternatives were rejected with it (a documentation note only — still the
|
|
3474
|
+
whole table; truncating at `pageSize` — silent, and a complete schedule capped
|
|
3475
|
+
at one page; an authorable `maxRows` — a new permanent key every author sets).
|
|
3476
|
+
|
|
3477
|
+
**Why 2,000.** One constant for all four, so the binding view sets it. Measured
|
|
3478
|
+
in this repo's jsdom lane: gantt, calendar and map hold their DOM flat as rows
|
|
3479
|
+
grow (virtualised task list; four events per day cell; auto-clustering above
|
|
3480
|
+
100 markers), while `ObjectTree` flattens every expanded node into the document
|
|
3481
|
+
at a linear **5.2 DOM elements per record** with no virtualisation. 2,000 rows
|
|
3482
|
+
is where the worst of the four lands at ~10,400 elements — an order of
|
|
3483
|
+
magnitude above Lighthouse's "excessive DOM size" warning, and still ~10x the
|
|
3484
|
+
real application result set this card came from.
|
|
3485
|
+
|
|
3486
|
+
New exports on `@object-ui/react`: `NON_GRID_ROW_CEILING`,
|
|
3487
|
+
`NON_GRID_ROW_CEILING_TOP`, `applyNonGridRowCeiling`, `NonGridRowCeilingNote`
|
|
3488
|
+
and the type `NonGridCeilingResult`. Two new `common.*` i18n keys carry the
|
|
3489
|
+
footnote copy in all ten packs.
|
|
3490
|
+
- db3896c: `ObjectGantt` refreshes in place when its query really changes, instead of tearing the chart down to the loading placeholder (objectui#7237).
|
|
3491
|
+
|
|
3492
|
+
Once the chart has painted, a change to its sort, filter, permissions, search or bound source re-runs the query with the chart still mounted, so it keeps its scroll position, its collapsed groups and any inline edit in progress. While the query runs, a thin indeterminate bar over the chart marks the rows on screen as the previous answer, and the rows are replaced when the new answer lands. It is the same `RefreshIndicator` that `ObjectGrid`, `ListView` and `ObjectChart` draw over their rows. The initial load still shows the loading placeholder.
|
|
3493
|
+
|
|
3494
|
+
The bar also shows during the chart's other in-place re-reads: the toolbar refresh, the re-read after a drag, a dependency link, a delete or a field edit, and a re-read triggered by the data-invalidation bus. Those already kept the chart mounted, but the only sign of them was a disabled refresh button.
|
|
3495
|
+
|
|
3496
|
+
A changed query that fails is still reported as an error, as it was before. Only a re-read of the same query keeps its last good rows when it fails.
|
|
3497
|
+
|
|
3498
|
+
`@object-ui/i18n`: new key `gantt.aria.refreshing`, the bar's accessible name, in all ten locale packs.
|
|
3499
|
+
- 3399704: Fix `AiUsageIndicator` to recognize the free plan's new `resetKind: 'weekly'` and its
|
|
3500
|
+
`resetsAt` (objectui#7371, consumer of cloud PR #1852's rolling 7-day AI quota window).
|
|
3501
|
+
|
|
3502
|
+
Before this change a `weekly` meter fell through to the component's unrecognized-kind
|
|
3503
|
+
path and rendered no reset line at all — not a crash, but silently wrong information
|
|
3504
|
+
next to a live progress ring. The indicator now shows "Resets in N days" (or "Resets in
|
|
3505
|
+
N hours" once inside the final day, e.g. `console.ai.usage.resetsWeeklyHours`), computed
|
|
3506
|
+
from the endpoint's `resetsAt`, in both languages via `@object-ui/i18n`
|
|
3507
|
+
(`console.ai.usage.resetsWeeklyDays` / `resetsWeeklyHours`, real i18next plural families
|
|
3508
|
+
with a base key so every locale pack resolves correctly, all ten packs translated). D5 is
|
|
3509
|
+
preserved — no token count is ever rendered, only the days/hours until reset.
|
|
3510
|
+
|
|
3511
|
+
Contract-first: `resetsAt` is read verbatim from the endpoint, never re-derived or
|
|
3512
|
+
guessed client-side. A `weekly` meter with `resetsAt: null` (nothing counted yet in the
|
|
3513
|
+
window) and any `resetKind` this build does not recognize both render no reset line —
|
|
3514
|
+
fail-soft, not a crash or stale copy.
|
|
3515
|
+
|
|
3516
|
+
`AiUsageResetKind` (`packages/app-shell/src/hooks/useAiUsage.ts`) gains the `'weekly'`
|
|
3517
|
+
member; `resetsAt` was already `string | null` and needed no shape change.
|
|
3518
|
+
- 2a5bf45: fix(i18n): `createI18n`'s `resources` deep-merges, so overriding one key of a namespace keeps the rest
|
|
3519
|
+
|
|
3520
|
+
A language pack's top-level keys ARE the namespace groups (`common`, `calendar`,
|
|
3521
|
+
`list`, ...), each a nested object, but `createI18n` merged `resources` over the
|
|
3522
|
+
built-in packs one level deep. Supplying a partial group therefore **replaced**
|
|
3523
|
+
it rather than merging into it:
|
|
3524
|
+
|
|
3525
|
+
```ts
|
|
3526
|
+
createI18n({ defaultLanguage: 'en', resources: { en: { calendar: { today: 'Heute' } } } });
|
|
3527
|
+
```
|
|
3528
|
+
|
|
3529
|
+
left `calendar.today` set and dropped `month`, `week`, `day`, `allDay`,
|
|
3530
|
+
`newEvent`, `moreEvents` and `unscheduled` from the instance. `t('calendar.allDay')`
|
|
3531
|
+
then returned the bare key, and `calendar.allDay` reached the DOM as literal
|
|
3532
|
+
text — silently, with no error and no warning.
|
|
3533
|
+
|
|
3534
|
+
The merge now recurses, so a partial group override touches only the keys it
|
|
3535
|
+
names. Packs nest up to four levels below the group
|
|
3536
|
+
(`console.ai.empty.build.title`), so it recurses rather than adding one fixed
|
|
3537
|
+
extra level. Two in-repo call sites were affected by this and are repaired by
|
|
3538
|
+
the change: `packages/plugin-gantt/demo` supplied 53 of the 80 `zh.gantt.*` keys
|
|
3539
|
+
and silently lost the other 27 (its own comment says the demo "is never
|
|
3540
|
+
half-translated" — it was), and the `skills/objectui/guides/i18n.md` setup
|
|
3541
|
+
example, the documented way to use this API, dropped 46 of the 48 `common.*`
|
|
3542
|
+
keys in both `en` and `zh`.
|
|
3543
|
+
|
|
3544
|
+
**Arrays are replaced, not concatenated** — stated because it is a decision, not
|
|
3545
|
+
a library default. No built-in pack carries an array value today (every leaf in
|
|
3546
|
+
all ten packs is a string), so nothing observable rides on it; the rule decides
|
|
3547
|
+
what a future array means. An author who writes an array is naming the whole
|
|
3548
|
+
list, so replacement is the only rule that lets them shorten or reorder one and
|
|
3549
|
+
the only one that stays idempotent when the merge runs again. This is
|
|
3550
|
+
deliberately narrower than i18next's own `deepExtend`, which the provider's
|
|
3551
|
+
async `addResourceBundle` path uses: that recurses into arrays index-wise and
|
|
3552
|
+
would leave a longer base array's tail behind, which is the same silent-hybrid
|
|
3553
|
+
shape this fix removes.
|
|
3554
|
+
|
|
3555
|
+
No caller depended on the old replacement semantics: a census of every
|
|
3556
|
+
`resources` literal in the repo (47 parsed sites, plus the dynamic ones resolved
|
|
3557
|
+
by hand) found no site that supplied a partial group in order to clear the rest.
|
|
3558
|
+
- c907a9c: fix(fields): a file or image upload that surfaces no `sys_file` id is refused by name, never submitted as an inline blob
|
|
3559
|
+
|
|
3560
|
+
**BREAKING (`@object-ui/fields`), graded `minor` by this repo's release model** (AGENTS.md: objectui's major follows the `@objectstack` family major, so its own breaking changes ship as `minor` with the breaking semantics stated here, never `major`):
|
|
3561
|
+
|
|
3562
|
+
- **Who breaks.** Two kinds of host. The first calls `fileValueForSubmit` itself. The second mounts an upload adapter whose result carries no id-shaped `meta.fileId`. That includes the object-URL default: `useUpload()` falls back to it when no `UploadProvider` is mounted, and `UploadProvider` uses it when given no `adapter`. It also includes any S3- or Azure-style adapter, `createS3Adapter` and `createAzureBlobAdapter` among them, because neither mints a `sys_file` row. This includes every host on a non-ObjectStack backend (the README's any-backend `DataSource` path) that stored the inline `UploadedFileMetadata` object: no adapter there mints a `sys_file` id.
|
|
3563
|
+
- **What they see now.** `fileValueForSubmit` throws `UploadIncompleteError` (`code` `UPLOAD_INCOMPLETE`, `fileName` naming the pick) where it used to return the inline object, and its return type is `string`. In `FileField`, `FileCell` and `ImageField` the pick is refused with the translated "did not complete" row, and the field is not changed.
|
|
3564
|
+
- **What to do.** Mount an `UploadProvider` whose adapter returns the `sys_file` id in `meta.fileId`. `createObjectStackUploadAdapter`, the ObjectStack presigned flow, does. A custom adapter has to register the file with the platform and return that id. A host that calls `fileValueForSubmit` directly catches `UploadIncompleteError` (or checks `code === 'UPLOAD_INCOMPLETE'`) where it used to accept an object. Such a host returns its own file id in `meta.fileId` (word characters and `-`, 1 to 64 long, as `isFileIdToken` requires) and has its read path return `{ id, name, url }`, because a bare id otherwise renders from ObjectStack's `/api/v1/storage/files/:id`.
|
|
3565
|
+
|
|
3566
|
+
`Clause-②: yes (narrowing)` — `fileValueForSubmit`, a public export of `@object-ui/fields`, narrows its return type to `string` and throws where it used to return an object; `UploadIncompleteError` is a new public export. No field-value accept set widens.
|
|
3567
|
+
|
|
3568
|
+
`fileValueForSubmit` used to fall back to the legacy inline `{ name, original_name, size, mime_type, url }` object when the upload adapter surfaced no `meta.fileId`. That was the file and image widgets' one path to the pre-D3 inline file object: the backend's stored contract has been the bare `sys_file` id since spec `17.0.0`, so the fallback turned a successful-looking upload into a save the engine refused (`invalid_type`) on every deployment that has verified its file-as-reference migration, and into a warned-about legacy value on the rest. ADR-0104's 2026-09-05 addendum fixes the physical column to the bare id as well.
|
|
3569
|
+
|
|
3570
|
+
The fallback is retired. `FileField`, `FileCell` and both upload paths of `ImageField` now submit the id the adapter minted, or refuse the pick with the translated `fields.file.uploadIncomplete` message ("did not complete") in the same error row a transport failure lands in — the field is not changed and no new blob reaches `onChange`. This includes the object-URL default `useUpload()` falls open to when no `UploadProvider` is mounted, and any S3/Azure-style adapter that mints no `sys_file` row. Reading is unchanged: a legacy blob already on a record still renders, and on a `multiple` field it is passed through untouched beside a new id.
|
|
3571
|
+
|
|
3572
|
+
`@object-ui/i18n` gains the `fields.file.uploadIncomplete` key in every locale pack.
|
|
3573
|
+
|
|
3574
|
+
`@object-ui/providers`'s `UploadProvider.tsx` JSDoc for the object-URL default now states it persists nothing and returns no id.
|
|
3575
|
+
- e1c27e4: The README states **ten** built-in locales, derived from the package rather than restated
|
|
3576
|
+
(objectui#7989).
|
|
3577
|
+
|
|
3578
|
+
`README.md` ships in this package's `files`, so the two lines a reader meets first went out
|
|
3579
|
+
in every tarball claiming **11** built-in locales. The package has ten —
|
|
3580
|
+
`BUILT_IN_LANGUAGE_CODES` and the `@object-ui/i18n/locales` map agree on
|
|
3581
|
+
`en zh ja ko de fr es pt ru ar` — and the same document already reasoned on ten six times
|
|
3582
|
+
further down ("the other nine are separate chunks", "all ten codes", "of the ten packs").
|
|
3583
|
+
The "and more" hedge goes with the number: that bullet enumerates all ten languages, so
|
|
3584
|
+
there is no more.
|
|
3585
|
+
|
|
3586
|
+
The correction is the smaller half. A **restated** number is the construct that permitted
|
|
3587
|
+
the error, and correcting one in isolation has already failed to stop the next instance —
|
|
3588
|
+
objectui#3351 fixed the same off-by-one in a changeset file and nobody swept the README.
|
|
3589
|
+
So the count is now read off the document and compared against what the package exports,
|
|
3590
|
+
on two doors that must agree: the payload-free `BUILT_IN_LANGUAGE_CODES` a consumer of the
|
|
3591
|
+
entry gets, and the `builtInLocales` map behind the published `./locales` subpath.
|
|
3592
|
+
|
|
3593
|
+
⛔ No runtime behaviour changes, and the two sites that were already correct —
|
|
3594
|
+
`src/index.ts`'s `@packageDocumentation` and this manifest's `description` — are untouched.
|
|
3595
|
+
- b1030c7: The record form now tells the user when a `visibleWhen` transition clears a field
|
|
3596
|
+
(objectui#8070, ruling letter A).
|
|
3597
|
+
|
|
3598
|
+
Since `6a449fc49` a field whose own `visibleWhen` / `visibleOn` turns it
|
|
3599
|
+
invisible while it holds a value has that value cleared, so the server does not
|
|
3600
|
+
refuse the row over a column that is no longer on screen. That clear was silent:
|
|
3601
|
+
the field disappeared and nothing said a stored value went with it. The ruling
|
|
3602
|
+
on objectui#8070 ports the naming half of the objectui#6499 ruling to this
|
|
3603
|
+
surface, and the clear itself is unchanged.
|
|
3604
|
+
|
|
3605
|
+
- When a transition clears one or more fields, the form raises one notice that
|
|
3606
|
+
names them by the labels the form draws, joined by the locale's
|
|
3607
|
+
`validation.formInvalidJoiner`, and says they no longer apply given the
|
|
3608
|
+
current values. A field that was already empty is not named (an unchecked
|
|
3609
|
+
two-state control counts as empty), and neither is the first render of a
|
|
3610
|
+
record, which only records the baseline.
|
|
3611
|
+
- The notice is published under the form's outcome-toast id. A later submit
|
|
3612
|
+
refusal replaces it, and the next attempt that passes client validation
|
|
3613
|
+
dismisses it, like the form's other outcome messages.
|
|
3614
|
+
- When a clear hides a second field whose `visibleWhen` reads the field just
|
|
3615
|
+
cleared, the notice names both. Any edit in between starts a new list.
|
|
3616
|
+
- `@object-ui/i18n` adds one key, `form.clearedOnHide`, to all ten packs. The
|
|
3617
|
+
form's built-in fallback table carries its `en` text for a form rendered
|
|
3618
|
+
without an `I18nProvider`.
|
|
3619
|
+
- 6778809: Key the `type="number"` bad-input refusal — the FIFTH sentence of this class,
|
|
3620
|
+
and the only shared one — into the locale packs (objectui#8148).
|
|
3621
|
+
|
|
3622
|
+
objectui#6755 ruled that a widget's OWN refusal sentence goes through
|
|
3623
|
+
`useFieldTranslation` + `FIELD_DEFAULTS`, and `320374d2a` applied that to
|
|
3624
|
+
`LocationField`'s residue arm as the fourth. The four already keyed are each one
|
|
3625
|
+
widget's. This one is not: a single literal in `widgets/numberBadInput.tsx`
|
|
3626
|
+
produced the sentence for `NumberField`, `CurrencyField`, `PercentField` and
|
|
3627
|
+
BOTH of `GeolocationField`'s boxes, through one hook, with five different
|
|
3628
|
+
example values.
|
|
3629
|
+
|
|
3630
|
+
**The measured consequence is on the geolocation surface.**
|
|
3631
|
+
`GeolocationField` sits beside `LocationField`, whose three refusal sentences
|
|
3632
|
+
are all keyed, so on the same form two adjacent coordinate widgets refused bad
|
|
3633
|
+
input in two different languages — a language switch mid-screen, which reads to
|
|
3634
|
+
a user as a bug rather than as a missing translation.
|
|
3635
|
+
|
|
3636
|
+
**The example stays a HOLE, and that is the authoring decision this card had to
|
|
3637
|
+
make.** Five different values reach the one sentence — `1234`, `1234.56`,
|
|
3638
|
+
`12.5`, `30.2741` and `120.1551` — so keying the example per widget would have
|
|
3639
|
+
meant five keys across ten packs, and each pack would then hold a decimal
|
|
3640
|
+
numeral it could legitimately re-punctuate. A pack writing `1234,56` reads as
|
|
3641
|
+
the `latitude, longitude` PAIR the adjacent widget's `fields.location.refusedFormat`
|
|
3642
|
+
asks for. With `{{example}}` filled by the widget in ASCII, no pack spells a
|
|
3643
|
+
digit at all, and all five values still reach the rendered sentence in every
|
|
3644
|
+
language. That property is pinned by name.
|
|
3645
|
+
|
|
3646
|
+
**No behaviour moves.** The English value is byte-identical to the literal it
|
|
3647
|
+
replaces, so English and provider-less rendering are unchanged — verified
|
|
3648
|
+
through the rendered widget for all five boxes, under an `en` provider and with
|
|
3649
|
+
no provider at all. objectui#6780's own announce suite keeps every verdict it
|
|
3650
|
+
had; it now spells its expected English sentence locally, because the helper
|
|
3651
|
+
that composes it takes the widget's `t` and reads
|
|
3652
|
+
`fields.number.badInput` rather than building an English string on its own. The
|
|
3653
|
+
guard still announces without refusing, and the new key is bound from here on by
|
|
3654
|
+
`check:i18n-keys`, `check:i18n-drift` and `all-locales-key-parity` like its four
|
|
3655
|
+
siblings.
|
|
3656
|
+
- 6e9a3d4: AI 搭建入口的空状态不再承诺 v1 拒绝的东西(objectui#8329)。
|
|
3657
|
+
|
|
3658
|
+
`console.ai.empty.build.description` 和 `console.ai.empty.editApp.description`
|
|
3659
|
+
原本告诉用户「描述一个应用或流程 —— 我会起草对象、界面和自动化」。cloud 的 v1
|
|
3660
|
+
创作边界(ADR-0112)不含流程、动作、定时,模型会明确拒绝这类请求,所以这是入口
|
|
3661
|
+
处对用户做的一个会被打回的承诺。它是这个家族里最后一处 —— 起始 chip 的措辞
|
|
3662
|
+
(cloud#1984)和模型收尾的「后续你可以」(cloud#2022)已经修过。
|
|
3663
|
+
|
|
3664
|
+
⚠️ **十个语言包每一个都有自己翻译好的违规版本**,全部一并修正;组件里两处硬编码
|
|
3665
|
+
`defaultValue` 兜底也同步更新,否则任一语言包丢 key 时旧承诺会静默回归。
|
|
3666
|
+
|
|
3667
|
+
未改动的是**手工**路径的文案(`home.build.subtitle`、Studio 落地页、导入时运行
|
|
3668
|
+
已有自动化、打包自动化运维、marketplace 分类)—— Studio 确实能建流程,那些承诺
|
|
3669
|
+
是真的。判据是「v1 的 AI 创作边界」,不是「automation 这个词」。
|
|
3670
|
+
|
|
3671
|
+
回滚:ADR-0112 v2 加回流程/动作时,这两句与 chip 文案同一行回滚;测试里的
|
|
3672
|
+
`RETIRED` 映射存了十个语言包的原句。
|
|
3673
|
+
- 309728c: A `user` reference that resolved to nothing is no longer rendered as a person
|
|
3674
|
+
(objectui#8434, routed from cloud#2074).
|
|
3675
|
+
|
|
3676
|
+
`UserCellRenderer`'s first branch printed any primitive as plain truncated text
|
|
3677
|
+
under the comment *"Primitive value: just display the ID/username as text"*. On a
|
|
3678
|
+
`user` field the premise of that comment is wrong: `user` is a lookup specialised
|
|
3679
|
+
to `sys_user`, so a primitive arriving there means precisely that nothing turned
|
|
3680
|
+
the reference into a person. Printing it as displayable text rendered
|
|
3681
|
+
**"resolution failed" as "resolution succeeded"** — and, measured, the cell was
|
|
3682
|
+
**byte-identical** to what a `text` cell prints for the same string. The only
|
|
3683
|
+
difference from a resolved person was the *absence* of the avatar, which is a
|
|
3684
|
+
subtractive signal; a user who has never seen the avatar has no reason to read
|
|
3685
|
+
absence as failure.
|
|
3686
|
+
|
|
3687
|
+
Such a cell now keeps the raw value **visible** and adds a stated affordance
|
|
3688
|
+
beside it: a muted marker glyph plus a sentence ("Unresolved reference: … was
|
|
3689
|
+
not resolved to a user", keyed as `detail.unresolvedReference` in all ten locale
|
|
3690
|
+
packs). The multi-value shape gets the same treatment, so a `user` field is not
|
|
3691
|
+
honest on one input shape and silent on the other.
|
|
3692
|
+
|
|
3693
|
+
**The sentence is deliberately epistemic, not ontological.** This branch has two
|
|
3694
|
+
populations and the renderer cannot tell them apart — it has no resolver at all,
|
|
3695
|
+
unlike `LookupCellRenderer`: an unexpanded `sys_user` id is the *legitimate*
|
|
3696
|
+
stored form (`packages/core/src/utils/expand-fields.ts`: "a `user` column that is
|
|
3697
|
+
NOT requested for expansion comes back as a raw user id", objectui#2032), and a
|
|
3698
|
+
name written into the column is dirty data. A "not found" claim would be false
|
|
3699
|
+
for the first, so the affordance states only what is true of both: this screen
|
|
3700
|
+
did not resolve it.
|
|
3701
|
+
|
|
3702
|
+
**Nothing else moves.** An expanded reference still renders avatar + name; a
|
|
3703
|
+
reference object carrying only an id still draws its avatar; `{}` still prints
|
|
3704
|
+
the coerced text (objectui#8596's boundary); the save-side `reference_not_found`
|
|
3705
|
+
refusal and the edit form are untouched — this change is display-only.
|
|
3706
|
+
- aa08d7e: Route the `repeater` and `file` cell COUNT labels through i18n (objectui#8441).
|
|
3707
|
+
|
|
3708
|
+
The `repeater` cell in the standard renderer table counted its rows with a hardcoded
|
|
3709
|
+
Chinese unit word. Two rules broke on that one literal: AGENTS.md #-1 (all user-facing
|
|
3710
|
+
text MUST be English) and, the half a translation to English would not have fixed, it
|
|
3711
|
+
bypassed i18n entirely — every reader on every locale read it, English ones included,
|
|
3712
|
+
beside the English siblings `[Vector]`, `[Grid]` and `FileCellRenderer` on the same
|
|
3713
|
+
detail page. It now reads `detail.repeaterItemCount` through `useFieldTranslate`, the
|
|
3714
|
+
channel this file already imported and four of its other cells already use.
|
|
3715
|
+
|
|
3716
|
+
`FileCellRenderer` moves with it, to `detail.fileCount`. Its `count === 1 ? 'file' :
|
|
3717
|
+
'files'` was not a rule violation — it is English — but it was equally unlocalized and
|
|
3718
|
+
plural-safe for English only: `ru` has four plural categories and `ar` six, and a
|
|
3719
|
+
two-branch ternary can spell neither. Two adjacent cells answering one concept two ways
|
|
3720
|
+
is what the single channel closes. **English output is byte-identical**: `2 files`
|
|
3721
|
+
stays `2 files`, and the provider-less fallback is pinned byte-equal to the `en` pack.
|
|
3722
|
+
|
|
3723
|
+
Both keys are REAL i18next plural families — base + `_one` + `_other` in all ten packs,
|
|
3724
|
+
not the two-sibling-key `xxxCountOne` shape. The base key is load-bearing (objectui#3863):
|
|
3725
|
+
i18next asks `Intl.PluralRules` for the one suffix a number needs and, finding no slot,
|
|
3726
|
+
walks `fallbackLng` to `en`, so without it a Russian reader gets English at counts 2-20.
|
|
3727
|
+
|
|
3728
|
+
The `repeater` entry also stops being an inline arrow in `getCellRenderer`'s table and
|
|
3729
|
+
becomes a module-level component. That table is rebuilt on every call and both call
|
|
3730
|
+
sites resolve inside render, so an inline entry is a fresh component type per render:
|
|
3731
|
+
React would remount the cell and tear down react-i18next's language subscription with
|
|
3732
|
+
it. No export was added — the published surface is unchanged.
|
|
3733
|
+
- 7c9145f: Fix the ChatDock AI usage indicator going blank against the single-pool AI usage
|
|
3734
|
+
endpoint (objectui#8524).
|
|
3735
|
+
|
|
3736
|
+
The cloud `GET /api/v1/ai/usage` endpoint now answers `{ pool, breakdown }`: one quota
|
|
3737
|
+
pool, plus `breakdown`, a read-only split of that same pool into app-building (`build`)
|
|
3738
|
+
and data Q&A (`dataChat`). `useAiUsage` only read the retired per-meter
|
|
3739
|
+
`{ meters: { build, dataChat } }` answer, so it returned `null` and `AiUsageIndicator`
|
|
3740
|
+
rendered nothing, with no error.
|
|
3741
|
+
|
|
3742
|
+
- `useAiUsage` reads `{ pool, breakdown? }` and only that shape. A missing `breakdown`,
|
|
3743
|
+
or one whose members are `null`, is a normal reading. Any other answer — the retired
|
|
3744
|
+
`{ meters }` one included — leaves `usage` `null` and now also sets `error`.
|
|
3745
|
+
- The hook's `AiUsageResponse` type is now `{ pool, breakdown? }`, with the new
|
|
3746
|
+
`AiUsageBreakdown` type for the split; `AiMeterUsage` keeps its fields and describes
|
|
3747
|
+
the pool.
|
|
3748
|
+
- `AiUsageIndicator` draws one ring from `pool`. When a `breakdown` member is a number,
|
|
3749
|
+
the popover lists it under the pool's row as a percentage of the pool, as text and
|
|
3750
|
+
never as a second ring. The upgrade / top-up button's test id is now `ai-usage-cta`
|
|
3751
|
+
(it was `ai-usage-cta-build` / `ai-usage-cta-dataChat`).
|
|
3752
|
+
- New locale key `console.ai.usage.breakdownTitle` ("Used so far") in every locale pack.
|
|
3753
|
+
- 676f677: Approvals inbox: a reference that cannot be resolved now renders a neutral
|
|
3754
|
+
"cannot be opened" affordance instead of degrading to the opaque record id
|
|
3755
|
+
(objectui#8631).
|
|
3756
|
+
|
|
3757
|
+
`1267508ea` tombstoned the class the platform FLAGS — `status: 'cancelled'`
|
|
3758
|
+
plus `cancel_reason: 'record_deleted'`. A terminal (`approved` / `rejected`)
|
|
3759
|
+
approval carries no such flag and never will: the upstream cancel path names
|
|
3760
|
+
`status: 'pending'` in its `where`, deliberately, so history is preserved. Those
|
|
3761
|
+
rows kept falling through to `formatIdentity(record_id)` and showing a truncated,
|
|
3762
|
+
meaningless identifier where a business identifier belongs.
|
|
3763
|
+
|
|
3764
|
+
The desktop row, the mobile card and the request drawer now all render the
|
|
3765
|
+
affordance for a reference that the viewer's own readability probe could not
|
|
3766
|
+
resolve AND that carries no snapshot title — the two conditions under which the
|
|
3767
|
+
opaque id was the only thing left on screen. A row whose snapshot kept a business
|
|
3768
|
+
identifier is unchanged: it still shows that identifier with its link suppressed,
|
|
3769
|
+
which is what objectui#5211 ruled. A row the server marked `record_deleted` still
|
|
3770
|
+
gets the tombstone `1267508ea` added, which says something stronger because the server
|
|
3771
|
+
asserted it.
|
|
3772
|
+
|
|
3773
|
+
**The copy names no cause, and that is the point.** The platform's read path
|
|
3774
|
+
fuses "this id names nothing" with "your grants filter it out" on purpose
|
|
3775
|
+
(existence non-disclosure), so nothing on the wire separates them and the console
|
|
3776
|
+
may not invent the separation: telling a viewer a record was deleted would
|
|
3777
|
+
confirm to them that it existed. The affordance therefore states only what is
|
|
3778
|
+
true either way — there is nothing here to open — and is pinned by a test
|
|
3779
|
+
asserting the rendered text carries neither a deletion word nor a permissions
|
|
3780
|
+
word.
|
|
3781
|
+
|
|
3782
|
+
This sentence is console-authored, through this repo's own catalogue in all ten
|
|
3783
|
+
locales, because it has no upstream original: `APPROVAL_CANCEL_REASON_LABELS`
|
|
3784
|
+
declares exactly one option (`record_deleted`) and there is no cause code for
|
|
3785
|
+
"unresolvable" — nor may one be requested, per the card's fence. That is why it
|
|
3786
|
+
does not contradict the neighbouring rule against authoring a second copy of the
|
|
3787
|
+
platform's tombstone sentence.
|
|
3788
|
+
|
|
3789
|
+
Also removed: the full, untruncated `record_id` that the desktop row hung in a
|
|
3790
|
+
`title` tooltip on **every** row, readable ones included. The reference slot now
|
|
3791
|
+
tooltips the identifier it displays.
|
|
3792
|
+
- b253c4e: The import wizard's "Download template" button now downloads the server's import template, an Excel workbook, instead of building a CSV of every field (objectui#9600).
|
|
3793
|
+
|
|
3794
|
+
The old CSV listed every field the wizard was handed, including the system and read-only columns every object carries (created by, owner, and so on). The server strips those columns on import without an error, so a user who filled them in lost that data silently. The server's template (`GET /api/v1/data/:object/export?template=true`, objectstack 17.6.0) lists only the columns this caller can import. It marks required ones with `*`, adds dropdowns for select and boolean columns, and includes an instructions sheet.
|
|
3795
|
+
|
|
3796
|
+
- **`@object-ui/types`: new optional `DataSource.downloadImportTemplate(resource)`**, resolving to the template file as a `Blob`. Additive. When a data source lacks it, no template is offered: there is no client-side fallback.
|
|
3797
|
+
- **`@object-ui/data-objectstack`: `ObjectStackAdapter.downloadImportTemplate(resource)`** sends `template=true` on the export route, on the same request path as `exportDownload`. When the client has a locale set (`getClient().setLocale`), the request carries it as `Accept-Language`, the way the import request does. The template's labels are written in the request's locale, and the import accepts a translated label only in its own request's locale, so the two requests must match.
|
|
3798
|
+
- **`@object-ui/plugin-grid`: `ImportWizard` offers the template** when its data source has `downloadImportTemplate` and the user can create records of the object (`usePermissions().can(object, 'create')`, the server's own gate). A 405 (object not open for import), a 403 (no create permission) and any other failure each show a message on the upload step, and no file is saved. The client-built CSV is deleted, with its helpers (`buildImportTemplateCsv`, `exampleForField`, `firstOptionValue`, `downloadTextFile`). The column mapping step is unchanged.
|
|
3799
|
+
- **`@object-ui/app-shell`**: the identity-import data source (`sys_user`) withholds `downloadImportTemplate`, because the server's template describes the generic import door, not the identity pipeline.
|
|
3800
|
+
- **`@object-ui/i18n`**: `grid.import.downloadTemplateHint` describes the Excel template in all ten packs, and `grid.import.templateDownloadFailed` and `grid.import.templateNotPermitted` are new.
|
|
3801
|
+
- 44152c4: Full-page search now agrees with its own number while browsing: at exactly one
|
|
3802
|
+
searchable item the header reads `1 item available`, not `1 items available`
|
|
3803
|
+
(objectui#9664).
|
|
3804
|
+
|
|
3805
|
+
The header is one ternary with two branches. The query branch already chose its
|
|
3806
|
+
key on `totalCount === 1` — `search.resultsCount` against
|
|
3807
|
+
`search.resultsCountPlural`. The browse branch beside it asked for
|
|
3808
|
+
`search.itemsAvailable` at every count, and that key had neither a plural family
|
|
3809
|
+
nor a sibling to fall to, so `en` shipped the disagreement in the default
|
|
3810
|
+
language; `de`, `es`, `fr` and `pt` read the same way (`1 Elemente verfügbar`,
|
|
3811
|
+
`1 elementos disponibles`, `1 éléments disponibles`, `1 itens disponíveis`).
|
|
3812
|
+
|
|
3813
|
+
`search.itemsAvailableOne` is the singular half, added to all ten packs, and the
|
|
3814
|
+
browse branch now picks between the two on `allItems.length === 1`. That is this
|
|
3815
|
+
repo's two-key plural convention — the one `common.itemCount`/`itemCountOne` and
|
|
3816
|
+
`detail.reactionCount`/`reactionCountOne` already use — and deliberately not an
|
|
3817
|
+
i18next `_one`/`_other` family: full key parity across ten packs caps a family at
|
|
3818
|
+
base plus `_one` plus `_other`, so every CLDR category a pack does not spell out
|
|
3819
|
+
falls through to the base key, which on this key is the plural. Russian meets
|
|
3820
|
+
that at 2 to 4 and Arabic at 2, 3 to 10 and 11 to 99. Choosing the key in the
|
|
3821
|
+
component keeps `Intl.PluralRules` and `fallbackLng` out of the path: both halves
|
|
3822
|
+
exist in every pack, so no count in any language can reach English.
|
|
3823
|
+
|
|
3824
|
+
`zh`, `ja` and `ko` carry the same string in both halves because the counter word
|
|
3825
|
+
holds the number and there is no separate singular form; `ru` does too, because
|
|
3826
|
+
its phrasing states no noun to agree with.
|
|
3827
|
+
|
|
3828
|
+
One translation changed rather than being added: `ar` wrote both numbers into one
|
|
3829
|
+
string as a parenthesised marker. Its key can no longer be reached at one item, so
|
|
3830
|
+
the singular half of that marker was dead weight while the parentheses still
|
|
3831
|
+
rendered at every count the key does serve. It now reads as a singular noun at one
|
|
3832
|
+
item and as a count label at every other count, the shape `ar`'s
|
|
3833
|
+
`common.itemCount`/`itemCountOne` uses (objectui#10425).
|
|
3834
|
+
|
|
3835
|
+
⚠️ **Dated note, 2026-10-02 — superseded in this release — objectui#11445.**
|
|
3836
|
+
Later in this same release the `search.itemsAvailable` / `search.itemsAvailableOne`
|
|
3837
|
+
switch gave way to a `search.itemsAvailable` count family in all ten packs: the
|
|
3838
|
+
browse branch calls the family key with `{ count }`, each pack spells every CLDR
|
|
3839
|
+
category its language selects (so the base-key fall-through described above no
|
|
3840
|
+
longer arises), and `search.itemsAvailableOne` left every pack. The rest of this entry is kept as the reading of this change.
|
|
3841
|
+
- a78cd37: Dates and numbers across the console and the plugins format in the session's
|
|
3842
|
+
display locale instead of the machine's (objectui#9909).
|
|
3843
|
+
|
|
3844
|
+
Every one of these faces handed `Intl` — directly, or through a formatter called
|
|
3845
|
+
without one — either no locale tag or an explicit `undefined`, which is not "the
|
|
3846
|
+
user's locale": it is the locale of the machine the browser runs on, which is
|
|
3847
|
+
neither of this repository's two locale channels.
|
|
3848
|
+
A German or Spanish session therefore read, beside a translated label, a date or
|
|
3849
|
+
an amount grouped and decimal-marked the machine's way — and an amount with
|
|
3850
|
+
inverted separators does not read as unformatted, it reads as a different
|
|
3851
|
+
number. They now go through `useDisplayLocale()` (tenant regional default, then
|
|
3852
|
+
the active UI language, then `'en'`), and a plain helper takes that tag from its
|
|
3853
|
+
caller. The surfaces:
|
|
3854
|
+
|
|
3855
|
+
- **`@object-ui/fields`** — `GridField`'s numeric and currency cells (list mode
|
|
3856
|
+
and computed columns) and both total cells. The same cell helper's date branch
|
|
3857
|
+
already used the display locale, so a single grid row read two conventions at
|
|
3858
|
+
once.
|
|
3859
|
+
- **`@object-ui/plugin-charts`** — ISO-date x-axis ticks, compact y-axis ticks,
|
|
3860
|
+
the single-value face, spec `format` strings and the tooltip value.
|
|
3861
|
+
- **`@object-ui/app-shell`** — the organization invitations, members and
|
|
3862
|
+
accept-invitation dates; marketplace version dates; the AI conversation row's
|
|
3863
|
+
older-than-a-week date and its tooltip; the build-debug token count; the
|
|
3864
|
+
record approvals timeline; and the metadata-admin audit, history, external
|
|
3865
|
+
datasource snapshot, schema-browser row estimate, flow-run start and job
|
|
3866
|
+
next-fire faces. The audit, history and flow-run panels format these in the
|
|
3867
|
+
DISPLAY locale, not in the `locale` prop they take for their UI strings
|
|
3868
|
+
(`AuditPanel` defaults that prop to `'en-US'`; `HistoryPanel` and
|
|
3869
|
+
`FlowRunsPanel` leave it optional).
|
|
3870
|
+
- **`@object-ui/console`** — the approvals inbox's timestamp tooltips, amounts
|
|
3871
|
+
and payload summary; the audit log's timestamp column; the flow runs table and
|
|
3872
|
+
its run detail.
|
|
3873
|
+
- **`@object-ui/components`** — the export dialog's record counts and the debug
|
|
3874
|
+
panel's event times.
|
|
3875
|
+
- **`@object-ui/plugin-form`** — the analytics submission count, the
|
|
3876
|
+
master-detail subtotal / tax / total stack and the edit-conflict dialog's
|
|
3877
|
+
"their save" time.
|
|
3878
|
+
- **`@object-ui/plugin-chatbot`** — the approvals inbox's past-30-days date and
|
|
3879
|
+
the times stamped on local-mode chat messages (the user's and the
|
|
3880
|
+
auto-response).
|
|
3881
|
+
- **`@object-ui/plugin-dashboard`** — the record-count badge, and the currency
|
|
3882
|
+
and date-format branches of `renderFieldValue`, which was handed the display
|
|
3883
|
+
locale and spent it only on its percent branch.
|
|
3884
|
+
- **`@object-ui/plugin-grid`** — the record-detail panel's inferred currency
|
|
3885
|
+
value and the mobile card's amount line, siblings of date cells already on the
|
|
3886
|
+
display locale.
|
|
3887
|
+
- **`@object-ui/plugin-designer`** — `VersionHistory`'s version times.
|
|
3888
|
+
|
|
3889
|
+
A host that mounts one of the components that newly read the display locale with
|
|
3890
|
+
NO `I18nProvider` now gets react-i18next's once-per-module `NO_I18NEXT_INSTANCE`
|
|
3891
|
+
notice in the console on the first such mount; the faces still render, in the
|
|
3892
|
+
channel's `'en'` last resort, and mounting an `I18nProvider` (as the console
|
|
3893
|
+
does) avoids the notice.
|
|
3894
|
+
|
|
3895
|
+
**Additive API** (nothing that compiled before stops compiling):
|
|
3896
|
+
|
|
3897
|
+
- `@object-ui/types`: `ValidationContext` gains an optional `locale`. At this
|
|
3898
|
+
change, the `@object-ui/core` validation engine prints the `date_min` /
|
|
3899
|
+
`date_max` bound in it; omitted, `Intl` follows the runtime default, as
|
|
3900
|
+
`formatDisplayNumber` already declares for a caller with no locale in hand.
|
|
3901
|
+
|
|
3902
|
+
⚠️ **Dated note, 2026-09-25 — that engine (`ValidationEngine`) is removed from
|
|
3903
|
+
`@object-ui/core` by a later change; `ValidationContext.locale` stays declared in
|
|
3904
|
+
`@object-ui/types`, and at that change nothing in `@object-ui/core` reads it —
|
|
3905
|
+
objectui#7659.**
|
|
3906
|
+
|
|
3907
|
+
⚠️ **Dated note, 2026-09-27 — `ValidationContext` itself is removed from
|
|
3908
|
+
`@object-ui/types` by a later change, with the rest of the Phase 3.5
|
|
3909
|
+
validation types it belonged to, so the `locale` member this entry adds no
|
|
3910
|
+
longer exists — objectui#10719.**
|
|
3911
|
+
- `@object-ui/plugin-report`: `exportReport`, `exportAsHTML` and `exportAsPDF`
|
|
3912
|
+
take a trailing optional `locale` for the exported file's "Generated:" time,
|
|
3913
|
+
and `LiveExportOptions` gains an optional `locale` that `exportWithLiveData`
|
|
3914
|
+
forwards. Omitted, the time uses the display channel's own last resort
|
|
3915
|
+
(`'en'`), never the machine's locale. `ReportViewer` passes the session's
|
|
3916
|
+
display locale. The locale is deliberately NOT a `ReportExportConfig` member:
|
|
3917
|
+
that type is authored report metadata, and a display locale belongs to the
|
|
3918
|
+
session.
|
|
3919
|
+
|
|
3920
|
+
**One census, repository-wide.** `plugin-detail`'s machine-locale census pin
|
|
3921
|
+
(objectui#9786) is now a single test, `machineLocaleCensus-9909.test.ts` in
|
|
3922
|
+
`@object-ui/i18n`, covering every workspace package whose manifest depends on
|
|
3923
|
+
`@object-ui/i18n`. It refuses any call site that passes nothing, `undefined`,
|
|
3924
|
+
the `'default'` pseudo-tag (a subtag no locale data answers, so `Intl` resolves
|
|
3925
|
+
it to the machine's locale) or a hard-coded tag, unless the site is declared
|
|
3926
|
+
with its reason — for example the `catch` fallback for a tag `Intl` itself
|
|
3927
|
+
rejected, or an ISO formatter feeding `<input type="date">`. Each declared site
|
|
3928
|
+
is counted exactly, so deleting one without its entry is refused too. The
|
|
3929
|
+
per-package pin it replaces is deleted. The runtime tripwire that observes the
|
|
3930
|
+
argument each locale-taking call (`Intl` constructors, `Date` and `Number`
|
|
3931
|
+
`toLocale*`) actually receives moved into the private `@object-ui/test-support`
|
|
3932
|
+
package, and every surface above is pinned with it: the same data under two
|
|
3933
|
+
declared locales must render differently, NO locale-taking call may receive the
|
|
3934
|
+
machine's locale (nothing, `undefined` or `'default'`), and the surface's own
|
|
3935
|
+
calls must receive the declared tag. A call carrying another declared tag, such
|
|
3936
|
+
as the `'en'` of the session's UI language, is tolerated by design: only the
|
|
3937
|
+
machine's locale is the defect.
|
|
3938
|
+
- bc5870c: fix(plugin-calendar): a record with no date is no longer placed on today
|
|
3939
|
+
|
|
3940
|
+
`ObjectCalendar` mapped a record whose declared `startDateField` carried no
|
|
3941
|
+
value to `new Date()` — the current moment — so it rendered on today's cell as
|
|
3942
|
+
an ordinary event, indistinguishable from a real one. The `isNaN` guard six
|
|
3943
|
+
lines below could not catch it by construction: a no-argument `new Date()` is
|
|
3944
|
+
always valid, so the absent-value case became a well-formed lie *before* the
|
|
3945
|
+
check that would have caught it.
|
|
3946
|
+
|
|
3947
|
+
The fabricating arm is deleted. Such records now leave the grid entirely and
|
|
3948
|
+
appear in a collapsed "Unscheduled (N)" area below the calendar — a visible
|
|
3949
|
+
count and an expandable list, with no invented date and no scheduling UI. The
|
|
3950
|
+
`isNaN` filter keeps its original job for values that are present but
|
|
3951
|
+
unparseable: absent and malformed stay two distinguishable outcomes.
|
|
3952
|
+
`allDay: !endDate` now applies only to records that have a start, so a record
|
|
3953
|
+
with no dates at all is unscheduled rather than silently all-day; a record with
|
|
3954
|
+
a start and no end still renders all-day exactly as before.
|
|
3955
|
+
|
|
3956
|
+
Adds `calendar.unscheduled` to all ten locale packs.
|
|
3957
|
+
- f760064: Localize every accessible name `CalendarView` authors itself.
|
|
3958
|
+
|
|
3959
|
+
The component hard-coded the accessible names of its own landmarks and controls
|
|
3960
|
+
in English, so no locale pack could reach them. A screen-reader user on a
|
|
3961
|
+
translated console heard "Calendar" and "Calendar grid" — the only names those
|
|
3962
|
+
two landmarks have — plus the toolbar buttons and both resize grips, in English,
|
|
3963
|
+
while every sighted string beside them was translated.
|
|
3964
|
+
|
|
3965
|
+
All of them now resolve through the `createSafeTranslation` hook the component
|
|
3966
|
+
already used for its visible copy, and the keys land in all ten locale packs
|
|
3967
|
+
under `calendar.a11y`. Two of the repaired strings were English frames wrapped
|
|
3968
|
+
around already-localized data (the current-date button and the day gridcell's
|
|
3969
|
+
event count); those are now pack-owned sentences whose translations can put the
|
|
3970
|
+
date and the count where each language's grammar wants them, and the gridcell's
|
|
3971
|
+
count is a plural family with a base key so the categories a pack does not
|
|
3972
|
+
enumerate resolve in its own language rather than falling through to English.
|
|
3973
|
+
- 8ea3bee: fix(plugin-chatbot): localize the build-progress panel, which was an English island (objectui#7388)
|
|
3974
|
+
|
|
3975
|
+
Every label the build panel is HANDED was already localized — the host passes
|
|
3976
|
+
`openBuiltAppLabel`, `designBuiltAppLabel`, `previewDraftLabel` and the three
|
|
3977
|
+
connection cues through its own `t()`. Every string the panel OWNED was a
|
|
3978
|
+
literal in the component, so a fully Chinese conversation watched its app get
|
|
3979
|
+
built under `Building your app…`, over `Objects` / `Views` / `Dashboards` /
|
|
3980
|
+
`App` / `Sample data` row headings — one per row, on every build — and a
|
|
3981
|
+
`+N more` overflow counter.
|
|
3982
|
+
|
|
3983
|
+
All of them now resolve through the console's pack as `chatbot.build.*`, added
|
|
3984
|
+
to all ten locales. Behaviour for a known phase is unchanged in English, and
|
|
3985
|
+
the unknown-artifact-type fallback still renders the raw type rather than a
|
|
3986
|
+
raw i18n key.
|
|
3987
|
+
- 3a58149: A cloud-connection bind failure now reads in the user's language whichever clock
|
|
3988
|
+
noticed it (objectui#5054).
|
|
3989
|
+
|
|
3990
|
+
One abandoned device approval could be noticed by either of two clocks, and the
|
|
3991
|
+
Cloud Connection panel had a different answer for each. When the panel's own
|
|
3992
|
+
`expires_in` deadline fired first it rendered `cloudConnection.errors.expired` —
|
|
3993
|
+
translated in all ten packs. When the SERVER noticed first, `/bind/poll` answered
|
|
3994
|
+
HTTP 400 with `message: 'Device authorization failed: expired_token'`; `getJson`
|
|
3995
|
+
threw a bare `Error` carrying only that sentence, and the catch rendered it
|
|
3996
|
+
verbatim. Same user, same failure, two languages, decided by which clock got
|
|
3997
|
+
there first — visible on a zh console as the same abandoned approval reading
|
|
3998
|
+
Chinese or English depending on whether the tab sat open past `expires_in`.
|
|
3999
|
+
|
|
4000
|
+
`getJson` now carries the envelope's `declaredCode` and `code` across its throw,
|
|
4001
|
+
and a single closed map turns the two RFC 8628 outcomes a user can actually cause
|
|
4002
|
+
into console copy: `expired_token` → the existing `cloudConnection.errors.expired`,
|
|
4003
|
+
`access_denied` → a new `cloudConnection.errors.accessDenied` added to all ten
|
|
4004
|
+
locale packs. `declaredCode` is read first, because ADR-0112 keeps the upstream
|
|
4005
|
+
spelling there — `code` is `DEVICE_CODE_FAILED` for both.
|
|
4006
|
+
|
|
4007
|
+
Every other code is unchanged: `invalid_grant`, and anything upstream invents
|
|
4008
|
+
next, still render the wire `message`, which stays the single source of truth for
|
|
4009
|
+
failures this console has no copy for. No API, export or resolver was widened.
|
|
4010
|
+
- 6ce89da: The 确认修改 (confirm changes) card now carries a UI-owned terminal state after approval (#5695): `detectReplayOutcome` lifts the confirm-replay envelope (`replay_*` tool results) into 应用中 / 已生效 / 已暂存为草稿(含内联发布)/ 未生效(含 publishError 首行), rendered on the original card across the live, hydration/share, and localStorage-cache converters. A failed in-turn publish no longer rehydrates as an ordinary draft card with a live Publish button — the UI-rendered refusal is the layer a model cannot narrate over. New `console.ai.changesApplying/Applied/Drafted/Failed` keys in all ten locale packs.
|
|
4011
|
+
- de570cc: fix(i18n): `useObjectTranslation`'s provider-less `t` now interpolates its inline `defaultValue`
|
|
4012
|
+
|
|
4013
|
+
With no `I18nProvider` mounted, react-i18next hands back its not-ready `t`, which
|
|
4014
|
+
returns `options.defaultValue` **verbatim** — so an inline default written
|
|
4015
|
+
`'Deleted {{count}} rows'` reached the user with the braces intact. 68 inline
|
|
4016
|
+
defaults across 24 files rendered through that path on any host that embeds an
|
|
4017
|
+
ObjectUI component without a provider, which is the configuration
|
|
4018
|
+
`createSafeTranslation` exists for.
|
|
4019
|
+
|
|
4020
|
+
`useObjectTranslation` now runs its not-ready result through the same one
|
|
4021
|
+
interpolator `createSafeTranslation`'s `fallbackT` uses, so both provider-less
|
|
4022
|
+
renderers fill exactly the `{{name}}` spelling the copy is already gated to. The
|
|
4023
|
+
ready path is untouched: with a provider, i18next's own `t` is returned by
|
|
4024
|
+
reference and nothing is interpolated twice. Pre-interpolated template-literal
|
|
4025
|
+
defaults (`` `Deleted ${n} rows` ``) stay correct — they have no holes left to
|
|
4026
|
+
fill — so no call site changes.
|
|
4027
|
+
- 1e66879: `console.ai.pendingDrafts` — the standing unpublished-changes bar's five strings —
|
|
4028
|
+
now exists in all ten locale packs. It previously existed only in `en` and `zh`, so
|
|
4029
|
+
`ar`, `ru`, `pt`, `es`, `fr`, `de`, `ko` and `ja` rendered the English defaults and
|
|
4030
|
+
`all-locales-key-parity` failed on `main` (objectui#5705).
|
|
4031
|
+
|
|
4032
|
+
The feature landed `en`-only in objectui#5696; the follow-up in objectui#5697 was
|
|
4033
|
+
titled for the locale packs but reached only `zh`, so eight packs × five keys stayed
|
|
4034
|
+
missing and the parity assertion — which carries no allowlist — was red on `main` and
|
|
4035
|
+
on every PR whose diff touched source. Source-free diffs skip the shard that runs it,
|
|
4036
|
+
which is why the breakage survived several merges.
|
|
4037
|
+
|
|
4038
|
+
Each pack keeps its own conventions rather than `en`'s: the eight all quote with `"`,
|
|
4039
|
+
`ru` puts the number last (`…: {{count}}`) as it already does for the sibling
|
|
4040
|
+
`home.pendingDrafts` counts, and `ja` uses the full-width `:` before `{{detail}}`
|
|
4041
|
+
because that value is a runtime message rather than a single token — both choices
|
|
4042
|
+
carry an in-pack note. Terminology is taken from each pack's existing publish-bar
|
|
4043
|
+
vocabulary (`home.pendingDrafts`, `console.ai.seedWarn`) so the two banners read
|
|
4044
|
+
alike.
|
|
4045
|
+
|
|
4046
|
+
Both interpolations survive verbatim in every pack — `{{count}}` in `count` and
|
|
4047
|
+
`{{detail}}` in `publishedWithFindings` — asserted mechanically against the evaluated
|
|
4048
|
+
packs, not by eye. The unrelated `home.pendingDrafts` block (`message` / `cta`) is a
|
|
4049
|
+
different node and is untouched.
|
|
4050
|
+
- c5200f0: Follow-up to #5696: the pending-drafts bar's strings live at `console.ai.pendingDrafts.*` with en+zh locale entries — the i18n call-site key gate and ratchet flagged the original root-level keys that existed nowhere.
|
|
4051
|
+
- fd8dace: Studio surfaces the runtime authoring gate's advisory findings after a **publish**, not only after a save
|
|
4052
|
+
|
|
4053
|
+
objectui#4133 / PR #4236 wired the gate's advisories to the save door and recorded, honestly, what that left unsurfaced: Studio's designer stages every edit as a `mode: 'draft'` save, drafts are never gated (the framework returns at its D1 early-return before a single rule runs), and the publish step that *is* gated returned no `advisories` field at all. So on the flow most tenants actually use, the author was told nothing at either door — for two different reasons, only one of which was objectui's.
|
|
4054
|
+
|
|
4055
|
+
The second reason has expired. `PublishMetaItemResponseSchema` now declares the same optional, omitted-when-empty `advisories` key that `SaveMetaItemResponseSchema` has carried since #4717, and `publishMetaItem` populates it. Measured against the installed `@objectstack/spec` (17.2.0) rather than inferred from the version number: the key survives a `safeParse`, a half-shaped finding is rejected, and a clean publish omits the key entirely. That reading is now a test rather than a note, so a spec drift fails CI instead of silently re-muting the door.
|
|
4056
|
+
|
|
4057
|
+
⚠️ **Dated note, 2026-09-29 — the save response's advisories key came from an objectstack card — objectui#11016.** The
|
|
4058
|
+
paragraph above dates the save response's `advisories` key by a bare number,
|
|
4059
|
+
which in this repository resolves to an unrelated objectui item. It is
|
|
4060
|
+
objectstack-ai/objectstack#4717, the card that put the runtime authoring gate's
|
|
4061
|
+
advisory findings on the response. The text above is kept as the reading of this change.
|
|
4062
|
+
|
|
4063
|
+
`MetadataClient.publish` and `MetadataClient.publishDraft` — the two methods over the single-item publish route `POST /meta/:type/:name/publish` — now report through the **same** sink, the same event and the same renderer the save door already used. No new UI shape: same warning tier, same 10s duration, same per-finding `rule` + `message` + `hint` formatting, findings still rendered verbatim as server prose. The wiring lands in the data layer rather than at the call sites, so `ResourceEditPage`'s Publish button and the runtime `RuntimeDraftBar` promotion (ObjectView / ReportView / DashboardView) are covered by one change, as are future ones.
|
|
4064
|
+
|
|
4065
|
+
One thing had to differ, and it is the frame's verb. Save and Publish are two different buttons in this product, so a toast that says "Saved" after a Publish tells the author their change is still a draft — the opposite of what happened. `MetadataSaveAdvisoryEvent` therefore gains a required `door: 'save' | 'publish'` and the renderer picks `console.publishAdvisoryTitle` (added to all ten locale packs) accordingly. `door` exists because `mode` cannot answer this: a direct active save and a draft promotion both report `mode: 'publish'`, since both land the body in the active overlay. It is required rather than optional so a future third door cannot be wired without saying which one it is, and the renderer branches on it through an exhaustive switch with a `never` check, so adding a third member is a compile error rather than a silently wrong verb.
|
|
4066
|
+
|
|
4067
|
+
**BREAKING for event constructors — `MetadataSaveAdvisoryEvent.door` is required.** Reading the event is unaffected: a listener that ignores `door` behaves exactly as before, and every other member is unchanged. Constructing one is a compile break — a door-less event literal that type-checked before now fails with TS2741, `Property 'door' is missing`. Measured on the emitted `dist/index.d.ts` of `@object-ui/data-objectstack` on both sides: that single required member is the entire non-comment delta of the package's published surface. **Migration:** add `door: 'save'` or `door: 'publish'` to the literal, whichever write it models — `'save'` for `PUT /meta/:type/:name`, `'publish'` for `POST /meta/:type/:name/publish`. Scored `minor` rather than `major` per the repo's version policy: objectui's major is pinned to `@objectstack`'s so that "same major means compatible" holds across the two repos, so objectui's own breaking changes ship as `minor` with the break named here (`scripts/check-changeset-no-major.mjs`). Every publishable package sits in one `fixed` group, so this entry carries the group.
|
|
4068
|
+
|
|
4069
|
+
Unchanged, deliberately: the **batch** door. "Publish whole app" (`POST /packages/:id/publish-drafts`) discarded per-draft advisories server-side when this change was written — objectstack#9343, open and unruled at the time — and nothing here compensated for that from the client side. A test pinned the absence, so a later traversal of a batch-shaped `published[]` could not be added without turning it red.
|
|
4070
|
+
|
|
4071
|
+
*Corrected before release (`ce986aafc`): both present-tense claims in the paragraph above went false after it was written — objectstack#9343 landed, the batch response now carries per-draft advisories, and `ce986aafc` routes that door through the same seam and flips the absence pin to a presence pin. The paragraph is kept in the past tense as the record of what this change did and did not do; this note is what the CHANGELOG publishes instead of a sentence that was true only while it sat here.*
|
|
4072
|
+
- 38a9568: `useObjectLabel` now keeps a stable identity when no i18next instance is bound,
|
|
4073
|
+
so the memoization it advertises holds on the no-provider path too
|
|
4074
|
+
(objectui#5564).
|
|
4075
|
+
|
|
4076
|
+
react-i18next's `useTranslation` builds its return value out of a fresh `{}` on
|
|
4077
|
+
every render when it has nothing to bind to (`const finalI18n = i18n || {}`,
|
|
4078
|
+
which then feeds that hook's own `useMemo` deps), so the `i18n` object arrived
|
|
4079
|
+
with a new identity each render. `useObjectLabel` keyed its memo on `[t, i18n]`,
|
|
4080
|
+
so the memo never held: measured 4 distinct returned objects across 4 renders
|
|
4081
|
+
with no instance, against 1 with one. That is the wrong way round — the memo
|
|
4082
|
+
exists to stop downstream `useMemo`/`useCallback` deps from being re-keyed in
|
|
4083
|
+
heavy consumers, and `useSafeFieldLabel`'s docstring names the no-provider case
|
|
4084
|
+
as the one it exists to serve.
|
|
4085
|
+
|
|
4086
|
+
Both memo dependencies are now pinned to module-level constants while no
|
|
4087
|
+
instance is bound. The substitution is unobservable rather than merely
|
|
4088
|
+
convenient: every `t()` call in the module sits inside a
|
|
4089
|
+
`for (… of getAppNamespaces())` loop, and `getAppNamespaces()` returns `[]`
|
|
4090
|
+
under exactly the same "is there a usable instance" predicate — so while the
|
|
4091
|
+
substitution is in effect, the closures cannot read either value. When an
|
|
4092
|
+
instance appears the dependencies become the live values again, so a provider
|
|
4093
|
+
mounting after first render recomputes the object exactly once and resolves
|
|
4094
|
+
real translations from then on.
|
|
4095
|
+
|
|
4096
|
+
No API change: no new exports, no signature changes, and the returned surface is
|
|
4097
|
+
identical on both paths. Direct `useObjectLabel()` consumers are fixed alongside
|
|
4098
|
+
`useSafeFieldLabel()` ones, including `ListView.filterFields` — the consumer the
|
|
4099
|
+
memo's own docstring names.
|
|
4100
|
+
- b2437a7: `setLocalized`'s published docblock states the single-locale write rule that is
|
|
4101
|
+
actually in force, instead of deferring the multi-locale-authoring question to a
|
|
4102
|
+
closed card (objectui#5591).
|
|
4103
|
+
|
|
4104
|
+
The docblock read "is not a multi-locale authoring UI (objectui#4163)". objectui#4163
|
|
4105
|
+
closed as completed on 2026-08-15 with that product question still unanswered, so the
|
|
4106
|
+
parenthetical pointed at nothing — and it read as though the question had been settled
|
|
4107
|
+
somewhere a reader could go and check. This is the failure mode objectui#5428
|
|
4108
|
+
demonstrated is not harmless: there, a dangling deferral of exactly this shape let an
|
|
4109
|
+
expired justification sit unread for a release cycle at two surfaces.
|
|
4110
|
+
|
|
4111
|
+
The remedy is objectui#5428's, not a re-pointing at a successor card: state the rule in
|
|
4112
|
+
force (`setLocalized` reaches only the entry for the locale the author is in), keep the
|
|
4113
|
+
open product question open **in place**, and record why there is deliberately no tracker
|
|
4114
|
+
reference — so the next reader cannot restore one. Re-pointing is how the class
|
|
4115
|
+
regenerates, because the next card closes too. The same wording form already landed in
|
|
4116
|
+
`plugin-designer`'s `writeWidgetTitle` and `DashboardWidgetInspector`.
|
|
4117
|
+
|
|
4118
|
+
Prose only. No behaviour, no signature, no test changes — `setLocalized`'s pairing with
|
|
4119
|
+
`pickLocalized` is unchanged and still pinned by `src/__tests__/setLocalized.test.ts`.
|
|
4120
|
+
|
|
4121
|
+
Declared as a `patch` for `@object-ui/i18n` alone because the emit was measured per
|
|
4122
|
+
package rather than assumed, and the two packages this change touches differ:
|
|
4123
|
+
|
|
4124
|
+
- `@object-ui/i18n` — the docblock sits on the **exported** `setLocalized`, so it reaches
|
|
4125
|
+
the published artifacts. Rebuilt with `tsconfig.tsbuildinfo` cleared first (the build is
|
|
4126
|
+
`composite`, which otherwise skips emit), and compared by SHA-256 rather than byte count:
|
|
4127
|
+
`dist/pickLocalized.d.ts` `1e2170ad…` -> `124a1c07…` and `dist/pickLocalized.js`
|
|
4128
|
+
`06eb88bd…` -> `568cb703…`. A consumer reads this text on hover and in the API docs, so
|
|
4129
|
+
it publishes something.
|
|
4130
|
+
- `@object-ui/plugin-dashboard` — the two comments changed there are a `//` banner between
|
|
4131
|
+
declarations and a test docblock, neither attached to an exported declaration.
|
|
4132
|
+
`dist/WidgetConfigPanel.d.ts` is **byte-identical** across the rebuild
|
|
4133
|
+
(`93252e8cdf5a6faa…` both sides). The only artifact that moved is
|
|
4134
|
+
`dist/WidgetConfigPanel.d.ts.map`, whose mappings shift because lines were added above
|
|
4135
|
+
the declarations; no declaration text changed. Nothing user-visible publishes from that
|
|
4136
|
+
package, so it is not named here.
|
|
4137
|
+
- eddc1dd: The Studio copilot tells the agent WHAT the user is discussing (cloud#1610 send half): `ChatPane` accepts a `surfaceContext` and sends it as `context.surface` on every turn (the transport reads the body per send, so it stays fresh); the Studio copilot derives it from the URL alone — the `:tab` pillar segment plus the `?surface=type:name` deep-link the pillars already mirror, so the artifact carries its type discriminator (page/object/dashboard/report). A display chip above the composer (「正在讨论:…」, new `console.ai.discussing` key in all ten packs) makes the sent context visible instead of invisible grounding.
|
|
4138
|
+
- Updated dependencies [97abedc]
|
|
4139
|
+
- Updated dependencies [ad694ac]
|
|
4140
|
+
- Updated dependencies [c131d9e]
|
|
4141
|
+
- Updated dependencies [7b395d8]
|
|
4142
|
+
- Updated dependencies [8cedb0d]
|
|
4143
|
+
- Updated dependencies [6cc910b]
|
|
4144
|
+
- Updated dependencies [061f5e8]
|
|
4145
|
+
- Updated dependencies [4ab4f1b]
|
|
4146
|
+
- Updated dependencies [961ceaa]
|
|
4147
|
+
- Updated dependencies [544aca2]
|
|
4148
|
+
- Updated dependencies [86982ac]
|
|
4149
|
+
- Updated dependencies [ff14e29]
|
|
4150
|
+
- Updated dependencies [64563a9]
|
|
4151
|
+
- Updated dependencies [8acc51b]
|
|
4152
|
+
- Updated dependencies [3261e64]
|
|
4153
|
+
- Updated dependencies [6c2f3c5]
|
|
4154
|
+
- Updated dependencies [9a5f998]
|
|
4155
|
+
- Updated dependencies [4758b33]
|
|
4156
|
+
- Updated dependencies [baac95a]
|
|
4157
|
+
- Updated dependencies [29b45f6]
|
|
4158
|
+
- Updated dependencies [39b8d51]
|
|
4159
|
+
- Updated dependencies [17b323e]
|
|
4160
|
+
- Updated dependencies [33e58d8]
|
|
4161
|
+
- Updated dependencies [256b4c9]
|
|
4162
|
+
- Updated dependencies [30b11ad]
|
|
4163
|
+
- Updated dependencies [3d6badf]
|
|
4164
|
+
- Updated dependencies [990a2d6]
|
|
4165
|
+
- Updated dependencies [6650259]
|
|
4166
|
+
- Updated dependencies [9e6619f]
|
|
4167
|
+
- Updated dependencies [b2683a2]
|
|
4168
|
+
- Updated dependencies [328abeb]
|
|
4169
|
+
- Updated dependencies [e227156]
|
|
4170
|
+
- Updated dependencies [6cd8f66]
|
|
4171
|
+
- Updated dependencies [63ab761]
|
|
4172
|
+
- Updated dependencies [ae582b7]
|
|
4173
|
+
- Updated dependencies [f61dab1]
|
|
4174
|
+
- Updated dependencies [81f8498]
|
|
4175
|
+
- Updated dependencies [a782fa7]
|
|
4176
|
+
- Updated dependencies [76e9df0]
|
|
4177
|
+
- Updated dependencies [6158e4c]
|
|
4178
|
+
- Updated dependencies [bf43afa]
|
|
4179
|
+
- Updated dependencies [858eafb]
|
|
4180
|
+
- Updated dependencies [58da8ae]
|
|
4181
|
+
- Updated dependencies [138ad45]
|
|
4182
|
+
- Updated dependencies [5638529]
|
|
4183
|
+
- Updated dependencies [c476be0]
|
|
4184
|
+
- Updated dependencies [063119f]
|
|
4185
|
+
- Updated dependencies [f9c8c4e]
|
|
4186
|
+
- Updated dependencies [83e3f83]
|
|
4187
|
+
- Updated dependencies [4abc0aa]
|
|
4188
|
+
- Updated dependencies [f560ded]
|
|
4189
|
+
- Updated dependencies [aea682a]
|
|
4190
|
+
- Updated dependencies [64dae8e]
|
|
4191
|
+
- Updated dependencies [9801765]
|
|
4192
|
+
- Updated dependencies [9cebfca]
|
|
4193
|
+
- Updated dependencies [053fdc8]
|
|
4194
|
+
- Updated dependencies [ae476b8]
|
|
4195
|
+
- Updated dependencies [490d9a9]
|
|
4196
|
+
- Updated dependencies [2c3cd1b]
|
|
4197
|
+
- Updated dependencies [546ddf7]
|
|
4198
|
+
- Updated dependencies [44d075b]
|
|
4199
|
+
- Updated dependencies [a26b9e4]
|
|
4200
|
+
- Updated dependencies [06b82b8]
|
|
4201
|
+
- Updated dependencies [8f1d995]
|
|
4202
|
+
- Updated dependencies [3c2b6f7]
|
|
4203
|
+
- Updated dependencies [5961030]
|
|
4204
|
+
- Updated dependencies [299102e]
|
|
4205
|
+
- Updated dependencies [831be72]
|
|
4206
|
+
- Updated dependencies [d0889e2]
|
|
4207
|
+
- Updated dependencies [4d5f9b4]
|
|
4208
|
+
- Updated dependencies [7977ff9]
|
|
4209
|
+
- Updated dependencies [3beef6d]
|
|
4210
|
+
- Updated dependencies [2acd8e1]
|
|
4211
|
+
- Updated dependencies [045d20b]
|
|
4212
|
+
- Updated dependencies [a2d2515]
|
|
4213
|
+
- Updated dependencies [3619792]
|
|
4214
|
+
- Updated dependencies [9e37d9b]
|
|
4215
|
+
- Updated dependencies [48c19bd]
|
|
4216
|
+
- Updated dependencies [a6d8b8d]
|
|
4217
|
+
- Updated dependencies [e75f4c9]
|
|
4218
|
+
- Updated dependencies [19f1639]
|
|
4219
|
+
- Updated dependencies [47547d0]
|
|
4220
|
+
- Updated dependencies [cfc9b6d]
|
|
4221
|
+
- Updated dependencies [81a2eb1]
|
|
4222
|
+
- Updated dependencies [00d2fa6]
|
|
4223
|
+
- Updated dependencies [c6198c2]
|
|
4224
|
+
- Updated dependencies [721d1e0]
|
|
4225
|
+
- Updated dependencies [1237ae4]
|
|
4226
|
+
- Updated dependencies [feac439]
|
|
4227
|
+
- Updated dependencies [e62c44e]
|
|
4228
|
+
- Updated dependencies [b041b9c]
|
|
4229
|
+
- Updated dependencies [ce2aaef]
|
|
4230
|
+
- Updated dependencies [1e215c4]
|
|
4231
|
+
- Updated dependencies [da6e191]
|
|
4232
|
+
- Updated dependencies [aef97e5]
|
|
4233
|
+
- Updated dependencies [52cac38]
|
|
4234
|
+
- Updated dependencies [93fea2e]
|
|
4235
|
+
- Updated dependencies [335abea]
|
|
4236
|
+
- Updated dependencies [1bd79c8]
|
|
4237
|
+
- Updated dependencies [af9e957]
|
|
4238
|
+
- Updated dependencies [0ea7054]
|
|
4239
|
+
- Updated dependencies [9a853f2]
|
|
4240
|
+
- Updated dependencies [fc32921]
|
|
4241
|
+
- Updated dependencies [8f9d87a]
|
|
4242
|
+
- Updated dependencies [846cec0]
|
|
4243
|
+
- Updated dependencies [91facae]
|
|
4244
|
+
- Updated dependencies [b38014e]
|
|
4245
|
+
- Updated dependencies [317dbce]
|
|
4246
|
+
- Updated dependencies [f76f436]
|
|
4247
|
+
- Updated dependencies [ce45a03]
|
|
4248
|
+
- Updated dependencies [abc1b18]
|
|
4249
|
+
- Updated dependencies [fb3a101]
|
|
4250
|
+
- Updated dependencies [f391ede]
|
|
4251
|
+
- Updated dependencies [f5cfbbd]
|
|
4252
|
+
- Updated dependencies [f5cfbbd]
|
|
4253
|
+
- Updated dependencies [64c3cdd]
|
|
4254
|
+
- Updated dependencies [4d65991]
|
|
4255
|
+
- Updated dependencies [0a174f3]
|
|
4256
|
+
- Updated dependencies [d1865d2]
|
|
4257
|
+
- Updated dependencies [561abef]
|
|
4258
|
+
- Updated dependencies [abf710d]
|
|
4259
|
+
- Updated dependencies [c372b29]
|
|
4260
|
+
- Updated dependencies [84defab]
|
|
4261
|
+
- Updated dependencies [681d3f1]
|
|
4262
|
+
- Updated dependencies [969d4f2]
|
|
4263
|
+
- Updated dependencies [f3bc481]
|
|
4264
|
+
- Updated dependencies [b79aac2]
|
|
4265
|
+
- Updated dependencies [19a0b0e]
|
|
4266
|
+
- Updated dependencies [f8e3e9a]
|
|
4267
|
+
- Updated dependencies [3b6bc69]
|
|
4268
|
+
- Updated dependencies [804831c]
|
|
4269
|
+
- Updated dependencies [da45e6b]
|
|
4270
|
+
- Updated dependencies [835f0f3]
|
|
4271
|
+
- Updated dependencies [ed35b44]
|
|
4272
|
+
- Updated dependencies [729e851]
|
|
4273
|
+
- Updated dependencies [20b507a]
|
|
4274
|
+
- Updated dependencies [72d6587]
|
|
4275
|
+
- Updated dependencies [ba0b61a]
|
|
4276
|
+
- Updated dependencies [ab856ed]
|
|
4277
|
+
- Updated dependencies [43c0d17]
|
|
4278
|
+
- Updated dependencies [f0f4d6c]
|
|
4279
|
+
- Updated dependencies [4a7ef0d]
|
|
4280
|
+
- Updated dependencies [276d174]
|
|
4281
|
+
- Updated dependencies [2982ed9]
|
|
4282
|
+
- Updated dependencies [05a49f2]
|
|
4283
|
+
- Updated dependencies [a78cd37]
|
|
4284
|
+
- Updated dependencies [5eabe86]
|
|
4285
|
+
- Updated dependencies [ff5ef1c]
|
|
4286
|
+
- Updated dependencies [81c0bc4]
|
|
4287
|
+
- Updated dependencies [60500cb]
|
|
4288
|
+
- Updated dependencies [6791717]
|
|
4289
|
+
- Updated dependencies [d7573b3]
|
|
4290
|
+
- Updated dependencies [bf3edfe]
|
|
4291
|
+
- Updated dependencies [5aed9e4]
|
|
4292
|
+
- Updated dependencies [83c77dc]
|
|
4293
|
+
- Updated dependencies [e719ebd]
|
|
4294
|
+
- Updated dependencies [516583b]
|
|
4295
|
+
- Updated dependencies [fa429cf]
|
|
4296
|
+
- Updated dependencies [8e74b27]
|
|
4297
|
+
- Updated dependencies [8ebd57f]
|
|
4298
|
+
- Updated dependencies [617707a]
|
|
4299
|
+
- Updated dependencies [7138bc1]
|
|
4300
|
+
- Updated dependencies [cef27e2]
|
|
4301
|
+
- Updated dependencies [06973aa]
|
|
4302
|
+
- Updated dependencies [50798f3]
|
|
4303
|
+
- Updated dependencies [105f3c5]
|
|
4304
|
+
- Updated dependencies [42df928]
|
|
4305
|
+
- Updated dependencies [af3861f]
|
|
4306
|
+
- Updated dependencies [1f4e029]
|
|
4307
|
+
- Updated dependencies [c00bf28]
|
|
4308
|
+
- Updated dependencies [f2158ec]
|
|
4309
|
+
- Updated dependencies [759606e]
|
|
4310
|
+
- Updated dependencies [a51fa0c]
|
|
4311
|
+
- Updated dependencies [6c6cee7]
|
|
4312
|
+
- Updated dependencies [83fe6e7]
|
|
4313
|
+
- Updated dependencies [d1ab06f]
|
|
4314
|
+
- Updated dependencies [91783c4]
|
|
4315
|
+
- Updated dependencies [ca39427]
|
|
4316
|
+
- Updated dependencies [2d36552]
|
|
4317
|
+
- Updated dependencies [ed71d9e]
|
|
4318
|
+
- Updated dependencies [7776fc2]
|
|
4319
|
+
- Updated dependencies [e76634c]
|
|
4320
|
+
- Updated dependencies [92814db]
|
|
4321
|
+
- @object-ui/core@17.7.0
|
|
4322
|
+
|
|
3
4323
|
## 17.6.0
|
|
4
4324
|
|
|
5
4325
|
### Minor Changes
|