From 02cd2d97dbc582d4c8e5477a75cbd85f81ab4209 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 13 Aug 2026 04:05:10 +0000 Subject: [PATCH 1/2] fix(plugin-timeline): dates follow the active locale instead of hardcoded en-US (#4513) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `renderer.tsx` passed a literal 'en-US' to four Intl calls (the hour, day and month gantt headers, and the `long` item date) and passed nothing at all to a fifth (the `short` item date), so a fully Chinese timeline rendered an English axis and English item dates. The bare call is the same defect spelled as an omission: no tag means the MACHINE's locale, which agreed with the other four only by the accident of an en-US runner. All five now resolve through `useDisplayLocale()` (tenant regional default → active UI language → 'en'), the one channel objectui#4468 / PR #4512 converged `@object-ui/fields` onto. The two date helpers are module-level functions, so the locale is read once in `TimelineRenderer` and threaded down: `generateTimeScaleHeaders` takes an optional trailing `locale` defaulting to 'en' (it is exported and driven directly by the spec-parity test), and the module-private `formatDate` takes it as a required parameter so no future branch can silently reintroduce either wrong channel. English output is byte-identical at every site. The locale-free header vocabularies (`Week n`, `Qn YYYY`, `YYYY`) are untouched — they need the package's translate channel rather than a locale tag, filed as objectui#4520. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3 --- .changeset/tall-pumas-shave.md | 28 ++ .../timeline-date-locale-fallback.test.tsx | 96 +++++++ .../__tests__/timeline-date-locale.test.tsx | 254 ++++++++++++++++++ packages/plugin-timeline/src/renderer.tsx | 59 +++- 4 files changed, 424 insertions(+), 13 deletions(-) create mode 100644 .changeset/tall-pumas-shave.md create mode 100644 packages/plugin-timeline/src/__tests__/timeline-date-locale-fallback.test.tsx create mode 100644 packages/plugin-timeline/src/__tests__/timeline-date-locale.test.tsx diff --git a/.changeset/tall-pumas-shave.md b/.changeset/tall-pumas-shave.md new file mode 100644 index 0000000000..8d9ad901cf --- /dev/null +++ b/.changeset/tall-pumas-shave.md @@ -0,0 +1,28 @@ +--- +'@object-ui/plugin-timeline': patch +--- + +fix(plugin-timeline): dates follow the active locale instead of a hardcoded en-US + +A `zh` console rendered a fully Chinese timeline widget whose axis read +`Aug 11` / `Sep 2026` and whose item dates read `August 11, 2026` +(objectui#4513). `renderer.tsx` handed `Intl` a literal `'en-US'` at four sites +— the hour, day and month gantt headers, and the `long` item date — so nothing +a user or a tenant configured could reach them. + +A fifth site was the same defect spelled as an omission: the `short` item date +called `toLocaleDateString()` with no tag at all, which means the *machine's* +locale. It agreed with the other four only by the accident of an en-US runner, +and rendered a third locale on anyone else's machine. + +All five now resolve through `useDisplayLocale()` from `@object-ui/i18n` +(tenant regional default → active UI language → `en`) — the one channel every +field, number and currency renderer already uses, converged there in +objectui#4468. The locale is read once in `TimelineRenderer` and threaded into +the two module-level date helpers, which cannot host a hook themselves. + +English output is byte-identical at all five sites: `'en'` and the retired +`'en-US'` produce the same forms, and `generateTimeScaleHeaders` gained an +optional trailing `locale` parameter that defaults to `'en'`, so existing +three-argument callers are unaffected. The locale-free header vocabularies +(`Week n`, `Qn YYYY`, `YYYY`) and all non-date rendering are untouched. diff --git a/packages/plugin-timeline/src/__tests__/timeline-date-locale-fallback.test.tsx b/packages/plugin-timeline/src/__tests__/timeline-date-locale-fallback.test.tsx new file mode 100644 index 0000000000..402fc7e34d --- /dev/null +++ b/packages/plugin-timeline/src/__tests__/timeline-date-locale-fallback.test.tsx @@ -0,0 +1,96 @@ +/** + * 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. + */ + +/** + * objectui#4513 — the timeline's date sites with NO provider mounted. + * + * ── Why this is a separate file (objectui#4514) ────────────────────────── + * Nothing here mounts an `I18nProvider`, and that is load-bearing rather than + * incidental. `useObjectTranslation()` outside a provider reports the language + * of react-i18next's GLOBAL instance, and every `I18nProvider` mounted in a + * file leaves that global on whatever language it was given. A provider-less + * assertion written after a `zh` case in the same file therefore resolves + * `'zh'` — a fact about test ordering, not about the fallback. The session + * cases live in `timeline-date-locale.test.tsx`; these stay alone. + * + * ── What this pins, and what it deliberately cannot ────────────────────── + * `useDisplayLocale()`'s third step is a concrete `'en'`, never `undefined`. + * That matters because `undefined` is what `Intl` reads as "the MACHINE's + * locale" — the exact defect at `renderer.tsx:157` before this change, where a + * bare `toLocaleDateString()` made an embedded timeline render in whatever + * locale the viewer's machine happened to be set to. + * + * ⚠️ These cases are green on BOTH sides of the fix on an en-US runner, by + * construction: `'en'` and the machine's `en-US` agree on all of these forms, + * which is precisely why the old code could ship looking correct. They are a + * must-not-change pin, NOT the red-first evidence — the red-first half is the + * `zh session` block in the sibling file, which is the only place the machine + * locale and the session locale disagree. Spelled `'en'` explicitly rather + * than as a bare `toLocale*` call computed here, so the expectation does not + * silently follow the runner the way the code under test used to. + */ + +import React from 'react'; +import { render } from '@testing-library/react'; +import { describe, it, expect } from 'vitest'; +import { TimelineRenderer, generateTimeScaleHeaders } from '../renderer'; + +const AUG_11 = '2026-08-11T00:00:00'; +const SEP_11 = '2026-09-11T00:00:00'; + +describe('no provider mounted — the resolver’s last resort is a concrete `en`', () => { + it('the item date renders `en`, not the machine locale', () => { + const schema = { + type: 'timeline', + variant: 'vertical', + dateFormat: 'long', + items: [{ time: AUG_11, title: 'Beta Release' }], + } as any; + const { container } = render(); + expect(container.textContent).toContain( + new Date(AUG_11).toLocaleDateString('en', { year: 'numeric', month: 'long', day: 'numeric' }), + ); + }); + + it('the gantt axis renders `en`', () => { + const schema = { + type: 'timeline', + variant: 'gantt', + scale: 'month', + minDate: AUG_11, + maxDate: SEP_11, + items: [{ label: 'Backend', items: [{ title: 'API Design', startDate: AUG_11, endDate: SEP_11 }] }], + } as any; + const { container } = render(); + expect(container.textContent).toContain('Aug 2026'); + }); +}); + +describe('generateTimeScaleHeaders — the exported helper called without a locale', () => { + /** + * The helper is exported (the spec-parity test drives it directly), so its + * locale parameter is optional and defaults to `'en'` — the same concrete + * last resort the hook resolves to, so an existing 3-argument call site + * keeps producing exactly what the retired `'en-US'` literal produced. + * + * This is a pure function with no React in it, so unlike the renders above + * it is not sensitive to provider state at all — it is safe in any file. + */ + it('defaults to `en`, byte-identical to the retired `en-US` literal', () => { + expect(generateTimeScaleHeaders('month', AUG_11, SEP_11)).toEqual(['Aug 2026', 'Sep 2026']); + expect(generateTimeScaleHeaders('day', AUG_11, '2026-08-12T00:00:00')).toEqual(['Aug 11', 'Aug 12']); + expect(generateTimeScaleHeaders('hour', '2026-08-11T09:00:00', '2026-08-11T10:00:00')).toEqual([ + 'Aug 11, 9 AM', + 'Aug 11, 10 AM', + ]); + }); + + it('honours an explicit locale when one is passed', () => { + expect(generateTimeScaleHeaders('month', AUG_11, SEP_11, 'zh')).toEqual(['2026年8月', '2026年9月']); + }); +}); diff --git a/packages/plugin-timeline/src/__tests__/timeline-date-locale.test.tsx b/packages/plugin-timeline/src/__tests__/timeline-date-locale.test.tsx new file mode 100644 index 0000000000..da92888925 --- /dev/null +++ b/packages/plugin-timeline/src/__tests__/timeline-date-locale.test.tsx @@ -0,0 +1,254 @@ +/** + * 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. + */ + +/** + * objectui#4513 — every date the timeline renders follows the ACTIVE locale. + * + * ── The measured defect ────────────────────────────────────────────────── + * `renderer.tsx` handed `Intl` a literal `'en-US'` at four sites and nothing + * at all at a fifth, so a fully Chinese timeline widget rendered an English + * axis. The five, as the #4468 census recorded them: + * + * :71 hour header `toLocaleString('en-US', { month, day, hour })` + * :77 day header `toLocaleDateString('en-US', { month, day })` + * :108 month header `toLocaleDateString('en-US', { month, year })` + * :157 short item `toLocaleDateString()` ← the omission, not a literal + * :160 long item `toLocaleDateString('en-US', { year, month, day })` + * + * `:157` is the same defect spelled the other way: a bare call means "the + * MACHINE's locale", so it agreed with the other four only by the accident of + * an en-US runner, and would have rendered a third locale on anyone's laptop. + * + * ── The fix ────────────────────────────────────────────────────────────── + * All five resolve through `useDisplayLocale()` (tenant regional default → + * active UI language → `'en'`), the one channel `@object-ui/fields` was + * converged onto in objectui#4468 / PR #4512. `'zh'` is a well-formed BCP-47 + * subtag, so `Intl` takes it verbatim — there is no mapping table here or + * anywhere else. The two date helpers are module-level functions, so the + * locale is read once in `TimelineRenderer` and threaded down. + * + * ── Directions ─────────────────────────────────────────────────────────── + * Reverting `renderer.tsx` to `origin/main` and keeping this file turns every + * `zh session` case RED and leaves every `en session` case GREEN. The `en` + * cases are green on BOTH sides deliberately — they are the must-not-change + * half: `'en'` and the retired `'en-US'` produce byte-identical output at all + * five sites, so English rendering must not move by one character. + * + * The locale-free headers (`Week n`, `Qn YYYY`, `YYYY`) and the non-date + * rendering (titles, descriptions, row labels) are likewise green on both + * sides: they never went through `Intl`, and this file pins that the change + * did not disturb them. + * + * The provider-LESS last resort is deliberately not measured here — see + * `timeline-date-locale-fallback.test.tsx` for why it cannot be. + */ + +import React from 'react'; +import { render, screen, cleanup } from '@testing-library/react'; +import { describe, it, expect, afterEach } from 'vitest'; +import { I18nProvider, LocalizationProvider } from '@object-ui/i18n'; +import { TimelineRenderer } from '../renderer'; + +/** + * Dates are spelled with an explicit local time-of-day rather than as bare + * `YYYY-MM-DD`, which `Date` parses as UTC midnight: a runner west of UTC + * would otherwise render the neighbouring day and the pins would read as + * locale failures when they were timezone failures. + */ +const AUG_11_09 = '2026-08-11T09:00:00'; +const AUG_11_10 = '2026-08-11T10:00:00'; +const AUG_11 = '2026-08-11T00:00:00'; +const AUG_12 = '2026-08-12T00:00:00'; +const SEP_11 = '2026-09-11T00:00:00'; + +/** + * A session: the UI language the user picked, plus the tenant's regional + * default (usually absent — the state the card was measured in). + * + * `persistLanguage={false}` keeps each case on its own language instead of + * inheriting whatever the previous one wrote to `localStorage`. + */ +function renderSession(language: string, node: React.ReactNode, tenantLocale?: string) { + return render( + + {node} + , + ); +} + +/** A gantt schema pinned to an explicit range, so the axis never depends on + * today's date. */ +const gantt = (scale: string, minDate: string, maxDate: string) => + ({ + type: 'timeline', + variant: 'gantt', + scale, + minDate, + maxDate, + rowLabel: 'Projects', + items: [{ label: 'Backend', items: [{ title: 'API Design', startDate: minDate, endDate: maxDate }] }], + }) as any; + +/** A vertical schema whose single item carries one date, formatted by + * `dateFormat`. */ +const vertical = (dateFormat: string, time: string) => + ({ + type: 'timeline', + variant: 'vertical', + dateFormat, + items: [{ time, title: 'Beta Release', description: 'Released beta version to testers' }], + }) as any; + +afterEach(() => cleanup()); + +describe('zh session — every date site renders Chinese (objectui#4513)', () => { + it('site :71 — the hour-granularity gantt header', () => { + const { container } = renderSession('zh', ); + expect(container.textContent).toContain('8月11日 9时'); + expect(container.textContent).not.toContain('Aug 11, 9 AM'); + }); + + it('site :77 — the day gantt header', () => { + const { container } = renderSession('zh', ); + expect(container.textContent).toContain('8月11日'); + expect(container.textContent).not.toContain('Aug 11'); + }); + + it('site :108 — the month gantt header (the renderer default scale)', () => { + const { container } = renderSession('zh', ); + expect(container.textContent).toContain('2026年8月'); + expect(container.textContent).toContain('2026年9月'); + expect(container.textContent).not.toContain('Aug 2026'); + }); + + it("site :157 — the `short` item date, the bare call that read the MACHINE's locale", () => { + const { container } = renderSession('zh', ); + expect(container.textContent).toContain('2026/8/11'); + expect(container.textContent).not.toContain('8/11/2026'); + }); + + it('site :160 — the `long` item date', () => { + const { container } = renderSession('zh', ); + expect(container.textContent).toContain('2026年8月11日'); + expect(container.textContent).not.toContain('August 11, 2026'); + }); + + it('the horizontal variant formats its item dates through the same channel', () => { + const schema = { + type: 'timeline', + variant: 'horizontal', + dateFormat: 'long', + items: [{ time: AUG_11, title: 'Q3' }], + } as any; + const { container } = renderSession('zh', ); + expect(container.textContent).toContain('2026年8月11日'); + expect(container.textContent).not.toContain('August 11, 2026'); + }); + + it('the gantt bar tooltip — a date site that renders into an attribute, not text', () => { + renderSession('zh', ); + const bar = document.querySelector('[title*="API Design"]'); + expect(bar?.getAttribute('title')).toContain('2026年8月11日'); + expect(bar?.getAttribute('title')).not.toContain('August 11, 2026'); + }); +}); + +describe('en session — output is byte-identical to the retired en-US (must-not-change)', () => { + it('site :71 — the hour-granularity gantt header', () => { + const { container } = renderSession('en', ); + expect(container.textContent).toContain('Aug 11, 9 AM'); + }); + + it('site :77 — the day gantt header', () => { + const { container } = renderSession('en', ); + expect(container.textContent).toContain('Aug 11'); + expect(container.textContent).toContain('Aug 12'); + }); + + it('site :108 — the month gantt header', () => { + const { container } = renderSession('en', ); + expect(container.textContent).toContain('Aug 2026'); + expect(container.textContent).toContain('Sep 2026'); + }); + + it('site :157 — the `short` item date', () => { + const { container } = renderSession('en', ); + expect(container.textContent).toContain('8/11/2026'); + }); + + it('site :160 — the `long` item date', () => { + const { container } = renderSession('en', ); + expect(container.textContent).toContain('August 11, 2026'); + }); +}); + +describe('non-date rendering is undisturbed (green both sides)', () => { + it('the locale-free header vocabularies stay exactly as they were — zh', () => { + // `Week n` / `Qn YYYY` / `YYYY` never went through `Intl`, so threading a + // locale must not touch them. (The first two ARE English on a zh axis, but + // they need the package's translate channel rather than a locale tag — + // filed as objectui#4520. Converting them here would be an unrelated + // behavior change, and this case pins that #4513 left them alone.) + const week = renderSession('zh', ); + expect(week.container.textContent).toContain('Week 1'); + cleanup(); + + const quarter = renderSession('zh', ); + expect(quarter.container.textContent).toContain('Q3 2026'); + cleanup(); + + const year = renderSession('zh', ); + expect(year.container.textContent).toContain('2026'); + }); + + it('the `iso` date format is not a locale format and must not become one', () => { + // `dateFormat: 'iso'` returns `toISOString().split('T')[0]`. It is a + // machine format by definition; a zh session must still see the ISO day. + const { container } = renderSession('zh', ); + expect(container.textContent).toContain('2026-08-11'); + }); + + it('titles, descriptions and row labels are untouched — zh', () => { + // `getByText` throws when the node is absent, so it carries the assertion; + // `toBeDefined()` is the package's spelling for it (jest-dom's matchers are + // loaded by `vitest.setup.ts` at runtime but not declared to `tsc -p + // tsconfig.test.json`, so no test here uses them). + renderSession('zh', ); + expect(screen.getByText('Beta Release')).toBeDefined(); + expect(screen.getByText('Released beta version to testers')).toBeDefined(); + cleanup(); + + renderSession('zh', ); + expect(screen.getByText('Projects')).toBeDefined(); + expect(screen.getByText('Backend')).toBeDefined(); + }); +}); + +describe('channel precedence is the resolver’s, not this renderer’s (green both sides)', () => { + /** + * `useDisplayLocale()` puts the TENANT locale above the UI language: an org + * that configured `en` means it, even for a user reading Chinese chrome. + * Pinned here so a future "simplification" that reads the UI language + * directly is caught — the point of the card is one resolver, not one hook + * call. + */ + it('an explicit tenant locale outranks the active UI language', () => { + const { container } = renderSession('zh', , 'en'); + expect(container.textContent).toContain('August 11, 2026'); + expect(container.textContent).not.toContain('2026年8月11日'); + }); + + it('the tenant locale reaches the gantt axis too, not only the item dates', () => { + const { container } = renderSession('zh', , 'en'); + expect(container.textContent).toContain('Aug 2026'); + expect(container.textContent).not.toContain('2026年8月'); + }); +}); diff --git a/packages/plugin-timeline/src/renderer.tsx b/packages/plugin-timeline/src/renderer.tsx index 4b94837743..616c1888c4 100644 --- a/packages/plugin-timeline/src/renderer.tsx +++ b/packages/plugin-timeline/src/renderer.tsx @@ -29,6 +29,7 @@ import { TimelineGanttBarContent, } from './index'; import { renderChildren, cn } from '@object-ui/components'; +import { useDisplayLocale } from '@object-ui/i18n'; // Constants /** @@ -58,8 +59,21 @@ export function resolveTimelineScale(schema: { scale?: unknown; timeScale?: unkn * scale produces a non-empty header row — `hour` / `quarter` / `year` used to * fall through the month/week/day chain and return `[]`, blanking the axis * (#2942). Exported for the spec-parity test. + * + * `locale` is threaded in rather than read here: this is a pure function, and + * the session's locale lives behind a hook (#4513). The three `Intl` branches + * below used to pass a literal `'en-US'`, so a fully Chinese timeline rendered + * an English axis. The default is `'en'` — the same concrete last resort + * `useDisplayLocale()` falls back to, and byte-identical to the retired + * `'en-US'` at all three sites — so the existing 3-argument call sites keep + * producing exactly what they produced before. */ -export function generateTimeScaleHeaders(scale: string, minDate: string, maxDate: string): string[] { +export function generateTimeScaleHeaders( + scale: string, + minDate: string, + maxDate: string, + locale: string = 'en', +): string[] { const headers: string[] = []; const start = new Date(minDate); const end = new Date(maxDate); @@ -68,13 +82,13 @@ export function generateTimeScaleHeaders(scale: string, minDate: string, maxDate switch (scale) { case 'hour': while (current <= end) { - headers.push(current.toLocaleString('en-US', { month: 'short', day: 'numeric', hour: 'numeric' })); + headers.push(current.toLocaleString(locale, { month: 'short', day: 'numeric', hour: 'numeric' })); current.setHours(current.getHours() + 1); } break; case 'day': while (current <= end) { - headers.push(current.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })); + headers.push(current.toLocaleDateString(locale, { month: 'short', day: 'numeric' })); current.setDate(current.getDate() + 1); } break; @@ -105,7 +119,7 @@ export function generateTimeScaleHeaders(scale: string, minDate: string, maxDate case 'month': default: while (current <= end) { - headers.push(current.toLocaleDateString('en-US', { month: 'short', year: 'numeric' })); + headers.push(current.toLocaleDateString(locale, { month: 'short', year: 'numeric' })); current.setMonth(current.getMonth() + 1); } break; @@ -150,14 +164,24 @@ function calculateBarDimensions( }; } -// Helper function to format date -function formatDate(dateString: string, format?: string): string { +/** + * Format one item date for display. + * + * `locale` is a REQUIRED parameter, not an optional one (#4513): this helper is + * module-private, every call site sits inside `TimelineRenderer`, and the two + * ways it used to get a locale were both wrong in the same session. `'short'` + * passed nothing at all — and no tag means the MACHINE's locale, which has + * nothing to do with the user — while `'long'` passed a literal `'en-US'`. A + * required parameter is what keeps a future branch from quietly reintroducing + * either. `'iso'` is a machine format by definition and stays locale-free. + */ +function formatDate(dateString: string, format: string | undefined, locale: string): string { const date = new Date(dateString); if (format === 'short') { - return date.toLocaleDateString(); + return date.toLocaleDateString(locale); } if (format === 'long') { - return date.toLocaleDateString('en-US', { + return date.toLocaleDateString(locale, { year: 'numeric', month: 'long', day: 'numeric', @@ -211,6 +235,14 @@ export const TimelineRenderer = ({ schema, className, ...props }: { schema: Time onItemClick, } = schema; + // The one locale channel every renderer in this repo resolves through: + // tenant regional default → active UI language → 'en' (#4513, the channel + // #4468 / PR #4512 converged `@object-ui/fields` onto). Read once here, + // above every variant's early return so the hook count can never depend on + // `variant`, and threaded down — `formatDate` and `generateTimeScaleHeaders` + // are module-level functions and cannot host a hook themselves. + const displayLocale = useDisplayLocale(); + // Vertical Timeline if (variant === 'vertical') { // Detect whether the data was annotated with a `group` key @@ -230,10 +262,10 @@ export const TimelineRenderer = ({ schema, className, ...props }: { schema: Time ? { backgroundColor: `${item.color}33`, borderColor: item.color } : undefined; const dateLabel = item.time - ? formatDate(item.time, dateFormat) - : (item.startDate ? formatDate(item.startDate, dateFormat) : ''); + ? formatDate(item.time, dateFormat, displayLocale) + : (item.startDate ? formatDate(item.startDate, dateFormat, displayLocale) : ''); const endLabel = item.endDate && item.endDate !== item.startDate - ? formatDate(item.endDate, dateFormat) + ? formatDate(item.endDate, dateFormat, displayLocale) : ''; const meta = Array.isArray(item.meta) ? item.meta : []; return ( @@ -318,7 +350,7 @@ export const TimelineRenderer = ({ schema, className, ...props }: { schema: Time
{item.time && ( - {formatDate(item.time, dateFormat)} + {formatDate(item.time, dateFormat, displayLocale)} )} {item.title && {item.title}} @@ -353,6 +385,7 @@ export const TimelineRenderer = ({ schema, className, ...props }: { schema: Time resolveTimelineScale(schema as { scale?: unknown; timeScale?: unknown }), minDate, maxDate, + displayLocale, ); return ( @@ -408,7 +441,7 @@ export const TimelineRenderer = ({ schema, className, ...props }: { schema: Time width={dimensions.width} variant={item.variant || 'default'} onClick={() => onItemClick?.(item, row, rowIndex, itemIndex)} - title={`${item.title || ''}\n${formatDate(item.startDate, dateFormat)} - ${formatDate(item.endDate, dateFormat)}`} + title={`${item.title || ''}\n${formatDate(item.startDate, dateFormat, displayLocale)} - ${formatDate(item.endDate, dateFormat, displayLocale)}`} > {item.title} From cd4122a80e0c92c59fbb8cfb118b5bfbfc511efc Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 13 Aug 2026 04:23:43 +0000 Subject: [PATCH 2/2] chore(changeset): regrade plugin-timeline patch -> minor (#4513) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `generateTimeScaleHeaders` is reachable from the package entry (`index.tsx` does `export * from './renderer'`) and its published declaration grew an optional trailing `locale?: string`. Entry-reachable additive API growth is minor, not patch — patch is for changes with no API-surface movement at all. `dist/index.d.ts` being byte-identical does not argue for patch: the entry re-exports by reference, so the resolved public surface moved even though the entry file's bytes did not. The contrapositive of #4496, which was graded patch precisely because its .d.ts additions were NOT re-exported from the entry; #4403 / #4177 / #4485 / #4495-regrade are the line this follows. Additive and back-compatible is what minor means — no consumer breaks, existing three-argument callers keep compiling and keep producing byte-identical output. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3 --- .changeset/tall-pumas-shave.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/tall-pumas-shave.md b/.changeset/tall-pumas-shave.md index 8d9ad901cf..9055273ba0 100644 --- a/.changeset/tall-pumas-shave.md +++ b/.changeset/tall-pumas-shave.md @@ -1,5 +1,5 @@ --- -'@object-ui/plugin-timeline': patch +'@object-ui/plugin-timeline': minor --- fix(plugin-timeline): dates follow the active locale instead of a hardcoded en-US