Retire the three i18n exemptions once the platform ships their keys — and re-take the two English-island screenshots - #125
Merged
Conversation
… refuses
`@objectstack/*` ^17.2.0 -> ^17.3.0 across all 14 packages. That release
(objectstack#14381) gives all three of this app's declared i18n exemptions a
bundle key, and tightens one multi-row hook contract this app was on the wrong
side of.
i18n — 84 strings that reached a zh-CN user in English now do not:
dataset label / description / dimension / measure labels (36) -> the new
`datasets` group, resolved by `translateDataset`.
a view's `bulkActionDefs` copy (35) -> `_views.<v>.bulkActions`, resolved by
`translateView`.
a custom validation rule's `message` (13) -> `_validations.<r>.message`,
resolved by `@objectstack/objectql` at THROW time, in the locale of
whoever was stopped.
Each verdict in `authored-text.ts` records where its key is resolved, because
in two of the three cases it is NOT the document translator a reader would
reach for first: `translateObject` does not rewrite `validations[].message`,
and `translateView` reads `bulkActionDefs` off `view.config`. Measured on the
installed 17.3.0, not read off the schema.
The platform pin in `test/i18n-coverage.test.ts` was RE-DERIVED, not widened —
it went red on the bump, before anything else was touched, which is what it is
for. The three counts it holds are pinned at zero rather than deleted, so the
gap cannot silently reopen.
task.hook.ts — the clearing branch was gated on `wasDone`, this row's
pre-image, so a predicate write reopening a mixed batch wrote the nulls for
some matched rows and not others. 17.3.0 refuses that outright with
`MultiUpdateHookKeyDivergenceError`. Gated on the PAYLOAD's status instead,
the key set is row-invariant and the write goes through; null was already the
correct value for every row leaving `done`, including one never completed.
The completing direction is untouched — that divergence is the one the
platform is right to refuse.
Gates on 17.3.0: validate 0 · typecheck 0 · test 872/872 · build 0.
Closes #106
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
… proof
The two directional coverage gates named `'zh-CN'` as a literal, so a third
locale that EXISTED but was partial went unchecked — the locale-set assertion
catches a declared-and-missing `ja-JP`, but not a `ja-JP.ts` carrying ten of
the keys. They are now driven by the locales `dulyTranslations` actually
ships. Measured on a synthetic three-locale barrel: a `ja-JP` carrying 2 of
the 363 keys reports 361 missing and goes red, where before it was not looked
at.
Two guards come with the generalisation, because `it.each` over an empty array
passes with zero cases:
- `zh-CN` must be in the derived locale list, so the per-locale gates cannot
quietly become no gates at all.
- the barrel's `zh-CN` entry must BE the authored `dulyChinese` bundle — the
refactor moved the key source from the file to the barrel, and this closes
the gap that move opened.
docs/i18n/ holds the three screenshots from a real `pnpm demo:zh` boot on
17.3.0, taken with the accept-language a Chinese Chrome sends:
01 dashboard — tile sub-labels are Chinese (超过 14 天无动静 / 最久未动 /
按期率 / 逾期未完成 / 审定率), the dataset measure labels that were the
English island. The pie legend is still English: filed as objectui#7696.
02 bulk bar — 已选择 1 项 · 完成 · 跳过 · 清除. "Duly" is the only Latin
run left on the page.
03 a validation refusal in Chinese, from the object's own rule.
Closes #102
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
os-warren
marked this pull request as ready for review
September 5, 2026 06:29
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #106
Closes #102
@objectstack/*^17.2.0→^17.3.0across all 14 packages, the three i18n exemptions retired against the keys that release ships, and the one hook the bump refuses.Gates on
b8bbdb0(the current head — the union below was re-run after mergingmain, on that tree):validate✓ Validation passed ·typecheckexit 0 ·test885 passed (885) ·build✓ Build complete.1. The upgrade, and the hook it refuses
14 packages bumped. Baseline on 17.3.0 before any fix reproduced the dispatch preflight exactly — 870/872, two reds, both this card's:
The second one is a real contract tightening, not a test that drifted:
The clearing branch was gated on
wasDone— this row's pre-image — so reopening a mixed batch wrote the nulls for the rows that had been done and not for the rows that had not. Different key set per row; the platform is right to refuse it.Fixed at the cause: the branch is gated on the payload's status instead.
'status' in inputis the load-bearing half.isDonefalls back to the stored status when the payload carries none, so testing!isDonealone would reopen the same divergence one shape over — an administrative bulk write with nostatus(a re-owner, a business-unit backfill) would write nulls on the not-done rows and skip the done ones. Gated on the payload,isDoneis read off the one shared payload and is identical for every matched row. Null was already the correct value for every row leavingdone, including one never completed, so writing it for all of them is a no-op on the rows that were not done.The completing direction is untouched — that divergence is the one the platform is right to refuse, and the test above it still pins it. Both clearing tests now run and pass (the second was previously skipped only because the first aborted the file):
test/task-hook.test.ts64 passed (64).2. The three exemptions, retired
84 strings that reached a zh-CN user in English now do not. Each verdict in
authored-text.tsrecords where its key is resolved, because in two of the three cases it is not the document translator a reader would reach for first — all measured against the installed 17.3.0, not read off the schema:datasets.<d>.…translateDataset(new in 17.3.0)bulkActionDefscopyobjects.<o>._views.<v>.bulkActions.<def>.…translateViewobjects.<o>._validations.<r>.message@objectstack/objectqlat throw timeTwo measurements worth keeping:
translateObjectdoes not rewritevalidations[].message. Fed a bundle carrying_validations.<rule>.messageit returns the authored English untouched, while the object's ownlabelbeside it translates. The resolution happens inauthoredRuleMessage, in the locale of whoever was stopped — which is the correct design (one stored rule, every locale), and the bridge is live here: booting logs[ObjectQLPlugin] Bridging i18n service to ObjectQL for validation messages.translateViewreadsbulkActionDefsoffview.config, while this walk reads the authored node where it sits at top level. The bundle key depends on neither — only on object, view key, def name, param name — so both halves address the same slot.The pin was re-derived, not widened.
TRANSLATABLE_METADATA_TYPESgaineddataset, and that assertion went red on the bump before any of this file was touched — which is what pinning a set the platform owns is for. Widening the list without extending the walk would have turned the one assertion that noticed into the one that hid it. The three counts it holds are pinned at zero rather than deleted, matching the file's own idiom for the gap #69 closed: a count that is merely deleted cannot notice the gap reopening.Reverse verification — the issue asked for evidence the exemptions were load-bearing rather than decorative. With the exemptions retired and
main's zh-CN bundle (read out of git, no tracked file mutated):Terminology follows the header in
zh-CN.ts(职责 vs 任务 above all): a bulk action's label sits next to a task list, so完成/跳过match the row actions rather than inventing a second vocabulary. The toolbar copy is one sharedTASK_BULK_ACTIONSobject rather than five hand-copies — the platform keys it per view, so the same action in five views is five key sets, and five copies is four that eventually go stale.The exemption list is now designer- and operator-facing text only. No end-user screen is on it.
3. Browser proof — real
pnpm demo:zhboot on 17.3.0Clean
.objectstack/data, port 4319, browser contextlocale: 'zh-CN'. Screenshots indocs/i18n/.01-dashboard-zh.png超过 14 天无动静·最久未动·按期率·逾期未完成·审定率. These are the dataset measure labels: exactly theUntouched > 14 daysisland the issue describes.02-bulk-bar-zh.png已选择 1 项 · 完成 · 跳过 · 清除.Dulyis the only Latin run left on the page.03-validation-refusal-zh.png请写明打回的原因——负责人需要据此知道该改什么。— the object's own rule, refused in Chinese.Two things the browser found that are NOT this card, both filed
a) The work-mix pie legend is still English —
One-off / Recurring / Standing. Not a missing key: those are field option labels, already keyed and already translated (the duty list beside it renders周期性). The server bakes the English into the row:fields[].labelfollows the locale — that is this PR's dataset translation working end to end.rows[].formis byte-identical in all three. objectui#4324 fixed charts where labels resolve client-side; objectui#4330/#4388 fixed local select dimensions on table/pivot. A local select dimension on a chart is the uncovered cell. Filed as objectstack-ai/objectui#7696.b)
accept-language: zhrefuses in English while the same response's dataset labels are Chinese.resolveBundleLocalein the spec already resolveszh→zh-CNby variant match, and every document translator gets it viapickData; the validation-message bridge asks the i18n service with the raw tag and matches exactly, so it misses.zh-CNandzh-CN,zh;q=0.9(what a Chinese Chrome sends) both give Chinese correctly. Filed as objectstack-ai/objectstack#15757. The screenshots use the real-Chrome header form.Neither is worked around here.
4. #102 folded in — it was cheap and in this exact assertion
The two directional gates named
'zh-CN'as a literal, so a third locale that existed but was partial went unchecked. They are now driven by the localesdulyTranslationsactually ships. Proven rather than assumed — a synthetic three-locale barrel with aja-JPcarrying 2 of the 363 keys:Two guards ship with it, because
it.eachover an empty array passes with zero cases:zh-CNmust be in the derived list, and the barrel'szh-CNentry must be the authoreddulyChinesebundle — the refactor moved the key source from the file to the barrel, and that check closes the gap the move opened.Notes for review
validateprints the expectedhierarchy-securitywarning (AGENTS.md: do not silence). The threeduly_assignment_fanouttry_catchwarnings the dispatch addendum named are gone — Assignment fan-out: wrap the loop body in try_catch so one failing person does not abort the fan-out #124 fixed them onmainand this branch now carries that merge.objectstack.config.tsuntouched.🤖 Generated with Claude Code
https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p