{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"gantt-i18n","type":"registry:ui","title":"Default UI texts, date-format strings, and formatter functions for the gantt, fully overridable per key.","description":"Default UI texts, date-format strings, and formatter functions for the gantt, fully overridable per key.","dependencies":["date-fns"],"registryDependencies":["@neui/gantt-types"],"files":[{"path":"gantt-i18n.tsx","type":"registry:ui","content":"// Title: Gantt I18n\n// Description: Default UI texts, date-format strings, and formatter functions for the gantt, fully overridable per key.\n\nimport type {\n GanttDateRange,\n GanttScale,\n} from \"@/components/neui/gantt/gantt-types\"\nimport {\n format,\n isSameMonth,\n isSameYear,\n subMilliseconds,\n type Locale,\n} from \"date-fns\"\n\ninterface GanttI18nConfig {\n labels: {\n today: string\n previous: string\n next: string\n addEvent: string\n /** \"Add task\" hint at the foot of the tree. */\n addTask: string\n allDay: string\n loading: string\n event: string\n events: (count: number) => string\n week: (weekNumber: number) => string\n resources: string\n goToDate: string\n /** Hover hint over empty row space, click-only create. */\n scheduleHint: string\n /** Same hint where dragCreate is on and a drag paints a range. */\n scheduleHintDrag: string\n reorder: string\n /** Scale switcher label (\"Timeline scale\"). */\n selectView: string\n zoomIn: string\n zoomOut: string\n /** Aria-label of the tree/timeline splitter. */\n resizePanel: string\n /** Aria-label of the off-screen bar chips. */\n jumpToBar: (title: string) => string\n /** Read to screen readers as part of the bar label. */\n progress: (percent: number) => string\n /** Live duration readout on the resize indicator. */\n durationDays: (days: number) => string\n /** Appended to the bar aria-label when its segment is clipped by the range. */\n continues: string\n scales: {\n day: string\n week: string\n month: string\n quarter: string\n year: string\n }\n }\n /** date-fns format strings, applied with the gantt `locale`. */\n formats: {\n monthTitle: string\n dayTitle: string\n timeGutter: string\n eventTime: string\n }\n functions: {\n formatTitle: (\n scale: GanttScale,\n ctx: {\n date: Date\n activeRange: GanttDateRange\n visibleRange: GanttDateRange\n locale?: Locale\n }\n ) => string\n formatEventTime: (\n start: Date,\n end: Date,\n allDay: boolean,\n locale?: Locale\n ) => string\n formatDayRange: (range: GanttDateRange, locale?: Locale) => string\n /** Composes the bar's screen-reader label from its localized parts. */\n formatEventAriaLabel: (parts: {\n title: string\n timeLabel: string\n rowTitle?: string\n progressLabel?: string\n continues: boolean\n }) => string\n }\n}\n\nconst DEFAULT_LABELS: GanttI18nConfig[\"labels\"] = {\n today: \"Today\",\n previous: \"Previous\",\n next: \"Next\",\n addEvent: \"Add event\",\n addTask: \"Add task\",\n allDay: \"All day\",\n loading: \"Loading events\",\n event: \"event\",\n events: (count) => (count === 1 ? \"1 event\" : `${count} events`),\n week: (weekNumber) => `W${weekNumber}`,\n resources: \"Resources\",\n goToDate: \"Go to date\",\n scheduleHint: \"Click to add a schedule\",\n scheduleHintDrag: \"Click or drag to add a schedule\",\n reorder: \"Reorder\",\n selectView: \"Select view\",\n zoomIn: \"Zoom in\",\n zoomOut: \"Zoom out\",\n resizePanel: \"Resize panel\",\n jumpToBar: (title) => `Scroll to \"${title}\"`,\n progress: (percent) => `${percent}% complete`,\n durationDays: (days) => (days === 1 ? \"1 day\" : `${days} days`),\n continues: \"continues\",\n scales: {\n day: \"Day\",\n week: \"Week\",\n month: \"Month\",\n quarter: \"Quarter\",\n year: \"Year\",\n },\n}\n\nconst DEFAULT_FORMATS: GanttI18nConfig[\"formats\"] = {\n monthTitle: \"MMMM yyyy\",\n dayTitle: \"EEEE, MMMM d, yyyy\",\n timeGutter: \"h a\",\n eventTime: \"h:mm a\",\n}\n\n/**\n * Default formatting functions BOUND to a config's labels/formats, so that\n * `formats` overrides flow into the default renderers (a consumer overriding\n * formats.eventTime without replacing formatEventTime still sees it applied).\n */\nfunction makeDefaultGanttFunctions(\n cfg: Pick\n): GanttI18nConfig[\"functions\"] {\n return {\n formatTitle: (scale, { date, activeRange, locale }) => {\n const opts = { locale }\n if (scale === \"day\") {\n return format(date, cfg.formats.dayTitle, opts)\n }\n if (scale === \"month\") {\n return format(date, cfg.formats.monthTitle, opts)\n }\n if (scale === \"quarter\") {\n return format(date, \"QQQ yyyy\", opts)\n }\n if (scale === \"year\") {\n return format(date, \"yyyy\", opts)\n }\n // week: smart range label, last day is activeRange.end - 1ms.\n // subMilliseconds keeps the zoned date type (a plain new Date(ms)\n // would flip the label to the machine zone near midnight)\n const rangeEnd = subMilliseconds(activeRange.end, 1)\n const start = activeRange.start\n if (isSameMonth(start, rangeEnd)) {\n return `${format(start, \"MMMM d\", opts)} - ${format(rangeEnd, \"d, yyyy\", opts)}`\n }\n if (isSameYear(start, rangeEnd)) {\n return `${format(start, \"MMM d\", opts)} - ${format(rangeEnd, \"MMM d, yyyy\", opts)}`\n }\n return `${format(start, \"MMM d, yyyy\", opts)} - ${format(rangeEnd, \"MMM d, yyyy\", opts)}`\n },\n formatEventTime: (start, end, allDay, locale) => {\n const opts = { locale }\n if (allDay) {\n // a gantt bar is a DATE RANGE: show it, never a bare \"All day\".\n // Ends are exclusive midnights, so the last shown day is end - 1ms;\n // subMilliseconds keeps the caller's zoned date type intact.\n const last =\n end.getTime() - 1 >= start.getTime() ? subMilliseconds(end, 1) : start\n const sameDay =\n format(start, \"yyyy-MM-dd\") === format(last, \"yyyy-MM-dd\")\n if (sameDay) return format(start, \"MMM d, yyyy\", opts)\n if (isSameYear(start, last)) {\n return `${format(start, \"MMM d\", opts)} - ${format(last, \"MMM d, yyyy\", opts)}`\n }\n return `${format(start, \"MMM d, yyyy\", opts)} - ${format(last, \"MMM d, yyyy\", opts)}`\n }\n const fmt = cfg.formats.eventTime\n // Multi-day timed events carry the date on both sides. Compare calendar\n // days off the last rendered instant (end is exclusive, so a 14:00 to\n // midnight bar still ends on the start day). Elapsed ms would miss an\n // exactly-24h bar and a DST day that only runs 23 hours.\n const lastInstant =\n end.getTime() - 1 >= start.getTime() ? subMilliseconds(end, 1) : start\n if (format(start, \"yyyy-MM-dd\") !== format(lastInstant, \"yyyy-MM-dd\")) {\n return `${format(start, `MMM d, ${fmt}`, opts)} - ${format(end, `MMM d, ${fmt}`, opts)}`\n }\n return `${format(start, fmt, opts)} - ${format(end, fmt, opts)}`\n },\n formatDayRange: (range, locale) => {\n const opts = { locale }\n const rangeEnd = subMilliseconds(range.end, 1)\n return `${format(range.start, \"MMM d\", opts)} - ${format(rangeEnd, \"MMM d\", opts)}`\n },\n formatEventAriaLabel: ({\n title,\n timeLabel,\n rowTitle,\n progressLabel,\n continues,\n }) =>\n [\n title,\n timeLabel,\n rowTitle,\n progressLabel,\n continues ? cfg.labels.continues : undefined,\n ]\n .filter(Boolean)\n .join(\", \"),\n }\n}\n\nconst DEFAULT_GANTT_I18N: GanttI18nConfig = {\n labels: DEFAULT_LABELS,\n formats: DEFAULT_FORMATS,\n functions: makeDefaultGanttFunctions({\n labels: DEFAULT_LABELS,\n formats: DEFAULT_FORMATS,\n }),\n}\n\n/** Deep-partial override shape: replace individual keys, never sections. */\ninterface GanttI18nOverrides {\n labels?: Partial> & {\n scales?: Partial\n }\n formats?: Partial\n functions?: Partial\n}\n\n/**\n * Shallow merge per nested object, matching the filters.tsx i18n contract:\n * a partial override replaces individual keys, never whole sections. Default\n * functions are re-bound to the MERGED labels/formats so a `formats` (or\n * `labels.continues`) override reaches the default renderers; explicit\n * `functions` overrides still win.\n */\nfunction mergeGanttI18n(overrides?: GanttI18nOverrides): GanttI18nConfig {\n if (!overrides) return DEFAULT_GANTT_I18N\n const labels = {\n ...DEFAULT_LABELS,\n ...overrides.labels,\n // nested section: replace individual scale names, never the whole set\n scales: {\n ...DEFAULT_LABELS.scales,\n ...overrides.labels?.scales,\n },\n }\n const formats = { ...DEFAULT_FORMATS, ...overrides.formats }\n return {\n labels,\n formats,\n functions: {\n ...makeDefaultGanttFunctions({ labels, formats }),\n ...overrides.functions,\n },\n }\n}\n\nexport { DEFAULT_GANTT_I18N, mergeGanttI18n }\nexport type { GanttI18nConfig, GanttI18nOverrides }","target":"components/neui/gantt/gantt-i18n.tsx"}]}