Maintenance recipes for the core. For architecture context see
ARCHITECTURE.md; for capability status see
CAPABILITY_ROADMAP.md. Each satellite has its
own docs/UPDATING.md for backend-side maintenance.
fluent_bundle is a clean-room implementation — it wraps neither fluent-rs nor fluent.js. Upstream is tracked two ways:
| Source | Why |
|---|---|
| Mozilla's fluent-rs fixture corpus | Compliance — parser fixtures here, resolver fixtures in each satellite |
| Project Fluent spec (Syntax 1.0) | Source of truth for parser semantics |
| Trigger | Recipe |
|---|---|
| New fluent-rs release / new fixtures | §1 — Refresh the corpus (core + BOTH satellites together) |
| A new ECMA-402 option (or option value) should be supported | §2 — Add an option end to end |
| New Fluent Syntax spec revision | §3 — Track the spec |
| A new backend satellite is wanted | §4 — Stamp a satellite |
package:html breaking change |
§5 — Standard pub upgrade |
| Cutting a release | §6 — Release (pre-release today) |
The corpus is split by what it needs:
- Parser fixtures (
test/_corpus/syntax/, here) — no backend needed. - Resolver fixtures (
test/_corpus/bundle/, vendored byte-identical in BOTHfluent_icuandfluent_intl) — need a real CLDR backend.
All three copies pin the same upstream commit and MUST refresh in the same session — the PROVENANCE files cross-reference each other.
# 1. Scratch checkout
git clone --depth 1 https://github.com/projectfluent/fluent-rs.git /tmp/fluent-rs
# 2. Diff each copy against upstream
diff -r /tmp/fluent-rs/fluent-syntax/tests/fixtures test/_corpus/syntax
diff -r /tmp/fluent-rs/fluent-bundle/tests/fixtures ../fluent_icu/test/_corpus/bundle
diff -r /tmp/fluent-rs/fluent-bundle/tests/fixtures ../fluent_intl/test/_corpus/bundle
# 3. Copy intentional changes in (fixtures stay byte-pristine — never
# hand-edit vendored YAML/JSON), run each package's corpus suite,
# bump every PROVENANCE.md (commit + dates).A newly-failing fixture is either a new spec feature to implement or a
regression to bisect. The upstream-skip list (skip: true in fixtures,
plus the stream-specific skips documented in each runner) is upstream's
own — never add a local skip to make the suite pass.
The recurring maintenance recipe. One option touches four layers, in this order:
| Step | Where | What |
|---|---|---|
| 1 | lib/src/values/fluent_number.dart (or fluent_datetime.dart) |
Add the nullable field; wire ctor + isEmpty + merge. EVERY field joins all three. |
| 2 | lib/src/builtins/ |
Parse the FTL named arg; validate against the ECMA value set (strOf) or shape rule; record FluentFormatError + drop on bad values. Test in test/builtins/. |
| 3 | If the option affects plural operands | Extend resolveDigits (PluralRules accepts rounding options, never notation). Test in test/values/resolve_digits_test.dart. |
| 4 | Each satellite | Wire rendering OR a per-call degrade (FluentTypeError.unsupportedOption). Every construction-affecting option joins every formatter cache key — a field left out is a cache-collision bug. |
| 5 | lib/src/testing/ |
Add a BackendExpectations flag + a positive check in the matching checks_* group + a degrade row in checks_degrade.dart. Satellites declare their truth in their conformance_test.dart. |
| 6 | Docs | A row in CAPABILITY_ROADMAP.md's NUMBER matrix (no blank cells: ✓ or ✗-with-degrade per backend). |
Then the full sweep: fvm dart analyze . && fvm dart test in all three
packages.
Target: Fluent Syntax 1.0. Revisions are rare. When one lands:
read the changelog at
https://github.com/projectfluent/fluent/blob/master/spec/, update
lib/src/syntax/parser/, pick up upstream's new fixtures via §1.
The locked template lives in ARCHITECTURE.md §7.
- Copy the skeleton (
backend.dart+common/ datetime/ number/ plural/with*_map.dartentries; mirrored test tree;docs/). - The barrel re-exports
fluent_bundle.dart+ the backend class. - Implement the three
*_map.dartentries against the new rendering library. Route every unsupported option throughFluentTypeError.unsupportedOptionper call — never inside a cached builder. UseisValidCurrencyCodefor the currency guard. - Declare honest
BackendExpectationsintest/conformance_test.dart(recordsUnsupportedOptionErrors: trueonce the degrades are wired). - Vendor the resolver corpus (same commit as the siblings; PROVENANCE cross-references + sync-together rule).
fvm dart analyze . && fvm dart testgreen; add the backend column to the CAPABILITY_ROADMAP matrix; write the satellite's three docs.
fvm dart pub upgrade html
fvm dart test test/markup/Its API is stable; if a major bump changes parseFragment, update
lib/src/markup/markup_parser.dart and re-run the markup tests.
The family packages are pre-release (publish_to: none, path deps).
Until DC calls the release there is no per-change changelog; git
history records the work. The release pass, when called, covers per
package:
- Path deps → hosted deps (
fluent_bundleversion pinned by the satellites;icu_kitalready hosted); droppublish_to: none. Then activate the platforms gates fully: every satellite'splatformstarget is blocked-loud pre-release (each package is its own repo; pana snapshots the git repo and can never resolve a sibling-repo path dep) — replace the blocked targets with realplatforms_gate.shruns and flip them into eachcheck(fluent_intl / fluent_icu / fluent_flutter expect all 6 platforms; fluent_gen expects native-only, no web — it is a dart:io build_runner tool). fluent_bundle's gate is live already. - pubspec description (60–180 chars) +
topics. - CHANGELOG two-lane (
.prefor prereleases,.mdfor stable). Every package versions INDEPENDENTLY, pre-1.0 and after — each moves only when it itself changes (e.g. fluent_icu shipped 0.1.1 for a dependency bump while the rest stayed 0.1.0). No lockstep: forcing every package to the same number would republish unchanged packages for nothing. Compatibility is carried by the shared core, not by matching version numbers — every package pinsfluent_bundle: ^0.N.0, so pub resolves one common fluent_bundle across whatever the user picked, or fails loudly. The fluent_bundle line is the compatibility axis. - Verify the three canonical docs + README against the shipped surface (the sprint-end doc check, run once more at release).
- README family conventions hold (they apply to any future satellite
too): every package carries the compass blockquote up top naming
the two front doors (Flutter apps → fluent_flutter, pure Dart →
fluent_bundle; backends/gen are add-ons, never landing points);
the full family table lives ONLY in the core's README (satellites
link, never restate); banner images go on the two front doors
only — add-ons use the plain centered
<h1>title. The core keeps the which-backend table; fluent_flutter keeps the hot-reload recipe. Front-door banner assets (assets/banner_{dark,light}-web-min.webp) must exist before publish — the README blocks reference them. example/runs against the hosted deps.dart pub publish --dry-run+ pana on every package; fix every point it flags before the real publish.
The suite is pure Dart, so the chrome lane needs only Chrome:
make test-web # the web runner (test/runners/web_runner_test.dart) in ChromeThe icu-backed chrome coverage lives in fluent_icu (its make test-web
installs icu_kit's web engine via dart run icu_kit:setup web).
| Failure | First check |
|---|---|
| Corpus fixture fails after a refresh | New spec feature vs regression — diff the fixture's expectation against actual, bisect. Never local-skip. |
| A satellite's conformance run fails after a core change | The harness grew a check (flag default true): either the satellite wires the capability or declares false + degrade. That's the harness working. |
dart analyze finds undefined symbols after pub upgrade |
The dep broke API — read its CHANGELOG, update the wiring. |
Chrome run fails fetching index.mjs |
Web assets not materialized — see above. |
Not yet implemented: a job that watches fluent-rs tags and opens a draft PR when the corpus is behind. Manual quarterly refreshes are acceptable at current cadence.