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
10 changes: 10 additions & 0 deletions .changeset/11676-retire-overdue-key.md
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 1 addition & 1 deletion .changeset/11676-timeline-past-bucket.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
1 change: 0 additions & 1 deletion packages/i18n/src/locales/ar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -939,7 +939,6 @@ const ar = {
},
timeline: {
bucket: {
overdue: "متأخر",
earlier: "سابقاً",
today: "اليوم",
tomorrow: "غداً",
Expand Down
1 change: 0 additions & 1 deletion packages/i18n/src/locales/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -828,7 +828,6 @@ const de = {
},
timeline: {
bucket: {
overdue: "Überfällig",
earlier: "Früher",
today: "Heute",
tomorrow: "Morgen",
Expand Down
1 change: 0 additions & 1 deletion packages/i18n/src/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1058,7 +1058,6 @@ const en = {
},
timeline: {
bucket: {
overdue: 'Overdue',
earlier: 'Earlier',
today: 'Today',
tomorrow: 'Tomorrow',
Expand Down
1 change: 0 additions & 1 deletion packages/i18n/src/locales/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -856,7 +856,6 @@ const es = {
},
timeline: {
bucket: {
overdue: "Atrasado",
earlier: "Antes",
today: "Hoy",
tomorrow: "Mañana",
Expand Down
1 change: 0 additions & 1 deletion packages/i18n/src/locales/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -852,7 +852,6 @@ const fr = {
},
timeline: {
bucket: {
overdue: "En retard",
earlier: "Plus tôt",
today: "Aujourd'hui",
tomorrow: "Demain",
Expand Down
1 change: 0 additions & 1 deletion packages/i18n/src/locales/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -828,7 +828,6 @@ const ja = {
},
timeline: {
bucket: {
overdue: "期限超過",
earlier: "以前",
today: "今日",
tomorrow: "明日",
Expand Down
1 change: 0 additions & 1 deletion packages/i18n/src/locales/ko.ts
Original file line number Diff line number Diff line change
Expand Up @@ -828,7 +828,6 @@ const ko = {
},
timeline: {
bucket: {
overdue: "기한 초과",
earlier: "이전",
today: "오늘",
tomorrow: "내일",
Expand Down
1 change: 0 additions & 1 deletion packages/i18n/src/locales/pt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -851,7 +851,6 @@ const pt = {
},
timeline: {
bucket: {
overdue: "Atrasado",
earlier: "Antes",
today: "Hoje",
tomorrow: "Amanhã",
Expand Down
1 change: 0 additions & 1 deletion packages/i18n/src/locales/ru.ts
Original file line number Diff line number Diff line change
Expand Up @@ -887,7 +887,6 @@ const ru = {
},
timeline: {
bucket: {
overdue: "Просрочено",
earlier: "Ранее",
today: "Сегодня",
tomorrow: "Завтра",
Expand Down
1 change: 0 additions & 1 deletion packages/i18n/src/locales/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -853,7 +853,6 @@ const zh = {
},
timeline: {
bucket: {
overdue: '已逾期',
earlier: '更早',
today: '今天',
tomorrow: '明天',
Expand Down
8 changes: 4 additions & 4 deletions packages/plugin-timeline/src/ObjectTimeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -772,10 +772,10 @@ export const ObjectTimeline: React.FC<ObjectTimelineProps> = ({
// 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));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<Record<string, unknown>>);
Expand Down Expand Up @@ -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' },
Expand All @@ -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');
}
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
10 changes: 4 additions & 6 deletions packages/plugin-timeline/src/useTimelineTranslation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,10 @@ import { createSafeTranslation } from '@object-ui/i18n';
* embed) without an I18nProvider on the React tree.
*/
export const TIMELINE_DEFAULT_TRANSLATIONS: Record<string, string> = {
// 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',
Expand Down
Loading