Part 14 of 14 28 Jan 2026 By Raj Patil 30 min read

Part 14: Internationalization - Making Your App Global

Part 14 of the Freya Rust GUI series. Learn to internationalize your application with Fluent, supporting multiple languages, dynamic translations, and locale management.

Intermediate #rust #freya #gui #i18n #internationalization #localization #fluent #tutorial
Building Native GUIs with Rust & Freya 14 / 14

Internationalization

Internationalization (i18n) makes your application accessible to users around the world by supporting multiple languages. Freya uses the Fluent localization system, developed by Mozilla.

[!NOTE] i18n vs l10n

  • Internationalization (i18n) - Designing your app to support multiple languages
  • Localization (l10n) - Actually translating content to specific languages

Setup

Enable the Feature

Add the i18n feature to your Cargo.toml:

[dependencies]
freya = { version = "0.3", features = ["i18n"] }

Create Translation Files

Create Fluent (.ftl) files for each language:

translations/en-US.ftl

hello_world = Hello, World!
welcome = Welcome, {$user}!
item_count = {$count} items
settings = Settings
save = Save
cancel = Cancel

translations/es-ES.ftl

hello_world = Hola, Mundo!
welcome = Bienvenido, {$user}!
item_count = {$count} artículos
settings = Configuración
save = Guardar
cancel = Cancelar

translations/ja-JP.ftl

hello_world = こんにちは世界!
welcome = ようこそ、{$user}さん!
item_count = {$count}件のアイテム
settings = 設定
save = 保存
cancel = キャンセル

Initialize i18n

Set up i18n at the root of your application:

use freya::prelude::*;
use freya::i18n::prelude::*;

fn app() -> impl IntoElement {
    use_init_i18n(|| {
        I18nConfig::new(langid!("en-US"))
            .with_locale(Locale::new_static(
                langid!("en-US"),
                include_str!("../translations/en-US.ftl"),
            ))
            .with_locale(Locale::new_static(
                langid!("es-ES"),
                include_str!("../translations/es-ES.ftl"),
            ))
            .with_locale(Locale::new_static(
                langid!("ja-JP"),
                include_str!("../translations/ja-JP.ftl"),
            ))
            .with_fallback(langid!("en-US"))
    });

    MainContent
}

Using Translations

t! Macro

Use the t! macro to translate strings:

// Simple translation
let text = t!("hello_world");  // "Hello, World!" or "Hola, Mundo!"

// With arguments
let greeting = t!("welcome", user: "Freya");  // "Welcome, Freya!"

// With multiple arguments
let message = t!("greeting", name: "Alice", time: "morning");

In Components

#[derive(PartialEq)]
struct WelcomeScreen;

impl Component for WelcomeScreen {
    fn render(&self) -> impl IntoElement {
        rect()
            .direction(Direction::Vertical)
            .gap(16.0)
            .child(
                label()
                    .text(t!("hello_world"))
                    .font_size(32.0)
            )
            .child(
                Button::new()
                    .child(t!("settings"))
            )
    }
}

Dynamic Translations

#[derive(PartialEq)]
struct UserProfile {
    username: String,
}

impl Component for UserProfile {
    fn render(&self) -> impl IntoElement {
        label().text(t!("welcome", user: self.username.clone()))
    }
}

Language Management

Language Selector

Let users change the language:

fn language_selector() -> impl IntoElement {
    let mut i18n = I18n::get();
    let current_lang = i18n.language();

    rect()
        .direction(Direction::Vertical)
        .gap(8.0)
        .child(
            label()
                .text(format!("Current: {}", current_lang))
                .font_weight(FontWeight::Bold)
        )
        .child(
            rect()
                .direction(Direction::Horizontal)
                .gap(8.0)
                .child(
                    Button::new()
                        .on_press(move |_| i18n.set_language(langid!("en-US")))
                        .child("English")
                )
                .child(
                    Button::new()
                        .on_press(move |_| i18n.set_language(langid!("es-ES")))
                        .child("Español")
                )
                .child(
                    Button::new()
                        .on_press(move |_| i18n.set_language(langid!("ja-JP")))
                        .child("日本語")
                )
        )
}

Language Methods

let mut i18n = I18n::get();

// Get current language
let current: LanguageIdentifier = i18n.language();

// Set language
i18n.set_language(langid!("es-ES"));

// Try to set (returns Result)
if let Err(e) = i18n.try_set_language(langid!("fr-FR")) {
    println!("Failed to set language: {}", e);
}

Fluent Syntax

Simple Messages

hello = Hello, World!
title = My Application

Variables

Insert dynamic values:

greeting = Hello, {$name}!
message = You have {$count} new messages
price = Price: {$amount} {$currency}

Plurals

Handle singular/plural forms:

items = {$count ->
    [one] One item
    *[other] {$count} items
}

messages = {$count ->
    [0] No messages
    [one] One message
    *[other] {$count} messages
}

Usage:

let text = t!("items", count: 1);   // "One item"
let text = t!("items", count: 5);   // "5 items"

Selectors

Choose between options:

role = {$gender ->
    [male] He
    [female] She
    *[other] They
} completed the task.

Attributes

Add metadata to messages:

button =
    .label = Submit
    .tooltip = Click to submit your form
    .aria = Submit form

login =
    .title = Sign In
    .description = Enter your credentials

Access attributes with dot notation:

