Skip to main content

Crate fluent_typed_codegen

Crate fluent_typed_codegen 

Source
Expand description

Generate typed Rust translation APIs from modular Fluent catalogs.

See the setup guide for installation, and the migration guide when upgrading from 0.1.4. For 0.2.1 applications, see the 0.2.2 migration.

Language directories are discovered automatically. FTL file paths become named Rust types, and message parameters become typed accessor arguments.

Built on fluent-typed, which generates the typed message accessors and provides Fluent formatting. This crate adds module discovery and a typed translation tree around that foundation. Applications choose their own resource loading, active language and presentation layer.

§Feature selection

  • build (default): discovery, schema validation, source generation and the Extension API for custom integrations. Enable in build-dependencies.
  • manifest: optional TOML parsing and file-manifest factory.
  • No default features: translations! and std-only source contracts, with no dependencies. Use this configuration in normal dependencies. The generated code still requires the application’s fluent-typed and fluent-syntax dependencies.

Cargo resolver 2/3 keeps the build and normal feature contexts separate. See the dependency setup for the build and runtime dependency declarations.

§Configure the consuming package

[package.metadata.localization]
catalog = "assets/localizations/localization.toml"

catalog is a filesystem path relative to the package’s Cargo.toml; .. can locate shared sources. Its filename is configurable. The generator does not select an engine asset root. The TOML contains:

translations-directory = "translations"
source-language = "en"
default-language = "en"

translations-directory is optional and relative to this TOML. Omit it to use locale folders beside the file (default .). The legacy languages-directory alias is accepted, but specifying both names is an error.

With the explicit path above, put matching FTL modules under assets/localizations/translations/en/, es/ and any other locale directories. The source language defines message keys, references and argument annotations. default-language is emitted as DEFAULT_LANGUAGE metadata for application startup; Locale::default() identifies the source language. Each language must provide the same contract.

§Generate and use the API

Return fluent_typed_codegen::build() from the consuming build.rs to run validation and generation. Errors produce readable diagnostics and a failing exit code. cargo check also regenerates for IDE indexing; no application execution is needed. Source files are never rewritten.

For a file ui/menu.ftl containing title = Settings, the generated type is texts::ui::Menu. A directory becomes a namespace/group, a file becomes a leaf type, and each Fluent message has a typed accessor:

ⓘ
fluent_typed_codegen::translations!(pub mod texts);

let manifest = texts::embed_manifest!();
let translations = texts::Translations::from_manifest(texts::Locale::En, &manifest)?;
let ui: &texts::Ui = translations.ui();
let menu: &texts::ui::Menu = ui.menu();
println!("{}", menu.msg_title());

This snippet needs consumer-owned FTL and build output; the runnable Rust example demonstrates the complete setup. There is no public module at a leaf path. Message keys in different files stay independent, including their argument types.

§Output and checked loading

Cargo entrypoints emit Rust, per-module FTL and source metadata into OUT_DIR. Keep that output under target/ and out of source control. generate also accepts an explicit tool-owned output directory for custom build frontends. Failed generation can leave partial output; always propagate build errors.

Generated leaves expose checked new(locale, bytes), safe new_unchecked, and standalone validate. Translations::from_modules validates and parses caller-supplied FTL strings before returning a snapshot. Compatible prose edits can be loaded without rebuilding; changes to the schema or language/module inventory require regeneration. These runtime methods perform no filesystem reads. File loading and replacing snapshots are caller-controlled, not a filesystem watcher. Sources can come from decoded archive entries or any application-owned buffers; module keys remain paths below the locale directory, not storage URLs. The returned snapshot retains no input borrows. Read a coherent revision before parsing and replace application state only after validation succeeds. Archive I/O and pack installation remain application choices; there is no implicit fallback. Generated from_manifest constructors read files through a LocalizationManifest contract, while texts::embed_manifest!() explicitly includes a build-prepared raw source set. Without that macro invocation, generated APIs contain no FTL payload. The embedded macro is crate-local. It accepts only an empty invocation for the complete tree or a block of named manifest constants for selected scopes. Ordinary catalog imports and aliases remain unrestricted. Unselected FTL is not included even in unoptimized builds without LTO or stripping.

Named embedded manifests use selectors relative to this translation tree:

ⓘ
texts::embed_manifest! {
    pub const MENU = ui::Menu;
    const COMPLETE = Translations;
}

use texts::ui::Menu as Interface;
let menu: Interface = Interface::from_manifest(texts::Locale::En, &MENU)?;

These constants have type LocalizationManifest. They contain source bytes, not parsed catalogs. A private localization module can export its constants and catalog aliases without exposing its generated tree. Selectors are generated schema paths, independent of application imports; strings and type aliases are rejected as selectors. Attributes such as #[cfg(...)] apply to each declaration. Reading bytes borrows the selected static data; LocalizationManifest::config initializes shared metadata on first access. Bind let manifest = &MENU; before retaining a borrow of that metadata. Fluent parsing happens in from_manifest.

See the complete loading recipes for bytes, files, embedding, all languages and explicit module lifetimes.

§Numbers, plural selection and presentation

