A translation engine for date-formatting languages.
rosetta-date translates date-format strings between formatting languages by routing every token through a canonical intermediate representation before rendering it into the target language.
import { convert } from 'rosetta-date'
import { ldml, moment } from 'rosetta-date/dialects'
convert('YYYY-MM-DD', { from: moment, to: ldml }) // 'yyyy-MM-dd'Although YYYY-MM-DD and yyyy-MM-dd look different, they express the same date format using different formatting languages. rosetta-date rewrites the tokens — not the date.
- 🔁 Bidirectional — every mapping works in both directions.
- 🧭 Canonical semantic model — tokens convert through a shared meaning, never dialect-to-dialect.
- 🧩 Extensible by design — a new dialect or library connects to the hub with two mappings, not a fleet of pairwise converters.
- 🛡️ Escape-aware tokenizer — literals round-trip intact across dialects.
- 🌐 Native
Intlbridge — turn a token pattern intoIntl.DateTimeFormatoptions, and back. - 🔍 Introspectable —
describewhat a pattern means andexplainwhat a conversion would do, without converting. - 🌳 Tree-shakeable — dialects and libraries ship from their own entrypoints.
- 🪶 Zero runtime dependencies.
This README is a quick start. The full documentation covers everything else:
- How it works — the canonical model behind every conversion.
- Dialects & Libraries — when to use which.
- Custom dialects & libraries — teach it a new dialect or library.
- Unsupported tokens — what happens when a token has no clean target.
- Token mapping — the full per-token grammar tables.
- Libraries — tool-specific coverage and caveats.
- API reference — every export, signature, and type.
npm install rosetta-date
rosetta-dateis in active development. Minor releases may include breaking changes. Pin an exact version in yourpackage.jsonand review the changelog before upgrading.
pnpm / yarn / bun
pnpm add rosetta-date
yarn add rosetta-date
bun add rosetta-dateRequirements: Node ≥ 22, and an ESM project — import the package; do not require() it.
The conversion API lives at the package root; dialects and libraries are imported from their own entrypoints and passed in.
import { convert, createConverter } from 'rosetta-date'
import { ldml, moment } from 'rosetta-date/dialects'
// One-off, direction travels with the call:
convert('DD/MM/YYYY', { from: moment, to: ldml }) // 'dd/MM/yyyy'
convert('yyyy-MM-dd', { from: ldml, to: moment }) // 'YYYY-MM-DD'
// Fixed direction reused many times — bind once, call often:
const toLdml = createConverter(moment, ldml)
toLdml('YYYY-MM-DD') // 'yyyy-MM-dd'
toLdml('hh:mm A') // 'hh:mm a'A Dialect is a formatting language (grammar + literals); a Library models one tool on top of a
dialect (the tokens it renders, its aliases, its limits). from and to each take either, so you can
mix them freely. Converting to a library flags tokens it cannot render:
import { convert, createConverter } from 'rosetta-date'
import { dateFns, dayjs, momentjs } from 'rosetta-date/libraries'
// Library → library reads like the intent:
convert('DD/MM/YYYY', { from: momentjs, to: dateFns }) // 'dd/MM/yyyy'
// Day.js can't render `Mo`, so a strict converter throws instead of passing through a token it would mis-format:
const safeForDayjs = createConverter(momentjs, dayjs, { onUnsupportedToken: 'throw' })
safeForDayjs('YYYY-MM-DD') // 'YYYY-MM-DD'
safeForDayjs('Mo') // throws UnsupportedTokenErrorBridge a token pattern to the native Intl.DateTimeFormat API, and back, from rosetta-date/intl:
import { moment } from 'rosetta-date/dialects'
import { fromIntlOptions, toIntlOptions } from 'rosetta-date/intl'
import { dayjs } from 'rosetta-date/libraries'
// Stop hardcoding the layout — hand the components to the locale:
toIntlOptions('DD/MM/YYYY', { from: moment }) // { day: '2-digit', month: '2-digit', year: 'numeric' }
// The style axis round-trips through each library's localized preset:
fromIntlOptions({ dateStyle: 'short' }, { to: dayjs }) // 'L'Or inspect without converting — describe what a pattern means, or explain what a conversion would do:
import { describe, explain } from 'rosetta-date'
import { moment } from 'rosetta-date/dialects'
import { dayjs } from 'rosetta-date/libraries'
describe('DD/MM', moment)
// [ { kind: 'field', token: 'DD', canonical: 'day-of-month/2-digit', field: 'day-of-month', style: '2-digit', qualifiers: [] },
// { kind: 'literal', value: '/' },
// { kind: 'field', token: 'MM', canonical: 'month/2-digit', field: 'month', style: '2-digit', qualifiers: [] } ]
// Dry-run a conversion to audit it — `DDD` (day of year) has no Day.js token:
explain('DDD', { from: moment, to: dayjs })
// [ { kind: 'field', token: 'DDD', canonical: 'day-of-year/numeric', ..., status: 'unsupported', reason: 'unsupported-by-target' } ]See Converting, Describing formats, Explaining conversions, Intl Options, Dialects & Libraries, and Custom dialects & libraries for the full guides.
Dialects — at rosetta-date/dialects:
| Dialect | Grammar | Example | Literals |
|---|---|---|---|
moment |
Moment.js token grammar | DD/MM/YYYY |
[...] |
ldml |
Unicode Technical Standard #35 / LDML date field symbols | dd/MM/yyyy |
'...' |
strftime |
C / POSIX strftime %-directive grammar |
%d/%m/%Y |
%% |
Libraries — at rosetta-date/libraries:
| Library | Tool | Speaks | Coverage |
|---|---|---|---|
momentjs |
Moment.js | moment |
the full grammar |
dayjs |
Day.js | moment |
a core subset + the common plugins (AdvancedFormat, LocalizedFormat) |
dateFns |
date-fns | ldml |
the full grammar + its own extensions (P…, t/T, R/I/i); some tokens gated behind date-fns options |
The per-token grammar tables are in Token mapping; tool-specific behaviour is in Libraries.
rosetta-date never converts one dialect directly into another. Every token is first mapped to a canonical
semantic representation — what it means, independent of any dialect — and only then rendered into the target:
Source Library → Source Dialect → Canonical Representation → Target Dialect → Target Library
Because every dialect maps to and from that one shared vocabulary, a dialect needs only two mappings to interoperate with all the others, and the engine stays the single place that has to be correct. That is what makes the model extensible: a new dialect or library connects to the canonical hub, not to every dialect that already exists.
Contributions are welcome. The Contributing guide covers local setup, the project scripts, the testing layout, and how to add a new dialect or library.
MIT © João Pedro Antunes Silva