Skip to content

fix(i18n): converge the merged Loading… group to one rendering per language - #6612

Merged
os-sales merged 1 commit into
mainfrom
claude/issue-5972-loading-group-per-language-uniformity
Aug 27, 2026
Merged

fix(i18n): converge the merged Loading… group to one rendering per language#6612
os-sales merged 1 commit into
mainfrom
claude/issue-5972-loading-group-per-language-uniformity

Conversation

@claude

@claudeclaudeBot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Fixes#5972

objectui#3878 converged all ten packs on the typographic ellipsis and, as a side effect, merged the ASCII Loading... group into the U+2026 Loading… group. Nobody re-measured the wording afterwards. This converges each pack to one rendering across that merged group and pins it beside the glyph rule that created it.

Working the grading ruling (concentrated triage, 2026-08-25), not the card body's "Suggested disposition", which the ruling superseded.

Population, re-derived on today's tree

Not taken from the card's table. Flattened all ten packs from source and selected every key whose en value is byte-identically Loading…, on origin/main @ 22ba9271f:

10 keys — lookup.loading, common.loading, fields.recipient.loading, grid.import.historyLoading, grid.bulk.loading, detail.loading, report.loading, dashboard.loading, auth.device.loading, approvalsInbox.loadingMore.

Tallies matched the filer's reading exactly: de four renderings, ko two, ar two; en/zh/ja/fr/es/pt/ru unanimous at ten.

What moved — 9 values, copy only

packtokeys
deWird geladen…detail.loading, report.loading (were Laden…); approvalsInbox.loadingMore (was Lädt…)
ko로딩 중…fields.recipient.loading, grid.bulk.loading, grid.import.historyLoading, approvalsInbox.loadingMore (were 불러오는 중…)
arجارٍ التحميل…common.loading, detail.loading (were جاري التحميل…)

Each direction is the group majority and independently supported:

  • de — the passive is also the pack's dominant register for in-flight states generally: 37 values whose en is a bare gerund render as Wird ….
  • ko — matches the pack's own pattern. 불러오는 중 is what ko uses when the string names the thing being loaded (객체를 불러오는 중…, 에이전트를 불러오는 중…, 초대를 불러오는 중…); the bare form, which is what all ten of these are, is 로딩 중 (그리드 로딩 중…, 차트 로딩 중…, 양식 로딩 중…).
  • ar — see the classification below.

No key is added or removed, no en value is touched, and every value keeps its U+2026. check:i18n-drift says so directly: "No en value changed in this range."

The ar pair is an orthography split, not a wording split — reported as its own class

The dispatch asked whether the ar difference is a separate class rather than one more majority vote. It is.

جارٍ and جاري are not two translations; they are two spellings of one word, differing by a single code point at the end of the first word. جارٍ (jārin) is the indefinite منقوص participle — final yāʾ dropped, tanwīn on the rāʾ — which is the prescriptive MSA form; جاري retains the yāʾ and is the definite/annexed form, common informally.

The consequence that matters: the split does not respect this group's boundary. Counted pack-wide with a Unicode-aware standalone-word match (positive and negative controls run, including the التجارية substring trap an ASCII-boundary regex falls into), the participle is جارٍ ×90 against جاري ×10 — and only 2 of those 10 are in this group. Converging the 2 is what the ruling asks for and what the pin can hold; the other 8 are filed as objectui#6610 rather than smuggled in here, the same way #3878 fenced off the per-package fallback tables. Read this PR's green as a statement about this group only.

The fork: deauth.device.loading stays Lade…

The card floated two members as possibly deliberate. Measurement splits them.

approvalsInbox.loadingMore — hypothesis falsified, converged. The theory was that a continuation load may want its own wording. In de, the namespace's other four in-flight states are all passive (Wird genehmigt…, Wird abgelehnt…, Wird zurückgezogen…, Wird erneut eingereicht…), so Lädt… broke with its own neighbours rather than marking anything — converging it restores the namespace. In ko, the same 불러오는 중… sits on three plainly initial loads (fields.recipient.loading, grid.bulk.loading, grid.import.historyLoading), so it cannot have been marking continuation either.

auth.device.loading — genuine fork, not picked.apps/console/src/pages/auth/DeviceAuthPage.tsx renders all three of that namespace's in-flight states, and de writes all three in the same first-person voice: Genehmige…, Ablehne…, Lade…. The first two are outside this group (their en is Approving…/Denying…), so converging the third alone would leave that one screen reading Genehmige… / Ablehne… / Wird geladen… — a new same-screen inconsistency manufactured by the pass meant to remove one. Resolving it the other way means moving the whole namespace to the passive, which is a copy-voice decision over keys this card does not fence in.

So it is held out by name, in LOADING_GROUP_FORKS, with the reason. The decision is filed as objectui#6611 — which also records a second thing found while measuring: Ablehne… is not a grammatical German form (ablehnen is separable; first-person singular is ich lehne ab). That weakens, without settling, the reading that the first person was deliberate here. objectui#6611 stays open either way; this PR does not act on it.

The pin, and proof it can go red

packages/i18n/src/__tests__/ellipsis-glyph-3878.test.ts gains a second describe. #3878's own assertions are untouched — the glyph invariant, the CONVERGED_KEYS census, the four drift waivers and the placeholder check all stand exactly as they were; this adds a rule beside them.

The new block derives the group from en and asserts membership and per-language value counts before asserting uniformity, because a uniformity assertion over an empty set passes while checking nothing. Ablation, run from the committed tree with a restoring trap, each mutation confirmed on disk by grepping for the injected and removed text and each restore proven by an empty git diff HEAD:

