All notable changes to erli18n will be documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning 2.0.0.

Versioning policy

Per SemVer 2.0.0 §4, this project is in the 0.x.y initial-development phase:

  • 0.x.y0.x.y+1 (patch): backward-compatible bug fixes only.
  • 0.x.y0.x+1.0 (minor): may introduce backward-incompatible changes, announced in advance via CHANGELOG. Additive changes (new functions, new arities, new opt-in flags, new telemetry events) are the norm.
  • Telemetry events are versioned per the schema policy documented in the erli18n_telemetry module -moduledoc; events marked @stable cannot change schema within 0.x series, events marked @unstable may.

Criteria for 1.0.0

The 1.0.0 release commits to API stability. Tag bumps to 1.0.0 only when all of the following hold:

  1. At least one external project uses erli18n in production for ≥ 6 months without reporting breaking issues.
  2. The Post-0.1.0 Roadmap items that affect public API surface (charset support, hot upgrade behavior, async load) are either implemented or formally rejected with rationale.
  3. Parity SUITE (erli18n_parity_SUITE) passes end-to-end against the real GNU gettext / ngettext CLI (gettext-tools ≥ 0.21) as oracle (currently 6 scenarios; target ≥ 20 covering the full PSD-001…009 semantics matrix).
  4. No unfixed @unstable telemetry events remain — all events either promoted to @stable or removed.
  5. CHANGELOG documents zero behavioral changes for at least 2 consecutive minor releases.

[Unreleased]

No unreleased changes.

[0.1.0] — 2026-06-14

Initial development release. The public API is functional but subject to backward-incompatible changes on minor bumps per the 0.x SemVer policy.

Requires OTP 27 or newer. The public modules carry native -doc / -moduledoc documentation attributes (EEP-59), which only compile on OTP 27+; OTP 25.3 and 26 reject them at compile time with attribute doc after function definitions.

Added

  • Core OTP application: erli18n_app, erli18n_sup (intensity {5, 10} hardcoded per AMB-002).
  • erli18n_server — genserver + ETS catalog store with anti-bottleneck pattern (hot path `lookup*is lock-free direct ETS from caller process; writes serialized throughprotected` table owner).
  • erli18n_po — hand-written recursive-descent parser for GNU gettext .po format. Honors PSDs 001-009:
    • PSD-001: fuzzy entries dropped by default; opt-in via #{include_fuzzy => true}.
    • PSD-002: charset support restricted to UTF-8, Latin-1, US-ASCII (native to unicode:characters_to_binary/3).
    • PSD-003: empty msgstr preserved; fallback-to-msgid handled at lookup.
    • PSD-004: header Plural-Forms is runtime source of truth; CLDR consulted at load only for divergence warning.
    • PSD-005: BOM UTF-8 stripped silently.
    • PSD-006: msgctxt stored as a separate ETS key field, matching how GNU gettext keys contextual entries (msgctxt + EOT + msgid).
    • PSD-007: obsolete #~ entries skipped.
    • PSD-008: degenerate plural (nplurals=1) accepted.
    • PSD-009: nplurals mismatch rejected with structured error.
  • erli18n_plural — recursive-descent C-expression evaluator for Plural-Forms header. CLDR data inlined for 49 locales. Bignum-clean.
  • erli18n_server:ensure_loaded/3,4 and reload/3,4 — atomic catalog load (parse → compile plural → validate vs CLDR → insert), with idempotency fast-path (RISK-012 mitigation).
  • erli18n (façade) — full GNU gettext C-macro API surface: gettext family (singular), ngettext family (plural), pgettext family (contextual), npgettext family (contextual + plural), with d/dc aliases. Per-process locale via process dictionary; application-wide defaults via application:get_env/2.
  • erli18n_telemetry — 7 :telemetry events as first-class observability concern (catalog load/reload/unload spans; lookup miss/fuzzy_skip opt-in; plural divergence warning; memory warning rate-limited). telemetry declared as optional dep via optional_applications (OTP 24+).
  • Test suite: 289 Common Test cases, green on OTP 27 and 28 — façade API, gen_server / catalog, .po parser, plural evaluator, loader, and telemetry suites, plus PropEr properties (200 runs each) and fuzz scenarios (100–500 runs each). 6 of these are parity scenarios run against the real GNU gettext / ngettext CLI oracle; that suite skips cleanly when gettext-tools or the pt_BR.UTF-8 / ru_RU.UTF-8 locales are absent.
  • Coverage: 100% of behaviorally reachable lines. Dead defensive code removed (no silent fallbacks for invariant violations — crashes are explicit via function_clause / case_clause / badmatch).
  • Apache 2.0 license.
  • GitHub Actions CI (.github/workflows/ci.yml) — three jobs on pinned ubuntu-24.04 runners: lint (fast quality gate on OTP 28), test (Common Test + coverage across OTP 27 and 28, with gettext installed and the pt_BR.UTF-8 / ru_RU.UTF-8 locales generated so erli18n_parity_SUITE exercises the oracle path), dialyzer (isolated job with PLT cache). CI runs automatically only on main; every other branch runs on demand via workflow_dispatch. Concurrency cancellation per ref, least-privilege contents: read token, rebar3 build cache keyed per OTP.
  • Local CI emulation via act and a custom runner image (Dockerfile.act-runner): extends ghcr.io/catthehacker/ubuntu:full-24.04 with ELP 2026-02-27 (SHA256-verified per SLSA v0.2). Reuses the workflow YAML unchanged — GitHub-hosted runners gracefully [SKIP] the ELP steps in real CI. Bootstrap is declarative in compose.yml (act-toolcache volume init + image build). actionlint 1.7.12 pinned via mise.toml for static workflow analysis.
  • Repo hygiene: README.md (with usage / install / compatibility / dev sections), CONTRIBUTING.md, SECURITY.md, CODE_OF_CONDUCT.md (Contributor Covenant 3.0), .editorconfig.

Architecture decisions

The design rationale is captured inline in the source: PO-semantics decisions (PSD-001PSD-009), risk mitigations (RISK-*), and ambiguity resolutions (AMB-*) are referenced from the relevant module -moduledoc / -doc attributes and code comments. The internal planning corpus that originally tracked them is not part of the published package.