Introduction

leptos-fluent is a framework for internationalizing Leptos applications using Fluent. It has all the batteries included to handle language switching, translations management, multiple strategies for activating translations at initialization, different modes of persistent storage, and more.

Crates.io License docs.rs Crates.io downloads

Project goals

The main goals of leptos-fluent are:

  • To provide a simple and easy-to-use API for internationalizing Leptos applications.
  • Be the most fully featured internationalization framework in any language.
  • Be the most performant internationalization framework available.

Help and support

Discord channel

You can ask for help and support in the #leptos-fluent channel of Leptos Discord server, open a discussion in the GitHub repository or report bugs by opening an issue.

Contributing

Help wanted issues

See CONTRIBUTING.md file for more information about how to setup the development environment and contribute to the project.

Installation

CSR

For client side rendering apps install leptos-fluent and fluent-templates:

[dependencies]
leptos-fluent = "0.1"
fluent-templates = "0.11"
Minimal (recommended)
[dependencies]
leptos-fluent = { version = "0.1", default-features = false }
fluent-templates = { version = "0.11", default-features = false, features = [
  "macros",
  "walkdir"
] }
  • Using default-features = false for leptos-fluent the json default feature will not be enabled, so the languages parameter of leptos_fluent! macro will not be available.
  • Using default-features = false and features = ["macros", "walkdir"] for fluent-templates ensures that the dependency tree is the minimal possible because more dependencies are shared between leptos-fluent and fluent-templates.

SSR

For server side rendering apps install leptos-fluent, fluent-templates and activate the hydrate, ssr and actix/axum features in their respective features set.

[dependencies]
leptos-fluent = "0.1"
fluent-templates = "0.11"

[features]
hydrate = [
  "leptos-fluent/hydrate"
]
ssr = [
  "leptos-fluent/ssr",
  "leptos-fluent/actix",  # actix and axum are supported
]

# Using cargo-leptos
[package.metadata.leptos]
watch-additional-files = ["locales"]

Desktop applications

leptos-fluent can be installed on non-wasm targets, like desktop applications. You need to install leptos-fluent, fluent-templates and enable the system feature:

[dependencies]
leptos-fluent = { version = "0.1", features = ["system"] }
fluent-templates = "0.11"

Example

See the GTK example.

Features

  • Server Side Rendering: ssr
  • Hydration: hydrate
  • Actix Web integration: actix
  • Axum integration: axum
  • Nightly toolchain: nightly
  • Desktop applications: system
  • JSON languages file: json (enabled by default)
  • YAML languages file: yaml
  • JSON5 languages file: json5
  • Tracing support: tracing
  • Debugging: debug

Nightly toolchain

leptos-fluent builds nightly functionalities by enabling the nightly feature:

[dependencies]
leptos-fluent = { version = "0.1", features = ["nightly"] }
fluent-templates = "0.11"

Language files

By default, leptos-fluent supports JSON languages files. To use other formats to load custom languages, the json5 or yaml features can be enabled:

[dependencies]
fluent-templates = "0.11"
leptos-fluent = { version = "0.1", features = ["json5"], default-features = false }

Tip

Tracking locales files with cargo leptos

Using cargo leptos watch of the locales/ folder for reloads:

# Relative to Cargo.toml file
[package.metadata.leptos]
watch-additional-files = ["locales"]

When inside a workspace, use the full path to the folder from the workspace Cargo.toml file:

 # Relative to workspace Cargo.toml file
[package.metadata.leptos]
watch-additional-files = ["examples/csr/locales"]

Tracing

To enable tracing support, add the tracing feature to leptos-fluent:

[dependencies]
leptos-fluent = { version = "0.1", features = ["tracing"] }
fluent-templates = "0.11"

Example

See the GTK example.

Basic usage

Minimal skeleton

The most basic CSR app is reproduced here:

.
β”œβ”€β”€ πŸ“„ Cargo.toml
β”œβ”€β”€ πŸ“ locales
β”‚   β”œβ”€β”€ πŸ“ en
β”‚   β”‚   └── πŸ“„ main.ftl
β”‚   └── πŸ“ es
β”‚       └── πŸ“„ main.ftl
└── πŸ“ src
    β”œβ”€β”€ πŸ“„ main.rs
    └── πŸ“„ lib.rs
# locales/en/main.ftl
select-a-language = Select a language:
language-selected-is = The selected language is { $lang }.
# locales/es/main.ftl
select-a-language = Selecciona un idioma:
language-selected-is = El idioma seleccionado es { $lang }.
// src/lib.rs
use fluent_templates::static_loader;
use leptos::*;
use leptos_fluent::{expect_i18n, leptos_fluent, move_tr, Language};

static_loader! {
    pub static TRANSLATIONS = {
        locales: "./locales",
        fallback_language: "en",
    };
}

#[component]
pub fn App() -> impl IntoView {
    leptos_fluent! {
        translations: [TRANSLATIONS],
        locales: "./locales",
    };

    view! { <LanguageSelector/> }
}

