Skip to content

Releases And Upgrading

Where Ferrocat changelogs live and how to upgrade between stable releases safely.

Ferrocat follows semantic versioning from 1.0 onward: breaking API changes ship in a new major version, while minor and patch releases stay backward compatible. The 2.1.0 release was a reviewed, one-time exception for removing accidental public coupling while the user base was still small. That cleanup window is closed. Start every upgrade from the changelog and keep the workspace crate versions aligned.

Where release notes live

  • GitHub Releases gives the repository-level release view.
  • ferrocat CHANGELOG is the best starting point for application users of the umbrella crate.
  • Direct sub-crate users should also read the matching ferrocat-po or ferrocat-icu changelog for low-level parser, catalog, or ICU changes.

Release Please generates these changelogs from Conventional Commits. Breaking changes normally use a major-release marker such as feat!: or a BREAKING CHANGE note so they are visible in release notes and changelog sections. The historical 2.1.0 cleanup exceptions are documented in that release's notes without a major-release marker.

MSRV bumps follow the same release-note rule. Ferrocat aligns its declared MSRV with OXC when practical while avoiding churn from tracking only the newest stable toolchain. Raising the MSRV is treated as a minor-version change and called out in the changelog; patch releases should not raise the MSRV.

Lockstep versions

The public library crates are released in lockstep: every release advances all of them to the same version. In normal application code, prefer depending on the umbrella crate:

[dependencies]
ferrocat = "2"

If you depend on lower-level crates directly, keep every Ferrocat crate on the same version line so you get the combination that was released and tested together:

[dependencies]
ferrocat-po = "2"
ferrocat-icu = "2"

Within a normal major line, minor and patch releases stay backward compatible, so Cargo resolves a shared "2" requirement to a single, latest-compatible version. Avoid pinning the umbrella crate and a direct sub-crate to different exact versions, for example ferrocat = "=2.2.0" with ferrocat-po = "=2.1.1": that can pull two copies of the Ferrocat types into the build and fail with confusing type-mismatch errors.

The 2.2 release history includes a temporary 3.0.0 publication from when the self-closing-tag change was first classified as breaking. Those packages were yanked and the compatible change shipped as 2.2.0. Use the 2.2 line rather than selecting 3.0.0 from the historical changelog entries.

Upgrade routine

  1. Read the ferrocat changelog for the target version.
  2. If you use direct sub-crates, read their matching changelogs too.
  3. Update all Ferrocat crate requirements to the same version line.
  4. Run cargo update -p ferrocat -p ferrocat-po -p ferrocat-icu when those packages are present in your lockfile.
  5. Run your normal test suite plus the Ferrocat APIs you call in release or CI paths.

When a future release needs a durable migration note — for example a major version with breaking changes — this page should link that note from the upgrade routine instead of leaving it buried in a PR.

Upgrading to 2.1.0

Ferrocat 2.1.0 includes a reviewed cleanup window for accidental public API coupling left after 2.0.0. These are source-level breaks, but they are small renames or construction changes with direct replacements.

Renamed entry points

BeforeAfter
catalog_reviewreview_catalogs
catalog_coveragemeasure_catalog_coverage
has_selectordinalhas_select_ordinal
MergeExtractedMessageMergeMessageInput

Search for the old name in compiler errors and replace it with the matching new name. The MergeExtractedMessage compatibility alias was removed, so direct type annotations need to move to MergeMessageInput.

MsgStr accessors

MsgStr::first() now returns Option<&str>, and iterator items are &str. Code that previously used first_str() should call first() and handle the empty translation case explicitly:

let Some(text) = message.msgstr.first() else {
    return;
};

Iteration no longer needs an extra string conversion:

for variant in message.msgstr.iter() {
    println!("{variant}");
}

PoVec construction

PoVec is now an opaque inline-vector newtype instead of exposing SmallVec<[T; 1]> in public signatures. Reading is still slice-like, but constructing affected fields from a Vec now needs From<Vec<T>> or .into():

message.origin = vec![CatalogOrigin {
    file: "app.ftl".into(),
    scope: "checkout.title".into(),
}]
.into();

Direct equality checks against a Vec should compare slices instead:

assert_eq!(message.origin.as_slice(), expected_origins.as_slice());

Non-exhaustive options

The public options structs are now #[non_exhaustive]. Downstream code can no longer use functional-record-update syntax such as ..Options::new() outside the defining crate. Start from new() and use the builder methods instead:

let options = UpdateCatalogOptions::new()
    .with_mode(CatalogMode::IcuPo)
    .with_obsolete_strategy(ObsoleteStrategy::Keep);

This keeps future options additive without forcing another public constructor break.

API cleanup shipped in 2.1.1

Ferrocat 2.1.1 removed the remaining cross-product entry points for ICU syntax policy and folded those choices into the existing options structs. The same API is part of the current 2.2 line. If your code called a _with_icu_options or _with_syntax_policy function variant, move the ICU configuration onto the operation's options value instead.

For catalog audits, build CatalogAuditOptions with embedded CatalogAuditIcuOptions:

use ferrocat::{CatalogAuditIcuOptions, CatalogAuditOptions, IcuSyntaxPolicy};

let options = CatalogAuditOptions::new("en").with_icu_options(
    CatalogAuditIcuOptions::new()
        .with_syntax_policy(IcuSyntaxPolicy::RuntimeLiteralApostrophes),
);

For runtime artifact compilation, use CompileCatalogArtifactOptions:

use ferrocat::{
    CompileCatalogArtifactIcuOptions, CompileCatalogArtifactOptions,
    IcuSyntaxPolicy,
};

let options = CompileCatalogArtifactOptions::new("de", "en").with_icu_options(
    CompileCatalogArtifactIcuOptions::new()
        .with_syntax_policy(IcuSyntaxPolicy::RuntimeLiteralApostrophes),
);

For pseudolocalization of compiled artifacts, pass the syntax policy through CompiledCatalogPseudolocalizationOptions:

use ferrocat::{CompiledCatalogPseudolocalizationOptions, IcuSyntaxPolicy};

let options = CompiledCatalogPseudolocalizationOptions::new()
    .with_syntax_policy(IcuSyntaxPolicy::RuntimeLiteralApostrophes);

Diagnostic report fields now use the DiagnosticCode newtype in Rust instead of plain String. Existing JSON consumers do not need to change: reports still serialize diagnostic codes as the same canonical strings, for example "catalog.missing_translation". Rust code that stores or compares codes should use code.as_ref(), code.to_string(), the exported diagnostic_codes constants, or direct comparison with a string literal:

use ferrocat::diagnostic_codes::catalog;

if diagnostic.code == catalog::MISSING_TRANSLATION {
    // Handle missing translations.
}