Fix Arabic RTL calendar in public booking date pickers (Task #674)

Problem: In the public room-booking form (protocol-request.tsx), the date
picker calendar showed English weekday headers and a scrambled day grid under
RTL. Root cause: the shared Calendar (react-day-picker v9) was rendered without
`locale` or `dir`, so weekdays stayed English and its flex week rows reversed
visually under CSS RTL.

Changes (artifacts/tx-os/src/components/ui/calendar.tsx):
- Pass date-fns Arabic locale (`ar`) + `dir="rtl"` when Arabic is active, else
  `enUS` + `dir="ltr"`; placed before `{...props}` so callers can still override.
- Added `formatCaption` and `formatDay` formatters (alongside existing
  `formatMonthDropdown`) using Intl `numberingSystem: "latn"` to keep Western
  digits, matching the app-wide AR_LATN convention (Arabic locale would
  otherwise emit Arabic-Indic numerals).
- `isArabic()` now resolves from `document.documentElement.lang`/`dir` first,
  falling back to `i18n.language`.

Deviation from plan: after code review, changed language detection from
i18n.language-only to DOM-first. Reason: the booking page forces Arabic by
setting documentElement.lang/dir without changing i18n.language, so a guest with
saved English would otherwise get an English/LTR calendar on that page.

Out of scope (per user): no time-picker or من/إلى changes.

Verified: `tsc --noEmit` passes for @workspace/tx-os. English calendar
unaffected (default enUS/LTR).
This commit is contained in:
Riyadh
2026-07-07 14:00:17 +00:00
parent 6cca0de595
commit 8e526d7365
+24 -1
View File
@@ -7,16 +7,33 @@ import {
ChevronRightIcon,
} from "lucide-react"
import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker"
import { ar as dfAr, enUS as dfEn } from "date-fns/locale"
import { cn } from "@/lib/utils"
import { Button, buttonVariants } from "@/components/ui/button"
import i18n from "@/i18n"
function isArabic(): boolean {
// The public booking page forces Arabic by setting documentElement.lang/dir
// without changing i18n.language, so prefer the DOM signal (i18n keeps it in
// sync elsewhere) and fall back to the i18n language.
if (typeof document !== "undefined") {
const el = document.documentElement
if (el.lang) return el.lang.toLowerCase().startsWith("ar")
if (el.dir) return el.dir === "rtl"
}
return i18n.language === "ar"
}
function activeLocaleLatn(): string {
const lang = i18n.language === "ar" ? "ar" : "en-US"
const lang = isArabic() ? "ar" : "en-US"
return `${lang}-u-nu-latn`
}
function activeDfLocale() {
return isArabic() ? dfAr : dfEn
}
function Calendar({
className,
classNames,
@@ -41,9 +58,15 @@ function Calendar({
className
)}
captionLayout={captionLayout}
locale={activeDfLocale()}
dir={isArabic() ? "rtl" : "ltr"}
formatters={{
formatMonthDropdown: (date) =>
new Intl.DateTimeFormat(activeLocaleLatn(), { month: "short", numberingSystem: "latn" }).format(date),
formatCaption: (date) =>
new Intl.DateTimeFormat(activeLocaleLatn(), { month: "long", year: "numeric", numberingSystem: "latn" }).format(date),
formatDay: (date) =>
new Intl.DateTimeFormat(activeLocaleLatn(), { day: "numeric", numberingSystem: "latn" }).format(date),
...formatters,
}}
classNames={{