#[component]
fn LanguageSelector() -> impl IntoView {
    // Use `expect_i18n()` to get the current i18n context:
    let i18n = expect_i18n();

    view! {
        <p>{move_tr!("select-a-language")}</p>
        <fieldset>
            {move || {
                i18n.languages.iter().map(|lang| render_language(lang)).collect::<Vec<_>>()
            }}
        </fieldset>
        <p>
            {move_tr!(
                 "language-selected-is",
                 { "lang" => i18n.language.get().name }
            )}
        </p>
    }
}

fn render_language(lang: &'static Language) -> impl IntoView {
    // Passed as atrribute, `Language` is converted to their code,
    // so `<input id=lang` becomes `<input id=lang.id.to_string()`
    view! {
        <div>
            <label for=lang>{lang.name}</label>
            <input
                id=lang
                value=lang
                name="language"
                checked=lang.is_active()
                on:click=move |_| lang.activate()
                type="radio"
            />
        </div>
    }
}
// src/main.rs
pub fn main() {
    console_error_panic_hook::set_once();
    leptos::mount_to_body(minimal_example::App);
}
# Cargo.toml
[package]
name = "minimal-example"
edition = "2021"
version = "0.1.0"

[lib]
name = "minimal_example"
path = "src/lib.rs"

[dependencies]
leptos = { version = "0.6.12", features = ["csr"] }
leptos-fluent = "0.1"
fluent-templates = "0.11"
console_error_panic_hook = "0.1"

# Using cargo-leptos
[package.metadata.leptos]
watch-additional-files = ["locales"]

Translating messages

Use the move_tr! macro to translate a string. The macro takes the key of the translation and an optional object with the variables to interpolate:

move_tr!("select-a-language")

move_tr!("language-selected-is", { "lang" => i18n.language.get().name })

Additionally, use the tr! macro to translate a string inside a reactive context. Note that if is not inside a reactive context, the translation won't be updated on the fly when the language changes. This can lead to warnings in console output like:

Warning

At `./path/to/file.rs:ln`, you access a signal or memo (defined at
`./path/to/file.rs:ln`) outside of a reactive context. This might mean your
app is not responding to changes in signal values in the way you expect.

Can be fixed by replacing calls to tr! with move_tr! or wrapping the tr! calls in a reactive context.

The previous code could be rewritten as:

move || tr!("select-a-language")

move || tr!("language-selected-is", { "lang" => i18n.language.get().name })

The main difference is that move_tr! encapsulates the movement in a leptos::Signal, strictly would be rewritten as:

leptos::Signal::derive(move || tr!("select-a-language"))

Retrieving the I18n context

Use the expect_i18n function to get the current i18n context:

let i18n = leptos_fluent::expect_i18n();

It is exported as i18n too:

let i18n = leptos_fluent::i18n();

The function use_i18n returns an Option with the current i18n context:

let i18n = leptos_fluent::use_i18n().expect("No `I18n` context found");

Using the I18n context

The i18n context has the following fields:

  • language: A read-write signal with a pointer to the static current active language.
  • languages: A pointer to a static list of pointers of the static available languages.
  • translations: A signal to the vector of fluent-templates loaders that stores the translations.

Update language

To update the language, use lang.activate or the set method of language:

lang.activate();

expect_i18n().language.set(lang);

Nightly

When nightly feature is enabled, can be updated passing a new language to the context as a function with:

let i18n = leptos_fluent::i18n();
i18n(lang);

Get active language

To get the current active language, use get method of language field:

let i18n = leptos_fluent::i18n();
let lang = i18n.language.get();

Nightly

When nightly enabled, can get the active language with:

let i18n = leptos_fluent::i18n();
let lang = i18n();

Get available languages

To get the available languages, iterate over the languages field:

i18n.languages.iter()

Check if a language is active

To check if a language is the active one, use is_active method of a leptos_fluent::Language struct:

lang.is_active()

lang == expect_i18n().language.get()

Strategies

All the features of the framework are optional, following a declarative "opt-in" configuration method.

Loading the initial language of the user

The initial language of the user can be set in different ways:

StrategyCSRSSRDesktopleptos_fluent!
URL parameterβœ…βœ…βŒinitial_language_from_url_param
URL pathβœ…βœ…βŒinitial_language_from_url_path
Cookieβœ…βœ…βŒinitial_language_from_cookie
Server functionβœ…βœ…βŒinitial_language_from_server_function
Browser local storageβœ…βŒβŒinitial_language_from_localstorage
Browser navigator.languagesβœ…βŒβŒinitial_language_from_navigator
Accept-Language headerβŒβœ…βŒinitial_language_from_accept_language_header
System languageβŒβŒβœ…initial_language_from_system
Data fileβŒβŒβœ…initial_language_from_data_file

All of them can be defined at the same time or just one of them at the same time. The first language source found will be used and nexts discharged. The order of precedence is:

CSR | Updating the language on the client

