Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions .changeset/residue-locale-keys-3546-slice7.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
---
"@object-ui/i18n": patch
---

Backfill the last 17 missing locale keys and both remaining template-key families, emptying the call-site key ratchet (objectui#3546, slice seven — final)

`scripts/check-i18n-call-site-keys.mjs` (objectui#3530) opened this backlog with
**258 keys and 4 template-key families** that a `t()` call site asks for and that
**no locale pack defined**. Seven slices later the last of it is paid: this change
takes the ratchet from 17 keys to **zero** and from 2 prefix families to **zero**,
and the gate now reports every one of the **2320** literal call-site keys
resolving against `en`.

The residue was the long tail — nine namespaces across `app-shell`,
`plugin-detail`, `plugin-dashboard`, `plugin-kanban` and `plugin-gantt`, none of
them big enough to have been its own slice. 17 distinct keys at **23** call sites
(five keys are used at more than one site) plus **3** call sites behind the two
families.

What that meant on the page for a `zh` (or `ja`, `de`, `ar`, …) user: the "App not
available" empty state a user lands on when an app is still publishing, including
its whole explanation and its Retry button; the interface page's "source is not
available" message; the system navigation's **Administration** group header,
**Datasources** and **Documentation** entries; the "creating new organizations is
disabled on this instance" guard in the workspace dialog; the invitation list's
five status labels (All / Pending / Accepted / Rejected / Canceled) on both the
filter tabs and every invitation badge; the Gantt dependency-drag hint that names
which endpoint the drop will link (`start` / `end`); the record detail's Add,
"Record deleted", "No history yet" and the concurrent-update dialog's "this
record"; the kanban empty board's column count; the dashboard widget's screen
reader "Loading…"; and the page editor's "Edit in studio" tooltip and accessible
name. All of it rendered English, in every one of the ten languages.

Nothing here rendered a raw key — slice one (PR #3583) held those sites, and the
three keys the issue body named as unprotected (`detail.viewSource`,
`wizard.missingRequired`, `gantt.toolbar.refresh`) have resolved in `en` since.

Both families are repaired as **enumerations, not wildcards**, and the assertion
that used to live in the ratchet's `missingPrefixes` moves into a test that fails
if either union grows a member without a key:

- `gantt.linkEnd.` — the closed union `'start' | 'end'`, declared by GanttView's
own `linkDrag` state.
- `organization.invitations.status.` — `StatusFilter`
(`all | pending | accepted | rejected | canceled`), declared by InvitationsPage.

Every `en` value is byte-identical to the English the call site rendered before,
so no string a user sees today changes: 16 keys match an inline
`t(key, { defaultValue: … })`; `dashboard.loading` matches `useSafeTranslate`'s
positional fallback `tt(key, 'Loading…')`; `gantt.linkEnd.*` match
`useGanttTranslation`'s per-key fallback map; and the five status labels match the
CSS-capitalised wire value each badge and tab showed. The nine translations follow
each pack's own neighbourhood and reuse an existing neighbour's row wherever the
`en` string already existed verbatim **and** that row is grammatical here — the
four invitation adjectives are deliberately not reused from the approvals family,
because those agree with each pack's word for "request" (`ru` masculine `Отклонён`)
while an invitation needs its own agreement (`ru` neuter `Отклонено`).

`scripts/i18n-call-site-key-baseline.json` is kept rather than deleted: empty is
its terminal, load-bearing state — against an empty baseline any NEW unresolved
call-site key is unexpected and fails the build.

No component changed.
20 changes: 8 additions & 12 deletions packages/i18n/src/__tests__/auth-namespace-3546.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -256,20 +256,16 @@ describe('objectui#3546 slice three — the auth / oauth / acceptInvitation name
);
expect(stillBaselined).toEqual([]);
// 163 before this slice, 54 removed — then slice four (console, 41 keys) took
// it to 68, slice five (marketplace + preview, 37 keys) to 31 and slice six
// (perm + home, 14 keys) to 17. The other namespaces' debt is not this
// slice's to spend, and this number is what catches a slice that overreaches;
// it moves once per slice, and only downwards.
expect(Object.keys(baseline.missingKeys).length).toBe(17);
// it to 68, slice five (marketplace + preview, 37 keys) to 31, slice six
// (perm + home, 14 keys) to 17 and slice seven (the 17-key residue) to ZERO.
// The counter moved once per slice and only downwards; at zero it stops being
// "how much is left" and becomes "nothing may be added back".
expect(Object.keys(baseline.missingKeys).length).toBe(0);
// None of the template-key FAMILIES belonged to the auth family, so this slice
// left all four. Slice four then took `console.ai.group.` (it is a `console`
// key) and slice five `marketplace.disclosure.runtime.`, leaving two. This
// assertion is what stops a later slice from thinking one of the remaining
// two was already handled.
expect(Object.keys(baseline.missingPrefixes).sort()).toEqual([
'gantt.linkEnd.',
'organization.invitations.status.',
]);
// key), slice five `marketplace.disclosure.runtime.`, and slice seven the last
// two (`gantt.linkEnd.`, `organization.invitations.status.`).
expect(Object.keys(baseline.missingPrefixes).sort()).toEqual([]);
});

describe('through the real binding — bare useObjectTranslation, provider mounted', () => {
Expand Down
25 changes: 13 additions & 12 deletions packages/i18n/src/__tests__/console-namespace-3546.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -322,18 +322,19 @@ describe('objectui#3546 slice four — the console namespace', () => {
};
expect(Object.keys(baseline.missingKeys).filter((k) => k.startsWith('console.'))).toEqual([]);
// 109 before this slice, 41 removed — then slice five (marketplace + preview,
// 37 keys) took it to 31 and slice six (perm + home, 14 keys) to 17. The other
// namespaces' debt is not this slice's to spend; this number moves once per
// slice, and only downwards.
expect(Object.keys(baseline.missingKeys).length).toBe(17);
// The prefix family this slice handled is GONE from the ratchet, and the ones
// that remain are untouched — none of them belongs to `console`. Slice five
// then took `marketplace.disclosure.runtime.`, leaving two.
expect(Object.keys(baseline.missingPrefixes).sort()).toEqual([
'gantt.linkEnd.',
'organization.invitations.status.',
]);
expect(Object.keys(baseline.missingPrefixes)).not.toContain('console.ai.group.');
// 37 keys) took it to 31, slice six (perm + home, 14 keys) to 17 and slice
// seven (the 17-key residue) to ZERO. The counter moved once per slice and
// only downwards; at zero it stops being "how much is left" and becomes
// "nothing may be added back".
expect(Object.keys(baseline.missingKeys).length).toBe(0);
// The prefix family this slice handled is GONE from the ratchet. Slice five
// then took `marketplace.disclosure.runtime.` and slice seven the last two
// (`gantt.linkEnd.`, `organization.invitations.status.`), so the list is empty.
// The `not.toContain('console.ai.group.')` that used to sit here was dropped
// rather than kept: against an empty list it passes because nothing is
// produced, not because the logic holds. The set equality above is the
// stronger statement and it is not vacuous.
expect(Object.keys(baseline.missingPrefixes).sort()).toEqual([]);
});

describe('through the real binding — bare useObjectTranslation, provider mounted', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -576,16 +576,17 @@ describe('objectui#3546 slice five — the marketplace and preview namespaces',
),
).toEqual([]);
// 68 before this slice, 37 removed — then slice six (perm + home, 14 keys)
// took it to 17. The other namespaces' debt is not this slice's to spend;
// this number moves once per slice, and only downwards.
expect(Object.keys(baseline.missingKeys).length).toBe(17);
// The prefix family this slice handled is GONE from the ratchet, and the two
// that remain are untouched — neither belongs to these namespaces.
expect(Object.keys(baseline.missingPrefixes).sort()).toEqual([
'gantt.linkEnd.',
'organization.invitations.status.',
]);
expect(Object.keys(baseline.missingPrefixes)).not.toContain('marketplace.disclosure.runtime.');
// took it to 17 and slice seven (the 17-key residue) to ZERO. The counter
// moved once per slice and only downwards; at zero it stops being "how much
// is left" and becomes "nothing may be added back".
expect(Object.keys(baseline.missingKeys).length).toBe(0);
// The prefix family this slice handled is GONE from the ratchet, and slice
// seven took the last two (`gantt.linkEnd.`,
// `organization.invitations.status.`). The `not.toContain(…)` that used to sit
// below this line was dropped rather than kept: against an empty list it
// passes because nothing is produced, not because the logic holds. The set
// equality is the stronger statement and it is not vacuous.
expect(Object.keys(baseline.missingPrefixes).sort()).toEqual([]);
});

