Skip to Content
Contributing

Contributing

Adding a dialect or library is two mappings, not a fleet of pairwise converters — the engine stays dialect-agnostic, and every dialect maps to and from the shared canonical model.

Local setup

Prerequisites: Node ≥ 22 and pnpm .

git clone https://github.com/JoaoPedroAS51/rosetta-date.git cd rosetta-date pnpm install
ScriptPurpose
pnpm devRebuild on change (tsdown --watch).
pnpm buildEmit the ESM bundle and types to dist/.
pnpm playgroundServe the interactive playground from playground/.
pnpm typecheckType-check the package without emitting.
pnpm lint / pnpm lint:fixLint, and autofix, with the antfu config.
pnpm testRun the suite once.
pnpm test:coverageRun with coverage, gated at 100%.

Adding a dialect or library

To add a dialect, register it in src/dialects/registry.ts; to add a library, register it in src/libraries/registry.ts and give it a libraryDeltas entry in test/fixtures.ts. A dialect in an existing syntax family (delimited or directive) is pure data — pick its syntax and map its tokens. A grammar that needs a new family adds one strategy module under src/core/syntax/ and one member to the TokenSyntax union; the parser and renderer stay untouched.

The cross-endpoint matrix in test/fixtures.ts is a curated oracle (its flat canonical → spelling shape currently fits the delimited dialects). A delimited dialect joins it with an expectations entry; a directive dialect like strftime is exercised by its own focused test (src/dialects/strftime.test.ts) until that oracle is extended.

Then update the documentation so the new dialect or library is discoverable. The generic suites cover the code automatically, but the docs do not — keep these pages in sync by hand:

  • Canonical Vocabulary — only if the new dialect or library needs a canonical field that does not exist yet; add the new member here, following the naming grammar below.
  • Dialects — add a row to the index and a page for the new dialect (grammar, literals, aliases, caveats), plus a column in Token Mapping.
  • Libraries — add a row to the index and a page for the new library (its supports subset and any extends tokens).
  • API Reference — list the new export under its entrypoint.

Adding a canonical field

Most dialects and libraries reuse existing canonical fields — you add one only when a token expresses a meaning no member covers yet. Canonical values follow a small grammar:

field/style[/qualifier...]
  • field — the date component (month, hour, week-of-year, …).
  • style — how it is represented (numeric, 2-digit, ordinal, wide, …).
  • qualifier (optional, repeatable) — refines the base field/style; its absence is the default form. Examples: iso (ISO week rules), the hour cycle h11 / h12 / h23 / h24, standalone (grammatical context), and presentation refinements such as space-padded (fill) and lower (case).

The member name is the PascalCase of the value’s segments, so name and value stay in lockstep: week-of-year/ordinal/isoWeekOfYearOrdinalIso, month/wide/standaloneMonthWideStandalone.

Qualifier order is part of a value’s identity, so each meaning has exactly one string and dialects never disagree on a spelling. Order qualifiers by a fixed precedence: identity qualifiers that change the rendered value (iso, the hour cycle, standalone) come first, then presentation qualifiers that only change its surface (space-padded, lower). A blank-padded 24-hour is hour/2-digit/h23/space-padded, never hour/2-digit/space-padded/h23.

Prefer refining an existing field/style with a qualifier over inventing a parallel field — it keeps related meanings grouped and lets conversions share the base. Adding a member is a minor (additive) change; renaming or removing one is breaking.

TSDoc conventions

Use a consistent TSDoc structure for dialect and library exports.

Dialect TSDoc documents only the grammar it defines:

  • Scope: the supported token grammar.
  • Literals: how literal text is represented.
  • Rendering: how primary mappings are chosen.
  • Case sensitivity: only when token case changes meaning.
  • @see: official specs or token references.

Avoid mentioning concrete libraries in dialect TSDoc unless the behavior belongs to the dialect itself.

Library TSDoc documents the rendering target:

  • Base grammar: the dialect used by the library.
  • Extensions: tokens added on top of the base grammar.
  • Support model: whether supports is omitted or defines a subset.
  • Runtime behavior: only for runtime requirements outside conversion logic.
  • @see: official library docs.

Wrap code identifiers, plugin names, token names, and package names in backticks.

CI runs lint, typecheck, test:coverage, and build on every pull request. Record user-facing changes with a changeset:

pnpm changeset

Testing

Where a test lives signals what it covers.

LocationScopeExamples
Beside the module (src/)One module’s units, plus the mechanics the oracle cannot expresssyntax/delimited.test.ts, dialects/strftime.test.ts; the unsupported-token reasons in libraries.test.ts
test/Systematic behaviour across the curated endpointsmatrix.test.ts, round-trip.test.ts

Unit tests sit next to the code they exercise, so they travel with it on a refactor. The cross-cutting suites span every pair of curated endpoints and derive from one shared oracle, test/fixtures.ts (each dialect’s expectations, plus each library’s libraryDeltas). A delimited endpoint with an oracle entry earns conformance, the matrix, round-trip, and totality automatically; the colocated tests carry the per-tool mechanics (unsupported-token reasons, handler context, adjacency) and any endpoint not yet in the matrix oracle (such as the strftime dialect).

Run pnpm test for both, or pnpm test:coverage to enforce the 100% threshold.

License

MIT  © João Pedro Antunes Silva.

Last updated on