Skip to content

Internationalization

sh
npm install @jalali-js/i18n

Most apps never import this package directly: useCalendar()/format() are already wired up through locale props in @jalali-js/react and @jalali-js/vue. Reach for it directly when you need to format a date outside a component, or build your own display logic on the same locale data those bindings use.

format()

ts
import { format, en, fa, ps } from '@jalali-js/i18n';

const date = {
  precision: 'date' as const,
  system: 'jalali' as const,
  year: 1403,
  month: 5,
  day: 15,
};

format(date, en); // '15 Mordad 1403'
format(date, fa); // '۱۵ مرداد ۱۴۰۳'
format(date, ps); // '۱۵ زمری ۱۴۰۳' (the Afghan month names; see below)
format(date, en, { style: 'short' }); // '15 Mor 1403'
format(date, en, { weekday: true }); // 'Monday, 15 Mordad 1403'
format(date, fa, { numerals: 'latin' }); // '15 مرداد 1403' (Latin digits, Persian text)

format() is display-only: it reads date.system's own month names (so a Gregorian-system date formats with Gregorian month names, a Jalali-system date with Jalali ones, in either locale), and never affects what toStorageValue() returns. See Display value vs. storage value.

Templates

When you need an exact shape, pass a template. It replaces the preset layout, so style and weekday are ignored. The numerals option still applies.

ts
format(date, en, { template: 'YYYY/MM/DD' }); // '1403/05/15'
format(date, fa, { template: 'YYYY/MM/DD' }); // '۱۴۰۳/۰۵/۱۵'
format(date, en, { template: 'D MMMM YYYY' }); // '15 Mordad 1403'
format(date, en, { template: 'dddd D MMM YYYY' }); // 'Monday 15 Mor 1403'
TokenMeaningExample
YYYYYear, 4 digits1403
MMMonth, 2 digits05
MMonth5
DDDay, 2 digits15
DDay15
MMMMMonth name, longMordad
MMMMonth name, shortMor
ddddWeekday name, longMonday
dddWeekday name, shortMon

Text between tokens passes through as-is. Keep it to punctuation and spaces: a letter that matches a token (for example the D in Day:) is read as that token.

parseTemplate()

The reverse of a template format: strict parsing of a known shape. Free-form input belongs to @jalali-js/nlp instead.

ts
import { parseTemplate, en, fa } from '@jalali-js/i18n';

parseTemplate('1403/05/15', 'YYYY/MM/DD', en);
// { precision: 'date', system: 'jalali', year: 1403, month: 5, day: 15 }
parseTemplate('۱۴۰۳/۰۵/۱۵', 'YYYY/MM/DD', fa); // same date; native digits accepted
parseTemplate('15 Mordad 1403', 'D MMMM YYYY', en); // same date
parseTemplate('2024/08/05', 'YYYY/MM/DD', en, { system: 'gregorian' });

It returns null instead of guessing:

  • The input must match the template's exact shape, with nothing left over.
  • Digits can be Latin or the locale's native set.
  • The date must exist: 1402/12/30 (Esfand 30 in a non-leap year) is null.
  • A weekday name must match the parsed date: 'Tuesday 15 Mordad 1403' is null, because that day is a Monday.
  • The template must produce a year, a month, and a day.

formatNumber()

The digit-formatting format() uses internally, available on its own:

ts
import { formatNumber, fa } from '@jalali-js/i18n';

formatNumber(1403, 'native', fa.digits); // '۱۴۰۳'
formatNumber(1403, 'latin', fa.digits); // '1403'
formatNumber(5, 'latin', fa.digits, 2); // '05' (the optional minimum width zero-pads)

Locale packs

en, fa, and ps are each a LocalePack: month names for both calendar systems (English transliterations of the Jalali months in en, Persian transliterations of the Gregorian months in fa), weekday names, defaultNumerals, digits, and text direction. Persian has no widely standardized abbreviated month form the way English does, so fa's short month names reuse long; weekday names do have a well-known one-letter short form, so those differ. Full shape: LocalePack.

ps is Pashto, one of Afghanistan's two official languages. Afghanistan uses the same Jalali solar calendar as Iran, and names its months after the zodiac signs. So ps's Jalali month names (وری, غویی, ..., کب) are alternative names for the same months, and share nothing with fa's Persian ones. The data comes from CLDR's own ps locale, read through ICU, not from memory. CLDR has no abbreviated Pashto forms, so short reuses long throughout.

Adding a locale

A LocalePack is plain data against one exported interface. Adding a locale changes no other code in this package: format() reads whatever pack you pass it. The steps, using ps as the worked example:

  1. Write the pack. Copy fa.ts (RTL) or en.ts (LTR) in packages/i18n/src/ and fill in the fields: code, direction, the 10 digits characters, defaultNumerals, weekdaySeparator, month names for both calendar systems, weekday names (index 0 is Sunday), and the two picker placeholders. Source the names from real data you can verify. CLDR through Node's Intl works well: new Intl.DateTimeFormat('ps-AF-u-ca-persian', { month: 'long' }) gives the Jalali month names, and Intl.NumberFormat gives the digits.
  2. Register it. Export the pack from packages/i18n/src/index.ts, and add its code to LocaleCode and the pack table in packages/i18n/src/locale-packs.ts. That one table is what the React, Vue, and Web Components bindings re-export, so the locale prop accepts the new code with no binding change.
  3. Add tests mirroring the existing fa coverage in format.test.ts and numerals.test.ts: one formatted string per style, the weekday prefix, and an explicit numerals: 'latin' override.
  4. Optional: NLP phrases. A word list in packages/nlp/src/word-list.ts adds natural-language parsing for the locale. This is separate from the display pack and ships only when the phrase set is well understood. Intl.RelativeTimeFormat is a verifiable source for the relative terms.