let label = t!("button.label");
let tooltip = t!("button.tooltip");

Locale Configuration

Static Locales

Embed translations at compile time:

.with_locale(Locale::new_static(
    langid!("en-US"),
    include_str!("../translations/en-US.ftl"),
))

Dynamic Locales

Load translations at runtime:

.with_locale(Locale::new_dynamic(
    langid!("es-ES"),
    "./translations/es-ES.ftl",
))

Auto-Discovery

Automatically discover locales from a directory:

I18nConfig::new(langid!("en-US"))
    .with_auto_locales(PathBuf::from("./translations"))

Fallback Chain

Freya searches for translations in this order:

  1. Exact match (e.g., en-US)
  2. Language with script (e.g., en-Latn)
  3. Base language (e.g., en)
  4. Fallback language
I18nConfig::new(langid!("fr-FR"))
    .with_fallback(langid!("en-US"))

Programmatic Translation

Using I18n Directly

let i18n = I18n::get();

// Simple translation
let text = i18n.translate("hello_world");

// With error handling
match i18n.try_translate("hello_world") {
    Ok(text) => println!("{}", text),
    Err(e) => eprintln!("Translation error: {}", e),
}

// With arguments
let mut args = FluentArgs::new();
args.set("name", "Freya");
let text = i18n.translate_with_args("greeting", Some(&args));

Multi-Window Applications

Share i18n state across windows:

Create Global i18n

let i18n = I18n::create_global(config)?;

Share with Windows

WindowConfig::new_app(Window1 { i18n: i18n.clone() })
WindowConfig::new_app(Window2 { i18n: i18n.clone() })

Use in Window

impl App for Window1 {
    fn render(&self) -> impl IntoElement {
        use_share_i18n(|| self.i18n.clone());
        // Now t!() and I18n::get() work in this window
        label().text(t!("hello_world"))
    }
}

Complete Example

use freya::prelude::*;
use freya::i18n::prelude::*;

fn main() {
    launch(
        LaunchConfig::new()
            .with_window(
                WindowConfig::new_app(|| {
                    use_init_i18n(|| {
                        I18nConfig::new(langid!("en-US"))
                            .with_locale(Locale::new_static(
                                langid!("en-US"),
                                include_str!("../translations/en-US.ftl"),
                            ))
                            .with_locale(Locale::new_static(
                                langid!("es-ES"),
                                include_str!("../translations/es-ES.ftl"),
                            ))
                            .with_fallback(langid!("en-US"))
                    });
                    App
                })
                .with_title(t!("app_title"))
            )
    );
}

#[derive(PartialEq)]
struct App;

impl Component for App {
    fn render(&self) -> impl IntoElement {
        let mut i18n = I18n::get();
        let username = "Alice";

        rect()
            .expanded()
            .padding(24.0)
            .direction(Direction::Vertical)
            .gap(24.0)
            .child(
                // Header
                rect()
                    .direction(Direction::Horizontal)
                    .main_align_space_between()
                    .child(
                        label()
                            .text(t!("app_title"))
                            .font_size(24.0)
                            .font_weight(FontWeight::Bold)
                    )
                    .child(
                        // Language selector
                        rect()
                            .direction(Direction::Horizontal)
                            .gap(8.0)
                            .child(
                                Button::new()
                                    .on_press(move |_| i18n.set_language(langid!("en-US")))
                                    .child("EN")
                            )
                            .child(
                                Button::new()
                                    .on_press(move |_| i18n.set_language(langid!("es-ES")))
                                    .child("ES")
                            )
                    )
            )
            .child(
                // Welcome message
                rect()
                    .padding(16.0)
                    .background(Color::from_rgb(240, 240, 240))
                    .corner_radius(8.0)
                    .child(label().text(t!("welcome", user: username)))
            )
            .child(
                // Actions
                rect()
                    .direction(Direction::Horizontal)
                    .gap(8.0)
                    .child(Button::new().filled().child(t!("save")))
                    .child(Button::new().child(t!("cancel")))
            )
    }
}

Best Practices

1. Use Semantic Keys

Use descriptive keys, not English text:

# Good
login_button = Sign In
error_network = Network connection failed

# Bad
sign_in_button = Sign In
network_error_msg = Network connection failed

2. Never Concatenate Strings

Don’t build sentences from parts:

# Bad
welcome_start = Welcome,
welcome_end = !

# Good
welcome = Welcome, {$user}!

3. Provide Fallbacks

Always have a fallback language:

.with_fallback(langid!("en-US"))

4. Handle Plurals Correctly

Different languages have different plural rules:

items = {$count ->
    [one] One item
    *[other] {$count} items
}

5. Test All Languages

Ensure your UI works with:

button =
    .label = Submit
    .tooltip = Submit the form
    .aria = Submit form button

Summary

In this tutorial, you learned:


Previous: Part 13: Window Configuration ←

Congratulations! You’ve completed the Freya tutorial series. You now have all the knowledge you need to build professional, accessible, internationalized desktop applications with Rust and Freya!

Series Complete

All Parts:

  1. Getting Started
  2. Core Elements
  3. Layout System
  4. Styling
  5. Hooks
  6. State Management
  7. Event Handling
  8. Built-in Components
  9. Animation
  10. Theming
  11. Accessibility
  12. Routing
  13. Window Configuration
  14. Internationalization

Happy coding!