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.
ferrocatCHANGELOG is the best starting point for application users of the umbrella crate.- Direct sub-crate users should also read the matching
ferrocat-poorferrocat-icuchangelog 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
- Read the
ferrocatchangelog for the target version. - If you use direct sub-crates, read their matching changelogs too.
- Update all Ferrocat crate requirements to the same version line.
- Run
cargo update -p ferrocat -p ferrocat-po -p ferrocat-icuwhen those packages are present in your lockfile. - 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
| Before | After |
|---|---|
catalog_review | review_catalogs |
catalog_coverage | measure_catalog_coverage |
has_selectordinal | has_select_ordinal |
MergeExtractedMessage | MergeMessageInput |
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.
}