#localization #fluent #single-file

fluent-typed

Type-safe access to Fluent localization messages

8 releases

0.2.6 Sep 24, 2024
0.2.5 Sep 23, 2024
0.1.0 Aug 27, 2024

#50 in Internationalization (i18n)

Download history 149/week @ 2024-08-25 20/week @ 2024-09-01 172/week @ 2024-09-08 175/week @ 2024-09-15 382/week @ 2024-09-22 82/week @ 2024-09-29 21/week @ 2024-10-06 40/week @ 2024-10-13 13/week @ 2024-10-20 42/week @ 2024-10-27 35/week @ 2024-11-03 11/week @ 2024-11-10

102 downloads per month

Custom license

125KB
3.5K SLoC

Fluent-Typed

When using translation keys, there is often no easy way to know if they are being used correctly and if they are being used at all. This project generates, using the fluent ast, the function definitions for the translation keys in a fluent file.

In order to guarantee the safeness, funtions are only generated for messages that are found in all the locales. For those only found for some locales or if the signature of the messages are different a warning is printed.

Each locale's ftl resources are appended into a single ftl file, and you can configure it to either embed all of them into the binary with accessors suitable both for server-side where all of them loaded at startup and accessed via a LazyLock, or client-side where a single one is loaded and then can be used in a signal. This single ftl file can be compressed to your liking using a hook.

You also have the freedom to handle the loading of them yourself, which is especially useful if you want to download a single language at a time without the need for storing them in the binary.

A little extra feature is that if you name one of the messages as language-name and it doesn't use any variables plus it's present in all languages, then the generated L10n enum will also contain the names of all the languages, which is really useful when you want to present the user with a drop-down menu listing all the available languages.

Note that in order to get the warnings for unused message functions, you have to generate the file in the same crate as where you use them, and you cannot make L10nLanguage file part of the crate's public interface.

Usage

# in Cargo.toml
[dependencies]
fluent-typed = 0.1

[build-dependencies]
fluent-typed = { version = "0.1", features = ["build"] }
use fluent_typed::{build_from_locales_folder, BuildOptions};

// in build.rs
fn main() -> std::process::ExitCode {
    // Build with the default settings, which means to
    // generate the src/l10n.rs file from the fluent
    // translations found in the `locales/` folder,
    // prefix the generated functions with "msg_" and
    // indent the code with 4 spaces.It also generates a
    // single ftl file with all the languages, which is
    // embedded in the binary. See the BuildOptions and
    // FtlOutputOptions for all the configuration options.
    //
    // This function returns an ExitCode.
    build_from_locales_folder(BuildOptions::default())

    // Note: there are also `try_build_from_locales_folder`
    // which returns a Result
}
// in lib.rs or main.rs
mod l10n;
use l10n::L10n;

// Load english translations in an L10nLanguage struct.
// It provides safe function for accessing all messages.
let strs: L10nLanguage = L10n::EnGb.load();

// With the feature "langneg" enabled you can do automatic language
// negotiation, which falls back on the default language as
// configured in the BuildOptions in the build.rs when generating.
let found_lang: L10nLanguage = L10n::langneg("en");

// In Dioxus/Leptos/Silkenweb etc the L10nLanguage struct is typically
// used inside of a Signal or other reactive construct, so that all
// translations are automatically updated when the struct is changed.

// A message without arguments.
assert_eq!("Welcome!", strs.msg_greeting());
// A message with a string argument (AsRef<str>).
assert_eq!("Hello world", strs.msg_hello("world"));
// A message with a number argument (Into<FluentNumber>).
assert_eq!("You have 2 unread messages", strs.msg_unread_messages(2));

// the list of the translated, human-readable language names.
let language_names: Vec<&str>
  = L10n.iter().map(|lang| lang.language_name()).collect();

// typically server-side, you'll load all the languages
let languages = L10n::load_all();
// then you can use it like
assert_eq!("Welcome!", languages.get(L10n::en).msg_greeting());

Type deduction

Since the fluent syntax doesn't explicitly specify the type of the translation variables, this project uses the following rules to infer the type of the translation variables:

  • String:
    • If a variable's comment contains (String), as in # $name (String) - The name.
  • Number:
    • If a variable's comment contains (Number), as in # $count (Number) - How many.
    • If a NUMBER function is used, asin dpi-ratio = Your DPI ratio is { NUMBER($ratio) }
    • If a selector only contains numbers and CLDR plural catagories: zero, one, two, few, many, and other. Example: text your-rank = { NUMBER($pos, type: "ordinal") -> [1] You finished first! [one] You finished {$pos}st [two] You finished {$pos}nd [few] You finished {$pos}rd *[other] You finished {$pos}th } gg

Dependencies

~1.4–2MB
~40K SLoC