legmutationpredictedobserved
Adereport.loading back to Laden…uniformity redRED — "de: 2 renderings — Wird geladen… on 8 keys | Laden… on report.loading"
Benreport.loadingLoading report…membership redRED — "the group emptied or changed size: expected 9 to be 10", and the uniformity assertion also fired on en (diagnostics multiplied — the predicted direction held, plus one)
Cdeauth.device.loadingWird geladen…fork pin redRED — "de auth.device.loading moved — update or drop its LOADING_GROUP_FORKS row"

Leg C is the point of the exemption: the waiver cannot outlive its reason. The pin asserts both the forked value and that it still differs from the converged rendering, so converging it later fails until the row is deleted.

No rebuild was needed for the ablation and none could have masked it: the test imports ../locales/index — a relative source import, not the package exports field — and packages/i18n has no vitest.config.* of its own, so nothing resolves through dist/.

Verification

All of the below ran on the final commit, daa1f57, with a clean tree.

pnpm exec vitest run --maxWorkers=2 packages/i18n/
Test Files 56 passed (56)
Tests 917 passed (917)
pnpm --filter @object-ui/i18n run type-check (tsc --noEmit && tsc -p tsconfig.test.json)
pnpm --filter @object-ui/i18n run lint
pnpm check:control-bytes ✅ OK (scanned 5455 tracked text file(s); skipped 85 binary)
pnpm check:i18n-keys Scanned 1573 files, 3786 call sites: 2586/2586 literal keys resolve
pnpm check:i18n-drift No en value changed in this range.
node scripts/check-changeset-presence.mjs ✅ 4 source file(s) of 1 released package changed, 1 changeset declared
node scripts/check-changeset-no-major.mjs ✅ No changeset declares a `major` bump.

Chained with && under the shared verify lock, whose verdict line certifies the whole chain: VERDICT command-exit 0.

The new test file really is type-checked rather than excluded: tsc -p tsconfig.test.json --listFiles lists ellipsis-glyph-3878.test.ts in the program.

Lint is a narrowed run, and the narrowing is measured, not assumed.pnpm --filter @object-ui/i18n run lint is eslint . inside the only package this PR touches. Population read from eslint's own file selection rather than my guess: --format json reports 82 files under packages/i18n, 0 errors and 34 pre-existing warnings, with all four edited files present at 0/0. Invariance for everything not in the diff: eslint.config.js configures no type-aware linting (no parserOptions.project, no projectService), so a string-literal change in three locale files plus one new test file cannot move the verdict on any file it does not itself contain. The repo-wide farm runs in CI regardless.

Changeset: .changeset/5972-loading-group-per-language-uniformity.md, patch on @object-ui/i18n (user-visible copy; never major, per the fixed-group rule).

Out of scope, filed rather than fixed

  • objectui#6610 — the ar participle spelled two ways pack-wide (92:8 after this PR); the 8 remaining are outside this group and ungated. Not addressed here.
  • objectui#6611 — the deauth.device in-flight register, this PR's reported fork, plus the ungrammatical Ablehne…. Not addressed here.

Neither is touched by this branch. The card body's other open question — whether approvalsInbox.loadingMore's en should become Loading more… — is the forked outcome the dispatch fenced off, so it is reported and not taken; note the surface already reads Load more on the button (approvalsInbox.loadMore) with Loading… while it fetches, which is coherent as it stands.


Generated by Claude Code

…language
objectui#3878 merged the ASCII and U+2026 ellipsis groups, leaving the ten keys
whose `en` value is `Loading…` rendered four ways in `de`, two in `ko` and two in
`ar`. Converge each pack on its majority/most-idiomatic form — de `Wird geladen…`,
ko `로딩 중…`, ar `جارٍ التحميل…` — and pin per-language uniformity beside the glyph
rule that created the group.
`de` `auth.device.loading` stays `Lade…`: it is register-coherent with its two
namespace siblings `Genehmige…`/`Ablehne…` on the same DeviceAuthPage screen, and
those are outside this group. Reported as a fork and pinned as a named exemption
rather than picked.
Translation copy only — no key added or removed, no `en` value touched, every
value keeps its U+2026. Card relationships are declared once in the PR body,
since this branch squashes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CRJge11jso9TpXRWFt1Z49
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3236.3 KB3266.6 KB
Main entry chunk (gzip)157.1 KB350 KB
Entry fileindex-CccYWKEC.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)11.89KB4.50KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)506.01KB114.64KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)173.10KB47.96KB
fields (index.js)238.89KB60.02KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.53KB3.38KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.64KB1.50KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)1.93KB0.88KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.85KB12.89KB
plugin-charts (index.js)64.66KB18.32KB
plugin-chatbot (index.js)190.33KB45.10KB
plugin-dashboard (index.js)133.48KB34.49KB
plugin-designer (index.js)212.80KB43.15KB
plugin-detail (index.js)245.29KB62.39KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)131.78KB32.19KB
plugin-gantt (index.js)165.16KB40.33KB
plugin-grid (index.js)201.66KB54.58KB
plugin-kanban (index.js)53.11KB14.62KB
plugin-list (index.js)112.86KB27.54KB
plugin-map (index.js)20.09KB6.62KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)26.72KB7.71KB
plugin-tree (index.js)9.26KB3.13KB
plugin-view (index.js)85.87KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)65.97KB21.98KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)2.44KB1.21KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)12.13KB3.65KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)7.54KB2.63KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-sales
os-sales marked this pull request as ready for review August 27, 2026 13:40
@os-sales
os-sales added this pull request to the merge queueAug 27, 2026
Merged via the queue into main with commit 5ac2e2cAug 27, 2026
30 checks passed
@os-sales
os-sales deleted the claude/issue-5972-loading-group-per-language-uniformity branch August 27, 2026 13:53
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants

@os-sales@claude