Files
dyolink/.cursor/skills/i18n-formatting/SKILL.md

4.0 KiB

i18n formatting (dates, times, numbers)

Use when adding or changing user-visible dates/times/numbers, RTL layout, or locale-specific pickers.

Display formatting

  • Module: frontend/src/lib/i18n/format.ts
  • Hook: frontend/src/lib/hooks/useAppFormatters.ts (useLocale() inside)
  • Never use raw toLocaleDateString / toLocaleTimeString / bare Intl.DateTimeFormat(undefined, …) in UI.
Helper Use for
formatAppDate Date-only labels
formatAppTime Time-only labels
formatAppDateTime Combined stamp
formatAppTimeRange Appointment ranges
formatAppTableDate Table cells
formatAppNumber Counts, amounts (grouped)
formatAppInteger Calendar year/day — no comma grouping
formatAppPickerDateLabel Calendar trigger button label
APP_DATE.* Shared preset option objects

Form controls (dates & selects)

Piece Location / use
AppDateInput All form/filter date fields — every locale; masked typing + calendar popup; wire value YYYY-MM-DD or empty
FORM_DATE_INPUT_CLASS Visual shell for date input (same padding/inset as selects; no CSS chevron)
FORM_SELECT_CLASS Native <select> filters/fields — ps-3 pe-10, chevron from globals.css
FORM_SELECT_COMPACT_CLASS Tiny selects (e.g. sort direction /) — symmetric px-2, no chevron gutter
Dropdown Labeled form select — only where already used; prefer FORM_SELECT_CLASS for new filters
CompactSelect Year/month/day sub-selects inside calendar panels

AppDateInput behavior

  • fa: Jalali display YYYY/MM/DD (Persian digits), parse/mask in persianCalendar.ts
  • en / nl: Gregorian display YYYY-MM-DD, parse/mask in dateInputFormat.ts
  • Calendar icon at end-3 (matches select chevron inset); text uses text-start (logical — right in RTL, left in LTR)
  • Popup: CalendarDayPartsPanel (shared with schedule picker); positioned via useAnchoredPanelPosition (fixed + viewport clamp / flip) so RTL/LTR edge fields stay on-screen
  • Do not add native <input type="date"> — one component for all locales

Select chevron

  • Defined once in frontend/src/styles/globals.css on .form-select:not(.form-select-no-chevron)
  • RTL: background-position: left 0.75rem center; LTR: right 0.75rem center
  • text-align: start on selects

Calendar / appointment pickers

Component Use for
ScheduleDayPicker Appointments strip — nav arrows + today toggle + expandable panel
CalendarDaySelect Navigator wrapper (arrows + panel)
CalendarDayPartsPanel Year / month / day row — used by schedule picker and AppDateInput

Persian (fa): Jalali calendar + arabext digits via Intl (usesPersianCalendar). Internal model stays Date at local midnight (Gregorian) — APIs unchanged.

  • Conversion: frontend/src/lib/i18n/persianCalendar.ts
  • Gregorian typing: frontend/src/lib/i18n/dateInputFormat.ts
  • Gregorian month labels: schedule.monthJanuary … message keys

RTL

  • dir / lang on <html> from app/[locale]/layout.tsx
  • isRtlLocale in frontend/src/i18n/routing.ts
  • Use logical CSS: text-start, text-end, ps-*, pe-*, ms-*, me-*
  • Table: default [&_th]:text-start [&_td]:text-start; override with text-center or text-end on cells — never text-left / text-right on headers (causes header/body column drift in RTL)
  • Minimal overrides in globals.css — avoid double-mirroring (no extra row-reverse on shells that already inherit direction: rtl)

i18n strings

  • User-facing copy: frontend/messages/{en,fa,nl}.json — all three locales.

Verify

cd frontend && npx tsc --noEmit

Manual: switch to Persian — Cases date filters, Tasks filter row (single line + sort visible), Staff/Orgs table columns aligned; switch to English — date fields match adjacent dropdown alignment.