HIJRI CALENDAR
HIJRI CALENDAR
Present a date or datetime field in the Hijri (Umm al-Qura) calendar.
Pure presentation — no new field type, no new column, no DDL, no migration.
Gated behind HIJRI_CALENDAR_ENABLED; when off, no field can opt in and every
existing field behaves exactly as before.
The invariant
The store is always Gregorian. date stays DATE, datetime stays
TIMESTAMPTZ, and the stored bytes for a Hijri field are identical to those of
a Gregorian field holding the same day. Conversion happens only at the read
boundary.
That is not a stylistic choice — storing a Hijri 1447-09-12 as if it were
Gregorian is a ~579-year silent corruption. Keeping the store Gregorian is
what makes filters, BETWEEN, chronological sort, DATEDIFF/DATEADD, serial
year_format, scheduled workflows, dashboard group-by and audit diffs all keep
working untouched — they never see a Hijri value.
Flipping a field between calendars is therefore a config change with zero data migration: the rows were always Gregorian, so they simply re-render.
Configuration
The option lives on the field's existing options JSON:
{
"calendar_system": "hijri", // 'gregorian' (default) | 'hijri'
"min_date": "2000-01-01", // bounds stay Gregorian ISO
"max_date": "2077-12-31"
}Absent means gregorian. Any other value is rejected at config time with a 400
— it must not be ignored, because the read path treats anything that is not
'hijri' as Gregorian, so a typo'd "islamic" would save cleanly and then
silently render the wrong calendar.
What each surface returns
| Surface | Behavior |
|---|---|
| Record read (detail + list) | Adds a read-only companion key <slug>_hijri beside the untouched ISO value |
| Excel/CSV export | Adds an extra "<Label> (Hijri)" text column beside the real date column |
| Document templates (PDF) | Renders {{field}} in place as Hijri |
// GET /object/{slug}/data/{record}
{
"contract_date-a1b2": "2026-03-01", // canonical, unchanged
"contract_date-a1b2_hijri": "1447-09-12" // derived, read-only
}Why a companion key and not a replacement
A client that reads a record and echoes it back on write must never be able to send a Hijri year into a Gregorian column. Keeping the canonical ISO value under its own slug makes that structurally impossible.
The same reasoning drives the export: the real column keeps its Gregorian ISO date cell so a re-import reads the original day, and the suffixed header matches no field so import ignores it.
Document templates are the deliberate exception — a rendered PDF is terminal
output that never round-trips, and {{contract_date}} is the placeholder authors
actually write, so there is no round-trip hazard to protect against.
Format
Numeric YYYY-MM-DD in Hijri (1447-09-12). Arabic month names, RTL layout,
era marks and the Hijri picker grid are the frontend's concern — the backend
emits a parseable string and does not decide presentation.
Supported range
≈1937-01-01 – 2077-12-31 CE (≈1356–1500 AH) — the Umm al-Qura table window.
Outside it, conversion returns null rather than a value.
This guard is not optional: ICU does not fail past the table, it silently
extrapolates with tabular arithmetic (2078-06-01 → 1501-07-20). A
plausible-looking wrong date is worse than an absent one. Since these are reads,
null is correct — there is no request to reject.
Authority
Umm al-Qura (the Saudi civil calendar) is the single declared authority.
Tabular and sighting-based Hijri variants disagree with it by ±1–2 days, so every
consumer goes through
toHijriDateString — never a
local Intl call or a date library.
Conversion uses Node's built-in ICU (Intl.DateTimeFormat with the
islamic-umalqura calendar). No third-party dependency, but it does require
a full-ICU Node build.
Where it lives
| Concern | File |
|---|---|
| Conversion | src/common/utils/hijri-calendar.util.ts |
| "Is this field Hijri?" | src/api/v1/object/shared/utils/hijri-field.util.ts |
| Option type | .../field/options/interfaces/calendar-system.type.ts |
| Option validation | .../field/options/helpers/assert-calendar-system.ts |
| Feature flag | src/api/v1/object/fields/services/hijri-feature.ts |
Not implemented
Deliberately out of scope for this slice — the write path is untouched, which is what keeps the corruption class closed:
- Hijri input /
convert_input— values are still entered as Gregorian. allow_display_toggle— no per-viewer calendar switch.- Arabic month-name rendering server-side.
start_date/due_datefield types.- Messaging variable rendering — still Gregorian.
Flag behavior
HIJRI_CALENDAR_ENABLED gates config time only: it decides whether
calendar_system='hijri' can be saved onto a field, not whether an already
configured field renders. Turning it off stops new Hijri fields but leaves
existing ones rendering. Since the companion key is purely additive over an
unchanged Gregorian value, that leaves no way to corrupt data — only an extra
key a client may ignore.