diff --git a/.changeset/7874-timeline-relative-defaults-retired.md b/.changeset/7874-timeline-relative-defaults-retired.md new file mode 100644 index 0000000000..c817d5fbe8 --- /dev/null +++ b/.changeset/7874-timeline-relative-defaults-retired.md @@ -0,0 +1,29 @@ +--- +--- + +Internal residue removal, no published behaviour: the five `timeline.relative.*` +rows are deleted from `TIMELINE_DEFAULT_TRANSLATIONS` +(`packages/plugin-timeline/src/useTimelineTranslation.ts`), and a retirement pin +replaces them. + +The rows, transcribed verbatim because that — not "it is in git history +somewhere" — is what makes the deletion reversible: + +```ts +'timeline.relative.today': 'Today', +'timeline.relative.tomorrow': 'Tomorrow', +'timeline.relative.yesterday': 'Yesterday', +'timeline.relative.inDays': 'In {{n}} days', +'timeline.relative.daysAgo': '{{n}} days ago', +``` + +Nothing releases because nothing could reach them. The `en` pack defines no +`timeline.relative.*` leaf, so the provider path never served one; no call site +anywhere in the tree — sources, tests, JSON, ignored build outputs — asks for the +prefix `timeline.relative`, so the provider-less `fallbackT` never reached a row +either; and neither `TIMELINE_DEFAULT_TRANSLATIONS` nor `translateTimelineDefault` +is re-exported from the package entry, so no consumer of the tarball can name +them. Day-granularity relative phrases are produced by `formatRelativeDate` in +`@object-ui/core` through `Intl.RelativeTimeFormat`, which needs no copy row — +that is what left these five behind (objectui#7874, found by objectui#7567's +factory-default census printing its abstention count). diff --git a/packages/plugin-timeline/src/__tests__/timeline-relative-defaults-retired-7874.test.ts b/packages/plugin-timeline/src/__tests__/timeline-relative-defaults-retired-7874.test.ts new file mode 100644 index 0000000000..9255619597 --- /dev/null +++ b/packages/plugin-timeline/src/__tests__/timeline-relative-defaults-retired-7874.test.ts @@ -0,0 +1,175 @@ +/** + * ObjectUI + * Copyright (c) 2024-present ObjectStack Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * The five `timeline.relative.*` rows are retired from + * `TIMELINE_DEFAULT_TRANSLATIONS` as residue (objectui#7874). Same shape as the + * retirement-pin series in `@object-ui/i18n` (objectui#4392, objectui#4730, + * objectui#5504, objectui#6310, objectui#7125): the retired rows asserted + * absent, plus surviving-sibling assertions so a green here cannot be bought by + * deleting the neighbourhood. + * + * ## What was removed — transcribed verbatim, which is what makes it reversible + * + * The deletion is safe because it is reversible, and it is reversible because + * the content is written down HERE — not because "it is in git history + * somewhere". The five rows, exactly as they stood in + * `useTimelineTranslation.ts` before this change: + * + * ```ts + * 'timeline.relative.today': 'Today', + * 'timeline.relative.tomorrow': 'Tomorrow', + * 'timeline.relative.yesterday': 'Yesterday', + * 'timeline.relative.inDays': 'In {{n}} days', + * 'timeline.relative.daysAgo': '{{n}} days ago', + * ``` + * + * ## Why they are residue, re-measured on the retiring branch + * + * Not inherited from the card — re-run against `origin/main` at `83fe6e741`: + * + * - **No `en` leaf.** `packages/i18n/src/locales/en.ts` defines no + * `timeline.relative.*` key, so the provider path could never serve one + * either; `timeline.bucket.*` and `timeline.scale.*` are fully defined there. + * - **No caller, by PREFIX not by full key.** Searching the prefix + * `timeline.relative` over the whole tree — sources, tests, JSON, ignored + * build outputs included, `node_modules` excluded — returned exactly the five + * declaration lines and nothing else. The prefix is what a dynamically + * assembled key (`` t(`timeline.relative.${x}`) ``) would leave behind, and + * the tree carries no template-literal `t()` call at all. + * - **Positive control on that search.** The same command shape, run for + * `timeline.bucket.overdue` / `timeline.scale.week` / `timeline.gantt.rowLabel`, + * returned their call sites in `ObjectTimeline.tsx` and `renderer.tsx`. A + * search that has not been shown to fire is not a measurement. + * - **The job moved to a formatter, not to other copy.** Day-granularity + * relative phrases ("Today", "Tomorrow", "3 days ago") are produced by + * `formatRelativeDate` / `formatRelativeDays` in `@object-ui/core` + * (`utils/date-display.ts`) through `Intl.RelativeTimeFormat`, which needs no + * copy row in any pack. That is what left these five behind. + * + * ## Why this pin is NEGATIVE, and why it is needed at all + * + * Nothing in the repo can see these rows — that is the whole reason they + * survived, and it is equally true of them coming back: + * + * - `scripts/check-i18n-call-site-keys.mjs`'s `missing-key` class judges CALL + * SITES; a table row with no call site is structurally out of its reach. + * - `all-locales-key-parity.test.ts` compares the ten packs to EACH OTHER; ten + * packs identically lacking a key is full parity, hence green. + * - `scripts/check-i18n-dead-keys.mjs` sweeps PACK keys with no call site; + * these keys are in no pack to be swept (and it is report-only anyway). + * - `defaults-maps-mirror-en-pack.test.tsx` (objectui#4401) would catch a + * defaults row the `en` pack does not define — but it gates the detail, list + * and designer maps, and `TIMELINE_DEFAULT_TRANSLATIONS` is not among them. + * + * They were surfaced only by objectui#7567's census PRINTING its abstention + * count (5 of 846 factory-table rows). Re-adding them would be silent again, and + * "the timeline should say Today" is a plausible way for someone to put them + * back, so the pin is the guard. + * + * ## What this file does NOT claim + * + * - **It does not forbid relative-date copy.** Wiring the timeline's relative + * dates through `t()` is a FEATURE — it needs copy, plural rules and locale + * coverage across all ten packs, and the pack keys come first so + * `all-locales-key-parity.test.ts` demands the other nine. If that is what you + * are doing, delete this file with the card that does it; do not restore rows + * that no call site asks for as a shortcut. + * - **It does not assert anything about the `en` pack.** These keys were never + * in it, so their absence there is not this retirement's doing and pinning it + * would forbid the feature card above. + * - **`timeline.bucket.today` / `timeline.bucket.tomorrow` are DIFFERENT keys** + * that happen to carry the same two strings, `'Today'` and `'Tomorrow'`. A + * sweep by VALUE rather than by key takes them out as collateral, and the + * bucket header is a live render (`ObjectTimeline.tsx`). {@link SURVIVING} + * pins them for that reason. + */ +import { describe, it, expect } from 'vitest'; + +import { + TIMELINE_DEFAULT_TRANSLATIONS, + translateTimelineDefault, +} from '../useTimelineTranslation'; + +/** The retired rows, fully qualified, with the exact strings they carried. */ +const RETIRED: Readonly> = { + 'timeline.relative.today': 'Today', + 'timeline.relative.tomorrow': 'Tomorrow', + 'timeline.relative.yesterday': 'Yesterday', + 'timeline.relative.inDays': 'In {{n}} days', + 'timeline.relative.daysAgo': '{{n}} days ago', +}; + +/** The family prefix — what a dynamically assembled key would resolve onto. */ +const RETIRED_PREFIX = 'timeline.relative.'; + +/** + * Rows the deletion swept around, each confirmed live by a `t()` call site + * rather than by sitting nearby: the two bucket headers that carry the same + * strings as retired rows (`ObjectTimeline.tsx`), the gantt axis vocabulary + * (`renderer.tsx`) and the gantt row label (`renderer.tsx`). + */ +const SURVIVING: Readonly> = { + 'timeline.bucket.today': 'Today', + 'timeline.bucket.tomorrow': 'Tomorrow', + 'timeline.scale.week': 'Week {{n}}', + 'timeline.gantt.rowLabel': 'Items', +}; + +describe('the timeline relative-date defaults are retired as residue (objectui#7874)', () => { + it('reads a table with rows in it', () => { + // Non-vacuity, #4118 family standard: every assertion below reads the table, + // so an unresolved import or an emptied table is green without this. + expect(Object.keys(TIMELINE_DEFAULT_TRANSLATIONS).length).toBeGreaterThan(10); + }); + + it('carries no `timeline.relative.*` row, by key and by prefix', () => { + const revived = Object.keys(TIMELINE_DEFAULT_TRANSLATIONS).filter( + (key) => key.startsWith(RETIRED_PREFIX) || key in RETIRED, + ); + // Named, not counted, and prefix-wide: a row re-added under a NEW leaf of the + // same family (`timeline.relative.inWeeks`) is the same defect as restoring + // one of the five. + expect( + revived, + 'A `timeline.relative.*` row is back in TIMELINE_DEFAULT_TRANSLATIONS. ' + + 'These rows have no `en` leaf and no call site, so `fallbackT` can never ' + + 'be asked for one — and no i18n gate in this repo can see that, because ' + + 'each of them runs call site -> key, pack -> pack, or pack -> call site ' + + '(objectui#7874). Relative day phrases come from `formatRelativeDate` in ' + + '@object-ui/core via Intl.RelativeTimeFormat. If the timeline is genuinely ' + + 'growing translated relative dates, author it as a feature — pack keys in ' + + 'all ten locales plus the call sites that read them — and delete this file ' + + 'with that card.', + ).toEqual([]); + }); + + it('answers a retired key with the key itself, not with its old string', () => { + // The behavioural half: `translateTimelineDefault` falls through to the raw + // key for a row it does not carry. Asserting the OLD VALUE is gone (rather + // than only that the table lacks the key) is what a half-revert — the row + // restored under a renamed key — would trip over. + for (const [key, formerValue] of Object.entries(RETIRED)) { + expect(translateTimelineDefault(key), key).toBe(key); + expect(translateTimelineDefault(key), key).not.toBe(formerValue); + } + }); + + it('leaves the live rows that carry the same strings alone', () => { + const swept = Object.entries(SURVIVING) + .filter(([key, value]) => TIMELINE_DEFAULT_TRANSLATIONS[key] !== value) + .map(([key, value]) => `${key} (expected ${JSON.stringify(value)})`); + expect( + swept, + 'A live timeline default is gone or drifted. `timeline.bucket.today` and ' + + '`timeline.bucket.tomorrow` carry the same two strings the retired rows ' + + 'did — a sweep by VALUE rather than by key is exactly how the wrong rows ' + + 'go, and these two render the bucket headers on every feed timeline.', + ).toEqual([]); + }); +}); diff --git a/packages/plugin-timeline/src/useTimelineTranslation.ts b/packages/plugin-timeline/src/useTimelineTranslation.ts index 195f143734..c02661e28d 100644 --- a/packages/plugin-timeline/src/useTimelineTranslation.ts +++ b/packages/plugin-timeline/src/useTimelineTranslation.ts @@ -23,11 +23,15 @@ export const TIMELINE_DEFAULT_TRANSLATIONS: Record = { 'timeline.bucket.later': 'Later', 'timeline.bucket.noDate': 'No date', 'timeline.bucket.unassigned': 'Unassigned', - 'timeline.relative.today': 'Today', - 'timeline.relative.tomorrow': 'Tomorrow', - 'timeline.relative.yesterday': 'Yesterday', - 'timeline.relative.inDays': 'In {{n}} days', - 'timeline.relative.daysAgo': '{{n}} days ago', + // objectui#7874 — the five `timeline.relative.*` rows are retired as residue. + // They had no `en` leaf and no call site anywhere in the tree, so `fallbackT` + // could never be asked for one: they rendered for nobody. Day-granularity + // relative phrases are produced by `formatRelativeDate` (`@object-ui/core`) + // through `Intl.RelativeTimeFormat`, which needs no copy row at all. The five + // keys and their exact default strings are transcribed in + // `__tests__/timeline-relative-defaults-retired-7874.test.ts`, which also pins + // them absent — every i18n gate here runs call site -> key, so none of them + // can see a dead row come back. // Gantt axis vocabulary (objectui#4520). These were `Week ${n}` and // `Q${q} ${year}` template literals in `renderer.tsx`, so a zh session read // an English bucket label beside the Chinese date axis objectui#4513 had just