fix(console,i18n): localize the Applications page's own chrome, and keep the server's words verbatim (#4307) - #4344
Merged
Conversation
…eep the server's words verbatim (#4307) `AppManagementPage` was raw English end to end — headings, search, the selection and bulk controls, six per-row actions with their tooltip/ARIA pairs, the status badges and every toast. It was the last un-i18n'd system page, and #4233 / PR #4300 had just given it four live mutations, so the gap became user-visible exactly when operators started using it. 45 keys land under `appManagement.*` in all ten packs, reached through `useObjectTranslation` with the call site's `defaultValue` inline — the convention `ProfilePage` and `ApprovalsInboxPage` already follow. The split that shapes this change is between the strings the PAGE authors and the strings the SERVER authors. A refusal like `forbidden: manage_metadata required` is the server's diagnosis of one request, and there is no fixed catalogue of those to key against, so each failure toast is a keyed template with a `{{reason}}` hole filled byte for byte. The one part that is the page's own — what it says when the server sent nothing — is keyed as `appManagement.toast.unknownError`. Two consequences of converting it properly rather than mechanically: the per-entry template and separator of a bulk toast are keys, because bracket style and list punctuation are locale properties (`validation. formInvalidJoiner`'s rule and its past defect); and the row controls name an app through the resolver the visible heading already used, with `t` passed — a keyed label previously rendered `Select [object Object]`. 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. |
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
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 #4307
Refs #4233, PR #4300 (the four live mutations this page gained), #4208 / #4163 (the keyed-label resolver used below).
apps/console/src/pages/system/AppManagementPage.tsxwas raw English end to end — headings, the search field, the selection and bulk controls, six per-row actions with their tooltip/ARIA pairs, the status badges, and every toast. It was the last un-i18n'd system page, and #4233 / PR #4300 had just wired its four mutation handlers, so the localization gap became user-visible on every non-English console exactly when operators started using the page for real work.45 keys land under
appManagement.*in all ten packs.(Angle brackets are spaced in this body —
< h1 >— so GitHub's sanitizer does not eat the DOM snippets, the same precaution PR #4208's body took. The tools printed them unspaced.)Gate
#4334(the #4294 sort-hint reword, samepackages/i18n/src/locales/*.tsfiles) was confirmed squashed onorigin/mainas7f1cb3323before this worktree was created; the branch is based on that commit. The locale diffs here are purely additive —git diffshows zero deletions inpackages/i18n/, so #4334's reworded key is untouched.Convention measured, not assumed
The card says to follow how the #4208-era pass keyed sibling system pages, so that was measured rather than guessed:
useObjectTranslationfrom@object-ui/i18n(ProfilePage,ApprovalsInboxPage)profile.*,approvalsInbox.*) —console.*is reserved for shell-level chromeappManagement.*t('key', { defaultValue: 'English' }), inline, matchingenbyte for byteresolveKeyedI18nLabel(label, t)—tpassed (AppSidebar,DashboardView,SearchResultsPage)Translations were taken from neighbouring keys rather than machine-translated:
home.stats.apps/layout.systemNav.applicationsfor the page title,view.setAsDefault,home.appCard.default,console.objectView.enableSearchfor the enable/disable verb per pack,console.error.connectionFailedfor the unreachable-service register, andfields.file.uploadFailed(Failed to upload "{{name}}": {{error}}) as the model for the failure templates.The one thing this PR is careful about: whose words are whose
The card makes this binding, and it is the axis the whole change is organized around.
PUT/DELETE /api/v1/meta/app/:nameis gated onmanage_metadata(ADR-0066 D1), so a refusal likeforbidden: manage_metadata requiredis the server's diagnosis of one specific request. There is no fixed catalogue of those sentences for a pack to key against. So every failure toast is a keyed template with a{{reason}}hole, and what fills the hole is interpolated raw:The single exception is the page's own word for "the server said nothing at all" —
reason()'s empty-message fallback — which is nowappManagement.toast.unknownErrorrather than a hardcoded'unknown error'. That is the only part of that string the page authors.Two decisions inside the conversion
1. The bulk toast's entry template and separator are keys, not literals.
Failed for 2: CRM (forbidden); Ops (in use)was assembled from a hardcoded`${name} (${reason})`and a hardcoded'; '. Both are now pack-owned (bulkFailureEntry,bulkFailureJoiner), because bracket style and list punctuation are locale properties — the CJK packs set their own. This is not a new opinion:validation.formInvalidJoinercarries a comment recording the defect that came from hardcoding exactly this (objectstack#5407, a CJK comma in an English toast), anddetail.userStatusTitleis keyed for its parentheses for the same reason.2. The row's controls now name an app through the resolver, with
t. This is the one behavioural change beyond pure keying, so it is called out rather than buried. The controls interpolatedapp.label || app.namedirectly while the visible heading two lines away in the same file already calledresolveKeyedI18nLabel(app.label). An app carrying objectui's keyed label form therefore renderedaria-label="Select [object Object]"on all six controls.Writing a fresh
name: app.label || app.namecall site would have knowingly re-authored that bug; oneappTitle(app)helper resolves it once per row, passest(so a keyed label reaches the pack instead of stopping at its authoringdefaultValue), and every label and toast in the row names the app the same way. The reverse verification below shows the pre-change string actually being produced.Tests
Two files, split by what they can actually see.
AppManagementPage.mutations.test.tsx(from PR #4300) keeps every assertion unchanged. Itstmock echoes the call site'sdefaultValueand fills the holes — the console convention (useConsoleActionRuntime.test.tsx,sharedInboxFeed.twoSurfaces.test.tsx) — so'Set Ops as default'and'2 apps disabled'still read as English, because those are still the sentences an English console renders. Re-spelling them as key names would have quietly moved that suite off its subject, which is the write, not the wording. The only edit is the mock itself (partial, viaimportOriginal, because@object-ui/componentsbuildscreateSafeTranslationprobes at module scope).That mock is also why a second file was necessary: a page that never called
tat all renders identical English and passes every one of those cases. SoAppManagementPage.i18n.test.tsx(new, 9 cases) mockstto answer in no natural language —«appManagement.title»,«appManagement.selectApp»[name=Ops]— which is what a non-English pack does to this page in the respect that matters: the English disappears. Each assertion is two-sided, naming the key that must be asked for and re-checking that the retired English sentence is nowhere on screen.Local runs — repo-root vitest (objectui#3378),
flock /tmp/os-heavy-verify.lock,NODE_OPTIONS=--max-old-space-size=4096,--maxWorkers=2:Both packages define two
tscinvocations each, so that is all four.i18n and byte gates
0 en value(s) changedis the mechanical confirmation of the gate note above: 45 keys added, no existing value edited.ESLint on the changed files: 0 errors, 14 warnings, all the pre-existing
no-explicit-anyconvention this page and its sister test already carry for app records.Reverse verification
Direction predicted before running: reverting only the page must turn the new i18n file red on both halves of each assertion, while the mutations suite stays green — that asymmetry is the entire reason the second file exists. Taken out with a scratchpad file swap after committing, never
git stash.Exactly as predicted: 9/9 red in
AppManagementPage.i18n.test.tsx, 12/12 green inAppManagementPage.mutations.test.tsx.Two assertion orders were tightened after this run, because the first pass showed the two sweep cases dying on a precondition (a sentinel query used as setup) rather than on their own claim. With that fixed, the revert now reports the real thing in each:
That second block is the
[object Object]defect from decision 2 above — demonstrated by the pre-change page rather than asserted in prose.Out of scope, filed not fixed
#4343 — the same file's search filter calls
(app.label || '').toLowerCase(), which throwsTypeErrorfor that same keyed label form (an object is truthy, so the|| ''guard does not catch it), taking out the page on the first keystroke. It is a read on a line this card does not touch and a different defect class from chrome localization, so it is filed unassigned for triage rather than ridden in here. Whether any shipping app metadata actually carries a keyedlabeltoday is not measured — that grading is the triage round's.Generated by Claude Code