When the user changes the language and I18n::language.set is called, the framework can perform a side effect to update the language in the client. The following strategies are available:

Strategyleptos_fluent!
URL parameterset_language_to_url_param
Cookieset_language_to_cookie
Browser local storageset_language_to_localstorage
Server functionset_language_to_server_function

When the user changes the language in the browser settings, the framework can perform a side effect to reflect the language change in the client. The following strategies are available:

Strategyleptos_fluent!
Browser navigator.languagesset_language_from_navigator

featsystem | Desktop applications

Strategyleptos_fluent!
Data fileset_language_to_data_file

CSR | Updating the language from initialization on the client

When a language is loaded from initialization, the framework can perform a side effect to persistently storage the language in the client. The following strategies are available:

Strategyleptos_fluent!
URL parameter to cookieinitial_language_from_url_param_to_cookie
URL parameter to local storageinitial_language_from_url_param_to_localstorage
URL path to cookieinitial_language_from_url_path_to_cookie
URL path to local storageinitial_language_from_url_path_to_localstorage
Cookie to local storageinitial_language_from_cookie_to_localstorage
Local storage to cookieinitial_language_from_localstorage_to_cookie
Local storage to server functioninitial_language_from_localstorage_to_server_function
navigator.languages to cookieinitial_language_from_navigator_to_cookie
navigator.languages to local storageinitial_language_from_navigator_to_localstorage
navigator.languages to server functioninitial_language_from_navigator_to_server_function
Server function to local storageinitial_language_from_server_function_to_localstorage

CSR + SSR

Strategyleptos_fluent!
URL parameter to server functioninitial_language_from_url_param_to_server_function
URL path to server functioninitial_language_from_url_path_to_server_function
Cookie to server functioninitial_language_from_cookie_to_server_function
Server function to cookieinitial_language_from_server_function_to_cookie

featsystem | Desktop applications

Strategyleptos_fluent!
System language to data fileinitial_language_from_system_to_data_file

CSR | Client side effects

When the user updates the language, the framework can perform side effects to update the language in the client. The following side effects are available:

Side effectleptos_fluent!
<html lang="..."> attributesync_html_tag_lang
<html dir="..."> attributesync_html_tag_dir

CSR + SSR | Names

The names of the settings can be configured using the following parameters:

Strategyleptos_fluent!Default value
Cookiecookie_name"lf-lang"
Cookie attributescookie_attrs""
Browser local storagelocalstorage_key"lang"
URL parameterurl_param"lang"
URL path extractor fnurl_path❌

featsystem | Desktop applications

Strategyleptos_fluent!Default value
Data filedata_file_key"leptos-fluent"

Languages

leptos-fluent follows a default strategy to generate the languages of the application. This strategy is based on the locales/ directory.

Giving the next directory structure:

.
└── πŸ“ locales
    β”œβ”€β”€ πŸ“ en
    β”‚   └── πŸ“„ main.ftl
    └── πŸ“ es-ES
        └── πŸ“„ main.ftl

The framework will generate something like the following languages array at compile time:

let LANGUAGES = [
  leptos_fluent::Language {
    id: unic_langid::langid!("en"),
    name: "English",
    dir: leptos_fluent::WritingDirection::Ltr,
  },
  leptos_fluent::Language {
    id: unic_langid::langid!("es-ES"),
    name: "EspaΓ±ol (EspaΓ±a)",
    dir: leptos_fluent::WritingDirection::Ltr,
  },
]
  • en is built with the name "English" because it's defined as an ISO 639-1 code, without a region code.
  • es-ES is built with the name "EspaΓ±ol (EspaΓ±a)" because it's defined as an ISO 639-1 code and a region code.

This enforces that an user will always be able to select their language in their own language, and not in the current language of the application.

Order

The order of the languages will be defined based on the alphabetical order of their names, not their codes.

The languages file

The languages array can be fully customized by defining a languages parameter in the leptos_fluent! macro pointing to a languages file. This file must be relative to the Cargo.toml file.

leptos_fluent! {
    languages: "./locales/languages.json",
    // ...
}
[
  ["en", "English"],
  ["es-ES", "Spanish (Spain)", "auto"]
]

The languages file must expose an array of arrays with the structure:

[
  // Code, Name,            "ltr"/"rtl"/"auto" (optional)
  ["code", "Language name", "Writing direction"],
]

Order

The order of the languages in leptos_fluent::I18n::languages will be the same as in the file regardless of the alphabetical order of the names.

The first in the languages file will used as the initial of the user when any other initialization value is discovered. Use the same as the one configured as fallback_language in static_loader!.

static_loader! {
    static TRANSLATIONS = {
        locales: "./locales",
        fallback_language: "en",
    };
}

#[component]
pub fn App() -> impl IntoView {
    leptos_fluent! {
        translations: [TRANSLATIONS],
        languages: "./locales/languages.json5",
    };
}
// ./locales/languages.json5
[
  ["en", "English"],
  ["es-ES", "EspaΓ±ol (EspaΓ±a)"],
]

