Display names and digits become configurable, and the package ships TypeScript declarations.
npm install jalali-date@1.5.0
Configurable display names
MONTH_NAMES, ABBR_DAYS and DAYS_NAMES were read straight out of constants.js by the format tokens, so rendering anything but Persian meant forking the library. They are now defaults behind a validated config object, settable at two levels:
JDate.setDefaultConfig({ monthNames }) // app-wide
new JDate([1396, 8, 26], { monthNames }) // one instance, layered over the defaultThree optional keys: monthNames (12 entries, calendar order), dayNames and abbrDays (7 each). Both day lists are indexed by Date#getDay(), so they are Sunday-first rather than Saturday-first. Keys left out keep their built-in Persian values, and only the name identifiers are affected — YYYY, MM and DD are untouched.
The config is the last constructor argument in all four initialization forms, told apart from a date by being a plain object. Instances resolve it once at construction and store the frozen result on this.config, so a later setDefaultConfig() does not retroactively change dates that already exist, and two locales can be rendered side by side. setDefaultConfig() replaces rather than merges, so two partial calls do not accumulate; resetDefaultConfig() restores the built-in names. An unknown key, a non-array, a wrong length or a non-string entry throws at the call site rather than surfacing later as undefined inside formatted output.
Persian digits
The numeric identifiers had emitted ASCII digits since 1.0, so a fully Persian rendering was impossible without post-processing — and post-processing is exactly what the caller cannot do safely, because by then the digits inside a month name are indistinguishable from the ones the tokens produced.
new JDate([1396, 8, 26], { persianNumerical: true }).format('YYYY/MM/DD') //=> ۱۳۹۶/۰۸/۲۶
JDate.setDefaultConfig({ persianNumerical: true }) // app-wideIt defaults to false: ASCII digits are what every existing caller gets today, and flipping that would silently change formatted output for all of them. Scope is the seven numeric identifiers only. Name identifiers print verbatim out of the config, so a Latin abbrDays keeps its own digits, and bracketed literals are untouched:
format('dddd DD MMMM YYYY') //=> جمعه ۲۶ آبان ۱۳۹۶
format('[Day 1] D') //=> Day 1 ۲۶Note that persianNumerical: false does not mean ASCII everywhere — the built-in abbrDays are written with Persian digits (۱ش, ۲ش, …) and they are names, so d and dd print them whatever the flag says.
TypeScript declarations
The package is now usable from TypeScript without being converted to TypeScript. The library stays JavaScript; the declarations are hand written in types/jdate.d.ts and emitted into lib/ by the build.
import JDate, { type JalaliDate, type JDateConfig } from 'jalali-date';
const jdate = new JDate([1396, 8, 26], { persianNumerical: true });
const formatted: string = jdate.format('dddd DD MMMM YYYY');One declaration cannot describe both bundles. lib/jdate.cjs ends with module.exports = module.exports.default, so require() returns the class itself and its declaration must say export =, where lib/jdate.mjs needs export default. The build appends one of two footers to the shared body, the same way it already appends footers to the JS bundles — lib/jdate.d.mts, lib/jdate.d.cts, and lib/index.d.ts for the top-level "types" field, which is not redundant: TypeScript did not learn the .d.cts extension until 4.7.
JalaliDate is a three-element tuple rather than number[] on purpose — new JDate([1396, 8]) throws nothing at runtime, it quietly builds an Invalid Date, so the arity is the only place that mistake can be caught.
Behaviour changes
- A trailing argument that is neither nullish nor a plain object now throws
Unexpected input, where it used to be silently ignored —new JDate([1396, 8, 26], 'x'). Now that the position means something, dropping a misshapen config quietly would hide the likely mistake of passing the name array directly. new JDate({})andnew JDate(1396, 8, 26, null)used to throw and now work, which cannot break existing code.new JDate({ foo: 1 })still throws, withunknown key "foo", expected one of …in place ofUnexpected input.
Bug fixes
- Date detection went through
instanceof, which is per-realm, sonew JDate(crossRealmDate)threwUnexpected inputfor a perfectly validDatefrom an iframe or avmcontext. All three checks now read the internal slot viaObject.prototype.toString, which is realm-independent. Found by smoke-testing the IIFE bundle inside avm. - Making the last argument meaningful had turned a trailing
nullorundefinedinto an error, which is the ordinary shape of an optional argument —new JDate(date, maybeConfig)from a wrapper. It is dropped again as "no config", but only when what remains is still a whole date form, sonew JDate(1396, 8, undefined)keeps its 1.4.0 meaning.
Development
The declarations are checked from both sides, all in CI after the build. npm run test:types compiles fixtures in tests/types/ against the shipped declarations under nodenext and bundler; npm run test:exports runs attw over the packed tarball; and npm run test:bundles asserts the built bundles still have the export shapes those declarations claim, each bundle exercised as well as inspected. Neither of the first two catches a build footer changing underneath the declarations — swapping the CJS footer for the ESM one leaves attw fully green.
101 tests pass, lint is clean, and npm audit reports 0 vulnerabilities. The packed tarball was installed into a scratch project and exercised through require, import and tsc 6.0.3 under both resolution modes before tagging.
lib/jdate.min.js is 6,756 bytes, up from 4,771.
Full changelog: 1.4.0...1.5.0