fix(app-shell): organization & invitation UI translates its six English holdouts (#4474) - #4496
Conversation
…sh holdouts (#4474) Three string families across `console/organizations/`, one sweep: 1. Role names route through the single shared ORG_ROLE_LABELS map at every site. The members/invitations badges and the accept page rendered the raw server identifier under CSS `capitalize`; the map's four `organization.roles.*` keys existed in no pack, so even the dropdown that did consult it resolved to English. All ten packs now carry them. 2. Server-echoed errors are mapped by better-auth's stable `code`, never by matching English text. `createAuthClient` dropped that code for every `organization.*` call while preserving it for sign-in/sign-up, so no consumer could have keyed on it; all sixteen now share `toAuthError`. Messages are unchanged. An unmapped code degrades to the server's sentence. 3. Icon-only aria-labels are translated — the only name a screen reader gets. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
Both assert the same capability as before; only the string they query changed. - acceptInvitationLink.mount.test.tsx (#4472/#4480): the share-link copy button was labelled a bare "Copy" and now carries the Invitations tab's own "Copy invitation link". Every URL assertion is untouched. - AcceptInvitationRoute.test.tsx (#3811): the role row rendered the raw `sys_member.role` identifier and now renders the display name, so `admin` reads as `Admin` in en and 管理员 in zh. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
CI state at handoff: 15 green, 0 red, 5 wedged on a stalled runnerRecording this so the five Green (15): Lint, Build & E2E, Live E2E, Bundle Analysis, Build Docs, Control Byte Scan, Changeset Declaration, Changeset Bump Policy, Changeset Fixed Group Check, Internal Docs Link Check, Skill Guide Path Check, label, Vercel Preview Comments (+2 skipped). Red: none. Wedged (5): Type Check and Test shards 1-4 — all in CI run 31641627123, which has reported Evidence the stall is infrastructural rather than this change:
Locally the same commands are green: A re-run of that one workflow is the likely fix, but that is the reviewer's call — I have not touched the run. Generated by Claude Code Generated by Claude Code |
|
ACCEPT (pending CI convergence) — step-7 复核 by PM session
Generated by Claude Code Generated by Claude Code |
`generateTimeScaleHeaders` is reachable from the package entry (`index.tsx` does `export * from './renderer'`) and its published declaration grew an optional trailing `locale?: string`. Entry-reachable additive API growth is minor, not patch — patch is for changes with no API-surface movement at all. `dist/index.d.ts` being byte-identical does not argue for patch: the entry re-exports by reference, so the resolved public surface moved even though the entry file's bytes did not. The contrapositive of #4496, which was graded patch precisely because its .d.ts additions were NOT re-exported from the entry; #4403 / #4177 / #4485 / #4495-regrade are the line this follows. Additive and back-compatible is what minor means — no consumer breaks, existing three-argument callers keep compiling and keep producing byte-identical output. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
Closes #4474
Three string families across
packages/app-shell/src/console/organizations/, one sweep, per the ruling in #4474 (comment).The defect table, as measured on
origin/mainat 338e2c4Captured verbatim from a pre-fix red run of the new zh-locale render test.
zhsessionOwner,Admin,Delegated Admin,Memberowner,member(raw, under CSScapitalize)You are not allowed to invite users to this organizationOrganization already existsYou are not the recipient of the invitationaria-labelsMember actions,Copy invitation link,Cancel invitationOne correction to the card, and it makes the defect larger. The card reports the dropdown's siblings already rendering 所有者 / 管理员 / 成员 beside an untranslated
Delegated Admin. That is not what this tree does:ORG_ROLE_LABELSnames fourorganization.roles.*keys that no locale pack defined, so all four entries fell through to their inline EnglishdefaultValue. The reporter's server was on an unverified commit, which is the likeliest source of the discrepancy. The premise — untranslated English in a zh locale — holds; the count does not.Sibling holdouts found in the five named files and fixed with them: the invitations-page role badge; the accept page's role row and the role interpolated into its otherwise-Chinese sentence; the invite dialog's
invitedAsinterpolation; a seventh icon-only button labelled a bareCopy; and five hardcoded English error fallbacks (Failed to load members,Failed to load invitations,Failed to invite member,Invitation not found or expired,Failed to create workspace).The fix
Family 1 — one role-label map, consumed everywhere. The map already existed and the dropdown already read it; the badges read nothing, rendering the raw server identifier under a
capitalizethat madeownerpass for a label in English. So the two sides never disagreed about a translation — one side was not translating at all, and CSS hid it. Every role display now routes throughresolveOrgRoleLabel(role, t)over the singleORG_ROLE_LABELS, keyed by the identifier the server stores insys_member.role. An unrecognized role renders verbatim rather than blank. The four keys are now in all ten packs.Why the gate never caught this:
scripts/check-i18n-call-site-keys.mjsfound and paid off 258 keys of exactly this class, but it scans string literals and these are reached asORG_ROLE_LABELS[r].key— a variable.Family 2 — mapped by
code, never by matching English. Measured first, and the measurement changed the shape of the fix:packages/auth/src/createAuthClient.tshas atoAuthErrorhelper whose own doc comment says it preserves the machinecode"so callers can map it to a localized message instead of surfacing the raw English server text" — wired to sign-in and sign-up only. All sixteenorganization.*methods threwnew Error(error.message ?? '…')and dropped the code at the boundary. The three sites therefore held an English sentence and nothing else, so no consumer-side change short of string-matching could have localized them.That is a producer defect, so it is fixed at the producer: the sixteen methods now share
toAuthError. Messages are unchanged — the code simply stops being thrown away.resolveOrgErrorMessage(err, t, fallback?)is the consumer half, mapping eight codes and degrading to the server's own sentence for anything else (prefer mapped, degrade to verbatim — the shapeLoginForm/RegisterFormalready use).Family 3 — icon-only
aria-labels now come from the same channel; for an icon-only control this is the only name a screen reader gets.Red-first
org-i18n-holdouts-4474.test.tsxmounts the realI18nProviderand mocks everything else. That is load-bearing: every sibling test here stubs i18n ast: (key, o) => o.defaultValue, which is an en oracle — under itDelegated Adminis the correct answer and the defect is invisible.Pre-fix, 13 of 17 red. Verbatim:
Reverse verification, direction predicted before running. The console test mocks
useAuthand constructs its own coded error, so it pins the consumer link and is structurally blind to the producer.packages/auth/src/__tests__/org-error-code-4474.test.tsdrives the real better-auth client through a mockedfetch. RevertingcreateAuthClient.tstoorigin/mainturns exactly 3 of its 5 cases red — the three.codeassertions (expected undefined to be 'ORGANIZATION_ALREADY_EXISTS') — while both.messageassertions stay green. That split is the point: the message was always correct, which is precisely why a message-only test could never have caught this, and why the producer fix needed its own pin rather than riding on the console suite.Must-not-change (green both before and after): en renders the same English through the same channel — the dropdown reads
Owner / Admin / Delegated Admin / Member, the badgesOwner / Member, the aria-labelsCopy invitation link/Cancel invitation, and an unmapped code still shows the server's sentence verbatim.Grading
.d.tsdiff over@object-ui/auth+@object-ui/app-shellis purely additive — two new module declarations, nothing removed or narrowed, andpackages/auth's emitted types are byte-identical (the producer change is module-private). Patch.Verification
pnpm exec vitest run packages/i18n/src/__tests__ packages/auth/src/__tests__ packages/app-shell/src/console/organizations— 66 files, 1015 passedpnpm exec vitest run packages/app-shell— 360 files, 3456 passed, 1 skippedpnpm --filter @object-ui/app-shell --filter @object-ui/auth --filter @object-ui/i18n type-check— bothtsc --noEmitandtsc -p tsconfig.test.json, greennode scripts/check-i18n-call-site-keys.mjs— green (2352/2352 literal keys resolve, 888 inline defaults match theirenvalue)node scripts/check-i18n-en-drift.mjs— green (19 keys added, 0envalues changed)--filter '...@object-ui/auth'— 5 packages green (auth,app-shell,console-starter,byo-backend-console,apps/console)linton the three packages — exit 0Two existing tests query strings this fix moved and were retargeted in a separate commit; both keep their original assertions.
AcceptInvitationRoute.test.tsx(#3811) asserted the rawadminand now assertsAdmin;acceptInvitationLink.mount.test.tsx(#4472/#4480) queried the bareCopylabel — its URL assertions are untouched, andresolveConsoleUrlis undisturbed.Scope
Strings only. Role/permission gating is untouched (#4475 stays pooled) —
invitableOrgRoles/assignableOrgRolesand every predicate around them are unchanged, so who sees which button is exactly as before.Generated by Claude Code