Skip to content

Latest commit

 

History

History
364 lines (260 loc) · 14.2 KB

File metadata and controls

364 lines (260 loc) · 14.2 KB

Contributing to to-words

Thank you for taking the time to contribute! This document covers everything you need — from filing a bug report to shipping a new locale.

Table of Contents


Code of Conduct

This project follows the Contributor Covenant Code of Conduct. By participating you agree to abide by its terms.


Getting Started

  1. Fork the repository on GitHub.
  2. Clone your fork locally:
    git clone https://github.com/<your-username>/to-words.git
    cd to-words
  3. Install dependencies (requires Node ≥ 20):
    npm install
  4. Create a branch for your change:
    git checkout -b feat/my-feature
    # or
    git checkout -b fix/issue-123

Development Setup

Command Purpose
npm test Run the full test suite in watch mode
npm run test -- run Run tests once (CI mode)
npm run test -- run --coverage Run tests with coverage report
npm run lint Check for linting errors
npm run lint:fix Auto-fix linting errors
npm run build Full production build (tsc CJS/ESM + Rolldown UMD + CLI)
npm run test:runtime Smoke-test compiled ESM, CJS, locale, and CLI entry points
npm run docs:dev Run the VitePress documentation locally
npm run docs:build Build and verify the deployable documentation site
npm run commit Interactive commit with Commitizen

Tip: Run npm run lint:fix before committing — the pre-commit hook will block if there are lint errors.


Project Structure

src/
  ToWordsCore.ts      # Core conversion engine (no bundled locales)
  ToWords.ts          # Full-bundle class + functional exports (toWords, toOrdinal, toCurrency)
                      # + locale auto-detection (detectLocale, setLocaleDetector)
  types.ts            # Shared TypeScript types
  cli.ts              # CLI entry point
  locales/
    index.ts          # LOCALES registry (maps locale code → class)
    en-US.ts          # Example locale
    en-IN.ts
    …                 # 135 locale files total

__tests__/
  ToWords.test.ts     # Full-bundle + functional helper tests
  ToWordsCore.test.ts # Core engine tests
  <locale>.test.ts    # Per-locale tests (one file per locale)

scripts/
  build-umd.ts        # UMD bundle script
  check-docs-build.mjs # Verifies VitePress output, base paths, links, and demo SSR
  verify-docs-deployment.mjs # Verifies the deployed Pages site and client bundle
  runtime-smoke.mjs   # Compiled ESM/CJS/locale/CLI release smoke test

How to Contribute

Reporting Bugs

Before opening a bug report, please search existing issues to avoid duplicates.

When filing a new issue, include:

  • Package version (npm list to-words)
  • Node.js version (node -v)
  • Locale code you are using
  • Minimal reproduction — a small code snippet that shows the wrong output
  • Expected vs actual output

Suggesting Enhancements

Open an issue with the label enhancement and describe:

  • The use case you are trying to solve
  • The proposed API or behaviour change
  • Any locales or edge cases that would be affected