Native Fluent numeric selectors remain available; this generator does not replace upstream argument types or select-expression behavior. For numbers, percentages, currencies and dates, it’s recommended to use dedicated ICU or ICU4X formatters. Formatting policies and component stability belong to the application. For decimal text, use DecimalFormatter. If grammar must follow visible precision, pass the same prepared Decimal to PluralRules. Annotate the displayed text and category keyword as (String) in source FTL. The runnable example uses ICU directly; no special generated numeric type, generator dependency or feature is required. Respect ICU’s operand limits for arbitrary-precision input. A compact ICU walkthrough shows reusable formatters and a numeric input. Catalogs and formatters can be kept in application state; the application decides when to recompute strings and how to use the result.

String categories match literal Fluent keys such as [one]; unmatched strings select the starred default. Numeric exact matches such as [1] still require a numeric selector. Keep number formatting aligned with the snapshot’s locale. Number formatting and Fluent interpolation isolation do not implement RTL layout, glyph shaping or fonts; those belong to the application’s renderer.

§Custom integrations

With build, Extension adds an entrypoint decorated with typed Rust syntax, while preserving the plain generated API. It can add attributes, imports or application-specific registration code. See the extension guide for its syntax hooks and consumer-compilation requirements. Repository links follow main; this API reference describes the viewed version.

§Typed errors

The standard build.rs remains unchanged: return fluent_typed_codegen::build() from main() -> std::process::ExitCode. It prints readable diagnostics and returns failure without a panic. Use from_cargo() when the caller needs to handle failures.

fn main() -> Result<(), fluent_typed_codegen::BuildError> {
    fluent_typed_codegen::from_cargo()
}

from_cargo, from_cargo_with, generate and generate_with return BuildError. Match variants to distinguish missing Cargo environment, configuration failures, I/O, catalog/module mismatches, schema/reference problems and generation failures. I/O errors retain the operation, native filesystem path and std::io::Error. Upstream failures retain their module and original fluent_typed::BuildError.

use fluent_typed_codegen::BuildError;

fn report(error: &BuildError) {
    match error {
        BuildError::Io { operation, path, source } => {
            eprintln!("{operation} {} failed: {:?}", path.display(), source.kind());
        }
        BuildError::ModuleMismatch(details) => {
            eprintln!("{}: missing {:?}, extra {:?}", details.locale, details.missing, details.extra);
        }
        _ => eprintln!("{error}"),
    }
}

Settings::from_manifest and CatalogConfig::parse return ConfigError. Its InvalidField variant identifies the field and distinguishes Missing, WrongType and Empty; InvalidPath preserves the original PathBuf and a PathError reason. Unknown parameters remain ignored. Invalid TOML retains the original parser error and span. No filesystem access occurs in these parsers.

LocalizationManifest reports ManifestError::Config { source } for configuration and logical-path failures, RequiresLoader { origin } for virtual addresses, Io for filesystem failures, and MissingModule for absent embedded entries. The error types implement Display and std::error::Error; source() exposes underlying parser, configuration, I/O and upstream errors where available.

Applications using ? with a compatible error type continue to propagate failures. Replace explicitly declared Result<_, String> with the appropriate typed error, or convert deliberately with map_err(|error| error.to_string()) at an application boundary. These changes concern configuration and host generation; generated catalog constructors retain their existing LoadError API. Build-only types and syntax dependencies remain behind build; macro-only consumers need no dependencies.

Re-exports§

pub use syn;

Macros§

translations
Declare a module containing this package’s generated Fluent translations.

Structs§

CatalogConfig
Contents of localization.toml, separate from Cargo’s catalog path.
LocalizationManifest
A source contract for readable FTL; creating one never parses FTL.
ModuleMismatch
Per-language difference from the complete source module inventory.
Scope
A generated type and its path of accessor calls from the root snapshot.
Settings
A package-relative filesystem path locating the catalog configuration.

Enums§

BuildError
Configuration, catalog validation, generation or filesystem failure.
ConfigError
Invalid TOML, a recognized setting, or a configuration path.
ConfigField
A localization setting or requested source address.
FieldError
Why a recognized field cannot be used as a string.
IoOperation
Filesystem operation that failed.
ManifestError
A manifest or requested module could not be obtained.
NameError
Why a source component cannot identify a Rust module or type.
NameOwner
The generated declaration owning a conflicting Rust name.
PathError
Why a filesystem input path is invalid.
SchemaError
Fluent syntax, duplicate key or contract mismatch.
SyntaxNode
Opaque Rust syntax category rejected before source printing.
UpstreamShapeError
Unsupported structure in upstream generated source.

Traits§

Extension
Syntax-level extension for a consuming framework’s generated entrypoint.

Functions§

build
Generate catalogs from a Cargo build script with readable failure output.
build_with
As build, with an opt-in framework source extension.
from_cargo
Generate using the consuming package’s Cargo.toml metadata and Cargo OUT_DIR.
from_cargo_with
As from_cargo, also emitting the extension’s entrypoint.
generate
Generate from explicit paths, for custom build frontends and tests.
generate_with
As generate, with an additional framework entrypoint.