@object-ui/i18n 17.5.0 → 17.6.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 +634 -0
- package/README.md +42 -0
- package/dist/i18n.d.ts +1 -1
- package/dist/i18n.js +1 -2
- package/dist/index.d.ts +23 -23
- package/dist/index.js +23 -23
- package/dist/locales/ar.d.ts +172 -262
- package/dist/locales/ar.js +187 -273
- package/dist/locales/de.d.ts +172 -262
- package/dist/locales/de.js +187 -273
- package/dist/locales/en.d.ts +173 -263
- package/dist/locales/en.js +223 -263
- package/dist/locales/es.d.ts +172 -262
- package/dist/locales/es.js +188 -274
- package/dist/locales/fr.d.ts +172 -262
- package/dist/locales/fr.js +187 -273
- package/dist/locales/index.d.ts +1740 -2640
- package/dist/locales/index.js +20 -20
- package/dist/locales/ja.d.ts +172 -262
- package/dist/locales/ja.js +189 -273
- package/dist/locales/ko.d.ts +172 -262
- package/dist/locales/ko.js +187 -273
- package/dist/locales/pt.d.ts +172 -262
- package/dist/locales/pt.js +187 -273
- package/dist/locales/ru.d.ts +172 -262
- package/dist/locales/ru.js +189 -273
- package/dist/locales/zh.d.ts +173 -263
- package/dist/locales/zh.js +180 -263
- package/dist/pickLocalized.d.ts +37 -0
- package/dist/pickLocalized.js +75 -0
- package/dist/provider.d.ts +1 -1
- package/dist/provider.js +2 -2
- package/dist/useDisplayLocale.js +2 -2
- package/dist/useObjectLabel.d.ts +86 -12
- package/dist/useObjectLabel.js +106 -13
- package/dist/useSafeTranslation.js +1 -1
- package/dist/utils/index.d.ts +4 -4
- package/dist/utils/index.js +4 -4
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,639 @@
|
|
|
1
1
|
# @object-ui/i18n
|
|
2
2
|
|
|
3
|
+
## 17.6.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- deb157a: Retire the field designer's `Indexed` toggle — the ObjectStack spec has no
|
|
8
|
+
field-level index flag
|
|
9
|
+
|
|
10
|
+
`indexed` was never a `FieldSchema` key. The field-level flag built no index
|
|
11
|
+
(objectstack#2377 removed it) and, since objectstack#4001 replaced silent
|
|
12
|
+
drops with loud rejection, `FieldSchema.safeParse` refuses it by name. Ticking
|
|
13
|
+
`Indexed` in Studio therefore made `PUT /api/v1/meta/object/:name` fail with
|
|
14
|
+
`422 INVALID_METADATA`, and — because the key was stored — every later save of
|
|
15
|
+
that object stayed blocked until the author found and cleared the toggle.
|
|
16
|
+
|
|
17
|
+
Both field designers stop offering the control and stop authoring the key
|
|
18
|
+
(`ObjectFieldInspector`'s Advanced section; `FieldDesigner`'s advanced
|
|
19
|
+
section, `MetadataFieldsPage`, `MetadataService`, `metadataConverters`), the
|
|
20
|
+
`designer.field.indexed` / `appDesigner.fieldDesigner.indexed` labels retire
|
|
21
|
+
with it across all ten locale packs, and `DesignerFieldDefinition.indexed` is
|
|
22
|
+
removed from `@object-ui/types`.
|
|
23
|
+
|
|
24
|
+
Drafts and objects that already carry the key are un-poisoned on load rather
|
|
25
|
+
than migrated, so an edit-and-save round-trip of previously blocked metadata
|
|
26
|
+
now succeeds. The strip is keyed to the retired key alone — every other
|
|
27
|
+
unknown key on a field definition still survives the round-trip.
|
|
28
|
+
|
|
29
|
+
Declare indexes on the object instead: `indexes: [{ name, fields, unique }]`.
|
|
30
|
+
- 9c60144: **Breaking (published API):** `NavigationRenderer` no longer accepts `resolveGroupLabel` or `resolveItemLabel`. If your build just broke on one of these props, delete the prop — it never did anything.
|
|
31
|
+
|
|
32
|
+
Both were id-keyed label resolvers on `NavigationRendererProps` (`@object-ui/layout`), typically wired to `useObjectLabel().navGroupLabel` from `@object-ui/i18n` to translate sidebar group and leaf labels from a client translation pack keyed `{ns}.apps.{appName}.navigation.{nodeId}.label`.
|
|
33
|
+
|
|
34
|
+
**They could never fire.** The renderer guards convention-based resolution with `isCustomized` — a case-insensitive "the authored label differs from this branch's comparison target" test. On the `object` / `dashboard` branches the target is the object or dashboard name, and the test is meaningful: it protects an author's custom label (`Projects`) from being overwritten by an `objects.project.label` translation. On the two id-keyed branches the target was the nav node's own **`id`** — `grp_workspace`, `group_sales` — while the label was its text — `Workspace`, `Sales`. Those never compare equal, so the guard was true for every real navigation entry and the resolver beneath it was unreachable. The only node that could have reached it is one whose label is literally its own id.
|
|
35
|
+
|
|
36
|
+
Nothing regresses when they go, because nothing was using them to begin with: **app-navigation localization is owned solely by the server-side `/meta` boundary.** `translateApp` (`@objectstack/spec`, `src/system/i18n-resolver.ts`) rewrites every navigation node's `label` by id, and `@objectstack/rest` applies it before the metadata reaches the client — so nav labels arrive already localized and the client-side path was never the one answering. One owner, not two.
|
|
37
|
+
|
|
38
|
+
**If you wired these hooks to localize navigation, your labels were never being localized by them.** Move the translation to the server side: add it to the app's i18n bundle that `translateApp` reads, keyed by navigation node id. A client-side pack keyed `apps.*.navigation.*.label` changes nothing in the sidebar.
|
|
39
|
+
|
|
40
|
+
Also in this change:
|
|
41
|
+
|
|
42
|
+
- `useObjectLabel().navGroupLabel` (`@object-ui/i18n`) is **kept**, but its docstring no longer promises `"Sales" → "销售"` for sidebar groups — that promise was false, and a live docstring describing an unreachable path is how an agent or a developer ends up wiring a translation pack and receiving silent non-localization. It is now documented for what it is: a plain reader for `{ns}.apps.{appName}.navigation.{groupId}.label` with no first-party caller, pointing at the server boundary for anything nav-related.
|
|
43
|
+
- `UnifiedSidebar` (`@object-ui/app-shell`) drops the two prop wirings, including a `studio` carve-out that existed only to stop `resolveItemLabel` from re-translating an already-translated "Package management" label.
|
|
44
|
+
- The `object` / `dashboard` / `viewName` branches — `resolveObjectLabel`, `resolveDashboardLabel`, `resolveViewLabel` — are untouched and keep both their resolvers and the `isCustomized` guard.
|
|
45
|
+
- b8ce7dc: Show the current organization in the console top bar for users with exactly one
|
|
46
|
+
membership.
|
|
47
|
+
|
|
48
|
+
`WorkspaceSwitcher` is a multi-membership affordance — with one organization
|
|
49
|
+
there is nothing to switch to, so it renders nothing — and it was also the only
|
|
50
|
+
place the console ever displayed the organization name. On a deployment whose
|
|
51
|
+
tenancy posture puts an organization wall in force (`isolated` or `group`), that
|
|
52
|
+
left a single-membership business user with no indication anywhere of which
|
|
53
|
+
organization they were looking at, while every list on the page was silently
|
|
54
|
+
scoped to it. Found in a downstream multi-tenant acceptance run.
|
|
55
|
+
|
|
56
|
+
The new `CurrentOrganizationIndicator` renders the active organization name
|
|
57
|
+
read-only — no trigger, no menu, no click target — in exactly the case the
|
|
58
|
+
switcher declines. The switcher's own visibility rule is unchanged, and
|
|
59
|
+
deployments without an organization wall (`single` posture, or a server that
|
|
60
|
+
reports no posture at all) render nothing new: the top bar stays
|
|
61
|
+
organization-silent where organizations are not a scope the user is inside.
|
|
62
|
+
|
|
63
|
+
Adds one translated string, `organization.current.label`, in all ten packs.
|
|
64
|
+
|
|
65
|
+
### Patch Changes
|
|
66
|
+
|
|
67
|
+
- 69251bf: `AddressField` is translatable, shows no US example placeholders, and formats its readonly line in the reader's address order.
|
|
68
|
+
|
|
69
|
+
The five sub-labels ("Street Address", "City", "State / Province", "ZIP /
|
|
70
|
+
Postal Code", "Country") were English string literals with no i18n key. On a
|
|
71
|
+
non-English console every address field showed five English words in the middle
|
|
72
|
+
of an otherwise fully translated form, and an app had no way to reach them: the
|
|
73
|
+
parts are not fields on the object (`billing_address` is a single `address`
|
|
74
|
+
column), so a translation bundle had nothing to key on, there is no `subLabels`
|
|
75
|
+
property to declare, and the widget cannot be replaced from metadata. They now
|
|
76
|
+
resolve through `fields.address.street` / `.city` / `.state` / `.postalCode` /
|
|
77
|
+
`.country`, added to all ten locale packs. The `en` values are byte-identical to
|
|
78
|
+
the literals they replace, and `FIELD_DEFAULTS` carries the same five, so
|
|
79
|
+
English and provider-less rendering are unchanged.
|
|
80
|
+
|
|
81
|
+
The five input placeholders (`123 Main St`, `San Francisco`, `CA`, `94102`,
|
|
82
|
+
`United States`) are **removed** rather than keyed. They were untranslated and
|
|
83
|
+
US-specific — a zh/ja/ar user was shown an American address as the example of
|
|
84
|
+
what to type — and the right example is a function of the address's country,
|
|
85
|
+
not the reader's language, which no channel in the stored value can supply
|
|
86
|
+
today. Each box keeps the visible label that names it.
|
|
87
|
+
|
|
88
|
+
The readonly line's part order now follows the reader's display locale
|
|
89
|
+
(`useDisplayLocale()`): `zh`, `ja` and `ko` read largest-first (`Country, ZIP
|
|
90
|
+
State, City, Street`), every other locale keeps the unchanged small-to-large
|
|
91
|
+
order (`Street, City, State ZIP, Country`). The display cell renderer takes the
|
|
92
|
+
same locale through the same shared `formatAddress`, so a stored address reads
|
|
93
|
+
identically in a readonly form and in a grid cell.
|
|
94
|
+
- 57e668f: An admin override of an approval now looks like one — before the click, and in the timeline afterwards.
|
|
95
|
+
|
|
96
|
+
A platform/tenant admin who holds **no slot** in a request's pending-approver
|
|
97
|
+
slate saw the exact same filled **Approve** / **Reject** / **Reassign** a
|
|
98
|
+
designated approver sees: no distinct styling, no warning, nothing. One unmarked
|
|
99
|
+
click takes framework#3424's privileged branch, which is *authoritative* — it
|
|
100
|
+
finalises the node even under `per_group` / `unanimous` / `quorum`, silently
|
|
101
|
+
bypassing every co-sign group that has not voted. The measured consequence in a
|
|
102
|
+
real app project was a PM who clicked Approve *"to see if it was real"* and
|
|
103
|
+
finalised a stage for an approver who never acted, then filed it as "countersign
|
|
104
|
+
is broken". The backend was working as designed; the console gave the admin no
|
|
105
|
+
way to see that.
|
|
106
|
+
|
|
107
|
+
Two halves, both client-side:
|
|
108
|
+
|
|
109
|
+
- **Affordance.** A viewer the server reports as `can_act: false` **and**
|
|
110
|
+
`can_override: true` now gets a warning-styled action labelled as an override
|
|
111
|
+
(`Override Approve`), and the dialog it opens **names the pending approvers
|
|
112
|
+
being bypassed**. The wording is the fix, not decoration — a warning that does
|
|
113
|
+
not say what is about to be bypassed would not have stopped that click.
|
|
114
|
+
- **Timeline.** `sys_approval_action.via_override` has been written since
|
|
115
|
+
framework#4466 and sent on the wire ever since, but **no console surface read
|
|
116
|
+
it** — an override rendered byte-for-byte like an ordinary approval in both the
|
|
117
|
+
record page's approvals panel and the Approval Center. Override rows are now
|
|
118
|
+
marked with an `Admin override` chip and a distinct timeline dot. This is
|
|
119
|
+
framework#4466's own *Expected* ("surfaced in the timeline"), which never
|
|
120
|
+
landed.
|
|
121
|
+
|
|
122
|
+
Nothing here relaxes anything. Who may act or override is unchanged, the request
|
|
123
|
+
the console sends is byte-identical, and no audit record is altered — this only
|
|
124
|
+
renders one that was already being written, and adds friction in front of a
|
|
125
|
+
privileged path.
|
|
126
|
+
|
|
127
|
+
Two details worth knowing, because both are load-bearing:
|
|
128
|
+
|
|
129
|
+
- The warning rides the **param dialog's** title and description rather than a
|
|
130
|
+
chained `confirmText`. These decision actions collect params, so the param
|
|
131
|
+
dialog is already the confirm — nothing is POSTed until its own Confirm — and
|
|
132
|
+
putting a second dialog in front of it produces a first prompt that reads as
|
|
133
|
+
"the action ran" (framework#7278, maintainer ruling 2026-08-10). One condition,
|
|
134
|
+
one wording, one dialog.
|
|
135
|
+
- The notice travels as its own dispatch key, **not** folded into the action's
|
|
136
|
+
`description`. The runtime resolves `description` through
|
|
137
|
+
`_actions.<name>.description`, preferring a bundle hit over the passed literal,
|
|
138
|
+
and `plugin-approvals` ships exactly such an entry for `approval_reject` — so a
|
|
139
|
+
warning routed that way would have been silently replaced by the ordinary
|
|
140
|
+
reject copy in every locale carrying the bundle. A safety notice a translation
|
|
141
|
+
can delete is not a safety notice.
|
|
142
|
+
|
|
143
|
+
Which actions get the treatment is read from each action's **own declared
|
|
144
|
+
`visible` gate** (does it OR in `can_override`?), not from a hard-coded name
|
|
145
|
+
list, so a future decision action still ships as metadata alone — and
|
|
146
|
+
`approval_recall`, gated on `is_submitter`, is never relabelled. Every new string
|
|
147
|
+
goes through the `approvalsInbox.*` i18n path in all ten locale packs.
|
|
148
|
+
- 516663d: RecordAttachmentsPanel no longer offers a Retry for an api-disabled `sys_attachment` read.
|
|
149
|
+
|
|
150
|
+
`OBJECT_API_DISABLED` (404, `enable.apiEnabled: false`) and its sibling
|
|
151
|
+
`OBJECT_API_METHOD_NOT_ALLOWED` (405, the operation is absent from
|
|
152
|
+
`enable.apiMethods`) are pure functions of the object's metadata — no user, no
|
|
153
|
+
session, no request body — so every retry of every persona re-fetches the
|
|
154
|
+
identical refusal. Before this change both landed in `RecordAttachmentsPanel`'s
|
|
155
|
+
`unavailable` state and offered a Retry that was guaranteed to change nothing,
|
|
156
|
+
the same wrong advice `ListView`'s error panel already stops giving for list
|
|
157
|
+
reads.
|
|
158
|
+
|
|
159
|
+
The panel gains a fifth status, `api-unavailable`: no Retry button, and honest
|
|
160
|
+
copy ("The attachments list is not available on this object.", new
|
|
161
|
+
`detail.attachmentsApiUnavailable` key in all ten locale packs) instead of
|
|
162
|
+
"We couldn't load the attachments for this record." The pre-existing `denied`
|
|
163
|
+
(authorization) and `unavailable` (network/5xx/expired-session) states and
|
|
164
|
+
their affordances are unchanged.
|
|
165
|
+
|
|
166
|
+
`ListView.classifyLoadError` — the classifier that already separated this case
|
|
167
|
+
into its own `api-disabled` kind for list views — is lifted out of
|
|
168
|
+
`packages/plugin-list/src/ListView.tsx`'s module scope into
|
|
169
|
+
`@object-ui/react` (`classifyLoadError`, `LoadErrorKind`), so both surfaces
|
|
170
|
+
consume one classification instead of `RecordAttachmentsPanel` re-deriving it.
|
|
171
|
+
`ListView`'s own behavior is unchanged — it now imports the function it
|
|
172
|
+
previously defined locally. The classifier delegates its api-disabled check to
|
|
173
|
+
`isApiAccessDeniedError` (`@object-ui/data-objectstack`), removing a second,
|
|
174
|
+
independently-maintained copy of the same code list.
|
|
175
|
+
- 41ac1b7: RecordAttachmentsPanel distinguishes a DENIED attachment list from an empty one.
|
|
176
|
+
|
|
177
|
+
A `sys_attachment` list read refused for authorization reasons (HTTP 403 /
|
|
178
|
+
`PERMISSION_DENIED` / `FORBIDDEN` / a row-level-security denial) was swallowed
|
|
179
|
+
into the panel's empty state, so a member denied the parent record was told
|
|
180
|
+
"No attachments yet. Upload a file to get started." about a record holding
|
|
181
|
+
2095+ attachments — and was offered an Upload the server would refuse.
|
|
182
|
+
|
|
183
|
+
The panel now classifies that refusal with the same `isPermissionError`
|
|
184
|
+
predicate the kanban, calendar and form surfaces branch on, renders a distinct
|
|
185
|
+
denied state ("You don't have access to these attachments.", new
|
|
186
|
+
`detail.attachmentsAccessDenied` key in all ten locale packs), and withdraws
|
|
187
|
+
the Upload affordance. The denied state renders the translated sentence and
|
|
188
|
+
nothing sourced from the error — no status code, no server text, no row count.
|
|
189
|
+
The empty state is now reserved for a genuine 200-with-zero-rows; non-authz
|
|
190
|
+
failures keep their pre-existing handling.
|
|
191
|
+
- 1eaf0a1: RecordAttachmentsPanel distinguishes an UNLOADED attachment list from an empty one.
|
|
192
|
+
|
|
193
|
+
A `sys_attachment` list read that failed for any non-authorization reason — a
|
|
194
|
+
network failure (server unreachable, DNS, aborted request), a 5xx, or a 401 /
|
|
195
|
+
`AUTH_REQUIRED` (an expired session is authentication, not authorization, so the
|
|
196
|
+
denied predicate deliberately does not claim it) — was swallowed into the
|
|
197
|
+
panel's empty state. All three rendered "No attachments yet. Upload a file to
|
|
198
|
+
get started.": an affirmative claim about the record's contents, made by a panel
|
|
199
|
+
that never got an answer, over a record that may hold thousands of files.
|
|
200
|
+
|
|
201
|
+
The panel now carries the same four-way status vocabulary its siblings use —
|
|
202
|
+
`loading` / `loaded` / `denied` / `unavailable` — and every state that means
|
|
203
|
+
"the panel does not know" is answered before `rows.length === 0` is allowed to
|
|
204
|
+
mean "the record holds nothing". A failed read renders a distinct unavailable
|
|
205
|
+
state ("We couldn't load the attachments for this record.", new
|
|
206
|
+
`detail.attachmentsLoadFailed` and `detail.retryLoadAttachments` keys in all ten
|
|
207
|
+
locale packs) with a **retry** — unlike the denied state, an outage and a lapsed
|
|
208
|
+
session are both things a second attempt can fix — and withdraws the Upload
|
|
209
|
+
affordance, because offering an upload against a list the panel could not reach
|
|
210
|
+
is the same over-assertion as the empty state it replaces. Like the denied
|
|
211
|
+
state, it renders the translated sentence and nothing sourced from the error: no
|
|
212
|
+
status code, no server message, no host.
|
|
213
|
+
|
|
214
|
+
The empty state is now reserved for a genuine 200-with-zero-rows, and the
|
|
215
|
+
denied state (403 / `PERMISSION_DENIED` / `FORBIDDEN` / a row-level-security
|
|
216
|
+
denial) is unchanged. The "table not provisioned on an older stack" case is
|
|
217
|
+
unaffected: the ObjectStack adapter degrades a bare 404 to `{ data: [], total: 0 }`,
|
|
218
|
+
so it resolves through the success path and still renders the empty state.
|
|
219
|
+
|
|
220
|
+
This restores a house rule that had already landed twice as a bug fix —
|
|
221
|
+
`HomeActionCenter` may only say "You're all caught up" once the inbox has
|
|
222
|
+
answered, and an unloadable app list is UNKNOWN rather than "no default app".
|
|
223
|
+
- 2e82ab2: The config panel footer translates: `ConfigPanelRenderer`'s Save / Discard labels come from the locale pack.
|
|
224
|
+
|
|
225
|
+
`saveLabel` and `discardLabel` carried the English literals `'Save'` and
|
|
226
|
+
`'Discard'` as parameter defaults, and no caller in the repo passes either prop,
|
|
227
|
+
so the sticky footer that appears the moment a config draft is dirty stayed
|
|
228
|
+
English in every locale — inside panels whose every other string had already
|
|
229
|
+
been routed through `t()`. The fix is in the renderer rather than per-caller:
|
|
230
|
+
the footer is the renderer's own chrome, so a caller-side fix would translate
|
|
231
|
+
one panel's footer and leave the next host's English.
|
|
232
|
+
|
|
233
|
+
Both labels now resolve through `createSafeTranslation` — the mechanism this
|
|
234
|
+
package already uses for its built-in copy in `form.tsx`,
|
|
235
|
+
`fullscreen-editor.tsx`, `data-table.tsx` and friends. An explicitly passed
|
|
236
|
+
`saveLabel` / `discardLabel` still wins, unchanged and untranslated.
|
|
237
|
+
|
|
238
|
+
`common.save` is reused rather than twinned: it already ships `Save` in all ten
|
|
239
|
+
packs and is what the console's other save buttons read. `common.discard` is
|
|
240
|
+
new, because the packs carried no shared spelling of the word — the three that
|
|
241
|
+
existed are each scoped to one surface (`form.discard`,
|
|
242
|
+
`console.settingsView.discard`, `console.objectView.discard`) and the last of
|
|
243
|
+
them diverges from the other two in zh/ko/fr. Its ten values are the majority
|
|
244
|
+
spelling, byte-identical to `form.discard` and `console.settingsView.discard`.
|
|
245
|
+
|
|
246
|
+
Both English defaults are byte-identical to the literals they replace, so a
|
|
247
|
+
host that mounts no `I18nProvider` renders exactly what it did before.
|
|
248
|
+
- ef0d150: The dashboard config sidebar translates: `WidgetConfigPanel` and `DashboardConfigPanel` are wired through `t()`.
|
|
249
|
+
|
|
250
|
+
Both panels build a `ConfigPanelSchema` — breadcrumb, section titles, field
|
|
251
|
+
labels, placeholders, help text and option labels — and neither imported a
|
|
252
|
+
translation hook at all, so all 61 of their user-visible strings were English
|
|
253
|
+
literals. Both are exported from the package barrel and mounted by
|
|
254
|
+
`DashboardWithConfig` as the dashboard editing sidebar, so a user on any
|
|
255
|
+
non-English console opened a panel that stayed English inside chrome that had
|
|
256
|
+
translated around it.
|
|
257
|
+
|
|
258
|
+
They now resolve through a new `dashboard.config.*` namespace — 75 keys, added
|
|
259
|
+
to all ten locale packs. The namespace sits beside `dashboard.trend.*` and
|
|
260
|
+
`dashboard.filters.*`, which is where this package's other translated surfaces
|
|
261
|
+
already read from, and the panels reach it through
|
|
262
|
+
`useConfigPanelTranslation`, a `createSafeTranslation` hook whose
|
|
263
|
+
`CONFIG_PANEL_DEFAULT_TRANSLATIONS` map carries the English defaults for hosts
|
|
264
|
+
that mount no `I18nProvider`.
|
|
265
|
+
|
|
266
|
+
The keys are authored fresh against the wording the panels actually ship rather
|
|
267
|
+
than restored from the retired `configPanel.*` block: that vocabulary had no
|
|
268
|
+
reader, was never validated against a shipped label, and covered 16 of the 61
|
|
269
|
+
strings. Where the two name the same word the translations are reused.
|
|
270
|
+
|
|
271
|
+
Every `en` pack value and every built-in default is byte-identical to the
|
|
272
|
+
literal it replaces, so English rendering and provider-less rendering are
|
|
273
|
+
unchanged — asserted row by row, in both directions, against a frozen table of
|
|
274
|
+
the pre-change literals.
|
|
275
|
+
- f34226e: Delete two dead i18n namespaces — `configPanel.*` (16 keys) and `renderer.*` (13 keys) — from all ten locale packs.
|
|
276
|
+
|
|
277
|
+
290 translated strings (29 keys x 10 packs) with **no reader anywhere in the
|
|
278
|
+
repo**. Measured with `node scripts/check-i18n-dead-keys.mjs`, the reverse sweep
|
|
279
|
+
from objectui#4658: it subtracts the referenced key set (the AST walk that
|
|
280
|
+
`check-i18n-call-site-keys.mjs` already uses, plus plural suffixes, plus
|
|
281
|
+
`returnObjects` branches, plus every dynamic template head) from the pack key
|
|
282
|
+
set, then re-checks each survivor with a fixed-string grep over the whole repo.
|
|
283
|
+
Both namespaces scored a clean sweep — `configPanel.*` 16/16 CONFIRMED and
|
|
284
|
+
`renderer.*` 13/13 CONFIRMED, zero NEEDS-REVIEW, and a fixed-string grep for all
|
|
285
|
+
29 keys returns nothing at all outside `packages/i18n/src/locales/`.
|
|
286
|
+
|
|
287
|
+
`configPanel.*` is a dashboard-widget config-panel vocabulary (`layout`,
|
|
288
|
+
`columns`, `gap`, `rowHeight`, `refreshInterval`, `appearance`, `theme`,
|
|
289
|
+
`general`/`advanced`, …). The two components that surface exactly that
|
|
290
|
+
vocabulary — `packages/plugin-dashboard/src/WidgetConfigPanel.tsx` and
|
|
291
|
+
`DashboardConfigPanel.tsx` — import no translation hook at all and hardcode the
|
|
292
|
+
same words in English (`title: 'Layout'`, `label: 'Columns'`, `label: 'Gap'`,
|
|
293
|
+
`label: 'Show description'`, `label: 'Theme'`). The two config panels that do
|
|
294
|
+
use i18n read other namespaces: `ViewConfigPanel.tsx` reads
|
|
295
|
+
`console.objectView.*`, `ReportConfigPanel.tsx` reads `common.*` and
|
|
296
|
+
`report.editor.*`. No component reads `configPanel.*`.
|
|
297
|
+
|
|
298
|
+
`renderer.*` is SchemaRenderer placeholder/status vocabulary (`noPageSchema`,
|
|
299
|
+
`noFormSchema`, `noDashboardSchema`, `pageRendering`, `dashboardRendering`,
|
|
300
|
+
`formRenderingMode`, …). It is dead in the stronger sense: the English strings
|
|
301
|
+
have no hardcoded twin either — `No page schema provided`, `No form schema
|
|
302
|
+
provided`, `No dashboard schema provided`, `Page rendering`, `Dashboard
|
|
303
|
+
rendering` and `Form rendering in` each return zero hits repo-wide outside the
|
|
304
|
+
packs, and `packages/react/src/SchemaRenderer.tsx` imports no translation hook.
|
|
305
|
+
The messages themselves are gone from the product, not merely un-translated.
|
|
306
|
+
|
|
307
|
+
No behaviour change is possible: a key with no reader cannot be read. No test
|
|
308
|
+
fixture pinned any of the 29 keys, and neither i18n baseline JSON names one, so
|
|
309
|
+
nothing else had to move.
|
|
310
|
+
|
|
311
|
+
Part of #4730.
|
|
312
|
+
- 564b605: Delete two dead i18n namespaces — `workflow.*` (58 keys) and `publicForm.demo.*` (36 keys) — from all ten locale packs.
|
|
313
|
+
|
|
314
|
+
940 translated strings (94 keys x 10 packs) with **no reader anywhere in the
|
|
315
|
+
repo**. Measured with `node scripts/check-i18n-dead-keys.mjs`, the reverse sweep
|
|
316
|
+
from objectui#4658: it subtracts the referenced key set (the AST walk that
|
|
317
|
+
`check-i18n-call-site-keys.mjs` already uses, plus plural suffixes, plus
|
|
318
|
+
`returnObjects` branches, plus every dynamic template head) from the pack key
|
|
319
|
+
set, then re-checks each survivor with a fixed-string grep over the whole repo.
|
|
320
|
+
`workflow.*` scored 54/58 CONFIRMED with the other 4 appearing only in two i18n
|
|
321
|
+
test fixtures — mentions, not consumers; `publicForm.demo.*` scored 36/36 with
|
|
322
|
+
zero textual footprint anywhere outside the packs.
|
|
323
|
+
|
|
324
|
+
`workflow.*` is a complete BPMN/workflow-designer vocabulary (`userTask`,
|
|
325
|
+
`serviceTask`, `parallelGateway`, `boundaryEvent`, `importBpmn`/`exportBpmn`,
|
|
326
|
+
`undo`/`redo`, …). Its one plausible consumer,
|
|
327
|
+
`packages/plugin-designer/src/ProcessDesigner.tsx`, hardcodes English
|
|
328
|
+
(`{ label: 'User Task', value: 'user-task' }`) and imports no translation hook
|
|
329
|
+
at all, while six sibling components in the same package do use one — the
|
|
330
|
+
vocabulary was never wired up. `publicForm.demo.*` is demo content (contact and
|
|
331
|
+
support form titles, field labels, industry/issue-type/priority options) for a
|
|
332
|
+
public-form showcase page that does not exist in this repo.
|
|
333
|
+
|
|
334
|
+
No behaviour changes: a key with no reader cannot be read. The `publicForm.*`
|
|
335
|
+
parent namespace and every other namespace are untouched.
|
|
336
|
+
- a1609a6: Console list filters: a `between` range is submitted only when both bounds are filled, and six operator labels stop rendering as raw i18n keys.
|
|
337
|
+
|
|
338
|
+
Two defects in the list-view filter panel (objectstack#8815), both in the Console
|
|
339
|
+
render layer, with no workaround available downstream.
|
|
340
|
+
|
|
341
|
+
**A half-filled range no longer refuses the whole view.** Picking a date column
|
|
342
|
+
and 「介于」 draws two inputs — that part landed in objectui#3958 — but typing
|
|
343
|
+
only one bound produced `["2024-01-01", ""]`, and both write paths read "is this
|
|
344
|
+
row filled in?" with one shape-blind predicate (`null` / `''` / empty array).
|
|
345
|
+
An array of length 2 passed it, so the empty bound went to the server, which
|
|
346
|
+
refuses the query outright (`400 INVALID_FILTER`): the list showed
|
|
347
|
+
「该视图的查询被拒绝」 and the filters the user had already applied stopped
|
|
348
|
+
applying too. The saved-view fold persisted the same half-range, so the refusal
|
|
349
|
+
came back on every later read of that view, for every user of it.
|
|
350
|
+
|
|
351
|
+
The spec cannot intercept this — `ViewFilterRuleSchema` accepts
|
|
352
|
+
`["2024-01-01", ""]` because it counts the two slots rather than what is in
|
|
353
|
+
them, while refusing a scalar or a one-element array. Authoring validation is
|
|
354
|
+
therefore green on exactly the shape that fails at query time, which makes not
|
|
355
|
+
emitting it the producer's job. `@object-ui/components` now exports
|
|
356
|
+
`isFilterValueComplete(operator, value)` — arity-aware, so a `pair` row needs
|
|
357
|
+
both bounds — and the two consumers that had each kept a copy of the old
|
|
358
|
+
predicate (`plugin-list`'s `convertFilterGroupToAST`, `app-shell`'s
|
|
359
|
+
`foldFilterGroupToSpecRules`) read it instead. A half-filled range is now
|
|
360
|
+
dropped exactly as a half-typed `equals` row already was: no filter, rather than
|
|
361
|
+
a filter the server will reject. Bounds of `0` and `false` stay real bounds.
|
|
362
|
+
|
|
363
|
+
**Six operator labels are translated in all ten locale packs.**
|
|
364
|
+
`startsWith`, `endsWith`, `isNull`, `isNotNull`, `exists` and `notExists` were
|
|
365
|
+
missing from every pack, so i18next resolved them to the raw key and the dropdown
|
|
366
|
+
showed `filterBuilder.operators.isNull` beside translated entries. The
|
|
367
|
+
component's own defaults table could not cover it: that table serves only the
|
|
368
|
+
no-provider path, and the Console mounts a provider. The report named four —
|
|
369
|
+
a `date` column's bucket offers the four nullness operators; a `text` column
|
|
370
|
+
showed all six.
|
|
371
|
+
|
|
372
|
+
Because the label key is built dynamically (`t(\`filterBuilder.operators.${op}\`)`),
|
|
373
|
+
no existing gate could see the gap: the call-site checker classifies a template
|
|
374
|
+
key as `missing-prefix` and only asks whether the prefix resolves, and
|
|
375
|
+
cross-pack parity is satisfied when all ten packs are missing a key together.
|
|
376
|
+
A new parity test pins the packs against `FILTER_BUILDER_OPERATORS` in both
|
|
377
|
+
directions, so an operator added to the dropdown now fails loudly until every
|
|
378
|
+
pack labels it.
|
|
379
|
+
- 37f6844: FilterConditionField can author the spec's `$icontains` — case-insensitive contains is reachable from the filter UI.
|
|
380
|
+
|
|
381
|
+
`@objectstack/spec`'s `FieldOperatorsSchema` gained `$icontains` between
|
|
382
|
+
`17.0.0-rc.2` and `rc.5`, and every driver and evaluation face the platform
|
|
383
|
+
ships now executes it. `FilterConditionField` had no builder operator that could
|
|
384
|
+
author it, so the capability was unreachable from the sharing-rule criteria
|
|
385
|
+
builder and sat in that widget's parity test as an explicit `KNOWN_UNREACHABLE`
|
|
386
|
+
entry.
|
|
387
|
+
|
|
388
|
+
The FilterBuilder gains a `containsCaseInsensitive` operator ("Contains (ignore
|
|
389
|
+
case)", translated in all ten locale packs). `condToMongo` emits
|
|
390
|
+
`{ field: { $icontains: value } }` and `kvToCondition` reads it back, so a saved
|
|
391
|
+
criteria reopens in the visual builder instead of falling into the raw-JSON
|
|
392
|
+
editor. Today's `contains` is unchanged and still emits the case-SENSITIVE
|
|
393
|
+
`$contains`; whether it should have been case-insensitive all along is a product
|
|
394
|
+
question that stays open, and stored filter views keep meaning what they meant.
|
|
395
|
+
|
|
396
|
+
The fold is ASCII-only by contract — `café` does not match `CAFÉ`.
|
|
397
|
+
|
|
398
|
+
The new operator is **opt-in per consumer**: `FilterBuilder` takes an
|
|
399
|
+
`extraOperators` prop, and only `FilterConditionField` passes it. The one
|
|
400
|
+
dropdown feeds three at-rest dialects and only the MongoDB-style criteria this
|
|
401
|
+
widget writes can carry the operator — the spec's `VIEW_FILTER_OPERATORS` (saved
|
|
402
|
+
views) and `VALID_AST_OPERATORS` (the live grid's filter AST) have no
|
|
403
|
+
case-insensitive contains, so offering it there would author a filter those
|
|
404
|
+
paths cannot execute. Every other FilterBuilder is unchanged.
|
|
405
|
+
- 2b50261: `FilterBuilder` gives the set and range operators an input that matches the value shape the spec accepts, and stops minting the shape it refuses.
|
|
406
|
+
|
|
407
|
+
Three independent paths let one filter row end up with `operator: 'in'` and a
|
|
408
|
+
SCALAR `value` — the shape `ViewFilterRuleSchema` refuses at save time since
|
|
409
|
+
objectstack#6227, and the shape the query path answered `400 INVALID_FILTER` on
|
|
410
|
+
before that (objectstack#5869):
|
|
411
|
+
|
|
412
|
+
- Changing the operator dropdown wrote `{ operator }` alone, so the seed `''`
|
|
413
|
+
(or whatever the previous family had produced) survived the switch into
|
|
414
|
+
`in` / `not_in` / `between`. The operator and the shape of its value are one
|
|
415
|
+
edit, so they are now made together: switching families re-shapes the value —
|
|
416
|
+
a typed scalar becomes a one-element list, an empty one becomes `[]`, a range
|
|
417
|
+
keeps its first bound and leaves the second open, and a list collapsing to a
|
|
418
|
+
scalar keeps its first entry.
|
|
419
|
+
- A plain text or number column has no static `options`, so `in` fell through to
|
|
420
|
+
the single-value input and the user could only ever type a scalar into it.
|
|
421
|
+
Those columns now get a token input (type, Enter or comma commits, `×` or
|
|
422
|
+
Backspace removes) that always emits an array; `between` gets its two bounds
|
|
423
|
+
instead of one box. The lookup picker's no-DataSource fallback, which also
|
|
424
|
+
handed back a scalar while `multiple`, emits a list too.
|
|
425
|
+
- The multi-value families were decided from a local `["in", "notIn"]` literal,
|
|
426
|
+
already one spelling adrift: `notIn` is an alias and the canonical member is
|
|
427
|
+
`not_in`, so a stored view read back in canonical form got the single-value
|
|
428
|
+
input for a set operator. The families are now read from `@objectstack/spec`'s
|
|
429
|
+
exported `VIEW_FILTER_LIST_VALUE_OPERATORS` / `VIEW_FILTER_PAIR_VALUE_OPERATORS`
|
|
430
|
+
and folded through `normalizeFilterOperator`, so both spellings of one operator
|
|
431
|
+
get one answer and a family the spec widens is picked up without an edit here.
|
|
432
|
+
|
|
433
|
+
`foldFilterGroupToSpecRules` is unchanged and needed no change: it normalizes the
|
|
434
|
+
operator and carries `value` through verbatim, so the shape that reaches storage
|
|
435
|
+
is the producer's to get right. An untouched `in` row arrives as `[]`, which the
|
|
436
|
+
fold's existing incomplete-row rule already drops.
|
|
437
|
+
|
|
438
|
+
Four locale keys are added to all ten packs for the new inputs
|
|
439
|
+
(`filterBuilder.addValue` / `.removeValue` / `.rangeStart` / `.rangeEnd`).
|
|
440
|
+
- af5e292: Emit explicit file extensions on relative import specifiers, so the published
|
|
441
|
+
entries can be imported by Node's own ESM resolver.
|
|
442
|
+
|
|
443
|
+
`@object-ui/react`'s built entry re-exported through extensionless relative
|
|
444
|
+
specifiers (`export * from './SchemaRenderer'`). Node does not extension-search
|
|
445
|
+
relative specifiers, so `import('@object-ui/react')` under plain Node — an SSR
|
|
446
|
+
host, or any consumer without a bundler — failed with `ERR_MODULE_NOT_FOUND`.
|
|
447
|
+
Bundled consumers were never affected and are unchanged by this.
|
|
448
|
+
|
|
449
|
+
`@object-ui/types`, `@object-ui/core` and `@object-ui/i18n` carried the same
|
|
450
|
+
emission; `@object-ui/react`'s entry stayed unloadable until they were fixed
|
|
451
|
+
too, because evaluation crosses into them. No exported API changed.
|
|
452
|
+
- bea374e: A KPI card's sub-caption now translates from its own convention key
|
|
453
|
+
|
|
454
|
+
objectui#4032 item 4. The metric card renders two authored strings, and they
|
|
455
|
+
are two different authored fields:
|
|
456
|
+
|
|
457
|
+
| authored field | rendered as | bundle key |
|
|
458
|
+
|-----------------------|-------------------------|-------------------------------------------|
|
|
459
|
+
| `widget.description` | the shared card header | `dashboards.<d>.widgets.<id>.description` |
|
|
460
|
+
| `options.description` | the KPI sub-caption | `dashboards.<d>.widgets.<id>.subCaption` |
|
|
461
|
+
|
|
462
|
+
Only the first resolved. The metric dispatch spread `...options` straight
|
|
463
|
+
through, so the sub-caption reached `MetricWidget` as the raw authored English
|
|
464
|
+
and a `zh` dashboard showed a translated header above an untranslated caption.
|
|
465
|
+
|
|
466
|
+
They get two keys, not one — the objectstack#5428 item-4 ruling (2026-08-06):
|
|
467
|
+
"两个作者字段两个 key". That is why PR #4358 landed items 1-3 and deliberately
|
|
468
|
+
stopped here: at the time `@objectstack/spec` accepted no segment for the
|
|
469
|
+
sub-caption and the only key it would take was `description`, the shared key the
|
|
470
|
+
ruling forbids. objectstack#8056 added `subCaption` to the widget translation
|
|
471
|
+
node, and it ships in `@objectstack/spec@17.0.0` — the version this repo pins.
|
|
472
|
+
|
|
473
|
+
The server half already existed: `translateDashboard` overlays `subCaption` onto
|
|
474
|
+
`options.description` on the `/meta` path, so a served document was already
|
|
475
|
+
correct. This is the client half — the same key path, for the app bundles
|
|
476
|
+
objectui loads into `I18nProvider` itself.
|
|
477
|
+
|
|
478
|
+
- `@object-ui/i18n` gains `widgetSubCaption(dashboardName, widgetId, fallback?)`,
|
|
479
|
+
mirroring `widgetDescription` limb for limb rather than re-implementing
|
|
480
|
+
namespace discovery inside the plugin.
|
|
481
|
+
- `DashboardRenderer`'s `tWidgetSubCaption` composes the two channels in the
|
|
482
|
+
order `tWidgetTitle` already fixed — the authored value is collapsed to the
|
|
483
|
+
active language first (an inline per-locale map, the `pickLocalized` seam),
|
|
484
|
+
and the plain string that falls out is offered to the bundle as its fallback —
|
|
485
|
+
so a bundle entry always wins over an inline map, and neither channel is
|
|
486
|
+
replaced by the other. The resolved value is assigned after the `...options`
|
|
487
|
+
spread in both the `object-metric` and static-value branches.
|
|
488
|
+
|
|
489
|
+
The separation is pinned in both directions, because a shared key is exactly
|
|
490
|
+
what a later tidy-up would reach for: the `description` key never reaches
|
|
491
|
+
`options.description`, and `subCaption` never reaches `widget.description`. On a
|
|
492
|
+
`kpi` / `gauge` / `bullet` widget both are on screen at once, so one shared key
|
|
493
|
+
would make a single translation entry overwrite the other field's text.
|
|
494
|
+
|
|
495
|
+
Untranslated dashboards are unchanged: with no bundle entry the resolver hands
|
|
496
|
+
back exactly what the spread would have, and with nothing authored and nothing
|
|
497
|
+
translated it answers `undefined` rather than `''`, so a card that has no
|
|
498
|
+
sub-caption grows no caption row.
|
|
499
|
+
- d109a4d: The `organizations.*` picker locale family (avatar menu / console picker —
|
|
500
|
+
`mine`, `title`, `heading`, `subtitle`, `searchPlaceholder`, `new`, `current`,
|
|
501
|
+
`emptyTitle`, `emptyDescription`, `noMatches`) is now internally consistent in
|
|
502
|
+
all eight non-en/zh packs (`ar`, `de`, `es`, `fr`, `ja`, `ko`, `pt`, `ru`).
|
|
503
|
+
|
|
504
|
+
`en` and `zh` were renamed from "organization" to "workspace" terminology in
|
|
505
|
+
full; the other eight packs only had `create` follow, leaving each pack mixing
|
|
506
|
+
both nouns in one dropdown (e.g. `de.ts` read `create: "Workspace erstellen"`
|
|
507
|
+
directly beside `mine: "Meine Organisationen"`). Since PR #4638 restored the
|
|
508
|
+
avatar menu's "My Workspaces" entry, both lines render two apart in the same
|
|
509
|
+
menu. Each pack now uses the workspace term its own `create` key already
|
|
510
|
+
committed to (`Arbeitsbereich`-style German phrasing → `Workspace`, French
|
|
511
|
+
`espace de travail`, Japanese `ワークスペース`, Korean `워크스페이스`, Spanish/
|
|
512
|
+
Portuguese `espacio de trabajo` / `workspace`, Russian `рабочее пространство`,
|
|
513
|
+
Arabic `مساحة عمل`), with grammatical agreement (gender, definiteness,
|
|
514
|
+
particles) adjusted per key.
|
|
515
|
+
|
|
516
|
+
The sibling `organization.*` (singular) namespace — organization
|
|
517
|
+
**management**, a deliberately distinct surface per the comment separating the
|
|
518
|
+
two blocks in `en.ts` — is untouched in every pack.
|
|
519
|
+
- ad13d63: Removed five locale keys that had no call site in any package, app, example,
|
|
520
|
+
or e2e test: `console.objectView.striped`, `console.objectView.bordered`,
|
|
521
|
+
`console.objectView.virtualScroll`, `appDesigner.stripedRows`, and
|
|
522
|
+
`appDesigner.bordered`. Deleted from all ten packs (`en` plus the nine
|
|
523
|
+
translations) to keep `all-locales-key-parity.test.ts` green.
|
|
524
|
+
|
|
525
|
+
The `console.objectView.*` three describe grid options ObjectStack retired
|
|
526
|
+
upstream (objectstack#7176, 2026-08-10) and objectui stopped forwarding
|
|
527
|
+
(objectui#4649); the `appDesigner.*` pair is textually identical in name only
|
|
528
|
+
and was independently dead — no `AppCreationWizard.tsx` control, no fallback
|
|
529
|
+
entry in `useDesignerTranslation.ts`'s `DESIGNER_DEFAULT_TRANSLATIONS`, and no
|
|
530
|
+
mention anywhere in the repo outside the locale packs themselves.
|
|
531
|
+
|
|
532
|
+
No runtime behavior changes: nothing rendered these labels before this patch.
|
|
533
|
+
- a9e17b4: `auth.forgotPassword.successDescription`'s address hole is now spelled with
|
|
534
|
+
single braces (`{email}`) instead of i18next's double braces (`{{email}}`),
|
|
535
|
+
in all ten locale packs.
|
|
536
|
+
|
|
537
|
+
This is a spelling-only change — rendered output is byte-identical in every
|
|
538
|
+
language, because the hole was never filled by i18next in the first place:
|
|
539
|
+
`ForgotPasswordForm` substitutes the address itself once the user submits
|
|
540
|
+
the form (the label renders before the address exists, so `t()` cannot do
|
|
541
|
+
it). `{{email}}` and a genuinely unfilled i18next hole were indistinguishable
|
|
542
|
+
at the call site, and passing `email` as an interpolation argument — the
|
|
543
|
+
natural "fix" for what looks like a missing argument — would let i18next
|
|
544
|
+
consume the hole and cause the address to be appended a second time
|
|
545
|
+
(objectui#4135).
|
|
546
|
+
|
|
547
|
+
Converging on `{x}` for every hole a component fills downstream of `t()`
|
|
548
|
+
(the convention `resendOtpCountdownText`'s `{seconds}` already used) puts
|
|
549
|
+
this hole outside i18next's `{{…}}` syntax entirely, so the ambiguity is
|
|
550
|
+
gone by construction rather than fenced by an exemption. Accordingly,
|
|
551
|
+
`scripts/check-i18n-call-site-keys.mjs`'s `EXTERNALLY_INTERPOLATED_HOLES`
|
|
552
|
+
registry entry for this key is retired — the gate needs no exemption for a
|
|
553
|
+
hole i18next was never going to touch.
|
|
554
|
+
|
|
555
|
+
`ForgotPasswordForm.tsx`'s replacement marker and its own built-in default
|
|
556
|
+
label move to the same spelling in the same change, as does the inline
|
|
557
|
+
`defaultValue` at `apps/console/src/pages/auth/ForgotPasswordPage.tsx`'s
|
|
558
|
+
call site.
|
|
559
|
+
- 8871c14: Gate Home's metadata-authoring CTAs on the `manage_metadata` capability the
|
|
560
|
+
server reports, with a visible localized reason.
|
|
561
|
+
|
|
562
|
+
"Build an app" and "Start with a template" were gated on `useIsWorkspaceAdmin()`
|
|
563
|
+
— a ROLE check. On a multi-tenant deployment that deliberately withholds
|
|
564
|
+
metadata authoring from tenants, a workspace owner is an admin by role yet holds
|
|
565
|
+
no `manage_metadata`, so the most prominent CTA on their home page led into
|
|
566
|
+
`/studio`, a filled-in new-package dialog, and a capability refusal at submit.
|
|
567
|
+
|
|
568
|
+
Both cover cards are now disabled, with the reason shown on screen and in the
|
|
569
|
+
tooltip, whenever the session lacks the capability. The marketplace shortcut in
|
|
570
|
+
the apps strip is withheld with them (it targets the same route, so leaving it
|
|
571
|
+
live would have made the gate cosmetic), as is the "Build with AI" hero CTA
|
|
572
|
+
(its output is draft metadata that cannot be published without the capability).
|
|
573
|
+
On a workspace with no apps yet, the admin empty state explains the posture
|
|
574
|
+
instead of directing the owner to build their first application.
|
|
575
|
+
|
|
576
|
+
The gate consumes the answer `GET /api/v1/auth/me/permissions` already returns,
|
|
577
|
+
surfaced through the permissions provider — no permission logic is re-derived in
|
|
578
|
+
the client. Unknown capabilities fail OPEN: a backend that reports no
|
|
579
|
+
`systemPermissions` at all is indistinguishable from one reporting an empty set,
|
|
580
|
+
and the server enforces regardless, so nothing is withheld on missing client
|
|
581
|
+
data. New key `home.build.noCapability` in all ten locale packs.
|
|
582
|
+
- 21e4585: A write-warning toast now words each strip reason on its own instead of calling everything that is not `readonly_when` "read-only".
|
|
583
|
+
|
|
584
|
+
`emitWriteWarning` picked its sentence with a two-way conditional on
|
|
585
|
+
`reason === 'readonly_when'`. The other arm was never "readonly" — it was
|
|
586
|
+
"everything that is not `readonly_when`", so any reason the server-side write
|
|
587
|
+
path gained afterwards was announced to the user as a read-only lock, pointing
|
|
588
|
+
them at a permission problem that does not exist. That is the same defect
|
|
589
|
+
objectui#3484 / framework#3794 fixed for `readonly_when` itself, one reason
|
|
590
|
+
later.
|
|
591
|
+
|
|
592
|
+
The reason is now resolved through a table declared
|
|
593
|
+
`Record<DroppedFieldsEvent['reason'], …>`, mirroring the framework side of the
|
|
594
|
+
same seam (`service-automation`'s `DROPPED_REASON_LABEL`) and the shape the
|
|
595
|
+
spec's own `DroppedFieldsEventSchema` comment asks consumers for. The next reason
|
|
596
|
+
added upstream is a `type-check` failure here, unworded, where the conditional
|
|
597
|
+
compiled forever — which is what re-exporting THE spec type rather than a
|
|
598
|
+
hand-widened `string` was for all along.
|
|
599
|
+
|
|
600
|
+
Not a latent fix: the pinned spec (`@objectstack/spec` 17.0.0-rc.6) already
|
|
601
|
+
carries `primary_key` (objectstack#6437), the strip that keeps a payload `id` the
|
|
602
|
+
update dispatch ruled is not an identifier out of the targeted rows' primary key.
|
|
603
|
+
Until now a user who triggered it was told the field was read-only. It gets its
|
|
604
|
+
own wording in all ten locale packs: "The record's identifier cannot be changed
|
|
605
|
+
by a save, so it did not take effect".
|
|
606
|
+
|
|
607
|
+
One more line was added for a reason ahead of this bundle's pin — the adapter
|
|
608
|
+
reads `reason` structurally off the wire without checking it against the enum, so
|
|
609
|
+
a server newer than the bundle can deliver a value no table can have an arm for.
|
|
610
|
+
It names the fields and claims nothing about the cause, rather than throwing
|
|
611
|
+
inside a listener invoked as `void emitWriteWarning(...)` (which would cost the
|
|
612
|
+
user the whole toast, including the reasons that did resolve) or reusing a
|
|
613
|
+
wording that would state a cause we do not know.
|
|
614
|
+
- Updated dependencies [2533ec5]
|
|
615
|
+
- Updated dependencies [bbe8b86]
|
|
616
|
+
- Updated dependencies [8477be5]
|
|
617
|
+
- Updated dependencies [279fb13]
|
|
618
|
+
- Updated dependencies [ad07b65]
|
|
619
|
+
- Updated dependencies [41f498b]
|
|
620
|
+
- Updated dependencies [e1d4251]
|
|
621
|
+
- Updated dependencies [ac600e5]
|
|
622
|
+
- Updated dependencies [c1ef923]
|
|
623
|
+
- Updated dependencies [af5e292]
|
|
624
|
+
- Updated dependencies [167ec42]
|
|
625
|
+
- Updated dependencies [b1119ec]
|
|
626
|
+
- Updated dependencies [9f23d2b]
|
|
627
|
+
- Updated dependencies [af025ee]
|
|
628
|
+
- Updated dependencies [31676be]
|
|
629
|
+
- Updated dependencies [9ce096f]
|
|
630
|
+
- Updated dependencies [e05db88]
|
|
631
|
+
- Updated dependencies [5ffcc14]
|
|
632
|
+
- Updated dependencies [d971e51]
|
|
633
|
+
- Updated dependencies [d2ce342]
|
|
634
|
+
- Updated dependencies [dfc6975]
|
|
635
|
+
- @object-ui/core@17.6.0
|
|
636
|
+
|
|
3
637
|
## 17.5.0
|
|
4
638
|
|
|
5
639
|
### Minor Changes
|