File format

By default, the json feature is enabled, which only allows to set the languages file in JSON format. To use other formats, disable the feature and enable another.

[dependencies]
leptos-fluent = { version = "*", default-features = false, features = ["json5"] }

Available features for languages file formats are:

  • json: JSON (default)
  • yaml: YAML
  • json5: JSON5

Checking translations

To check that the translations of the app are correct at compile time, set the check_translations parameter in the leptos_fluent! macro to a glob pattern that matches the Rust files that you want to check.

The pattern must be relative to the location of the Cargo.toml file.

For single crate projects, it would be something like:

leptos_fluent! {
    check_translations: "./src/**/*.rs",
}

For workspace projects, it could be something like:

leptos_fluent! {
    check_translations: "../{app,components}/src/**/*.rs",
}

Translations error messages

When the translations stop being synchronized, you will see errors like:

error: Translations check failed:
       - Message "select-a-language" defined at `move_tr!("select-a-language")` macro call in src/lib.rs not found in files for locale "en".
       - Message "select-a-lang" of locale "en" not found in any `tr!` or `move_tr!` macro calls.
  --> examples/csr-complete/src/lib.rs:18:29
   |
18 |         check_translations: "./src/**/*.rs",
   |                             ^^^^^^^^^^^^^^^

If placeable are missing in the translations, you will see errors like:

error: Translations check failed:
       - Variable "dir" defined at `move_tr!("html-tag-dir-is", { ... })` macro call in src/lib.rs not found in message "html-tag-dir-is" of locale "en".
       - Variable "name" defined in message "html-tag-dir-is" of locale "en" not found in arguments of `move_tr!("html-tag-dir-is", { ... })` macro call at file src/lib.rs.
  --> examples/csr-complete/src/lib.rs:18:29
   |
18 |         check_translations: "./src/**/*.rs",
   |                             ^^^^^^^^^^^^^^^

Why glob patterns to Rust files?

leptos-fluent provides a I18n context to Leptos when the macro leptos_fluent! is called. So multiple instances of a context with different localization files and strategies can be initialized in different component trees. This is useful, for example, in a multi page app.

The mechanism of translations checking needs to know where reside the calls to tr! and move_tr! macros to extract the messages that need to be checked. This is performed by parsing the source code looking for these macros invocations.

Why macros

leptos-fluent doesn't provide ways to translate directly using I18n context methods, as it would be impossible to extract the translations at compile time.

The only limitation for checking translations with glob patterns is that the tr! and move_tr! macros that consume each context must be in different file trees, but this enforces anyway a good practice of file-level separation of contexts in the codebase.

FAQs

How to get LanguageIdentifier?

use fluent_templates::LanguageIdentifier;

Tip

fluent-templates also depends externally on fluent-bundle whichs provides utilities for parsing the Fluent syntax.

How to get the i18n context at initialization?

use leptos_fluent::leptos_fluent;

let i18n = leptos_fluent! {
    // ...
};

leptos::logging::log!("i18n context: {i18n:#?}");

Use an expression to set the cookie attributes and will not be validated.

let attrs = "SameSite=Strict; MyCustomAttr=MyCustomValue;";
leptos_fluent! {
    cookie_attrs: attrs,
    // ...
};

How to get the fallback language

From fluent-templates v0.10 onwards can be obtained from your translations.

let fallback_language = expect_i18n().translations.get()[0].fallback();

tr! and move_tr! outside reactive graph

Outside the reactive ownership tree, mainly known as the reactive graph, we can't obtain the context of I18n using expect_context::<leptos_fluent::I18n>(), which is what tr! and move_tr! do internally. Instead, we can pass the context as first parameter to the macros:

let i18n = leptos_fluent! {
    // ...
};

let translated_signal = move_tr!(i18n, "my-translation");

And some shortcuts cannot be used. Rewrite all the code that calls expect_context internally:

  • Use i18n.language.set(lang) instead of lang.activate().
  • Use lang == i18n.language.get() instead of lang.is_active().

On events, panics

For example, the next code panics when the <div> container is clicked:

#[component]
pub fn App() -> impl IntoView {
    view! {
        <Show when=|| true>
            <Child/>
        </Show>
    }
}

#[component]
pub fn Child() -> impl IntoView {
    leptos_fluent! {
        // ...
    };
    view! {
        <div on:click=|_| {
            tr!("my-translation");
        }>"CLICK ME!"</div>
    }
}

With Leptos v0.7, whatever tr! macro used in the on: event will panic, but with Leptos v0.6, this outsiding of the ownership tree has been ignored from the majority of the cases as unintended behavior.

To avoid that, pass the i18n context as first parameter to tr! or move_tr!:

#[component]
pub fn App() -> impl IntoView {
    view! {
        <Show when=|| true>
            <Child/>
        </Show>
    }
}

