Skip to content

Repository files navigation

rosetta-date

A translation engine for date-formatting languages.

npm version CI coverage bundle size License: MIT

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.

Highlights

  • 🔁 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 Intl bridge — turn a token pattern into Intl.DateTimeFormat options, and back.
  • 🔍 Introspectable — describe what a pattern means and explain what a conversion would do, without converting.
  • 🌳 Tree-shakeable — dialects and libraries ship from their own entrypoints.
  • 🪶 Zero runtime dependencies.

Documentation

This README is a quick start. The full documentation covers everything else:

Install

npm install rosetta-date

rosetta-date is in active development. Minor releases may include breaking changes. Pin an exact version in your package.json and review the changelog before upgrading.

pnpm / yarn / bun
pnpm add rosetta-date
yarn add rosetta-date
bun add rosetta-date

Requirements: Node ≥ 22, and an ESM project — import the package; do not require() it.

Usage

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 UnsupportedTokenError

Bridge 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.

Supported dialects & libraries

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.

How it works

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.

Contributing

Contributions are welcome. The Contributing guide covers local setup, the project scripts, the testing layout, and how to add a new dialect or library.

License

MIT © João Pedro Antunes Silva

Releases

Contributors

Languages