describe('through the real binding — bare useObjectTranslation, provider mounted', () => {
Expand Down
22 changes: 13 additions & 9 deletions packages/i18n/src/__tests__/organization-namespace-3546.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -257,17 +257,21 @@ describe('objectui#3546 slice two — the organization namespace', () => {
missingPrefixes: Record<string, unknown>;
};
expect(Object.keys(baseline.missingKeys).filter((k) => k.startsWith('organization.'))).toEqual([]);
// Untouched on purpose: `organization.invitations.status.*` is a template
// key FAMILY, a different repair (enumerate the status values) than the 90
// literal keys, and it is still missing. Deliberately left for the
// prefix-family slice — this assertion is what stops it being forgotten.
expect(Object.keys(baseline.missingPrefixes)).toContain('organization.invitations.status.');
// `organization.invitations.status.*` was left untouched by THIS slice — a
// template key FAMILY needs a different repair (enumerate the status values)
// than the 90 literal keys — and the assertion here used to be
// `toContain(…)`, whose whole job was to stop the family being forgotten.
// Slice seven enumerated it (`StatusFilter` = all/pending/accepted/rejected/
// canceled) and emptied `missingPrefixes`, so the assertion inverts: it now
// states the family is gone, which is the fact a reverting change breaks.
expect(Object.keys(baseline.missingPrefixes)).not.toContain('organization.invitations.status.');
expect(Object.keys(baseline.missingPrefixes).sort()).toEqual([]);
// The other namespaces' debt is not this slice's to spend. Slice three
// (auth/oauth/acceptInvitation, 54 keys) took it from 163 to 109, slice four
// (console, 41 keys) to 68, slice five (marketplace + preview, 37 keys) to 31
// and slice six (perm + home, 14 keys) to 17; this number moves once per
// slice, and only downwards.
expect(Object.keys(baseline.missingKeys).length).toBe(17);
// (console, 41 keys) to 68, slice five (marketplace + preview, 37 keys) to 31,
// slice six (perm + home, 14 keys) to 17 and slice seven (the 17-key residue)
// to ZERO; this number moved once per slice, and only downwards.
expect(Object.keys(baseline.missingKeys).length).toBe(0);
});

describe('through the real binding — bare useObjectTranslation, provider mounted', () => {
Expand Down
17 changes: 8 additions & 9 deletions packages/i18n/src/__tests__/perm-home-namespace-3546.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -556,15 +556,14 @@ describe('objectui#3546 slice six — the perm and home namespaces', () => {
expect(
Object.keys(baseline.missingKeys).filter((k) => k.startsWith('perm.') || k.startsWith('home.')),
).toEqual([]);
// 31 before this slice, 14 removed. The other namespaces' debt is not this
// slice's to spend; this number moves once per slice, and only downwards.
expect(Object.keys(baseline.missingKeys).length).toBe(17);
// This slice owns NO prefix family — the two left belong to later slices, and
// neither is touched.
expect(Object.keys(baseline.missingPrefixes).sort()).toEqual([
'gantt.linkEnd.',
'organization.invitations.status.',
]);
// 31 before this slice, 14 removed, leaving 17 — which slice seven (the
// residue: 17 keys plus both remaining prefix families) took to ZERO. The
// counter moved once per slice and only downwards; at zero it stops being
// "how much is left" and becomes "nothing may be added back".
expect(Object.keys(baseline.missingKeys).length).toBe(0);
// This slice owned NO prefix family — the two left belonged to slice seven,
// which enumerated both, so the list is now empty.
expect(Object.keys(baseline.missingPrefixes).sort()).toEqual([]);
});

describe('through the real binding — provider mounted', () => {
Expand Down
Loading
Loading