3 unstable releases

0.1.1 Feb 19, 2023
0.1.0 Feb 19, 2023
0.1.0-beta.1 Oct 19, 2022
0.0.0 Aug 8, 2022

#105 in Internationalization (i18n)

MIT/Apache

115KB
2.5K SLoC

l10n

CI badge Crate badge Rustc badge

l10n is a high level and opinionated localization crate built upon the excellent fluent-bundle crate, the Fluent project and inspired by the thiserror crate.

The goal of this crate is to ease project localization and provide compile time checks (message exists, mandatory arguments are set, functions are defined).

You can check some examples here: https://github.com/MathieuTricoire/l10n-examples

Code repository: https://github.com/MathieuTricoire/l10n

⚠️ Important note about the 0.1 version ⚠️

This 0.1 version depends on fluent-bundle@0.15 which doesn't contains the fixes for the following issues:

  • String argument: #251
  • Lifetime issues for Cow: #264
  • Merge arguments: #271 which l10n depends on_

Because ot this, string arguments are not supported as is, you need to write "argument" = value.as_str() instead of "argument" = value and this version is theorically less performant since all Cow<String> returned are actually owned and because of how arguments are merged.

If you want to use the latest version that depends on fluent-bundle with these issues fixed, use the one from GitHub:

[dependencies]
l10n = { git = "https://github.com/MathieuTricoire/l10n.git" }

Installation

[dependencies]
l10n = "0.1

MSRV: rustc 1.61+

Quick start

There is no configuration needed to start using l10n, just create a l10n directory next to Cargo.toml, create as many locale directories (must be valid locales) containing fluent resources.

Example

Localization directory tree structure:

l10n
├── _brand.ftl          (global unnamed resource)
├── en
│   ├── _common.ftl     (unnamed resource)
│   ├── app.ftl         (named resource)
│   └── settings.ftl    (named resource)
├── en-CA
├── en-GB
│   └── app.ftl         (named resource)
├── fr
│   ├── _common.ftl     (unnamed resource)
│   ├── app.ftl         (named resource)
│   └── settings.ftl    (named resource)
└── fr-CA
    ├── _terms.ftl      (unnamed resource)
    └── settings.ftl    (named resource)
Cargo.toml

l10n/fr/app.ftl file:

greeting = Bonjour { $first-name } !

l10n/fr/settings.ftl file:

status =
    .online = En ligne
    .offline = Hors ligne
    .busy = { $gender ->
        [male] Occupé
        [female] Occupée
       *[other] Non disponible
    } ({ $reason })

Then in the root of your application or library initialize l10n (this create a L10N static ref used by other macros) and create l10n messages either with the message! macro or by deriving L10nMessage.

use l10n::unic_langid::langid;
use l10n::{message, message_args, L10nMessage};
use l10n::fluent_bundle::{FluentValue, FluentArgs}; // for functions, not necessary for this example

l10n::init!({
    // not necessary for this example
    functions: { "TIME": |_: &[FluentValue<'_>], _: &FluentArgs| FluentValue::None }
});

fn main() {
    let lang = langid!("fr");

    let username = "Alice";
    let greeting = message!("app", "greeting", "first-name" = username);
    assert_eq!(greeting.translate(&lang), "Bonjour \u{2068}Alice\u{2069} !");

    let status = Status::Busy {
        reason: "Meeting".to_string(),
    };
    assert_eq!(status.translate(&lang), "\u{2068}Non disponible\u{2069} (\u{2068}Meeting\u{2069})");
    assert_eq!(
        status.translate_with_args(&lang, Some(&message_args!("gender" => "female"))),
        "\u{2068}Occupée\u{2069} (\u{2068}Meeting\u{2069})"
    );
}

#[derive(L10nMessage)]
#[l10n_message("settings", "status")]
enum Status {
    #[l10n_message(".online")]
    Online,
    #[l10n_message(".offline")]
    Offline,
    #[l10n_message(".busy", "reason" = reason.as_str(), "gender" = "other")]
    Busy { reason: String },
}

Advanced usage

Configuration file

Create a l10n.toml or config.toml file next to Cargo.toml, to define the locales nor set a different path to the "localization" directory containing the locale directories and fluent files.

l10n.toml file example:

[l10n]
locales = [
    "en",
    { main = "en-GB", fallback = "en" },
    { main = "en-CA", fallback = "en-GB" },
    "fr",
    { main = "fr-CA", fallback = "fr" },
]
path = "localization_files"

To use another configuration file at compile time, set the environment variable L10N_CONFIG_FILE like this L10N_CONFIG_FILE=/path/to/specific-config.toml.

Path to localization directory

To have different paths to the "localization" directory according to your need, use a map value for path (or paths) where the key is the name of the environment and the value the path to the "localization" directory. A default environment is required.

Then to compile your artificat with this environment use the environment variable L10N_PATH_ENV.

l10n.toml file example:

[l10n]
paths = { default = "l10n", prod = "/path/to/l10n" }

Build command:

L10N_PATH_ENV=prod cargo build --release

You can also prefix your path with a special variable $ROOT and the library will replace this variable with the path to the configuration file.

/path/to/l10n.toml file example:

[l10n]
path = "$ROOT/localization_files"

Produced path: /path/to/localization_files.

Locales

Discovered locales

If no locales configuration is provided, l10n will discover the locales in the "localization" directory. l10n implements a very basic fallback mechanism between discovered locales, if a locale contains a "region" code it will fallback to the same locale without the "region" code if exists.

Localization directory tree structure (only locale directories are shown):

l10n
├── en
├── en-CA
├── en-GB
├── en-GB-variant
├── en-Latn
├── en-Latn-variant
├── en-Latn-GB
└── en-Latn-GB-variant

In the example above the fallbacks will be:

  • en: no fallback
  • en-CA: fallback to en
  • en-GB: fallback to en
  • en-GB-variant: no fallback
  • en-Latn: no fallback
  • en-Latn-variant: no fallback
  • en-Latn-GB: fallback to en-Latn
  • en-Latn-GB-variant: fallback to en-Latn-variant

Locales set in configuration

A locale can be used if set as a "main" locale, this means if a locale is only set as a fallback it will not be possible to translate messages in this locale.

l10n.toml file example

[l10n]
locales = [
  { main = "en-US", fallback = "en" },
  { main = "en-GB", fallback = "en" },
  { main = "en-CA", fallback = "en-GB" },
  { main = "fr" }, # same as writing `"fr",`
  { main = "fr-CA", fallback = "fr" },
]

In this example the messages can only be translated with the locales: en-US, en-GB, en-CA, fr, fr-CA and not en which is only set as a fallback "locale".

Details

Resources

There is 3 kind of resources:

  • Global unnamed resources: Under the l10n directory starting with _, these resources are shared across all named resources in all locales.
  • Unnamed resources: Under locale directories starting with _, these resources are shared across all named resources in the current locale.
  • Named resources: Under locale directories, these are the resources containing the messages you can use in your code.

"Global unnamed resources" and "Unnamed resources" can be freely created and will be load according to their attached locale.

"Named resources" must exists for all "mandatory locales". "Mandatory locales" are all the locales at the end of a resolution route, in the next example the "mandatory locales" are: "en" and "fr".

[l10n]
locales = [
  { main = "en-GB", fallback = "en" },
  { main = "en-CA", fallback = "en-GB" },
  "fr",
  { main = "fr-CA", fallback = "fr" },
]

License

Licensed under either of

at your option.

Contribution

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.

Dependencies

~1.7–2.3MB
~48K SLoC