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:
- Exact match (e.g.,
en-US) - Language with script (e.g.,
en-Latn) - Base language (e.g.,
en) - 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:
- Longer text (German, French)
- Right-to-left languages (Arabic, Hebrew)
- Different character sets (Japanese, Chinese)
6. Use Attributes for Related Text
button =
.label = Submit
.tooltip = Submit the form
.aria = Submit form button
Summary
In this tutorial, you learned:
- Setup - Enabling i18n and creating translation files
- t! macro - Simple translation with variables
- Fluent syntax - Variables, plurals, selectors, attributes
- Language management - Switching languages at runtime
- Locale configuration - Static, dynamic, fallback
- Multi-window - Sharing i18n across windows
- Best practices - Semantic keys, plurals, testing
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:
- Getting Started
- Core Elements
- Layout System
- Styling
- Hooks
- State Management
- Event Handling
- Built-in Components
- Animation
- Theming
- Accessibility
- Routing
- Window Configuration
- Internationalization
Happy coding!