#[component]
pub fn Child() -> impl IntoView {
    let i18n = leptos_fluent! {
        // ...
    };
    view! {
        <div on:click=|_| {
            tr!(i18n, "my-translation");
        }>"CLICK ME!"</div>
    }
}

Confused about what context is used?

Take into account that the reactive ownership graph is not the same as the component tree in Leptos. For example, the next code:

#[component]
fn Foo() -> impl IntoView {
    provide_context::<usize>(0);

    view! {
        <h1>"Foo"</h1>
        {
            let value = expect_context::<usize>();
            view! {
                <p>"Context value before Bar: "{value}</p>
            }
        }
        <Bar/>
        {
            let value = expect_context::<usize>();
            view! {
                <p>"Context value after Bar -> Baz: "{value}</p>
            }
        }
    }
}

#[component]
fn Bar() -> impl IntoView {
    provide_context::<usize>(1);
    view! {
        <h1>"Bar"</h1>
        {
            let value = expect_context::<usize>();
            view! {
                <p>"Context value before Baz: "{value}</p>
            }
        }
        <Baz/>
    }
}

#[component]
fn Baz() -> impl IntoView {
    provide_context::<usize>(2);
    view! {
        <h1>"Baz"</h1>
    }
}

Renders:

<h1>Foo</h1>
<p>Context value before Bar: 0</p>
<h1>Bar</h1>
<p>Context value before Baz: 1</p>
<h1>Baz</h1>
<p>Context value after Bar -&gt; Baz: 2</p>

Because Baz is a sibling of Foo children in the reactive graph. But maybe you think that is just a children of Bar in the component tree and that is outside the scope of Foo children. That doesn't matter for Leptos.

In those cases where you're using two or more contexts, pass the context as the first argument to the tr! and move_tr! macros to avoid confusion.

#[component]
fn Foo() -> impl IntoView {
    let i18n = leptos_fluent! {
        translations: [TRANSLATION_WITH_ONLY_FOO],
        // ...
    };
    <p>{move_tr!("my-translation-from-foo")}</p>
    <Bar/>
    // The next message will not be translated because after `<Bar>`
    // now the i18n context accessed by `move_tr!` is the one from `Bar`
    <p>{move_tr!("my-translation-from-foo")}</p>
    // instead, use:
    <p>{move_tr!(i18n, "my-translation-from-foo")}</p>
}

#[component]
fn Bar() -> impl IntoView {
    let i18n = leptos_fluent! {
        translations: [TRANSLATION_WITH_ONLY_BAR],
        // ...
    };
    <p>{move_tr!("my-translation-from-bar")}</p>
}

Why examples don't use <For/> component?

Bug

There are some cases in which the <For/> component is not reproducible between SSR and hydrate modes leading to different renders, so decided to use a simple vector to not bring confusion to main examples.

In any case, the <For/> component is safe on CSR contexts and leptos_fluent::Language implement Hash and Eq traits to be able to be passed directly to keys properties trigerring reactivity depending on the current active language.

use leptos_fluent::{i18n, Language};

leptos::logging::warn!("[WARNING]: Not secure on SSR");
view! {
    <p>{move_tr!("select-a-language")}</p>
    <For
        each=move || i18n().languages
        key=|lang| *lang
        children=move |lang| render_language(lang)
    />
}

