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| Script | Purpose |
|---|---|
pnpm dev | Rebuild on change (tsdown --watch). |
pnpm build | Emit the ESM bundle and types to dist/. |
pnpm playground | Serve the interactive playground from playground/. |
pnpm typecheck | Type-check the package without emitting. |
pnpm lint / pnpm lint:fix | Lint, and autofix, with the antfu config. |
pnpm test | Run the suite once. |
pnpm test:coverage | Run 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
supportssubset and anyextendstokens). - 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 basefield/style; its absence is the default form. Examples:iso(ISO week rules), the hour cycleh11/h12/h23/h24,standalone(grammatical context), and presentation refinements such asspace-padded(fill) andlower(case).
The member name is the PascalCase of the value’s segments, so name and value stay in lockstep:
week-of-year/ordinal/iso ↔ WeekOfYearOrdinalIso, month/wide/standalone ↔ MonthWideStandalone.
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: whethersupportsis 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 changesetTesting
Where a test lives signals what it covers.
| Location | Scope | Examples |
|---|---|---|
Beside the module (src/) | One module’s units, plus the mechanics the oracle cannot express | syntax/delimited.test.ts, dialects/strftime.test.ts; the unsupported-token reasons in libraries.test.ts |
test/ | Systematic behaviour across the curated endpoints | matrix.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.