Skip to content

i18n(gate): widen the defaults value-compare over the declared hand-rolled tables - #8101

Merged
baozhoutao merged 3 commits into
mainfrom
claude/issue-7877-hand-rolled-fallback-tables-registry
Sep 6, 2026
Merged

i18n(gate): widen the defaults value-compare over the declared hand-rolled tables#8101
baozhoutao merged 3 commits into
mainfrom
claude/issue-7877-hand-rolled-fallback-tables-registry

Conversation

@baozhoutao

Copy link
Copy Markdown
Contributor

Fixes #7877

The B half of the ruling on objectui#7567 Q2. PR objectui#7870 landed the
createSafeTranslation value-compare over the factory-resolved population —
every factory invocation, read from source with no registry. Two tables
hand-roll fallbackT instead of taking the factory, so nothing in their source
says they are defaults tables and that walk cannot reach them. Covering them
takes a declaration, which is why A went first and B was split out.

Head this was measured on: 924edd915.

The declaration: ONE list, not two pinned copies

HAND_ROLLED_TABLES already existed, in packages/test-support (it moved there
from the objectui#3512 test in objectui#7884). The gate is a bare
node scripts/check-*.mjs and cannot import it: exports["./defaults-table-scan"]
resolves to TypeScript source with no build artefact.

That wall already has a ruling — objectui#6923, whose first instance is
zod-wrapper-keys.json in the same package, read by two other .mjs gates in
scripts/. Same shape here: the DATA moves to hand-rolled-tables.json behind
its own exports subpath, resolveJsonModule types it for TypeScript,
createRequire reads the same bytes for node, and defaults-table-scan.ts
keeps the prose JSON cannot carry.

The alternative — a second literal list in the gate, pinned to this one by a test
(the objectui#7310 / PR objectui#8028 shape) — was available and deliberately
not taken: two lists can disagree for the window between one edit and the next
pin run, and the failure mode of a scan population that quietly loses an entry is
"0 drifted" over nothing. A shared declaration was measurable here, so it wins.

⛔ Option C on objectui#7567 (pattern-matching identifier names, or every
Record of strings that looks like a defaults map) is not built and is named as
rejected in both the gate header and the new pin block.

The census, re-measured (not carried forward from the card)

Factory defaults tables: 32 createSafeTranslation site(s) over 32 distinct table(s) (0 unreadable) —
  841 row(s) compared against their en value, 841 matching, 0 on a key en does not define,
  0 with no single comparable en value, 0 unreadable.
Hand-rolled defaults tables: 3 declared, 2 scanned here (1 already covered by the factory walk,
  0 unreadable) — 215 row(s) compared against their en value, 215 matching,
  0 on a key en does not define, 0 with no single comparable en value, 0 unreadable.

Per table, measured by dropping one registry entry at a time (below):
GANTT_DEFAULT_TRANSLATIONS 80, IMPORT_DEFAULT_TRANSLATIONS 135. That matches
the card's 2026-09-06 baseline exactly. Both read 0 drifted, so the widened
half ships blocking in this PR
rather than report-only.

⚠️ One correction to the card: it expected 5 abstaining rows on the factory half
(timeline.relative.*). Measured today that count is 0 — those rows were
retired in objectui#7887. The factory half's abstention counts are all zero on
this tree, and so are the widened half's. See the out-of-scope note at the end.

TIMELINE_DEFAULT_TRANSLATIONS is in the registry (it mirrors the needle-file
set objectui#3512 pins) and reaches the factory, so it is de-duplicated on
the literal's own position and reported as 1 already covered by the factory walk rather than counted twice.

The collapse guard is not weakened

factoryComparedRows < 500 at its original site is untouched. The widened half
gets its own counter and its own floor, handRolledComparedRows < 150.

How 150 was derived rather than picked: the two reachable-nowhere-else tables
compare 80 and 135 rows. The floor sits above the larger of the two, so
losing either table collapses the scan into a failure — not only losing both.
A floor below 135 would let the bigger table carry the smaller one's
disappearance. Demonstrated, one entry dropped at a time:

=== registry WITHOUT GANTT_DEFAULT_TRANSLATIONS ===
  VERDICT exit=1
  The hand-rolled defaults scan collapsed: 135 row(s) compared over 1 table(s) from 2 declared, ...
=== registry WITHOUT IMPORT_DEFAULT_TRANSLATIONS ===
  VERDICT exit=1
  The hand-rolled defaults scan collapsed: 80 row(s) compared over 1 table(s) from 2 declared, ...

Both legs restored under a trap, verified by blob hash (git diff HEAD = 0
bytes both times).

(a) Positive control on the widened half

A real drift planted on disk in GANTT_DEFAULT_TRANSLATIONS — a table the
factory walk structurally cannot see. On-disk mutation proved by counting the
removed and injected text before and after (1/0, then 0/1) and by the blob hash
moving 33fe2321… -> a6a2fcec…:

=== MUTATED RUN ===
VERDICT mutated-exit=1
Hand-rolled defaults tables: 3 declared, 2 scanned here (...) — 215 row(s) compared, 214 matching, ...
1 hand-rolled defaults row contradicts the en value of a key that EXISTS (1 distinct key) — ...
  packages/plugin-gantt/src/useGanttTranslation.ts:25:3  [hand-rolled-default-drift]  gantt.toolbar.jumpToToday  (GANTT_DEFAULT_TRANSLATIONS (packages/plugin-gantt/src/useGanttTranslation.ts))
      en renders:      "Jump to today"
      defaults table:  "Go to today"

=== RESTORED RUN ===
VERDICT restored-exit=0
Hand-rolled defaults tables: 3 declared, 2 scanned here (...) — 215 row(s) compared, 215 matching, ...

Restore proved by blob hash back to 33fe2321… and git diff HEAD = 0 bytes.
The finding carries its own reason, hand-rolled-default-drift, with its own
hint — the two halves are reached differently and the fix for one names a
registry the other does not have.

(b) The staleness reading on the registry — demonstrated, not asserted

A declared list rots when a table is added and not declared. What catches
that is not diligence and does not live in this gate:
packages/i18n/src/__tests__/fallback-placeholder-spelling-3512.test.ts asserts
the set of runtime files carrying the hand-rolled needle equals a pinned list.
Because the gate now reads the same bytes that test reads, that one ratchet
covers both readers.

Shown by planting an undeclared fourth hand-rolled fallbackT:

VERDICT intruder-exit=1
 FAIL  ... > covers the three hand-rolled interpolators, and no fourth exists
+   "packages/plugin-gantt/src/undeclaredFallback7877.ts"
Tests  1 failed | 17 passed (18)

Removed; restored run is 18 passed (18), exit 0.

The other direction — an entry that names a file or a const no longer there —
is covered here: a declaration that does not resolve is counted as an unreadable
TABLE, never skipped, and pinned both synthetically and against main.

(c) The abstention counts are printed

The widened half prints its own summary line in the shape objectui#7870 prints
the factory half's, including all four abstention buckets. objectui#7874 exists
only because the factory half printed its blind spot instead of absorbing it.

Also in this PR: one shared full-repo analyze() run in the pin file

analyze(repoRoot) parses about 1600 files. Seven cases in
scripts/__tests__/check-i18n-call-site-keys.test.ts each paid for their own
walk; the two new cases here took that to nine and pushed the slowest case past
vitest's 15s window under a full scripts/__tests__/ run. AGENTS.md 测试纪律
names that shape and prescribes moving the cost into the import phase rather than
raising the timeout, so the default-options run is now computed once at module
scope. Cases that inject their own registry keep their own runs.

Measured either side, same 112 files / 3348 tests: 219.72s to 113.76s. This
is the same failure objectui#8074 reports; that card is named here without a
closing keyword because this is a measured mitigation of its instance, not
necessarily the whole class it names — objectui#8074 stays open for a maintainer
to judge.

Gates, all on 924edd915

gate verdict
pnpm check:i18n-keys exit=0 (both census lines quoted above)
pnpm exec vitest run scripts/__tests__/check-i18n-call-site-keys.test.ts 137 passed
pnpm exec vitest run scripts/__tests__/ 112 files / 3351 tests passed, exit=0
packages/test-support/ + the 3512 test + the 4401 mirror test 7 files / 104 tests passed
pnpm --filter @object-ui/test-support type-check / lint exit=0 / exit=0
pnpm type-check:scripts exit=0
pnpm lint:root exit=0
pnpm check:i18n-drift · check:i18n-dead-keys · check:control-bytes exit=0
manual control-byte scan of all 5 changed paths no hits
node scripts/check-changeset-presence.mjs exit=0 — no changeset owed
node scripts/check-governed-queue-guard.mjs --test (5 paths) NOT GOVERNED

Every exit code captured by redirect-then-capture, never across a pipe.

Changeset, clause ② confirmed rather than assumed: the checker reports
0 of them published source of a package the release covers, 0 of them a manifest whose published contract moved, 3 under a package changesets ignores.
@object-ui/test-support is private: true and changesets ignores it; scripts/
publishes nothing. No published contract's accept/reject behaviour changes — a
gate's scan population widened.

CI invocation unchanged: ci.yml:487 runs pnpm check:i18n-keys, the same
script name, untouched. Every reader of the gate found by
git grep -l check-i18n-call-site-keys over scripts/__tests__,
.github/workflows and packages/i18n was run.

main moved under this branch while it was in flight (PRs objectui#8081,
objectui#8082, objectui#8087 and two others); origin/main was merged in and
everything above was re-run on the merged head.

Out of scope, filed separately

The class-8 header and one pin comment still say timeline.relative.* is five
abstaining rows on main; measured today it is zero. Not touched here — filed as
a finding rather than ridden in on this PR.

Back-links, none with a closing keyword: objectui#7567 · objectui#7870 ·
objectui#3512 · objectui#7874 · objectui#7454 · objectui#7574 · objectui#8028 ·
objectui#6923 · objectui#7884 · objectui#7887 · objectui#8074.

Drafted by the domain:devx @ objectui seat, session
session_01FhBNJcLRZLe8M87VcUgpKr, round R46.

🤖 Generated with Claude Code

https://claude.ai/code/session_01FhBNJcLRZLe8M87VcUgpKr


Generated by Claude Code

…and-rolled tables

`scripts/check-i18n-call-site-keys.mjs`'s class 8 compared the
factory-resolved population only: every `createSafeTranslation` /
`createSafeTranslationHook` invocation, read from source with no
registry. Two tables hand-roll `fallbackT` instead of taking the factory
(`GANTT_DEFAULT_TRANSLATIONS`, `IMPORT_DEFAULT_TRANSLATIONS`), so nothing
in their source says they are defaults tables and that walk cannot reach
them. Covering them takes a declaration, which is why objectui#7567
shipped the registry-free half first and split this out.

The declaration is the one that already exists — `HAND_ROLLED_TABLES` in
`packages/test-support` — and it is now ONE list rather than two pinned
copies: the data moves to `hand-rolled-tables.json` behind an `exports`
subpath, the shape objectui#6923 ruled for data crossing the `.mjs` /
TypeScript boundary (`zod-wrapper-keys` is its first instance). The gate
reads those bytes through `createRequire`; `defaults-table-scan.ts`
re-exports them typed, and keeps the prose JSON cannot carry.

The widened half keeps its own counters, its own collapse floor
(`handRolledComparedRows < 150` against a measured 215, chosen to sit
above the LARGER of the two tables so losing either one fails rather
than shrinking quietly) and its own summary line with its abstention
counts. `factoryComparedRows < 500` is untouched. A table that is in the
registry AND reachable from the factory — `TIMELINE_DEFAULT_TRANSLATIONS`
— is de-duplicated on the literal's own position and reported as such,
so neither census double-counts it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FhBNJcLRZLe8M87VcUgpKr
…hat ask about main

`analyze(repoRoot)` parses ~1600 files, and seven cases in this file each
paid for their own walk. That held until this card added two more and
pushed `main carries no factory drift` past vitest's 15s window under a
full `pnpm exec vitest run scripts/__tests__/` — measured, not predicted:
that case does two full-repo walks of its own.

AGENTS.md 测试纪律 names this exact shape (an unbounded workload counted
inside a bounded one) and prescribes moving the cost into the import
phase rather than raising the timeout, so the run is computed once at
module scope. Cases that inject their own registry keep their own runs,
because they are asking a different question.

Measured either side, same 112 files / 3348 tests: 219.72s -> 113.76s.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FhBNJcLRZLe8M87VcUgpKr

Copy link
Copy Markdown
Contributor Author

Standing down on Live E2E (informational): that check is red on main itself today (base-red, anchored on objectui#7990 / objectstack-ai/objectstack#16186), it is informational (not required), and this PR changes one gate script, its pin file and a private test-support package's declaration data — nothing the live lane exercises. No fix is ported because none exists yet upstream. The flip to ready happens only once every non-informational check on 924edd915 is green.


Generated by Claude Code

@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 50 chunks) 3186.2 KB 3191.4 KB
Main entry chunk (gzip) 143.5 KB 350 KB
Entry file index-CY8jHVpQ.js
Status PASS

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 15.67KB 5.75KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 5.13KB 2.35KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 497.06KB 113.79KB
core (index.js) 6.96KB 2.79KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 182.08KB 50.62KB
fields (index.js) 242.43KB 61.25KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 6.57KB 2.76KB
i18n (index.js) 3.65KB 1.47KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.84KB 10.94KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 11.71KB 4.29KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 5.12KB 1.74KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 15.16KB 3.68KB
plugin-calendar (index.js) 47.29KB 13.18KB
plugin-charts (index.js) 70.43KB 19.71KB
plugin-chatbot (index.js) 193.54KB 46.04KB
plugin-dashboard (index.js) 131.41KB 34.43KB
plugin-designer (index.js) 211.51KB 43.01KB
plugin-detail (index.js) 247.59KB 63.48KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 131.01KB 32.32KB
plugin-gantt (index.js) 167.16KB 40.99KB
plugin-grid (index.js) 208.56KB 56.63KB
plugin-kanban (index.js) 52.30KB 14.49KB
plugin-list (index.js) 113.24KB 27.66KB
plugin-map (index.js) 20.35KB 6.77KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.42KB 11.92KB
plugin-timeline (index.js) 29.95KB 8.67KB
plugin-tree (index.js) 9.19KB 3.19KB
plugin-view (index.js) 84.33KB 20.75KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 81.07KB 26.86KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 5.41KB 2.34KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 4.93KB 2.24KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 20.57KB 5.88KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 10.35KB 3.60KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.74KB 1.41KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

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

Copy link
Copy Markdown
Contributor Author

Type Check is red on 924edd915 for a reason outside this diff, and it died before any test body ran: the job's setup step pnpm --version crashed inside corepack's download of pnpm-10.31.0.tgz with Node's undici AssertionError [ERR_ASSERTION]: assert(!this.paused) (job 101512501067, 15:36:08Z) — no tsc was invoked, and the same job passed on every other PR this hour. Classified as a runner/network failure in the install phase, which is the one case the seat re-runs: one re-run of the failed job once the CI run (34042814334) finishes its other jobs (GitHub refuses rerun-failed-jobs while the run is in progress). If the re-run reds again on a real tsc diagnostic, that is this PR's to fix and it goes back to the dev as a patch round. No empty commit, no branch kick.


Generated by Claude Code

Copy link
Copy Markdown
Contributor Author

Re-run issued: run 34042814334 completed (failure, attempt 1, the Type Check setup crash being the only red) at 15:50Z; rerun-failed-jobs accepted at 15:51Z (attempt 2). This is the seat's one re-run for this head. A second Type Check red — on a real tsc diagnostic or otherwise — is treated as this PR's and goes back to the dev as a patch round.


Generated by Claude Code

@baozhoutao
baozhoutao marked this pull request as ready for review September 6, 2026 15:58
@baozhoutao
baozhoutao added this pull request to the merge queue Sep 6, 2026
Merged via the queue into main with commit 7dcda9c Sep 6, 2026
40 of 42 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-7877-hand-rolled-fallback-tables-registry branch September 6, 2026 16:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

i18n(gate): bring the hand-rolled fallbackT tables into the createSafeTranslation value-compare via a declared registry

2 participants