@object-ui/i18n 17.1.0 → 17.3.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 +586 -0
- package/README.md +26 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +4 -1
- package/dist/locales/ar.d.ts +100 -0
- package/dist/locales/ar.js +104 -3
- package/dist/locales/de.d.ts +100 -0
- package/dist/locales/de.js +104 -3
- package/dist/locales/en.d.ts +103 -3
- package/dist/locales/en.js +196 -3
- package/dist/locales/es.d.ts +100 -0
- package/dist/locales/es.js +111 -5
- package/dist/locales/fr.d.ts +100 -0
- package/dist/locales/fr.js +104 -3
- package/dist/locales/index.d.ts +1006 -6
- package/dist/locales/ja.d.ts +100 -0
- package/dist/locales/ja.js +104 -3
- package/dist/locales/ko.d.ts +100 -0
- package/dist/locales/ko.js +104 -3
- package/dist/locales/pt.d.ts +100 -0
- package/dist/locales/pt.js +104 -3
- package/dist/locales/ru.d.ts +100 -0
- package/dist/locales/ru.js +104 -3
- package/dist/locales/zh.d.ts +103 -3
- package/dist/locales/zh.js +103 -3
- package/dist/provider.d.ts +43 -1
- package/dist/provider.js +105 -3
- package/dist/useObjectLabel.js +21 -4
- package/dist/useSafeTranslation.js +17 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,591 @@
|
|
|
1
1
|
# @object-ui/i18n
|
|
2
2
|
|
|
3
|
+
## 17.3.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 0554e88: The console language choice now survives a reload. `I18nProvider` writes every language change to `localStorage` (`objectui-locale`, exported as `LOCALE_STORAGE_KEY`) and boots the next session in that language, so switching to 中文/日本語/… is no longer reverted to `en` by the next F5 or new tab (objectstack#5406).
|
|
8
|
+
|
|
9
|
+
Bootstrap precedence is **stored choice → browser language → `defaultLanguage`**: an explicit choice outranks browser detection, and a stored value the app no longer offers is ignored and purged rather than locking the UI to a locale with no translations. The restore lands in the instance's bootstrap language, so `<html lang>` — and therefore the `Accept-Language` header on the first wave of API calls — is already correct on the first render.
|
|
10
|
+
|
|
11
|
+
New public surface on `@object-ui/i18n`: `persistLanguage` (default `true`; set `false` for fixed-language previews/demos), `LOCALE_STORAGE_KEY`, and `readStoredLanguage()` for apps that bootstrap their own i18next instance.
|
|
12
|
+
|
|
13
|
+
- 5af2852: The record detail page now shows a read-gated approval panel (#3461). A record in approval used to expose NOTHING about the running approval to anyone but the current pending approver — `useRecordApprovals` was consumed solely to inject the header Approve/Reject buttons, while the pending-approver list, decision progress, and the `sys_approval_action` timeline existed only in the Approval Center's drawer, a `setup`-app surface that business roles can't navigate to (and whose backing object is tenant-wide, so granting read there is over-broad). The submitter couldn't tell whom to nudge; the record's own audit history was no help either, since the engine mirrors business fields as `runAs:'system'` and decisions never enter record history. The new surface is an **Approvals tab** on the record page — a peer of Details/Related (same promotion Attachments got in objectstack#4358), emitted by `buildDefaultTabs` only when the record actually has requests, with a request-count badge and the label localizing through the tab strip's KNOWN_LABEL_DICT (审批). The tab wraps the new `record:approvals` node (`RecordApprovalsPanel`), visible to EVERY viewer who can read the record: current flow/step with the enriched flow-steps strip, server-computed decision progress (quorum tally, per-group 会签 ticks), the waiting-on chips with server-resolved names and group labels (never raw ids), one chronological action timeline merged across all of the record's requests (a multi-level flow opens one request per node), decision comments and attachments, and an inline remind button for the submitter (`viewer.is_submitter`, with an id-match fallback for older backends) that POSTs the existing `/approvals/requests/:id/remind`. The host threads its live `useRecordApprovals` read through the node so the tab and the header decision buttons never disagree; on authored pages the `record:approvals` renderer self-fetches, and an authored page that omits the node gets a bottom-of-page fallback append so the approval story is never lost to a custom layout. Copy reuses the Approval Center's `approvalsInbox.*` keys so the two surfaces can't drift; `useRecordApprovals` now exposes the full `requests` array plus `listApprovalActions` / `remindApprovalRequest`, and its `ApprovalRequestLite` carries the display enrichment (`process_label`, `step_label`, `flow_steps`, `viewer`, `round`) the single-read endpoint already sent.
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- b71fc92: Localize the last untranslated console-chrome accessible names (objectstack#5430)
|
|
18
|
+
|
|
19
|
+
Four icon-only controls still carried hardcoded English accessible names, so
|
|
20
|
+
under a non-English session they were the only English left in the record
|
|
21
|
+
chrome — and because the controls have no visible label, that literal _is_ the
|
|
22
|
+
control to a screen reader and to the hover tooltip.
|
|
23
|
+
|
|
24
|
+
- `page:header`'s `role="toolbar"` — now `detail.pageHeaderActions` (its `⋯`
|
|
25
|
+
overflow trigger eight lines below was fixed in #5407; the toolbar was missed)
|
|
26
|
+
- `ReactionPicker`'s `role="listbox"` popup — now `detail.emojiPicker`
|
|
27
|
+
- `ReactionPicker`'s per-reaction chip, which built its name by concatenation
|
|
28
|
+
with English pluralization baked in (`reaction${count !== 1 ? 's' : ''}`) —
|
|
29
|
+
now `detail.reactionCount` / `detail.reactionCountOne`
|
|
30
|
+
- `NavigationOverlay`'s drawer close and split-panel close — now `common.close`
|
|
31
|
+
(the key the rest of the console already uses) and `common.closePanel`
|
|
32
|
+
|
|
33
|
+
The pluralized label follows this repo's **two-key** convention
|
|
34
|
+
(`detail.relatedRecords`/`relatedRecordOne`, `lookup.recordCount`/`recordCountOne`)
|
|
35
|
+
rather than an i18next `_one`/`_other` pair: zh/ja/ko have no separate singular
|
|
36
|
+
form, so those packs would legitimately omit the `_one` half and
|
|
37
|
+
`all-locales-key-parity` would read that as a lost key.
|
|
38
|
+
|
|
39
|
+
All five new keys are added to all ten locale packs.
|
|
40
|
+
|
|
41
|
+
- 65516ba: Name `CommentThread`'s three emoji-only buttons, and follow the session language past the 7-day mark (objectui#3441)
|
|
42
|
+
|
|
43
|
+
Two leftovers from objectstack#5506 / objectui#3424, in the same component. It
|
|
44
|
+
is an exported, published component with no in-repo consumer, so both only ever
|
|
45
|
+
bite an external host.
|
|
46
|
+
|
|
47
|
+
**One — three controls with no authored accessible name.** Each comment's two
|
|
48
|
+
quick-reaction buttons (`'👍'` and `'❤️'`) and the reply banner's dismiss button
|
|
49
|
+
(`'✕'`) carried no `aria-label` and no `title`. The `+` reaction picker right
|
|
50
|
+
beside them has had one since #3424 (`collaboration.addThumbsUp`), which is what
|
|
51
|
+
makes these three an omission rather than a design choice.
|
|
52
|
+
|
|
53
|
+
`aria-label`, not the `title` the `+` uses: a `button`'s accessible name is
|
|
54
|
+
computed from its CONTENT (accname §2F) before the `title` tooltip is ever
|
|
55
|
+
consulted (§2I), so on a button whose only child is a glyph a `title` decorates
|
|
56
|
+
the mouse and leaves the name alone. What a screen reader read out was the
|
|
57
|
+
codepoint — "thumbs up", "red heart", in English whatever the session language,
|
|
58
|
+
and for U+2715 MULTIPLICATION X very often nothing at all.
|
|
59
|
+
|
|
60
|
+
Three new keys in all ten packs: `collaboration.reactThumbsUp`,
|
|
61
|
+
`collaboration.reactHeart`, `collaboration.cancelReply`.
|
|
62
|
+
|
|
63
|
+
`reactThumbsUp` is deliberately NOT a reuse of `addThumbsUp`, even though both
|
|
64
|
+
dispatch the same `onReaction(id, '👍')` today. `addThumbsUp` names the reaction
|
|
65
|
+
bar's picker entry point, whose copy follows the picker if it ever picks; and on
|
|
66
|
+
any comment that already has reactions the two controls are on screen together,
|
|
67
|
+
so one shared key would put two visibly different buttons under one name.
|
|
68
|
+
`cancelReply` rather than the generic `common.cancel` for the same reason — an
|
|
69
|
+
accessible name has to say what is being cancelled (only the reply target is
|
|
70
|
+
dropped; anything typed into the composer survives).
|
|
71
|
+
|
|
72
|
+
**Two — the >= 7 day timestamp ignored the session language.** `formatTimestamp`
|
|
73
|
+
ended in a bare `date.toLocaleDateString()`, i.e. the RUNTIME's locale, so a
|
|
74
|
+
`zh` session read "6 天前" for a six-day-old comment and `8/1/2026` for an
|
|
75
|
+
eight-day-old one.
|
|
76
|
+
|
|
77
|
+
The fix passes the session `language`, but not straight through — that is the
|
|
78
|
+
trap #3424 flagged and declined to walk into. `toLocaleDateString(tag)`
|
|
79
|
+
canonicalizes its argument and throws `RangeError` on anything not well-formed
|
|
80
|
+
per BCP 47, and the session language reaches the component verbatim: a host that
|
|
81
|
+
configures `defaultLanguage: 'en_US'` (the POSIX spelling — well-formed-looking,
|
|
82
|
+
and rejected) hands `Intl` a tag it refuses. That `RangeError` would land in
|
|
83
|
+
`formatTimestamp`'s outer `catch`, whose fallback is `return iso`, replacing the
|
|
84
|
+
date with a raw `2026-08-01T09:30:00.000Z` — worse than the un-localized date it
|
|
85
|
+
set out to fix.
|
|
86
|
+
|
|
87
|
+
So the absolute-date branch gets its own local `try`/`catch` that falls back to
|
|
88
|
+
the no-argument call. A malformed tag degrades to exactly the previous
|
|
89
|
+
behaviour (the runtime's own locale); the worst case of following the session
|
|
90
|
+
language is the status quo, never a regression. A well-formed but unknown tag
|
|
91
|
+
such as `xx-YY` does not throw at all — `Intl` resolves it to the default — so
|
|
92
|
+
only genuinely malformed tags reach the guard. No date library, and no month or
|
|
93
|
+
weekday copy in the locale packs: `Intl` already owns the per-locale ordering
|
|
94
|
+
and separators.
|
|
95
|
+
|
|
96
|
+
Tests assert the computed accessible name via `getByRole('button', { name })`
|
|
97
|
+
rather than the presence of an attribute, which is the distinction the fix turns
|
|
98
|
+
on, and pin that no button is left answering to a bare emoji. The malformed-tag
|
|
99
|
+
case is recorded honestly as green on both sides of this change — `origin/main`
|
|
100
|
+
never passed a tag anywhere, so it could not trip over a bad one; its
|
|
101
|
+
counterfactual is the naive fix, and dropping the inner `catch` is what turns it
|
|
102
|
+
red with the raw ISO string in the DOM.
|
|
103
|
+
|
|
104
|
+
- 94c5b7c: Localize `@object-ui/collaboration` — `CommentThread` no longer hardcodes English (objectstack#5506)
|
|
105
|
+
|
|
106
|
+
`@object-ui/collaboration` depended only on `@object-ui/types` and carried every
|
|
107
|
+
user-visible string as an English literal, so a `zh` console rendered a Chinese
|
|
108
|
+
shell around an English comment thread: "3 comments", "Reply", "Resolve",
|
|
109
|
+
"just now", "Add a comment... (use @ to mention)".
|
|
110
|
+
|
|
111
|
+
The package now takes `@object-ui/i18n` as a dependency and exposes one
|
|
112
|
+
translation seam, `useCollaborationTranslation` /
|
|
113
|
+
`COLLAB_DEFAULT_TRANSLATIONS`, built on `createSafeTranslation` — the same
|
|
114
|
+
factory `data-table`, `form` and `filter-builder` use. Under an `I18nProvider`
|
|
115
|
+
it resolves the session locale; with no provider it resolves the English
|
|
116
|
+
defaults map, which is what keeps `CommentThread` usable standalone. There is
|
|
117
|
+
deliberately no `formatter`/label prop escape hatch: a host that wants
|
|
118
|
+
different copy overrides the locale keys, so one thread can never end up half
|
|
119
|
+
translated by the bundle and half by props.
|
|
120
|
+
|
|
121
|
+
The issue listed 13 sites. A site-by-site sweep of the file found **20** — the
|
|
122
|
+
seven the original sweep missed are `{n}h ago`, `{n}d ago`, `(edited)`, the
|
|
123
|
+
thread's own comment count, the `Oldest`/`Newest` sort options,
|
|
124
|
+
`Replying to {name}...`, and the composer's `Send` button. All 20 are keyed
|
|
125
|
+
here; leaving any behind would have shipped a thread that is 90% translated.
|
|
126
|
+
|
|
127
|
+
Two of them carry a second defect on top of being untranslated: the plural
|
|
128
|
+
**rule** was compiled into the component, not just the words.
|
|
129
|
+
|
|
130
|
+
- the header read `` `${n} comment${n !== 1 ? 's' : ''}` ``;
|
|
131
|
+
- the reaction chip tooltip read `` n === 1 ? '1 reaction' : `${n} reactions` ``.
|
|
132
|
+
|
|
133
|
+
Both produced correct _English_ — this is not the "1 items" bug objectui#3423
|
|
134
|
+
fixed on the tab badge — but the choice between the two forms was English
|
|
135
|
+
grammar hardwired into the render path. No locale could apply its own: ru needs
|
|
136
|
+
three forms and ja needs none, and neither could ever be expressed no matter
|
|
137
|
+
what the packs said.
|
|
138
|
+
|
|
139
|
+
Both now use the repo's **two-key** plural convention
|
|
140
|
+
(`collaboration.commentCount`/`commentCountOne`,
|
|
141
|
+
`collaboration.reactionCount`/`reactionCountOne`) rather than an i18next
|
|
142
|
+
`_one`/`_other` pair: zh/ja/ko have no separate singular form, so those packs
|
|
143
|
+
would legitimately omit the `_one` half and `all-locales-key-parity` reads a
|
|
144
|
+
legitimately-absent half as a lost key. Counts are interpolated as strings, so
|
|
145
|
+
i18next skips its own plural resolution and the two-key scheme stays in charge.
|
|
146
|
+
|
|
147
|
+
The reaction tooltip gets a **dedicated** key pair rather than reusing
|
|
148
|
+
`detail.reactionCount`: that one interpolates `{{emoji}}`, and at this call
|
|
149
|
+
site the emoji is the chip's visible label with nothing to hand the
|
|
150
|
+
placeholder — reuse would have left a literal `{{emoji}}` in the accessible
|
|
151
|
+
name under every locale.
|
|
152
|
+
|
|
153
|
+
Relative timestamps stayed word-level: the existing minute/hour/day buckets are
|
|
154
|
+
untouched and no date library was introduced. The `>= 7d` branch still uses the
|
|
155
|
+
runtime's own `toLocaleDateString()` — that is not a hardcoded English literal,
|
|
156
|
+
and pinning it to the session language has its own failure mode (an
|
|
157
|
+
unrecognised tag throws into the surrounding `catch`, which would render a raw
|
|
158
|
+
ISO string), so it is tracked separately.
|
|
159
|
+
|
|
160
|
+
`Save` / `Cancel` / `Edit` / `Delete` read from the shared `common` namespace
|
|
161
|
+
instead of being re-spelled under `collaboration` — they are the generic action
|
|
162
|
+
words, already translated in all ten packs, and a second spelling would only be
|
|
163
|
+
a second thing to keep in sync. The 21 genuinely new keys are added to all ten
|
|
164
|
+
locale packs with real translations.
|
|
165
|
+
|
|
166
|
+
- ca0fa8f: Localize `PresenceAvatars` — the avatar stack's accessible name and tooltips follow the session language (objectui#3440)
|
|
167
|
+
|
|
168
|
+
objectui#3424 wired `@object-ui/collaboration` up to `@object-ui/i18n` but only
|
|
169
|
+
converted `CommentThread`. `PresenceAvatars` in the same package kept three
|
|
170
|
+
English literals, and it is not a dormant export — the console renders it in
|
|
171
|
+
two places: `app-shell/src/layout/AppHeader.tsx` (tenant presence beside the
|
|
172
|
+
lifecycle badge) and `app-shell/src/views/RecordDetailView.tsx` (who else is on
|
|
173
|
+
this record). A `zh` session got them in English.
|
|
174
|
+
|
|
175
|
+
The three sites:
|
|
176
|
+
|
|
177
|
+
- the group's `aria-label`, `` `${n} user${n !== 1 ? 's' : ''} present` ``;
|
|
178
|
+
- the overflow badge's tooltip, `` `${n} more user${n !== 1 ? 's' : ''}` ``;
|
|
179
|
+
- each avatar's tooltip, `` `${name} (${status})` ``.
|
|
180
|
+
|
|
181
|
+
The first one is the whole control as far as a screen reader is concerned: the
|
|
182
|
+
stack renders images and initials and nothing else, so there was no other
|
|
183
|
+
accessible name to fall back on.
|
|
184
|
+
|
|
185
|
+
As with the comment count in #3424, the first two carried a second defect on
|
|
186
|
+
top of being untranslated — the plural **rule** was compiled into the component.
|
|
187
|
+
Both produced correct _English_ (each has a real singular branch, so this is
|
|
188
|
+
not the "1 items" defect objectui#3423 fixed on the tab badge), but
|
|
189
|
+
`n !== 1 ? 's' : ''` is English grammar in a render path and no locale could
|
|
190
|
+
apply its own. Both now use the repo's **two-key** plural convention
|
|
191
|
+
(`collaboration.presentUserCount`/`presentUserCountOne`,
|
|
192
|
+
`collaboration.moreUserCount`/`moreUserCountOne`) rather than an i18next
|
|
193
|
+
`_one`/`_other` pair, with the count interpolated as a string so i18next skips
|
|
194
|
+
its own plural resolution. German is what witnesses the move: "1 anwesender
|
|
195
|
+
Benutzer" vs "2 anwesende Benutzer" inflects the adjective, which the deleted
|
|
196
|
+
ternary could not have produced for any pack.
|
|
197
|
+
|
|
198
|
+
The avatar tooltip becomes a single `collaboration.userStatusTitle` key
|
|
199
|
+
(`{{name}} ({{status}})`) so the parentheses and their spacing belong to the
|
|
200
|
+
translation — the CJK packs drop the space English puts before `(`, matching
|
|
201
|
+
their existing `edited: '(已编辑)'`.
|
|
202
|
+
|
|
203
|
+
Its `status` is a **display-layer** translation
|
|
204
|
+
(`collaboration.statusActive` / `statusIdle` / `statusAway`): the
|
|
205
|
+
`PresenceUser['status']` enum value stays raw data everywhere it is stored,
|
|
206
|
+
compared or passed around — including the `statusColors` lookup — and is
|
|
207
|
+
translated only at this render exit. A status outside the declared union
|
|
208
|
+
renders as itself, the raw string: presence users arrive from a host-supplied
|
|
209
|
+
`PresenceSource` transport, so an unmapped value is reachable at runtime
|
|
210
|
+
whatever the type says, and the fallback invents nothing rather than leaving an
|
|
211
|
+
empty bracket pair.
|
|
212
|
+
|
|
213
|
+
Eight new keys, added to all ten locale packs with real translations.
|
|
214
|
+
|
|
215
|
+
- 3889ffb: Console chrome i18n gaps (objectstack#5407).
|
|
216
|
+
|
|
217
|
+
- A dependency-gated lookup now names its controlling field by its **label**
|
|
218
|
+
instead of its raw API name. The sentence was localized but the interpolated
|
|
219
|
+
name was not, so every locale — English included — read `Select crm_account
|
|
220
|
+
first`. The form renderer passes a new `dependsOnLabels` widget prop (the
|
|
221
|
+
lookup-side counterpart of `emptyHint`, which it already resolves to labels
|
|
222
|
+
for the fixed-option widgets); a name the host does not cover still falls
|
|
223
|
+
back to itself.
|
|
224
|
+
- The page-header overflow trigger's `More actions` accessible name now reads
|
|
225
|
+
`detail.moreActions`, the same key `action:menu`'s own overflow trigger uses,
|
|
226
|
+
so the two cannot diverge per locale.
|
|
227
|
+
- The activity-feed reaction button's `Add reaction` accessible name is now a
|
|
228
|
+
bundle key (`detail.addReaction`, added to all ten packs).
|
|
229
|
+
- The "check the highlighted fields" toast joins field names with a per-locale
|
|
230
|
+
separator (`validation.formInvalidJoiner`) instead of a hardcoded `、`
|
|
231
|
+
(U+3001) — right for zh/ja by accident, wrong in English and every Latin
|
|
232
|
+
locale. Latin packs use `, `, CJK `、`, Arabic `، `.
|
|
233
|
+
- The Spanish `validation.required` / `validation.unique` templates gained
|
|
234
|
+
their own masculine head noun (`El campo {{field}} es obligatorio`) so the
|
|
235
|
+
adjective agrees for feminine field labels too — `Cuenta es obligatorio` was
|
|
236
|
+
ungrammatical.
|
|
237
|
+
|
|
238
|
+
- 7e2406a: The group-tenancy write-target badge is now translated in all ten locales (objectui#3517)
|
|
239
|
+
|
|
240
|
+
`form.createTargetOrg` — the ADR-0105 badge `RecordFormPage` shows in create mode
|
|
241
|
+
to name the organization a new record will land in — was defined in **no** locale
|
|
242
|
+
pack, not even `en`. i18next therefore genuinely missed the key and rendered the
|
|
243
|
+
call site's inline `defaultValue`, so the badge read English `Creates in <org>` in
|
|
244
|
+
all ten languages: a Chinese console creating a record on an org-walled object
|
|
245
|
+
showed `Creates in 某某组织`.
|
|
246
|
+
|
|
247
|
+
`all-locales-key-parity.test.ts` could not see this. It asserts that every pack
|
|
248
|
+
defines every **`en`** key, so a key `en` itself lacks is outside the comparison —
|
|
249
|
+
ten packs missing it identically kept parity fully green.
|
|
250
|
+
|
|
251
|
+
## What changed
|
|
252
|
+
- `createTargetOrg` is backfilled into `en` as `Creates in {{org}}`, which makes
|
|
253
|
+
the parity gate demand it from the other nine; each is translated to its pack's
|
|
254
|
+
existing `form`-section tone rather than copied or machine-filled.
|
|
255
|
+
- The inline `defaultValue` in `RecordFormPage.tsx` is deleted, finishing what
|
|
256
|
+
objectui#3469 started — that key was the file's last remaining exception, and
|
|
257
|
+
every `t()` on the page now passes bare. Declared = enforced: the packs are the
|
|
258
|
+
single source of this copy, and a missing key must surface (raw key + dev
|
|
259
|
+
missing-key warning) instead of being papered over at the call site.
|
|
260
|
+
- The two exception-pinning tests objectui#3516 left behind invert. `form.createTargetOrg`
|
|
261
|
+
joins the `BARE_KEYS` list (pinned present in all ten packs), and the render
|
|
262
|
+
assertion now checks the badge against the **pack** copy in both `en` and `zh` —
|
|
263
|
+
the deleted English default could not satisfy the Chinese assertion, so the
|
|
264
|
+
badge fails loudly if the packs ever stop driving it.
|
|
265
|
+
|
|
266
|
+
- 4eeb932: The form renderer's last user-visible English literals now go through i18n (#3272). The fullscreen long-text editor (`mobile_fullscreen`) was an entire untranslated dialog — title, screen-reader description, `Cancel` / `Done` footer buttons, and the expand trigger's accessible name — rendering English inside an otherwise translated zh/ja/ar form; it now reads the new `form.fullscreen.*` keys, shipped in all ten locale packs.
|
|
267
|
+
|
|
268
|
+
**Behaviour change worth reading if you author forms:** `submitLabel` and `cancelLabel` no longer default to the literals `'Submit'` and `'Cancel'` in the renderer. They default to _unset_, and the action bar falls back at render time to `common.submit` / `common.cancel`, so a form that declares no button copy now follows the session language instead of being silently frozen to English. A label you DO declare still wins verbatim in every locale — including an English one under a zh session, and including an explicit empty string (the fallback uses `??`, so `submitLabel: ''` renders a blank button rather than being overwritten). The only forms whose rendered text changes are those that never declared the labels and are viewed in a non-English session — which is the bug. `FormSchema.submitLabel` / `cancelLabel` stay optional strings; no spec or type change.
|
|
269
|
+
|
|
270
|
+
Also removed the built-in `select` branch's second `|| 'Select an option'` fallback. The single call site already supplies `t('common.selectOption')`, so the literal was reachable only through an authored `placeholder: ''` — where it replaced the author's deliberate blank with an untranslated English word.
|
|
271
|
+
|
|
272
|
+
- 5c856ec: `ObjectGantt`'s quick-filter bar is now localized instead of pinned to Chinese. The four `QuickFilterBar` labels (`all`, `clear`, `empty`, `resultSummary`) were hardcoded as Chinese string literals at the `ObjectGantt` call site, so the bar read 全部 / 清除筛选 / 无可选项 / 显示 N / M 项任务 under an `en`, `ja`, `es` or `ar` session while the rest of the gantt toolbar localized correctly — a conspicuous mismatch, and a violation of the English-only-codebase rule. `QuickFilterBar` itself was never at fault: it is presentational and already falls back to English, so the host was the only thing pinning the copy.
|
|
273
|
+
|
|
274
|
+
The four strings moved into a new `gantt.quickFilter` namespace, added to all ten built-in locale packs, and the call site now resolves them through the gantt package's existing `useGanttTranslation` — the same per-key hook every other gantt string already uses, so a host dictionary that lags on these keys still renders the bundled English default rather than a raw key. `gantt.quickFilter.resultSummary` deliberately keeps SINGLE-brace placeholders (`{shown}` / `{total}`): the call site substitutes them with a literal `.replace`, not i18next interpolation, matching `gantt.autoScheduleDlg.body` and the placeholder convention `all-locales-key-parity` already recognises. Anyone retranslating these packs must keep that spelling — a respell to `{{shown}}` would render the raw placeholder to the user.
|
|
275
|
+
|
|
276
|
+
- 68b6a28: The list toolbar's "Filter" now saves. Saving a filter from the runtime toolbar PUT the FilterBuilder's whole group object (`{ id, logic, conditions }`) into the view's `filter`, where `@objectstack/spec`'s `ListViewSchema.filter` declares `ViewFilterRule[]` — so every save came back `422 invalid_metadata` and the filter was silently never persisted (objectstack#5159).
|
|
277
|
+
|
|
278
|
+
The producer now folds the builder's group to the spec's flat `{ field, operator, value }` rule list before persisting, sharing one transform with the Studio view inspector (which had the only copy). Operators normalize through the spec's own `normalizeFilterOperator`, so the four builder operators the Studio's local table had drifted behind — `startsWith`, `endsWith`, `isNull`, `isNotNull` — now persist correctly too. The builder's per-row `id` is no longer written: it is a React list key that the read path regenerates, so stored view bodies keep the declared vocabulary only.
|
|
279
|
+
|
|
280
|
+
A filter whose shape cannot be represented losslessly as a flat rule list — `OR` across several conditions, or nested condition groups — is now refused with a translated message instead of being quietly saved as `AND`, which would have returned a different set of records than the one on screen. Such a filter still applies to the current list; it just does not become part of the saved view.
|
|
281
|
+
|
|
282
|
+
- 28b2e65: Localize the create / edit / view form title `ObjectView` builds itself
|
|
283
|
+
(objectui#3462)
|
|
284
|
+
|
|
285
|
+
The same family as #3426 / PR #3457 and #3459 / PR #3464, one call site further
|
|
286
|
+
in. `ObjectView.getFormTitle()` string-built its three verbs in TypeScript:
|
|
287
|
+
|
|
288
|
+
case 'create': return `Create ${objectLabel}`;
|
|
289
|
+
case 'edit': return `Edit ${objectLabel}`;
|
|
290
|
+
case 'view': return `View ${objectLabel}`;
|
|
291
|
+
|
|
292
|
+
so a Chinese session whose object is labelled 联系人 read a drawer headed
|
|
293
|
+
**"View 联系人"** — an English verb glued onto a localized label. All three
|
|
294
|
+
consumers are visible chrome: `renderDrawerForm`'s `DrawerTitle`,
|
|
295
|
+
`renderModalForm`'s `DialogTitle`, and the `title` prop handed to
|
|
296
|
+
`NavigationOverlay` in the `popover` branch (a host-supplied `title` displaces
|
|
297
|
+
the overlay's own `resolvedTitle` default, so it is what the user sees).
|
|
298
|
+
|
|
299
|
+
The bar to reach it is lower than #3459's split panel: `ObjectViewSchema.layout`
|
|
300
|
+
already defaults to `'drawer'`, and `navigation` is a declared authorable input
|
|
301
|
+
on the registered `object-view` block whose `mode` union carries `drawer`,
|
|
302
|
+
`modal` and `popover`. A row click under any of them sets `formMode: 'view'` and
|
|
303
|
+
opens the container. `app-shell`'s wrapper pinning `layout: 'page'` is one host
|
|
304
|
+
overriding a registered block, not proof the branch is dead.
|
|
305
|
+
|
|
306
|
+
## What changed
|
|
307
|
+
|
|
308
|
+
The three verb branches resolve `form.createTitle` / `form.editTitle` /
|
|
309
|
+
`form.viewTitle`.
|
|
310
|
+
|
|
311
|
+
**No new key family was minted.** `form.createTitle` (`'Create {{object}}'`) and
|
|
312
|
+
`form.editTitle` (`'Edit {{object}}'`) already ship in all ten packs and are
|
|
313
|
+
already how `app-shell` heads the PAGE-mode record form
|
|
314
|
+
(`RecordFormPage.tsx`, `AppContent.tsx`). The drawer / modal / popover titles are
|
|
315
|
+
the same heading on a different surface, so they resolve the same keys — a
|
|
316
|
+
parallel per-plugin family would have guaranteed the two spellings drift, which
|
|
317
|
+
is what the sibling issues were about. Only the third verb had no sibling:
|
|
318
|
+
`form.viewTitle` is added to all ten packs, following each pack's existing
|
|
319
|
+
arrangement for its create/edit twins rather than a translated-verb-plus-label
|
|
320
|
+
concatenation (de puts the verb last, ja/zh use particles and no space).
|
|
321
|
+
|
|
322
|
+
`VIEW_DEFAULT_TRANSLATIONS` in `ObjectView.tsx` gains the three English entries,
|
|
323
|
+
which is what `createSafeTranslation` falls back to with no `I18nProvider`
|
|
324
|
+
mounted.
|
|
325
|
+
|
|
326
|
+
Two branches stay literal on purpose and are pinned by tests: `schema.form.title`
|
|
327
|
+
(the author wrote a title, so the author's title wins, in every locale) and the
|
|
328
|
+
`default` branch (bare object label, no verb to translate).
|
|
329
|
+
|
|
330
|
+
## Visible English change
|
|
331
|
+
|
|
332
|
+
None. Every branch is byte-identical in English — `Create Contacts`,
|
|
333
|
+
`Edit Contacts`, `View Contacts` — with and without a provider, so e2e specs and
|
|
334
|
+
host tests that address this chrome by its English name keep addressing it. The
|
|
335
|
+
provider-less path has its own test file, kept separate because
|
|
336
|
+
`initReactI18next` registers its instance as a module global that outlives
|
|
337
|
+
`cleanup()`.
|
|
338
|
+
|
|
339
|
+
The toolbar's create BUTTON keeps resolving `console.objectView.new`
|
|
340
|
+
("New" / 新建) and was deliberately not reused for the heading: a button verb and
|
|
341
|
+
a title are different contexts, and folding them together is how the next drift
|
|
342
|
+
of this shape would start.
|
|
343
|
+
|
|
344
|
+
- 825bbe3: The option widgets' "this list cannot be filled" message now has one source, and
|
|
345
|
+
it is translated (objectui#3231).
|
|
346
|
+
|
|
347
|
+
FROM: `SelectField`, `MultiSelectField`, `RadioField` and `CheckboxesField` each
|
|
348
|
+
carried their own copy of the empty/gated state, each destructured the declared
|
|
349
|
+
`emptyHint` prop into `_emptyHint` and dropped it, and each rendered a hardcoded
|
|
350
|
+
English literal (`'No options available'`, `` `Select ${…} first` ``) even in a
|
|
351
|
+
Chinese or Japanese session. TO: one shared `OptionsEmptyState` — the host's
|
|
352
|
+
`emptyHint` when it supplied one, otherwise a translated fallback
|
|
353
|
+
(`fields.options.empty` / `fields.options.selectFirst`, added to all ten locale
|
|
354
|
+
packs).
|
|
355
|
+
|
|
356
|
+
`emptyHint` was declared, produced by the form renderer and transported, then
|
|
357
|
+
lost three times over — so no registered widget could ever render it. All three
|
|
358
|
+
breaks are fixed, because closing only the last one delivers nothing:
|
|
359
|
+
|
|
360
|
+
- `isOptionField` compared the raw resolved type against `'select'` /`'radio'` /
|
|
361
|
+
`'multiselect'` / `'checkboxes'`. Object-derived forms emit
|
|
362
|
+
`mapFieldTypeToFormType`'s prefixed ids (`field:select`), which matched none of
|
|
363
|
+
them, so for every option field coming from an object schema — the normal case
|
|
364
|
+
in the console — the whole cascade block was skipped and no hint was computed
|
|
365
|
+
at all. It now normalizes the `field:` prefix, the same normalization
|
|
366
|
+
`stripRegisteredFieldProps` already applied a few lines below.
|
|
367
|
+
- `stripRegisteredFieldProps` then removed the `emptyHint` key from what was
|
|
368
|
+
left. It is now forwarded to the four cascade option types, alongside
|
|
369
|
+
`dependentValues`. This stays an allow-list rather than a blanket
|
|
370
|
+
pass-through: every other registered widget spreads its leftover props onto a
|
|
371
|
+
DOM node, where an unknown `emptyHint` attribute is a React warning.
|
|
372
|
+
- the widgets themselves discarded it. Keeping it out of the `...props` spread
|
|
373
|
+
was correct; not using it afterwards was not.
|
|
374
|
+
|
|
375
|
+
User-visible effect: a dependency-gated option list now prompts with the
|
|
376
|
+
controlling field's **label** ("Select Country first") instead of its raw
|
|
377
|
+
metadata name, in the session's language; an unconfigured list says so in the
|
|
378
|
+
session's language too. The gate sentence is one i18n key shared by the renderer
|
|
379
|
+
and the widget fallback, so the two sides cannot word it differently.
|
|
380
|
+
|
|
381
|
+
Untouched: the built-in (unregistered) `select` branch of the form renderer,
|
|
382
|
+
which already consumed `emptyHint`. That is a separate live path.
|
|
383
|
+
|
|
384
|
+
- 6195841: Localize the record-detail overlay heading that `ListView` and `ObjectGrid`
|
|
385
|
+
build themselves (objectui#3426)
|
|
386
|
+
|
|
387
|
+
#3423 gave `NavigationOverlay`'s `resolvedTitle` an i18n default
|
|
388
|
+
(`detail.recordDetail`), but two hosts never let that default run: they
|
|
389
|
+
string-built an English heading in TypeScript and passed it as the `title`
|
|
390
|
+
prop, so a zh/ja/de session got a fully localized drawer with one English
|
|
391
|
+
heading on it.
|
|
392
|
+
|
|
393
|
+
- `packages/plugin-list/src/ListView.tsx` — `` `${schema.label} Detail` ``
|
|
394
|
+
- `packages/plugin-grid/src/ObjectGrid.tsx` — the same template, plus a bare
|
|
395
|
+
`'Record Detail'` literal for the no-label case
|
|
396
|
+
|
|
397
|
+
Both are user-reachable, not dead defaults. `list-view` / `object-grid` are
|
|
398
|
+
public page blocks and `navigation` is an authorable key on their schema, so a
|
|
399
|
+
page that authors `navigation: { mode: 'drawer' }` opens exactly this overlay
|
|
400
|
+
on row click. (`app-shell`'s `ObjectView` does suppress it — it passes its own
|
|
401
|
+
`onRowClick`, which takes priority inside `useNavigationOverlay`, and renders
|
|
402
|
+
its own overlay — but that is one host overriding a public block, not proof the
|
|
403
|
+
branch is unreachable.)
|
|
404
|
+
|
|
405
|
+
## What changed
|
|
406
|
+
|
|
407
|
+
Both call sites now key their heading instead of concatenating it:
|
|
408
|
+
|
|
409
|
+
- a new `detail.recordDetailWithLabel` (`'{{label}} Detail'`) carries the
|
|
410
|
+
object label through interpolation, so a pack whose qualifier trails the noun
|
|
411
|
+
(`de`) or that needs a possessive particle (`ja`/`zh`) can write its own
|
|
412
|
+
arrangement rather than inherit English word order;
|
|
413
|
+
- the no-label branch reuses `detail.recordDetail` — the very key the overlay
|
|
414
|
+
itself defaults to — so one heading on one control cannot drift into two
|
|
415
|
+
translations.
|
|
416
|
+
|
|
417
|
+
The new key is added to all ten locale packs and to each plugin's English
|
|
418
|
+
defaults map (`LIST_DEFAULT_TRANSLATIONS` / `GRID_DEFAULT_TRANSLATIONS`), which
|
|
419
|
+
is what `createSafeTranslation` falls back to with no `I18nProvider` mounted.
|
|
420
|
+
|
|
421
|
+
English output is byte-identical in every branch (`Contacts Detail` /
|
|
422
|
+
`Contacts Detail` / `Record Detail`), with and without a provider — pinned by a
|
|
423
|
+
provider-less test file per plugin, kept separate because `initReactI18next`
|
|
424
|
+
registers its instance as a module global that outlives `cleanup()`.
|
|
425
|
+
|
|
426
|
+
- 5dd0127: Localize the record-overlay and tab-badge chrome that #5430's sweep left behind (objectstack#5506)
|
|
427
|
+
|
|
428
|
+
Four more console-chrome strings were still hardcoded English literals. Unlike
|
|
429
|
+
#5430's set they are not all accessible names — one is visible copy, and one was
|
|
430
|
+
a component **default** that only the console happened to override.
|
|
431
|
+
|
|
432
|
+
- `page:tabs`' count badge built its `aria-label` by template literal,
|
|
433
|
+
`` `${formatTabCount(count)} items` ``. The badge renders digits only, so that
|
|
434
|
+
label _is_ the badge to a screen reader — and the English plural was baked in
|
|
435
|
+
with no singular branch at all, so a related list with one row announced
|
|
436
|
+
"1 items". Now `common.itemCount` / `common.itemCountOne`.
|
|
437
|
+
- `NavigationOverlay`'s drag-resize handle (`role="separator"`, no visible label)
|
|
438
|
+
— now `common.resizeDrawer`.
|
|
439
|
+
- `NavigationOverlay`'s `expandLabel` **default**. Hosts may override it and the
|
|
440
|
+
console does, but the default is what every other host ships — and it feeds
|
|
441
|
+
both `aria-label` and `title` of an icon-only button. Now
|
|
442
|
+
`detail.openAsFullPage`, still overridable by the prop.
|
|
443
|
+
- `NavigationOverlay`'s `resolvedTitle` fallback, `'Record Detail'` — **visible**
|
|
444
|
+
overlay heading, not just an a11y name. Now `detail.recordDetail`.
|
|
445
|
+
- The sr-only `SheetDescription`/`DialogDescription` prose
|
|
446
|
+
`Record detail overlay for {title}.`, which existed in three copies
|
|
447
|
+
(drawer / modal / popover) — now one `detail.recordDetailOverlay` key with a
|
|
448
|
+
`{{title}}` placeholder.
|
|
449
|
+
|
|
450
|
+
The count badge follows this repo's **two-key** plural convention
|
|
451
|
+
(`detail.reactionCount`/`reactionCountOne`, `detail.relatedRecords`/`relatedRecordOne`)
|
|
452
|
+
rather than an i18next `_one`/`_other` pair: zh/ja/ko have no separate singular
|
|
453
|
+
form, so those packs would legitimately omit the `_one` half and
|
|
454
|
+
`all-locales-key-parity` would read that as a lost key. The formatted count
|
|
455
|
+
(`1.2k`, not `1200`) is interpolated so the accessible name and the visible
|
|
456
|
+
digits never disagree — and because i18next skips its own plural resolution when
|
|
457
|
+
`count` is a string, the two-key scheme stays in charge of the choice.
|
|
458
|
+
|
|
459
|
+
Both touched components moved from `useSafeTranslate` to `createSafeTranslation`,
|
|
460
|
+
which carries an options bag (two of the new keys interpolate) and an English
|
|
461
|
+
defaults map. That map is what keeps the provider-less path English, which
|
|
462
|
+
consumers outside this package depend on — `plugin-view`'s `ObjectView.test.tsx`
|
|
463
|
+
and `e2e/live/inline-edit-polish-2572.spec.ts` address this chrome by English
|
|
464
|
+
accessible name with no `I18nProvider` mounted.
|
|
465
|
+
|
|
466
|
+
All six new keys are added to all ten locale packs.
|
|
467
|
+
|
|
468
|
+
- a415684: The console server-action wrapper's `opensInNewTab` choreography no longer
|
|
469
|
+
ships hard-coded bilingual Chinese/English copy (objectui#3321, AGENTS.md
|
|
470
|
+
Commandment #-1): the pre-opened SSO spinner tab (title + body) and the
|
|
471
|
+
popup-blocked toast (title, description, action label) are now localized
|
|
472
|
+
through new `console.serverAction.*` keys in `@object-ui/i18n`, added at full
|
|
473
|
+
parity across all ten locale packs.
|
|
474
|
+
|
|
475
|
+
`createConsoleServerActionHandler` gains an optional i18next-style `t` option
|
|
476
|
+
(`t(key, englishDefault)`) — the wrapper is a plain function, so the translate
|
|
477
|
+
function is injected from the two hook-context call sites
|
|
478
|
+
(`useConsoleActionRuntime`, `RecordDetailView`) via `useObjectTranslation`.
|
|
479
|
+
When omitted (tests / standalone), every string falls back to its English
|
|
480
|
+
default; no non-English copy remains in code. Locale strings are HTML-escaped
|
|
481
|
+
before being written into the spinner document.
|
|
482
|
+
|
|
483
|
+
- a6ec93d: `createSafeTranslation`'s no-provider fallback interpolation now replaces **all**
|
|
484
|
+
occurrences of each placeholder, matching i18next semantics on the provider path.
|
|
485
|
+
|
|
486
|
+
The fallback used `value.replace('{{k}}', String(v))`, and `String.prototype.replace`
|
|
487
|
+
with a string needle substitutes only the _first_ match. A default string repeating a
|
|
488
|
+
placeholder — `'Selected {{count}} of {{count}} items'`, natural in many locales and
|
|
489
|
+
sometimes required by RTL / agglutinative word order — therefore leaked literal braces
|
|
490
|
+
to users on hosts with no `I18nProvider` mounted (standalone / embedded renderers),
|
|
491
|
+
while the same string interpolated correctly once a provider was present. A silent
|
|
492
|
+
semantic fork, in exactly the environments we observe least.
|
|
493
|
+
|
|
494
|
+
The replacement is `value.split(needle).join(String(v))` rather than `replaceAll`:
|
|
495
|
+
both `replace` and `replaceAll` interpret `$&`, `` $` ``, `$'` and `$$` in the
|
|
496
|
+
_replacement_ string, which i18next does not. Values here are runtime data (record
|
|
497
|
+
labels, search terms), so that second divergence was reachable today — a label
|
|
498
|
+
containing `$&` was mangled on the fallback path. split/join is literal on both sides
|
|
499
|
+
and needs no regex escaping of the placeholder name.
|
|
500
|
+
|
|
501
|
+
Key resolution (`defaults[key] || key`), the `String(v)` coercion, and the
|
|
502
|
+
leave-it-literal behaviour for a placeholder with no matching option are unchanged.
|
|
503
|
+
Fixes objectui#3418.
|
|
504
|
+
|
|
505
|
+
- c7ed4c3: `TagsField` no longer ships a hardcoded Chinese input placeholder
|
|
506
|
+
(objectui#3342, AGENTS.md Commandment #-1). The placeholder now resolves
|
|
507
|
+
through the pinned chain: the author-declared `field.placeholder` wins
|
|
508
|
+
(previously ignored by this widget); otherwise the widget's own copy arrives
|
|
509
|
+
via `useFieldTranslation()` under the new `fields.tags.placeholder` key, added
|
|
510
|
+
at full parity across all locale packs (Chinese lives in the zh pack, not in
|
|
511
|
+
code); with no `I18nProvider` mounted the English default from FIELD_DEFAULTS
|
|
512
|
+
renders — never a raw key.
|
|
513
|
+
- 2409e1d: `TextAreaField`'s character counter now announces itself in the session locale. The counter block — rendered only when the field declares `maxLength` — carried the accessible name `Character count: {n} of {max}` as an English literal, and the element is an `aria-live="polite"` region, so a zh/ja/ar session had that English sentence read out on every keystroke while sighted users saw only the language-independent `{n}/{max}` digits. Nothing was wrong on screen, which is why it survived: only screen reader users could perceive it (#3406).
|
|
514
|
+
|
|
515
|
+
Unlike #3404, no key existed to consume — none of the ten locale packs had any character-count string. `fields.textarea.characterCount` is new in all ten, interpolating `{{count}}` and `{{max}}` as one sentence rather than parts assembled in code, because `ja` and `ko` put the cap before the count ("of {{max}} characters, {{count}}"), an order no concatenation can produce. The English pack value and the `FIELD_DEFAULTS` fallback are byte-identical to the literal they replace, so an `en` session and a provider-less embed both render exactly what they did before.
|
|
516
|
+
|
|
517
|
+
Behaviour is unchanged: `aria-live="polite"` and the per-keystroke recompute are deliberately untouched here and tracked separately (#3408).
|
|
518
|
+
|
|
519
|
+
- 789fe3e: `TextAreaField`'s character counter no longer re-announces itself on every keystroke. Measured on `main` in a zh session with `maxLength: 500`, typing a 52-character sentence one character at a time produced 52 distinct screen-reader announcements totalling 979 spoken characters — roughly 19x the text being written, each one cutting off the reader's echo of the letter just typed. The counter element was simultaneously the visible `{n}/{max}` digits, the carrier of the translated sentence and the `aria-live` region itself, so "re-render" and "announce" were the same event; the field also had no `aria-describedby`, so focusing it said nothing about the cap at all (#3408).
|
|
520
|
+
|
|
521
|
+
It is now the three-node shape the GOV.UK Design System character-count component uses: the visible digits are `aria-hidden` and purely decorative; the counter sentence (`fields.textarea.characterCount`, unchanged in all ten packs) has moved onto the textarea's `aria-describedby`, so focus reads "Character count: 12 of 500" once and then stays quiet; and a separate visually-hidden `aria-live="polite"` region carries a new near-limit warning, `fields.textarea.charactersRemaining` (new in all ten packs), which stays silent until the value is inside the last 10% or last 20 characters of the cap — whichever the typist reaches first — and updates only after typing pauses for a second. The same 52-keystroke probe now produces zero announcements; a run that types all the way onto a 500-character cap produces five. Any `aria-describedby` the host already supplied (the form renderer's description and error-message ids) is appended to, never replaced.
|
|
522
|
+
|
|
523
|
+
No metadata change: the counter still renders exactly when the field declares `maxLength` (or the legacy `max_length`), and a widget rendered with no `I18nProvider` still shows the same English sentences.
|
|
524
|
+
|
|
525
|
+
## 17.2.0
|
|
526
|
+
|
|
527
|
+
### Minor Changes
|
|
528
|
+
|
|
529
|
+
- a889e31: A record's approval band now shows the quorum / per-group tally the server already computes.
|
|
530
|
+
|
|
531
|
+
The showcase's `showcase_committee_quorum` node declares `behavior: 'quorum'` with
|
|
532
|
+
`minApprovals: 2` over three approvers, and even ships a pre-rendered
|
|
533
|
+
`"Committee Sign-off (2 of 3)"` label; `showcase_expense_signoff` declares
|
|
534
|
+
`per_group` (会签) with named manager / finance groups. On the business record
|
|
535
|
+
the approval band rendered none of it — the lock badge, the recall button and
|
|
536
|
+
the approve/reject actions were all correct, but a two-of-three committee step
|
|
537
|
+
looked exactly like a one-approver step. An approver could not see whether their
|
|
538
|
+
own click finalized the node or was one of three, which is the single fact a
|
|
539
|
+
quorum node exists to express (objectstack#4478).
|
|
540
|
+
|
|
541
|
+
Nothing was wrong on the wire, and nothing here papers over the server. The
|
|
542
|
+
framework computes `decision_progress` — `{ behavior, got, need, groups? }`,
|
|
543
|
+
derived from the node's own `node_config_json` snapshot, so the count a client
|
|
544
|
+
shows is the count the engine will enforce. **It attaches that block in
|
|
545
|
+
`getRequest` only**: `listRequests` deliberately skips it, because the
|
|
546
|
+
`sys_approval_action` tally it costs is per row and a list read may return
|
|
547
|
+
hundreds. The record header's `useRecordApprovals` reads
|
|
548
|
+
`GET /approvals/requests?object=…&recordId=…` — the list route — so the
|
|
549
|
+
enrichment was never in the payload it had. The hook now follows up with one
|
|
550
|
+
single read for the ONE pending row and folds the result onto it; a failed or
|
|
551
|
+
mismatched follow-up leaves the row exactly as the list sent it, so a display-only
|
|
552
|
+
enrichment can never take the approval panel down and no tally is ever invented.
|
|
553
|
+
|
|
554
|
+
`InlineEditProvider` carries the block through as `approvalProgress`, and the
|
|
555
|
+
DetailView approval band renders it beside the existing badge: a labelled
|
|
556
|
+
`role="progressbar"` with one tick per required approval for `quorum` /
|
|
557
|
+
`unanimous`, and for `per_group` a chip per group marking which have signed
|
|
558
|
+
(`finance 1/1` ✓, `manager 0/1`). Group names come from the flow author's own
|
|
559
|
+
config, so they need no locale strings; the three new label keys are added to all
|
|
560
|
+
ten packs. `first_response` nodes carry no `decision_progress` and are unchanged —
|
|
561
|
+
one decision is the whole step there, and a "1 of 1" bar would be noise.
|
|
562
|
+
|
|
563
|
+
Scored `minor` rather than `patch`: this is new observable rendering plus a new
|
|
564
|
+
public `approvalProgress` prop / `ApprovalProgress` type on `@object-ui/react`,
|
|
565
|
+
not a behavior correction inside an existing surface.
|
|
566
|
+
|
|
567
|
+
### Patch Changes
|
|
568
|
+
|
|
569
|
+
- 4b470b9: Localize the environment entitlement dialog and read cloud's nested error envelope.
|
|
570
|
+
|
|
571
|
+
The free-plan "Development environments are a paid feature" prompt was built from
|
|
572
|
+
English string literals in `entitlements.ts` — including the lowercase `your free
|
|
573
|
+
plan` sentence users reported (cloud#959). Both spec builders now take a translator
|
|
574
|
+
and resolve `environment.entitlement.*`; all ten locale packs carry the strings.
|
|
575
|
+
`entitlements.ts` stays dependency-free: `t` is passed in, not imported, and
|
|
576
|
+
defaults to the English copy with local `{{token}}` interpolation.
|
|
577
|
+
|
|
578
|
+
The dialog now renders the Console's own copy rather than the server's prose — a
|
|
579
|
+
control plane upgrades independently and only localizes these messages from
|
|
580
|
+
cloud#959 on, so preferring the server string left the reactive path English
|
|
581
|
+
against every older deployment.
|
|
582
|
+
|
|
583
|
+
Also fixes the reactive dialog not firing at all: cloud#948 moved coded errors into
|
|
584
|
+
a nested envelope (`{ success, error: { code, … } }`), and
|
|
585
|
+
`entitlementDialogFromError` read `code` off the top level — returning `null` for
|
|
586
|
+
every entitlement 403, so the upgrade dialog degraded to a generic red error toast.
|
|
587
|
+
Both shapes are read now.
|
|
588
|
+
|
|
3
589
|
## 17.1.0
|
|
4
590
|
|
|
5
591
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -61,6 +61,32 @@ Wraps your application with i18n context:
|
|
|
61
61
|
</I18nProvider>
|
|
62
62
|
```
|
|
63
63
|
|
|
64
|
+
#### Language persistence
|
|
65
|
+
|
|
66
|
+
The active language is a **user preference**, so it survives a reload: every
|
|
67
|
+
language change is written to `localStorage` under `objectui-locale`
|
|
68
|
+
(`LOCALE_STORAGE_KEY`), and the provider boots the next session in that
|
|
69
|
+
language.
|
|
70
|
+
|
|
71
|
+
Precedence at bootstrap: **stored choice → browser language → `defaultLanguage`**.
|
|
72
|
+
An explicit choice outranks browser detection — otherwise a user who picked 中文
|
|
73
|
+
on a `ja` browser would be handed `ja` back on every reload. A stored value the
|
|
74
|
+
app no longer offers (not a built-in pack, not in `config.resources`) is ignored
|
|
75
|
+
*and purged*, so a stale entry can never lock the UI to a locale with no
|
|
76
|
+
translations.
|
|
77
|
+
|
|
78
|
+
```tsx
|
|
79
|
+
// Fixed-language surfaces (previews, demos, screenshot harnesses) opt out —
|
|
80
|
+
// they neither restore nor write the preference.
|
|
81
|
+
<I18nProvider config={{ defaultLanguage: 'en' }} persistLanguage={false}>
|
|
82
|
+
<Preview />
|
|
83
|
+
</I18nProvider>
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Bringing your own `instance`? Then its bootstrap language is yours to choose —
|
|
87
|
+
read the preference with `readStoredLanguage()` and pass it to `createI18n`.
|
|
88
|
+
Switching through such an instance is still persisted.
|
|
89
|
+
|
|
64
90
|
### useObjectTranslation
|
|
65
91
|
|
|
66
92
|
Hook for translations and language management:
|
package/dist/index.d.ts
CHANGED
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
* @packageDocumentation
|
|
32
32
|
*/
|
|
33
33
|
export { createI18n, getDirection, getAvailableLanguages, type I18nConfig, type TranslationKeys } from './i18n';
|
|
34
|
-
export { I18nProvider, useObjectTranslation, useI18nContext, type I18nProviderProps } from './provider';
|
|
34
|
+
export { I18nProvider, useObjectTranslation, useI18nContext, LOCALE_STORAGE_KEY, readStoredLanguage, type I18nProviderProps, } from './provider';
|
|
35
35
|
export { createSafeTranslation, useSafeTranslate } from './useSafeTranslation';
|
|
36
36
|
export { useObjectLabel, useSafeFieldLabel } from './useObjectLabel';
|
|
37
37
|
export { builtInLocales, isRTL, RTL_LANGUAGES } from './locales/index';
|