diff --git a/.changeset/11676-retire-overdue-key.md b/.changeset/11676-retire-overdue-key.md new file mode 100644 index 0000000000..ff03ca2232 --- /dev/null +++ b/.changeset/11676-retire-overdue-key.md @@ -0,0 +1,10 @@ +--- +'@object-ui/i18n': minor +'@object-ui/plugin-timeline': minor +--- + +The timeline's unused "Overdue" bucket label is removed: `timeline.bucket.overdue` is gone from all ten language packs and from the timeline's built-in English defaults (objectui#11676). + +Nothing has read the key since a past date on a timeline went under "Earlier". No timeline heads a group "Overdue": a date is overdue only when it is a due date and the record is still open, and a timeline declares neither fact. Overdue records are still marked where record state lives: the date cell's due treatment, the gantt's alert colour and conditional formatting. + +**Narrowed public surface (`@object-ui/i18n`).** The exported `en` pack and the `TranslationKeys` type derived from it lose one member, `timeline.bucket.overdue`. Code that reads `en.timeline.bucket.overdue`, or passes that key to `t()` expecting a translation, has to drop it. This is released as `minor` under objectui's version policy, which keeps the major aligned with `@objectstack`. The date cell's "Overdue 3d" phrase is a different key, `fields.relativeDate.overdue`, and stays in every pack. diff --git a/.changeset/11676-timeline-past-bucket.md b/.changeset/11676-timeline-past-bucket.md index dfc84547b3..1668a17c16 100644 --- a/.changeset/11676-timeline-past-bucket.md +++ b/.changeset/11676-timeline-past-bucket.md @@ -9,4 +9,4 @@ Without a `groupByField`, `ObjectTimeline` groups its entries into date buckets, "Overdue" needs two facts: that the date is a due date, and that the record is still open. Nothing the timeline reads declares either one. The timeline configuration (`startDateField`, `endDateField`, `titleField`, `groupByField`, `colorField`, `scale`) names no due-date role, and no field option marks a closed state. So a past day is now "Earlier", and the timeline does not guess either fact from a field name or a status value. Today, Tomorrow, This week, Next week, Later and No date are unchanged. -**Widened public surface (`@object-ui/i18n`).** One new key in all ten language packs, `timeline.bucket.earlier`, so the exported `en` pack and the `TranslationKeys` type derived from it gain one member. `timeline.bucket.overdue` is kept in every pack, and the timeline no longer reads it. No component prop or exported type changes. +**Widened public surface (`@object-ui/i18n`).** One new key in all ten language packs, `timeline.bucket.earlier`, so the exported `en` pack and the `TranslationKeys` type derived from it gain one member. The timeline no longer reads `timeline.bucket.overdue`, and this release also removes that key from every pack (the "Overdue" bucket label retirement entry). No component prop or exported type changes. diff --git a/packages/i18n/src/locales/ar.ts b/packages/i18n/src/locales/ar.ts index e97e93dd85..02a764627f 100644 --- a/packages/i18n/src/locales/ar.ts +++ b/packages/i18n/src/locales/ar.ts @@ -939,7 +939,6 @@ const ar = { }, timeline: { bucket: { - overdue: "متأخر", earlier: "سابقاً", today: "اليوم", tomorrow: "غداً", diff --git a/packages/i18n/src/locales/de.ts b/packages/i18n/src/locales/de.ts index 4ff0cdf47e..dad896ce81 100644 --- a/packages/i18n/src/locales/de.ts +++ b/packages/i18n/src/locales/de.ts @@ -828,7 +828,6 @@ const de = { }, timeline: { bucket: { - overdue: "Überfällig", earlier: "Früher", today: "Heute", tomorrow: "Morgen", diff --git a/packages/i18n/src/locales/en.ts b/packages/i18n/src/locales/en.ts index 289012b32a..032a6e5a8c 100644 --- a/packages/i18n/src/locales/en.ts +++ b/packages/i18n/src/locales/en.ts @@ -1058,7 +1058,6 @@ const en = { }, timeline: { bucket: { - overdue: 'Overdue', earlier: 'Earlier', today: 'Today', tomorrow: 'Tomorrow', diff --git a/packages/i18n/src/locales/es.ts b/packages/i18n/src/locales/es.ts index eca44bcf19..15de671462 100644 --- a/packages/i18n/src/locales/es.ts +++ b/packages/i18n/src/locales/es.ts @@ -856,7 +856,6 @@ const es = { }, timeline: { bucket: { - overdue: "Atrasado", earlier: "Antes", today: "Hoy", tomorrow: "Mañana", diff --git a/packages/i18n/src/locales/fr.ts b/packages/i18n/src/locales/fr.ts index b67968b446..805edd3f12 100644 --- a/packages/i18n/src/locales/fr.ts +++ b/packages/i18n/src/locales/fr.ts @@ -852,7 +852,6 @@ const fr = { }, timeline: { bucket: { - overdue: "En retard", earlier: "Plus tôt", today: "Aujourd'hui", tomorrow: "Demain", diff --git a/packages/i18n/src/locales/ja.ts b/packages/i18n/src/locales/ja.ts index 779f5718af..7d8f5a8e4b 100644 --- a/packages/i18n/src/locales/ja.ts +++ b/packages/i18n/src/locales/ja.ts @@ -828,7 +828,6 @@ const ja = { }, timeline: { bucket: { - overdue: "期限超過", earlier: "以前", today: "今日", tomorrow: "明日", diff --git a/packages/i18n/src/locales/ko.ts b/packages/i18n/src/locales/ko.ts index 1d1f3f80fa..0f99c3e2e6 100644 --- a/packages/i18n/src/locales/ko.ts +++ b/packages/i18n/src/locales/ko.ts @@ -828,7 +828,6 @@ const ko = { }, timeline: { bucket: { - overdue: "기한 초과", earlier: "이전", today: "오늘", tomorrow: "내일", diff --git a/packages/i18n/src/locales/pt.ts b/packages/i18n/src/locales/pt.ts index 5220971d73..88f919aca1 100644 --- a/packages/i18n/src/locales/pt.ts +++ b/packages/i18n/src/locales/pt.ts @@ -851,7 +851,6 @@ const pt = { }, timeline: { bucket: { - overdue: "Atrasado", earlier: "Antes", today: "Hoje", tomorrow: "Amanhã", diff --git a/packages/i18n/src/locales/ru.ts b/packages/i18n/src/locales/ru.ts index 7e8476fe14..a4bce464cc 100644 --- a/packages/i18n/src/locales/ru.ts +++ b/packages/i18n/src/locales/ru.ts @@ -887,7 +887,6 @@ const ru = { }, timeline: { bucket: { - overdue: "Просрочено", earlier: "Ранее", today: "Сегодня", tomorrow: "Завтра", diff --git a/packages/i18n/src/locales/zh.ts b/packages/i18n/src/locales/zh.ts index 753b9732ac..c683b85f40 100644 --- a/packages/i18n/src/locales/zh.ts +++ b/packages/i18n/src/locales/zh.ts @@ -853,7 +853,6 @@ const zh = { }, timeline: { bucket: { - overdue: '已逾期', earlier: '更早', today: '今天', tomorrow: '明天', diff --git a/packages/plugin-timeline/src/ObjectTimeline.tsx b/packages/plugin-timeline/src/ObjectTimeline.tsx index b44e43f8f2..d5dd6e0302 100644 --- a/packages/plugin-timeline/src/ObjectTimeline.tsx +++ b/packages/plugin-timeline/src/ObjectTimeline.tsx @@ -772,10 +772,10 @@ export const ObjectTimeline: React.FC = ({ // every record under "Overdue", Done records included. ⛔ Not inferred // from a field NAME (`due_date`) or a status VALUE (`done`): a guess here // reads like a declaration and is wrong for every object that spells - // either one differently. Whether a declared signal should bring the - // judgement back is an open question on objectui#11676; until one exists, - // the honest answer is the calendar position alone. Pinned in - // `__tests__/ObjectTimeline.pastBucket-11676.test.tsx`. + // either one differently. Triage ruled on objectui#11676 that no timeline + // says "Overdue" (ruling A), so the bucket is the calendar position alone; + // a due-date timeline would be a new card that declares its signals in the + // spec first. Pinned in `__tests__/ObjectTimeline.pastBucket-11676.test.tsx`. const dateBucket = (raw: any): string => { if (!raw) return t('timeline.bucket.noDate'); const ts = startOfDay(toDisplayDate(raw)); diff --git a/packages/plugin-timeline/src/__tests__/ObjectTimeline.pastBucket-11676.test.tsx b/packages/plugin-timeline/src/__tests__/ObjectTimeline.pastBucket-11676.test.tsx index 7a2f7e02d1..0e5648d1ac 100644 --- a/packages/plugin-timeline/src/__tests__/ObjectTimeline.pastBucket-11676.test.tsx +++ b/packages/plugin-timeline/src/__tests__/ObjectTimeline.pastBucket-11676.test.tsx @@ -25,9 +25,16 @@ * and `scale`, and nothing that names a due-date role or a closed state; the * spec's field options declare no closed marker either. So a past day gets * the neutral `timeline.bucket.earlier` ("Earlier"), and no field NAME and no - * status VALUE is read as one of those two facts. Whether an object-bound - * timeline should ever say "Overdue", and through which declared signal, is - * the open question on the card, not something this file decides. + * status VALUE is read as one of those two facts. + * + * ── The key retires (triage ruling A on the card) ────────────────────────── + * No timeline says "Overdue": that judgement stays where record state lives + * (the date cell's due treatment, the gantt's alert colour, conditional + * formatting). So `timeline.bucket.overdue`, which nothing read once the past + * bucket became "Earlier", is gone from `TIMELINE_DEFAULT_TRANSLATIONS` and + * from all ten packs; the last `describe` pins both. A due-date timeline, if a + * producer ever asks for one, is a new card that declares its signals in the + * spec first, not a row restored here. * * The clock is frozen on Tuesday 2026-10-06, and every value is a date-only * string, which the shared `toDisplayDate` step reads as local midnight of the @@ -40,7 +47,9 @@ import React from 'react'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { cleanup, render, screen } from '@testing-library/react'; import { I18nProvider, LocalizationProvider } from '@object-ui/i18n'; +import { BUILT_IN_LANGUAGE_CODES, builtInLocales } from '@object-ui/i18n/locales'; import { ObjectTimeline } from '../ObjectTimeline'; +import { TIMELINE_DEFAULT_TRANSLATIONS, translateTimelineDefault } from '../useTimelineTranslation'; vi.mock('@object-ui/react', async (importOriginal) => { const actual = await (importOriginal() as Promise>); @@ -143,8 +152,7 @@ describe('ObjectTimeline past-date bucket (objectui#11676)', () => { it('no field name is read as a due-date role: a past `due_date` on an open record is "Earlier" too', () => { // The card's rule: field names are not guessed. Nothing the timeline reads // DECLARES this field a due date or this status open, so the neutral bucket - // is the honest answer until a declared signal exists (the card's open - // question). A name heuristic would flip this case to "Overdue". + // is the honest answer. A name heuristic would flip this case to "Overdue". const rows: Row[] = [ { id: '1', title: 'Renew lease', due_date: '2026-10-02', status: 'open' }, { id: '2', title: 'File taxes', due_date: '2026-10-08', status: 'open' }, @@ -161,3 +169,43 @@ describe('ObjectTimeline past-date bucket (objectui#11676)', () => { expect(document.body.textContent).not.toContain('已逾期'); }); }); + +/** The retired key, fully qualified, and its leaf inside each pack's `timeline.bucket`. */ +const RETIRED_KEY = 'timeline.bucket.overdue'; +const RETIRED_LEAF = 'overdue'; + +type Pack = (typeof builtInLocales)[keyof typeof builtInLocales]; +const PACKS = Object.entries(builtInLocales) as Array<[string, Pack]>; + +describe('`timeline.bucket.overdue` is retired from the defaults row and every pack (objectui#11676)', () => { + it('reads every built-in pack and a defaults table that still has its bucket rows', () => { + // Non-vacuity: each assertion below reads these, so an unresolved import, a + // pack dropped from `builtInLocales` or an emptied table would be green + // without this. + expect(PACKS.map(([lang]) => lang).sort()).toEqual([...BUILT_IN_LANGUAGE_CODES].sort()); + expect(TIMELINE_DEFAULT_TRANSLATIONS['timeline.bucket.earlier']).toBe('Earlier'); + for (const [lang, pack] of PACKS) { + expect(Object.keys(pack.timeline.bucket), lang).toContain('earlier'); + } + }); + + it('the defaults row is gone, and the provider-less fallback answers the bare key', () => { + expect(Object.keys(TIMELINE_DEFAULT_TRANSLATIONS)).not.toContain(RETIRED_KEY); + expect(translateTimelineDefault(RETIRED_KEY)).toBe(RETIRED_KEY); + }); + + it('no pack carries it, while the `fields.relativeDate.overdue` family beside it stays', () => { + const carriers = PACKS.filter(([, pack]) => RETIRED_LEAF in pack.timeline.bucket).map(([lang]) => lang); + expect( + carriers, + 'A pack carries `timeline.bucket.overdue` again. No timeline reads it: a past day is ' + + '"Earlier", and triage ruled that no timeline says "Overdue" (objectui#11676). A ' + + 'due-date timeline is a new card that declares its signals in the spec first.', + ).toEqual([]); + // A different family that happens to share the leaf name: the date cell's + // "Overdue Nd" phrase. A sweep by leaf name rather than by key takes it out. + for (const [lang, pack] of PACKS) { + expect(typeof pack.fields.relativeDate.overdue, lang).toBe('string'); + } + }); +}); 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 index 9255619597..439a6c8964 100644 --- 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 @@ -45,7 +45,11 @@ * - **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. + * search that has not been shown to fire is not a measurement. (That was + * read at `83fe6e741`. `timeline.bucket.overdue` has since lost its call + * site and then its row and pack key, objectui#11676, so a re-run needs + * another `ObjectTimeline.tsx` key, such as `timeline.bucket.today`, as + * its control.) * - **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` diff --git a/packages/plugin-timeline/src/useTimelineTranslation.ts b/packages/plugin-timeline/src/useTimelineTranslation.ts index 2267442e74..9806e88f46 100644 --- a/packages/plugin-timeline/src/useTimelineTranslation.ts +++ b/packages/plugin-timeline/src/useTimelineTranslation.ts @@ -15,12 +15,10 @@ import { createSafeTranslation } from '@object-ui/i18n'; * embed) without an I18nProvider on the React tree. */ export const TIMELINE_DEFAULT_TRANSLATIONS: Record = { - // objectui#11676 — `overdue` has no call site here any more: a past day is - // bucketed `earlier`, because nothing the timeline reads declares a due date - // or a closed record (see `dateBucket` in `ObjectTimeline.tsx`). The row is - // kept with its pack key until the card's open question decides whether a - // declared signal brings "Overdue" back or the key retires. - 'timeline.bucket.overdue': 'Overdue', + // objectui#11676 — the `overdue` bucket row is retired with its pack key: a + // past day is `earlier` (see `dateBucket` in `ObjectTimeline.tsx`), and no + // timeline heads a group "Overdue". `ObjectTimeline.pastBucket-11676.test.tsx` + // pins the row and the key absent from all ten packs. 'timeline.bucket.earlier': 'Earlier', 'timeline.bucket.today': 'Today', 'timeline.bucket.tomorrow': 'Tomorrow',