Submitting a Pull Request

  1. Make sure npm test -- run and npm run lint both pass locally.
  2. Keep PRs focused — one feature or fix per PR.
  3. If you are adding a feature, add tests that cover it.
  4. Fill in the PR template completely.
  5. Link related issues in the PR description (Closes #123).

Adding a New Locale

This is the most common contribution. Follow these steps:

1. Create the locale file

Add src/locales/<locale-code>.ts. Use an existing locale as a template (e.g. src/locales/en-US.ts).

Your file must:

  • Export a default class that implements LocaleInterface from src/types.ts.

  • Export the three locale-level functional helpers at the bottom — they wrap the class with no extra arguments, so callers import them tree-shaken without needing a localeCode:

    import { type ConverterOptions, type NumberInput, type OrdinalOptions } from '../types.js';
    import { ToWordsCore } from '../ToWordsCoreBase.js';
    
    export default class Locale implements LocaleInterface {
      public config: LocaleConfig = {
        // … your locale config …
      };
    }
    
    export class ToWords extends ToWordsCore {
      constructor(options: ToWordsOptions = {}) {
        super(options);
        this.setLocale(Locale, '<locale-code>');
      }
    }
    
    let instance: ToWords | undefined;

    In practice, copy the bottom block verbatim from any existing locale file — the pattern is standardised across all 135 locales. The full registry validates locale configuration automatically and all entry points freeze it on first initialization, so define it completely and deterministically up front rather than mutating it after conversion starts. Standalone locale bundles use the prevalidated base to avoid shipping authoring-time validation code to every browser.

    numberWordsMapping must contain zero, use unique numeric thresholds, and be strictly descending because the core uses binary search. The cross-locale invariant test enforces these requirements.

    Strict cardinal, ordinal, and currency ceilings are derived from the configured scale structure. If reviewed native-language fixtures justify different ceilings, declare them with maximumSupportedValues and add boundary tests for every overridden form. Do not raise a ceiling merely because rangeMode: 'compose' can mechanically produce a string.

2. Register the locale

Open src/locales/index.ts and:

  1. Import your class.
  2. Add it to the LOCALES map with the correct BCP 47 locale code (e.g. 'sw-TZ').
  3. If this is a new language subtag, add its reviewed display name, exact IANA registry description, and documentation page to scripts/locale-language-identities.ts.

Locale identifiers are checked against the committed IANA Language Subtag Registry. The quality gate rejects invalid or non-canonical language, script, and region subtags, and also verifies that the registered language description matches the human-reviewed implementation identity. This second check catches valid-but-wrong codes, such as identifying an Estonian implementation with the Ewe language code.

Maintainers can refresh the committed registry snapshot after IANA publishes an update:

registry_file="$(mktemp)"
curl --fail --show-error --silent --location --proto '=https' --tlsv1.2 \
  https://www.iana.org/assignments/language-subtag-registry/language-subtag-registry \
  --output "$registry_file"
npm run locale-registry:update -- "$registry_file"
git diff -- scripts/data/iana-language-subtags.json
rm "$registry_file"

Downloading and importing are deliberately separate trust steps. The importer accepts only a local file, validates its size, structure, canonical subtag shapes, expected identities, and registry date, and refuses to roll the snapshot back. Review the generated diff before committing it.

3. Add tests

Create __tests__/<locale-code>.test.ts. Copy an existing test file and adjust the expected values. Your test file must contain:

  • Integers (0, positive, negative)

  • Decimals

  • Currency amounts ({ currency: true })

  • Ordinals (if the locale supports them)

  • A describe('Functional helpers (locale-level)') block with tests for the three exported helpers — required for 100% coverage:

    import {
      toWords as localeToWords,
      toOrdinal as localeToOrdinal,
      toCurrency as localeToCurrency,
    } from '../src/locales/<locale-code>';
    
    describe('Functional helpers (locale-level)', () => {
      test('toWords', () => {
        expect(localeToWords(5)).toBeDefined();
      });
      test('toOrdinal', () => {
        expect(localeToOrdinal(1)).toBeDefined();
      });
      test('toCurrency', () => {
        expect(localeToCurrency(10)).toBeDefined();
      });
    });

4. Update documentation

Add a row for your locale in the Supported Locales table in README.md.

5. Verify everything works

npm run lint
npm run test:locale-quality
npm test -- --run
npm run build
npm run test:runtime
npm run test:package

All tests must pass and coverage must remain at 100% for the files you touched.


Coding Guidelines

  • TypeScript — all source files are TypeScript. Avoid any; use unknown and type guards instead.
  • No runtime dependencies — the package has zero production dependencies. Do not add any.
  • ESM-first — source is native ESM. Import paths must include the .js extension (TypeScript resolves these to .ts during compilation).
  • No global state in locale files — each locale class is stateless.
  • Performance — the conversion hot path is called thousands of times in invoicing apps. Do not add per-call allocations (e.g. Array.from, Object.keys) inside convert() without benchmarking first (npm run bench).
  • Lintingnpm run lint must pass with zero warnings. The project uses Oxlint with @mastermunj/oxc-config.

Commit Message Format

This project uses Conventional Commits enforced by commitlint. The easiest way to commit is:

npm run commit

This launches an interactive prompt via Commitizen. Manual commit messages must follow the pattern:

<type>(<scope>): <short description>

[optional body]

[optional footer(s)]

Types: build, ci, chore, docs, feat, fix, perf, refactor, revert, style, test

Examples:

feat(locales): add sw-TZ (Swahili Tanzania) locale
fix(en-IN): correct ordinal for 11th
docs: update README bundle size figures
test(fr-FR): add decimal currency edge cases
init: bootstrap repository metadata

Running Tests

# Watch mode (default)
npm test

# Single run
npm test -- run

# Single run with coverage
npm test -- run --coverage

# Single file
npm test -- run __tests__/en-US.test.ts

# Benchmarks
npm run bench

Coverage is measured by Vitest v8. The project targets 100% coverage across all src/** files. PRs that drop coverage below 100% for touched files will not be merged.


Building

npm run build

This runs (in order): clean → CJS build → ESM build → UMD build → package.json injection → CLI chmod.

The UMD bundles in dist/umd/ are generated per-locale via Rolldown — one bundle for the full package (to-words.min.js) and one per locale (en-US.min.js, etc.).

After building, run npm run test:runtime to verify the compiled ESM, CommonJS, per-locale, and CLI entry points rather than only the TypeScript source used by unit tests.


Documentation

The documentation site is built with VitePress and deployed by .github/workflows/docs.yml.

# Local development with hot reload
npm run docs:dev

# Production build plus generated-data, base-path, link, asset, and demo checks
npm run docs:build

# Preview the already-built site
npm run docs:preview

The interactive demo imports the same per-locale source entry points that are published to npm. Its locale list is checked against the runtime registry, and locale implementations are loaded on demand so documentation pages do not download the complete locale bundle.

Repository administrators must keep Settings → Pages → Build and deployment → Source set to GitHub Actions. Do not select main/docs: that path publishes the VitePress Markdown sources through Jekyll instead of publishing the generated site. The deployment workflow runs for every push to main, verifies the built artifact before upload, and checks the public page after deployment.


Release Process

Releases are automated with Release Please and npm trusted publishing:

  1. Merge Conventional Commits into main. Mark incompatible changes with ! and a BREAKING CHANGE: footer.
  2. Release Please opens or updates chore(release): <version>. It owns the version changes in package.json, package-lock.json, .release-please-manifest.json, and CHANGELOG.md; do not edit those versions manually.
  3. Review and merge the release PR. Release Please creates the GitHub release and v<version> tag.
  4. The tag starts publish.yml, which installs cleanly, lints, tests, builds, smoke-tests compiled entry points, verifies package contents, and publishes through npm OIDC with automatic provenance.
  5. Every push to main, including the release merge, rebuilds and verifies the VitePress site before deploying it to GitHub Pages.

Before a release, verify that the RELEASE_PLEASE_TOKEN repository secret is active and that npm trusted publishing is configured for the mastermunj/to-words repository and publish.yml workflow. Do not create the tag or run npm publish manually.


Questions?