{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"event-calendar-i18n","type":"registry:ui","title":"Default UI texts, date-format strings, and formatter functions for the event calendar, fully overridable per key.","description":"Default UI texts, date-format strings, and formatter functions for the event calendar, fully overridable per key.","dependencies":["date-fns"],"registryDependencies":["@neui/event-calendar-types"],"files":[{"path":"event-calendar-i18n.tsx","type":"registry:ui","content":"// Title: Event Calendar I18n\n// Description: Default UI texts, date-format strings, and formatter functions for the event calendar, fully overridable per key.\n\nimport type {\n CalendarView,\n EventCalendarDateRange,\n} from \"@/components/neui/event-calendar/event-calendar-types\"\nimport {\n format,\n isSameMonth,\n isSameYear,\n subMilliseconds,\n type Locale,\n} from \"date-fns\"\n\ninterface EventCalendarI18nConfig {\n labels: {\n today: string\n previous: string\n next: string\n addEvent: string\n allDay: string\n more: (count: number) => string\n noEvents: string\n loading: string\n event: string\n events: (count: number) => string\n selectView: string\n week: (weekNumber: number) => string\n resources: string\n goToDate: string\n /** Cursor hint while a drag/resize hovers a rejected position. */\n dropNotAllowed: string\n /** Aria-label suffix on chip segments that continue past the cell. */\n continues: string\n /** Agenda label for the first day of a multi-day event. */\n timeFrom: (time: string) => string\n /** Agenda label for the last day of a multi-day event. */\n timeUntil: (time: string) => string\n /** View-switcher shortcut hint characters, per view. */\n viewShortcuts: Record\n /** Aria-label of the agenda day collapse/expand toggle. */\n toggleDayEvents: (count: number, expanded: boolean) => string\n /** Aria-label of the agenda event details toggle. */\n eventDetails: (title: string) => string\n /** Compact \"+N\" overflow (agenda summary dot stack). */\n moreCompact: (count: number) => string\n /** Joins a bounded from-to time span. */\n timeRange: (from: string, to: string) => string\n }\n viewNames: {\n month: string\n week: string\n day: string\n days: (count: number) => string\n agenda: string\n resource: string\n }\n /** date-fns format strings, applied with the calendar `locale`. */\n formats: {\n monthTitle: string\n /** Undefined = smart cross-month range via functions.formatTitle. */\n weekTitle?: string\n dayTitle: string\n /** Undefined = smart range label via functions.formatTitle. */\n agendaTitle?: string\n monthDayHeader: string\n /** Narrow variant used by the month view below the compact breakpoint. */\n monthDayHeaderNarrow: string\n timeGridDayHeader: string\n agendaDayHeader: string\n /** Agenda date-gutter day number. */\n agendaDayNumber: string\n /** Agenda date-gutter weekday label. */\n agendaWeekday: string\n /** \"+N more\" popover day header. */\n moreDayHeader: string\n /** Month cell aria-label date. */\n monthCellAriaLabel: string\n /** Time-grid day column aria-label date. */\n dayAria: string\n /** Undefined = the resource view title falls back to dayTitle. */\n resourceTitle?: string\n timeGutter: string\n /** Sub-hour gutter labels (interval below 60 minutes). */\n timeGutterMinute: string\n eventTime: string\n monthCellDay: string\n }\n functions: {\n formatTitle: (\n view: CalendarView,\n ctx: {\n date: Date\n activeRange: EventCalendarDateRange\n visibleRange: EventCalendarDateRange\n locale?: Locale\n }\n ) => string\n formatEventTime: (\n start: Date,\n end: Date,\n allDay: boolean,\n /** date-fns options (the calendar `locale`); trailing so a 3-arg override still fits. */\n opts?: { locale?: Locale }\n ) => string\n formatDayRange: (\n range: EventCalendarDateRange,\n opts?: { locale?: Locale }\n ) => string\n /** Chip native tooltip text; return undefined to drop the attribute. */\n formatEventLabel?: (title: string, timeLabel: string) => string | undefined\n /** Chip aria-label composition. */\n formatEventAriaLabel?: (\n title: string,\n timeLabel: string,\n continues: boolean\n ) => string\n }\n}\n\nconst DEFAULT_LABELS: EventCalendarI18nConfig[\"labels\"] = {\n today: \"Today\",\n previous: \"Previous\",\n next: \"Next\",\n addEvent: \"Add event\",\n allDay: \"All day\",\n more: (count) => `+${count} more`,\n noEvents: \"No events\",\n loading: \"Loading events\",\n event: \"event\",\n events: (count) => (count === 1 ? \"1 event\" : `${count} events`),\n selectView: \"Select view\",\n week: (weekNumber) => `W${weekNumber}`,\n resources: \"Resources\",\n goToDate: \"Go to date\",\n dropNotAllowed: \"Can't place here\",\n continues: \"continues\",\n timeFrom: (time) => `From ${time}`,\n timeUntil: (time) => `Until ${time}`,\n viewShortcuts: {\n month: \"M\",\n week: \"W\",\n day: \"D\",\n days: \"5\",\n agenda: \"A\",\n resource: \"G\",\n },\n toggleDayEvents: (count) => (count === 1 ? \"1 event\" : `${count} events`),\n eventDetails: (title) => title,\n moreCompact: (count) => `+${count}`,\n timeRange: (from, to) => `${from} - ${to}`,\n}\n\nconst DEFAULT_VIEW_NAMES: EventCalendarI18nConfig[\"viewNames\"] = {\n month: \"Month\",\n week: \"Week\",\n day: \"Day\",\n days: (count) => (count === 1 ? \"1 day\" : `${count} days`),\n agenda: \"Agenda\",\n resource: \"Time Grid\",\n}\n\nconst DEFAULT_FORMATS: EventCalendarI18nConfig[\"formats\"] = {\n monthTitle: \"MMMM yyyy\",\n weekTitle: undefined,\n dayTitle: \"EEEE, MMMM d, yyyy\",\n agendaTitle: undefined,\n monthDayHeader: \"EEE\",\n monthDayHeaderNarrow: \"EEEEE\",\n timeGridDayHeader: \"EEE d\",\n agendaDayHeader: \"EEEE, MMMM d\",\n agendaDayNumber: \"d\",\n agendaWeekday: \"EEE\",\n moreDayHeader: \"EEEE, MMMM d\",\n monthCellAriaLabel: \"PPPP\",\n dayAria: \"PPPP\",\n resourceTitle: undefined,\n timeGutter: \"h a\",\n timeGutterMinute: \"h:mm a\",\n eventTime: \"h:mm a\",\n monthCellDay: \"d\",\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.monthTitle without replacing formatTitle still sees it applied).\n */\nfunction makeDefaultFunctions(\n cfg: Pick\n): EventCalendarI18nConfig[\"functions\"] {\n return {\n formatTitle: (view, { date, activeRange, locale }) => {\n const opts = { locale }\n if (view === \"month\") {\n return format(date, cfg.formats.monthTitle, opts)\n }\n if (view === \"resource\") {\n return format(\n date,\n cfg.formats.resourceTitle ?? cfg.formats.dayTitle,\n opts\n )\n }\n if (view === \"day\") {\n return format(date, cfg.formats.dayTitle, opts)\n }\n if (view === \"week\" && cfg.formats.weekTitle) {\n return format(date, cfg.formats.weekTitle, opts)\n }\n if (view === \"agenda\" && cfg.formats.agendaTitle) {\n return format(date, cfg.formats.agendaTitle, opts)\n }\n // week / days / agenda: 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, opts) => {\n if (allDay) return cfg.labels.allDay\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 event still ends on the start day). Elapsed ms would miss an\n // exactly-24h event 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, opts) => {\n // subMilliseconds keeps the zoned date type, same reason as formatTitle\n const rangeEnd = subMilliseconds(range.end, 1)\n return `${format(range.start, \"MMM d\", opts)} - ${format(rangeEnd, \"MMM d\", opts)}`\n },\n }\n}\n\nconst DEFAULT_EVENT_CALENDAR_I18N: EventCalendarI18nConfig = {\n labels: DEFAULT_LABELS,\n viewNames: DEFAULT_VIEW_NAMES,\n formats: DEFAULT_FORMATS,\n functions: makeDefaultFunctions({\n labels: DEFAULT_LABELS,\n formats: DEFAULT_FORMATS,\n }),\n}\n\n/**\n * One level deeper than `Partial`, because the merge below is per nested\n * section: overriding a single label must not force a consumer to restate the\n * other 22. Unknown keys are still rejected by the excess property check.\n */\ntype EventCalendarI18nOverrides = {\n [K in keyof EventCalendarI18nConfig]?: 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; explicit `functions`\n * overrides still win.\n */\nfunction mergeEventCalendarI18n(\n overrides?: EventCalendarI18nOverrides\n): EventCalendarI18nConfig {\n if (!overrides) return DEFAULT_EVENT_CALENDAR_I18N\n const labels = { ...DEFAULT_LABELS, ...overrides.labels }\n const viewNames = { ...DEFAULT_VIEW_NAMES, ...overrides.viewNames }\n const formats = { ...DEFAULT_FORMATS, ...overrides.formats }\n return {\n labels,\n viewNames,\n formats,\n functions: {\n ...makeDefaultFunctions({ labels, formats }),\n ...overrides.functions,\n },\n }\n}\n\nexport { DEFAULT_EVENT_CALENDAR_I18N, mergeEventCalendarI18n }\nexport type { EventCalendarI18nConfig, EventCalendarI18nOverrides }","target":"components/neui/event-calendar/event-calendar-i18n.tsx"}]}