Let the date picker take a first day of the week and a format

An application that lets each person choose when their week starts and
how dates are written needs the picker to follow that choice rather
than the locale. <x-datepicker> takes week-start, 0 (Sunday) to 6
(Saturday), and format, dd, MM and yyyy each once around one delimiter
(dd.MM.yyyy, dd/MM/yyyy, MM/dd/yyyy, yyyy-MM-dd). They replace what
Intl derives for firstDayOfWeek() and inputFormat(), so the field, the
server-rendered value, the typed-date reader (year first too), the
calendar's columns and weekday header, Home and End, the dialog's text
fields and ranges all follow them. Month and weekday names stay the
locale's, and wire:model still stores Y-m-d. Values that are neither
are ignored, and without the props nothing changes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RHoXZSHc8gGpZjFmA5fPc2
This commit is contained in:
Andreas Reinhold / reini
2026-09-13 18:18:39 +02:00
co-authored by Claude Opus 5
parent fdb13313f7
commit fb29169d44
6 changed files with 222 additions and 17 deletions
+24 -10
View File
@@ -11,7 +11,9 @@
* Dates are ISO strings (`2026-09-13`) throughout, computed in UTC so no time zone or daylight
* saving change can move a day; only "today" is read in the browser's own zone. Month and weekday
* names, the week's first day and the typed format come from `Intl` for the locale the server
* passes (the application's).
* passes (the application's), unless the component names the first day (`weekStart`, 0 for Sunday
* to 6) or the format (`format`, such as `dd.MM.yyyy`); everything that reads `firstDay` and
* `format` below then follows those.
*
* The keyboard is WAI-ARIA's date picker dialog: arrows move a day or a week, Home and End go to
* the start and end of the week, PageUp and PageDown a month (with Shift a year), Space selects,
@@ -82,8 +84,12 @@ function localToday() {
return `${pad(now.getFullYear(), 4)}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`
}
/** 0 for Sunday … 6 for Saturday. */
function firstDayOfWeek(locale) {
/** 0 for Sunday … 6 for Saturday: `weekStart` when it is one of those, otherwise the locale's. */
function firstDayOfWeek(locale, weekStart = null) {
if (Number.isInteger(weekStart) && weekStart >= 0 && weekStart <= 6) {
return weekStart
}
try {
const tag = new Intl.Locale(locale)
const info = typeof tag.getWeekInfo === 'function' ? tag.getWeekInfo() : tag.weekInfo
@@ -111,9 +117,16 @@ function firstDayOfWeek(locale) {
/**
* The typed format: the locale's short numeric date, reduced to `dd`, `MM` and `yyyy` and one
* delimiter — androidx's `datePatternAsInputFormat`, fed from `formatToParts` because `Intl` has
* no pattern to give. `de` gives `dd.MM.yyyy`, `en-US` `MM/dd/yyyy`, `ja` `yyyy/MM/dd`.
* no pattern to give. `de` gives `dd.MM.yyyy`, `en-US` `MM/dd/yyyy`, `ja` `yyyy/MM/dd`. A `chosen`
* pattern of the same shape (each unit once, one delimiter) replaces the locale's.
*/
function inputFormat(locale) {
function inputFormat(locale, chosen = null) {
const units = /^(dd|MM|yyyy)([/\-.])(dd|MM|yyyy)\2(dd|MM|yyyy)$/.exec(typeof chosen === 'string' ? chosen : '')
if (units && new Set([units[1], units[3], units[4]]).size === 3) {
return formatOf(chosen)
}
let pattern = ''
try {
@@ -131,10 +144,11 @@ function inputFormat(locale) {
pattern = ''
}
if (!/^(?=.*dd)(?=.*MM)(?=.*yyyy)[dMy]+[/\-.][dMy]+[/\-.][dMy]+$/.test(pattern)) {
pattern = 'yyyy-MM-dd'
}
return formatOf(/^(?=.*dd)(?=.*MM)(?=.*yyyy)[dMy]+[/\-.][dMy]+[/\-.][dMy]+$/.test(pattern) ? pattern : 'yyyy-MM-dd')
}
/** A pattern, the placeholder it shows (`DD.MM.YYYY`) and the order its units are typed in (`dMy`). */
function formatOf(pattern) {
return {
pattern,
placeholder: pattern.toUpperCase(),
@@ -169,8 +183,8 @@ document.addEventListener('alpine:init', () => {
const locale = config.locale || document.documentElement.lang || 'en'
const format = (options) => new Intl.DateTimeFormat(locale, { timeZone: 'UTC', ...options })
this.firstDay = firstDayOfWeek(locale)
this.format = inputFormat(locale)
this.firstDay = firstDayOfWeek(locale, config.weekStart ?? null)
this.format = inputFormat(locale, config.format ?? null)
this.numbers = new Intl.NumberFormat(locale, { useGrouping: false })
this.formats = {
monthYear: format({ year: 'numeric', month: 'long' }),