fn render_language(lang: &'static Language) -> impl IntoView { ... }

How to manage translations on server actions

The translations reside on the client side, so the I18n can not be accessed as context on server actions. Pass the translations as values if the bandwidth is not a problem or use your own statics on server side.

use leptos::*;
use leptos_fluent::{tr, Language};

/// Server action showing client-side translated message on console
#[server(ShowHelloWorld, "/api")]
pub async fn show_hello_world(
    translated_hello_world: String,
    language: String,
) -> Result<(), ServerFnError> {
    println!("{translated_hello_world} ({language})");
    Ok(())
}

fn render_language(lang: &'static Language) -> impl IntoView {
    // Call on click to server action with a client-side translated
    // "hello-world" message
    let on_click = move |_| {
        lang.activate();
        spawn_local(async {
            _ = show_hello_world(
                tr!("hello-world"),
                lang.name.to_string(),
            ).await;
        });
    };

    view! {
        <div>
            <label for=lang>{lang.name}</label>
            <input
                id=lang
                name="language"
                value=lang
                checked=lang.is_active()
                on:click=on_click
                type="radio"
            />
        </div>
    }
}

How to get values of leptos_fluent! macro at runtime?

Use provide_meta_context at the macro initialization and get them with the method I18n::meta:

let i18n = leptos_fluent! {
    // ...
    provide_meta_context: true,
};

println!("Macro parameters: {:?}", i18n.meta().unwrap());

Configuration conditional checks

leptos_fluent! {
    // ...
    #[cfg(debug_assertions)]
    set_language_to_url_param: true,
    #[cfg(not(debug_assertions))]
    set_language_to_url_param: false,
}

leptos_fluent!

The leptos_fluent! macro is used to load the translations and set the current locale. It is used in the root component of the application.

leptos_fluent! {
    locales: "./locales",
    translations: [TRANSLATIONS],
};

Common configurations

CSR | Local storage from navigator

leptos_fluent! {
    locales: "./locales",
    translations: [TRANSLATIONS],

    set_language_to_localstorage: true,
    initial_language_from_localstorage: true,
    initial_language_from_navigator: true,
    initial_language_from_navigator_to_localstorage: true,
    initial_language_from_url_param: true,
    initial_language_from_url_param_to_localstorage: true,
    localstorage_key: "lang",
}
leptos_fluent! {
    locales: "./locales",
    translations: [TRANSLATIONS],

    set_language_to_cookie: true,
    initial_language_from_cookie: true,
    initial_language_from_navigator: true,
    initial_language_from_navigator_to_cookie: true,
    initial_language_from_url_param: true,
    initial_language_from_url_param_to_cookie: true,
    initial_language_from_accept_language_header: true,
    cookie_name: "lf-lang",
}

featsystem | Data files on Desktop applications

leptos_fluent! {
    locales: "./locales",
    translations: [TRANSLATIONS],

    initial_language_from_system: true,
    initial_language_from_system_to_data_file: true,
    initial_language_from_data_file: true,
    set_language_to_data_file: true,
    data_file_key: "system-language-example",
}

Processing steps

There is four kind of parameters for all the possible configurations and are executed in the next order:

Order

  1. Get the initial languaje from a source or target: initial_language_from_*
  2. Obtain the initial language and set to a target: initial_language_from_*_to_*
  3. Synchronize the current language with a target: set_language_to_*
  • The name of a source or a target: cookie_name, localstorage_key, navigator...

Sources and targets

Sources are read-only and targets are read-write.

  • Sources: navigator, system, accept_language_header
  • Targets: cookie_name, localstorage_key, url_param, data_file...

Commented example

Example

leptos_fluent! {
    // ..
    // Get the initial language from the source operative system
    initial_language_from_system: true,
    // and set to the target file.
    initial_language_from_system_to_data_file: true,
    // If a target data file exists, get the initial language from it.
    initial_language_from_data_file: true,
    // When the language is updated, set it to the file.
    set_language_to_data_file: true,
    // Unique file name to set the language for this app:
    data_file_key: "system-language-example",
};

Parameters

translations

Set the translations to be used in the application. It must be a reference to a static array of fluent_templates::static_loader! instances.

use fluent_templates::static_loader;
use leptos_fluent::leptos_fluent;

static_loader! {
    pub static TRANSLATIONS = {
        locales: "./locales",
        fallback_language: "en",
    };
}

leptos_fluent! {
    locales: "./locales",
    translations: [TRANSLATIONS],
    // ^^^^^^^^^^^^^^^^^^^^^^^^^
}

Must be the same identifier used in the fluent_templates::static_loader! macro, which returns an once_cell:sync::Lazy variable.

locales

Set the path to the locales directory which contain the Fluent files for the translations. Must be relative to the Cargo.toml file, the same used in the fluent_templates::static_loader! macro.

leptos_fluent! {
    locales: "./locales",
    // ^^^^^^^^^^^^^^^^^
    translations: [TRANSLATIONS],
}

core_locales

Common locale resources that are shared across all locales. Must be relative to the Cargo.toml file, the same used in the fluent_templates::static_loader! macro:

static_loader! {
    pub static TRANSLATIONS = {
        locales: "./locales",
        core_locales: "./locales/core",
    };
}

leptos_fluent! {
    locales: "./locales",
    core_locales: "./locales/core",
    // ^^^^^^^^^^^^^^^^^^^^^^^^^^^
    translations: [TRANSLATIONS],
}

languages

Path to a file containing the list of languages supported by the application. Must be relative to the Cargo.toml file.

leptos_fluent! {
    locales: "./locales",
    languages: "./locales/languages.json",
    // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    translations: [TRANSLATIONS],
}

Tip

The languages file should contain an array of arrays where each inner array contains a language identifier and a language name, respectively, at least.

The language identifier should be a valid language tag, such as en-US, en, es-ES, etc. By default, the languages file should be a JSON with a .json extension because the json feature is enabled. For example:

[
  ["en-US", "English (United States)"],
  ["es-ES", "EspaΓ±ol (EspaΓ±a)"]
]

Set default-features = false and enable the yaml or the json5 feature to use a YAML or JSON5 files. For example:

# locales/languages.yaml
- - en-US
  - English (United States)
- - es-ES
  - EspaΓ±ol (EspaΓ±a)
// locales/languages.json5
[
  ["en-US", "English (United States)"],
  ["es-ES", "EspaΓ±ol (EspaΓ±a)"],
]

Define a third element in the inner array with the direction of the language, to use it in the <html dir="..."> attribute (see sync_html_tag_dir). For example:

[
  ["en-US", "English (United States)", "ltr"],
  ["es-ES", "EspaΓ±ol (EspaΓ±a)", "auto"],
  ["ar", "Ψ§Ω„ΨΉΨ±Ψ¨ΩŠΨ©", "rtl"],
  ["it", "Italiano"]
]

check_translations

Check the translations at compile time. It is useful to ensure that all translations are correct and that there are no missing translations.

Must be a glob relative to the Cargo.toml file.

  • For single crate projects:

    leptos_fluent! {
        locales: "./locales",
        translations: [TRANSLATIONS],
        check_translations: "./src/**/*.rs",
        // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    }
  • For workspace projects:

    leptos_fluent! {
        locales: "./locales",
        translations: [TRANSLATIONS],
        check_translations: "../{app,components}/src/**/*.rs",
        // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    }

CSR | sync_html_tag_lang

Synchronize the global <html lang="..."> attribute with current language using leptos::create_effect. Can be a literal boolean or an expression that will be evaluated at runtime.

leptos_fluent! {
    // ...
    sync_html_tag_lang: true,
}

CSR | sync_html_tag_dir

Synchronize the global <html dir="..."> attribute with current language using leptos::create_effect.

Can be a literal boolean or an expression that will be evaluated at runtime.

leptos_fluent! {
    // ...
    sync_html_tag_dir: true,
}

For custom languages from a languages file, specify a third element in the inner array with the direction of the language, which can be "auto", "ltr", or "rtl". Discovered languages will be defined depending on the language.

Example

  • Arabic (ar): "rtl"
  • English (en): "ltr"
  • Japanese (ja): "auto"

CSR + SSR | url_param: "lang"

Name of URL parameter used to manage the current language.

leptos_fluent! {
    // ...
    url_param: "lang",
}

CSR + SSR | initial_language_from_url_param

Set initial language from URL parameter.

leptos_fluent! {
    // ...
    initial_language_from_url_param: true,
}

CSR | initial_language_from_url_param_to_localstorage

Get initial language from URL parameter and save it to local storage.

leptos_fluent! {
    // ...
    initial_language_from_url_param_to_localstorage: true,
}

Get initial language from URL parameter and save it to a cookie.

leptos_fluent! {
    // ...
    initial_language_from_url_param_to_cookie: true,
}

CSR | set_language_to_url_param

Synchronize current language with URL parameter.

leptos_fluent! {
    // ...
    set_language_to_url_param: true,
}

CSR + SSR | url_path

Language extractor from URL path. It must take the URI path as argument and return the possible language.

/// Get the language from the top directory in the URL path.
fn get_language_from_url_path(path: &str) -> &str {
    if let Some(language) = path.split('/').nth(1) {
        return language;
    }
    ""
}

leptos_fluent! {
    // ...
    url_path: get_language_from_url_path,
}

CSR + SSR | initial_language_from_url_path

Set initial language from URL path.

leptos_fluent! {
    // ...
    initial_language_from_url_path: true,
}

Set initial language from URL path to a cookie.

leptos_fluent! {
    // ...
    initial_language_from_url_path_to_cookie: true,
}

CSR | initial_language_from_url_path_to_localstorage

Set initial language from URL path to local storage.

leptos_fluent! {
    // ...
    initial_language_from_url_path_to_localstorage: true,
}

CSR | localstorage_key: "lang"

Local storage key to manage the current language.

leptos_fluent! {
    // ...
    localstorage_key: "lang",
}

CSR | initial_language_from_localstorage

Get initial language from local storage.

leptos_fluent! {
    // ...
    initial_language_from_localstorage: true,
}

Get initial language from local storage and save it to a cookie.

leptos_fluent! {
    // ...
    initial_language_from_localstorage_to_cookie: true,
}

CSR | set_language_to_localstorage

Set the current language to local storage.

leptos_fluent! {
    // ...
    set_language_to_localstorage: true,
}

CSR | initial_language_from_navigator

Get the initial language from navigator.languages.

leptos_fluent! {
    // ...
    initial_language_from_navigator: true,
}

CSR | initial_language_from_navigator_to_localstorage

Get the initial language from navigator.languages and save it in the local storage.

leptos_fluent! {
    // ...
    initial_language_from_navigator_to_localstorage: true,
}

Get the initial language from navigator.languages and save it in a cookie.

leptos_fluent! {
    // ...
    initial_language_from_navigator_to_cookie: true,
}

CSR | set_language_from_navigator

When the user changes the language in the browser settings, the language change will be reflected in the client.

leptos_fluent! {
    // ...
    set_language_from_navigator: true,
}

SSR | initial_language_from_accept_language_header

Get the initial language from the Accept-Language header.

leptos_fluent! {
    // ...
    initial_language_from_accept_language_header: true,
}

Name of the cookie that will be used to manage the current language. By default it is "lf-lang".

leptos_fluent! {
    // ...
    cookie_name: "lang",
}

Cookie attributes to set on the language cookie.

leptos_fluent! {
    // ...
    cookie_attrs: "SameSite=Strict; Secure",
}

If value is an expression the cookie will not be validated at compile time:

let attrs = "SameSite=Strict; Secure; MyCustomCookie=value"
leptos_fluent! {
    // ...
    cookie_attrs: attrs,
}

Get the initial language from the cookie.

leptos_fluent! {
    // ...
    initial_language_from_cookie: true,
}

Get the initial language from the cookie and save it in the local storage.

leptos_fluent! {
    // ...
    initial_language_from_cookie_to_localstorage: true,
}

Set the current language to the cookie.

leptos_fluent! {
    // ...
    set_language_to_cookie: true,
}

featsystem | initial_language_from_system

Get the initial language from the system.

leptos_fluent! {
    // ...
    initial_language_from_system: true,
}

featsystem | initial_language_from_data_file

Get the initial language from a data file.

leptos_fluent! {
    // ...
    initial_language_from_data_file: true,
}

featsystem | initial_language_from_system_to_data_file

Get the initial language from the system and save it in a data file.

leptos_fluent! {
    // ...
    initial_language_from_system_to_data_file: true,
}

featsystem | set_language_to_data_file

Set the current language to a data file.

leptos_fluent! {
    // ...
    set_language_to_data_file: true,
}

featsystem | data_file_key: "leptos-fluent"

Key to manage the current language in the data file. It should be unique per application.

leptos_fluent! {
    // ...
    data_file_key: "my-app",
}

provide_meta_context

Provide the macro meta information at runtime as a context. Get it using I18n::meta:

let i18n = leptos_fluent! {
    // ...
    provide_meta_context: true,
};

println!("Macro parameters: {:?}", i18n.meta().unwrap());

initial_language_from_server_function

Get the initial language from a server function.

leptos_fluent! {
    // ...
    initial_language_from_server_function: initial_language_server_function,
}

/// Server function to set the initial language
#[server(InitialLanguage, "/api")]
pub async fn initial_language_server_function(
) -> Result<Option<String>, ServerFnError> {
    // .. replace with your own logic
    Ok(Some("es".to_string()))
}

This parameter type is not like the initial_language_from_* parameters, it takes an identifier to the server function that will be called to get the initial language.

The function must return a Result<Option<String>, ServerFnError>.

set_language_to_server_function

Set the current language to a server function.

leptos_fluent! {
    // ...
    set_language_to_server_function: set_language_server_function,
}

/// Server function to update the current language
#[server(SetLanguage, "/api")]
pub async fn set_language_server_function(
    language: String,
) -> Result<(), ServerFnError> {
    // .. replace with your own logic
    Ok(())
}

This parameter type is not like the set_language_to_* parameters, it takes an identifier to the server function that will be called to update the current language.

The function must return a Result<(), ServerFnError>.

initial_language_from_localstorage_to_server_function

Get the initial language from local storage and set it to a server function.

leptos_fluent! {
    // ...
    initial_language_from_localstorage_to_server_function: set_language_server_function,
}

#[server(SetLanguage, "/api")]
pub async fn set_language_server_function(
    language: String,
) -> Result<(), ServerFnError> {
    // .. replace with your own logic
    Ok(())
}

Get the initial language from a cookie and set it to a server function.

leptos_fluent! {
    // ...
    initial_language_from_cookie_to_server_function: set_language_server_function,
}

#[server(SetLanguage, "/api")]
pub async fn set_language_server_function(
    language: String,
) -> Result<(), ServerFnError> {
    // .. replace with your own logic
    Ok(())
}

initial_language_from_navigator_to_server_function

Get the initial language from navigator.languages and set it to a server function.

leptos_fluent! {
    // ...
    initial_language_from_navigator_to_server_function: set_language_server_function,
}

#[server(SetLanguage, "/api")]
pub async fn set_language_server_function(
    language: String,
) -> Result<(), ServerFnError> {
    // .. replace with your own logic
    Ok(())
}

initial_language_from_url_param_to_server_function

Get the initial language from a URL parameter and set it to a server function.

leptos_fluent! {
    // ...
    initial_language_from_url_param_to_server_function: set_language_server_function,
}

#[server(SetLanguage, "/api")]
pub async fn set_language_server_function(
    language: String,
) -> Result<(), ServerFnError> {
    // .. replace with your own logic
    Ok(())
}

initial_language_from_url_path_to_server_function

Get the initial language from a URL path and set it to a server function.

leptos_fluent! {
    // ...
    initial_language_from_url_path_to_server_function: set_language_server_function,
}

#[server(SetLanguage, "/api")]
pub async fn set_language_server_function(
    language: String,
) -> Result<(), ServerFnError> {
    // .. replace with your own logic
    Ok(())
}

Get the initial language from a server function and set it to a cookie.

leptos_fluent! {
    // ...
    initial_language_from_server_function_to_cookie: true,
}

initial_language_from_server_function_to_localstorage

Get the initial language from a server function and set it to local storage.

leptos_fluent! {
    // ...
    initial_language_from_server_function_to_localstorage: true,
}