Skip to content

Design Internationalization

MeowLynxSea edited this page Jun 2, 2026 · 8 revisions

Internationalization (i18n)

v0.3 split: yororen-ui keeps the i18n mechanism in core (I18n, Locale, TextDirection, Translate, cx.t(), formatters, embedded en / zh-CN / ar JSON files) but the 9-language hardcoded DefaultPlaceholders table is now considered legacy — the recommended way to set placeholders is to pass them explicitly via .placeholder(...) on each component (or via a user-supplied yororen-ui-locale-* package). See MIGRATION.md.

The internationalization framework provides multi-language support for UI components, including translation system, plural forms handling, number/date formatting, and RTL (right-to-left) layout support.

Core Concepts

Locale

Locale represents a language environment, following the BCP 47 standard:

use yororen_ui::i18n::Locale;

// Create a locale
let locale = Locale::new("zh-CN").unwrap();
let locale = Locale::new("en").unwrap();
let locale = Locale::new("ar").unwrap(); // RTL language

The Locale type automatically parses:

  • Language code: en, zh, ar, fr, etc.
  • Region code: US, CN, GB, etc.
  • Variant: Hans (Simplified), Hant (Traditional), etc.

TextDirection

Text direction enum for controlling RTL/LTR layout mode:

use yororen_ui::i18n::TextDirection;

match locale.text_direction() {
    TextDirection::Ltr => { /* left-to-right layout */ }
    TextDirection::Rtl => { /* right-to-left layout */ }
}

RTL languages are automatically detected: ar (Arabic), he (Hebrew), fa (Persian), ur (Urdu), yi (Yiddish), ps (Pashto).

Quick Start

Initialize I18n

Yororen UI uses embedded JSON translation resources (located in locales/*.json), managed through the I18n global state:

use gpui::App;
use yororen_ui::i18n::{I18n, Locale};

fn init_i18n(cx: &mut App) {
    // Load all embedded locales and set the default language
    cx.set_global(I18n::with_embedded(Locale::new("en").unwrap()));
}

After the app starts, you can switch languages at any time:

// Switch to Arabic (RTL)
cx.set_global(I18n::with_embedded(Locale::new("ar").unwrap()));

// Switch back to Simplified Chinese
cx.set_global(I18n::with_embedded(Locale::new("zh-CN").unwrap()));

Get Translations

Get translations via I18nContext in the App context:

use yororen_ui::i18n::I18nContext;

// Get translated string
let text = cx.t("select.placeholder");

Translation System

Basic Translation

Translation files use JSON format with nested key support:

{
  "select": {
    "placeholder": "Select..."
  },
  "common": {
    "ok": "OK",
    "cancel": "Cancel"
  }
}

Use dot notation to access nested keys:

let text = cx.t("select.placeholder");  // "Select..."
let text = cx.t("common.ok");           // "OK"

Translation with Placeholders

Use {key} as placeholders in translation text:

use std::collections::HashMap;
use yororen_ui::i18n::I18nContext;

let mut args = HashMap::new();
args.insert("name", "World");
args.insert("count", "5");

// Translation file: "greeting": "Hello {name}, you have {count} items"
let text = cx.t_with_args("greeting", &args);
// Result: "Hello World, you have 5 items"

Plural Forms

Translation files support plural forms (following CLDR rules):

{
  "items": {
    "one": "{count} item",
    "other": "{count} items"
  }
}
// Automatically selects the correct plural form based on count
let text = cx.tn!("items", n = 5);  // "5 items"
let text = cx.tn!("items", n = 1);  // "1 item"

Number and Currency Formatting

Number Formatting Basics

NumberFormatter automatically selects appropriate formats based on locale:

use yororen_ui::i18n::{NumberFormatter, Locale};

let formatter = NumberFormatter::new(Locale::new("en").unwrap());
formatter.format_decimal(1234.5);   // "1,234.5"

let fr_formatter = NumberFormatter::new(Locale::new("fr").unwrap());
fr_formatter.format_decimal(1234.5);  // "1 234,5"

let ar_formatter = NumberFormatter::new(Locale::new("ar").unwrap());
ar_formatter.format_decimal(1234.5);  // "١٬٢٣٤٫٥" (Arabic-Indic digits)

Formatting Options

Customize formatting behavior via NumberFormatOptions:

use yororen_ui::i18n::{NumberFormatter, Locale, NumberFormatOptions};

let formatter = NumberFormatter::new(Locale::new("en").unwrap());

let options = NumberFormatOptions {
    min_fraction_digits: Some(2),
    max_fraction_digits: Some(2),
    use_grouping: true,
    currency: None,
    currency_as_suffix: None,
    currency_display: CurrencyDisplay::default(),
};

formatter.format_decimal_with_options(1000.0, &options);  // "1,000.00"

Currency Formatting

Currency formatting supports multiple display styles and localization:

use yororen_ui::i18n::{NumberFormatter, Locale, CurrencyDisplay};

// Symbol display
let formatter = NumberFormatter::new(Locale::new("en").unwrap());
formatter.format_currency(99.99, "USD");   // "$ 99.99"
formatter.format_currency(99.99, "EUR");   // "€ 99.99"
formatter.format_currency(99.99, "JPY");   // "¥99" (JPY has no decimals)

// Code display
let options = NumberFormatOptions {
    currency: Some("USD"),
    currency_display: CurrencyDisplay::Code,
    ..Default::default()
};
formatter.format_with_options(100.0, &options);  // "USD 100.00"

// Localized currency names
let zh_formatter = NumberFormatter::new(Locale::new("zh").unwrap());
zh_formatter.format_currency(100.0, "USD");  // "$ 100.00"
zh_formatter.format_currency(100.0, "EUR");  // "€ 100.00"

let ar_formatter = NumberFormatter::new(Locale::new("ar").unwrap());
ar_formatter.format_currency(100.0, "SAR");  // "١٠٠ ر.س" (Saudi Riyal localized)

Currency Symbol Position

Different languages have different conventions for currency symbol placement:

// Euro is prefixed in English, suffixed in French/German
let en_formatter = NumberFormatter::new(Locale::new("en").unwrap());
en_formatter.format_currency(100.0, "EUR");  // "€ 100.00"

let fr_formatter = NumberFormatter::new(Locale::new("fr").unwrap());
fr_formatter.format_currency(100.0, "EUR");   // "100,00 €"

// Specify position manually
let options = NumberFormatOptions {
    currency: Some("USD"),
    currency_as_suffix: Some(true),  // Force suffix
    ..Default::default()
};
formatter.format_with_options(100.0, &options);  // "100.00 $"

Supported Currencies

Currency Code Symbol Localized Example
USD $ $100.00
EUR €100.00
GBP £ £100.00
JPY ¥ ¥99
CNY ¥ ¥100.00
KRW ₩100
INR ₹100.00
RUB ₽100.00
SAR ر.س / SAR ١٠٠ ر.س

Percentage Formatting

let formatter = NumberFormatter::new(Locale::new("en").unwrap());
formatter.format_percent(0.75);   // "75%"
formatter.format_percent(0.123);  // "12.3%"

Locale-Specific Number Display

Different languages use different digit shapes and separators:

Language Number Example Decimal Thousands
English 1,234.56 . ,
French 1 234,56 , space
German 1.234,56 , .
Arabic ١٬٢٣٤٫٥٦ ٫ ٬

Arabic also automatically converts Latin digits to Arabic-Indic digits:

let ar_formatter = NumberFormatter::new(Locale::new("ar").unwrap());
ar_formatter.format_decimal(1234567.89);
// Result: "١٬٢٣٤٫٥٦٧٫٨٩"

Date and Time Formatting

Basic Formatting

use yororen_ui::i18n::{DateTimeFormatter, Locale};

let formatter = DateTimeFormatter::new(Locale::new("en").unwrap());
let timestamp = 1704067200; // 2024-01-01 00:00:00 UTC

formatter.format_date(timestamp);      // "2024-01-01"
formatter.format_time(timestamp);     // "00:00"
formatter.format_datetime(timestamp); // "2024-01-01 00:00"

Locale-Specific Date Formats

Different languages have different date formats:

let en_formatter = DateTimeFormatter::new(Locale::new("en").unwrap());
en_formatter.format_date(timestamp);  // "2024-01-01"

let zh_formatter = DateTimeFormatter::new(Locale::new("zh").unwrap());
zh_formatter.format_date(timestamp);  // "2024年01月01日"

let ja_formatter = DateTimeFormatter::new(Locale::new("ja").unwrap());
ja_formatter.format_date(timestamp);  // "2024年01月01日"

let de_formatter = DateTimeFormatter::new(Locale::new("de").unwrap());
de_formatter.format_date(timestamp);  // "01.01.2024"

let fr_formatter = DateTimeFormatter::new(Locale::new("fr").unwrap());
fr_formatter.format_date(timestamp);  // "01/01/2024"

Using the Combined Formatter

Formatter combines number and date/time formatting:

use yororen_ui::i18n::{Formatter, Locale};

let formatter = Formatter::new(Locale::new("en").unwrap());

formatter.format_number(1234.5);        // "1,234.5"
formatter.format_currency(99.99, "USD"); // "$ 99.99"
formatter.format_date(timestamp);        // "2024-01-01"

RTL Support

Yororen UI provides complete RTL (right-to-left) layout support for Arabic, Hebrew, and other RTL languages. All components automatically respond to the current text direction.

Reading Text Direction

Components should read the text direction from the theme for consistency:

let direction = cx.theme().text_direction;
let is_rtl = direction.is_rtl();

RTL Layout Helper Functions

The yororen_ui::rtl module provides helper functions to implement RTL layouts:

use yororen_ui::i18n::TextDirection;
use yororen_ui::rtl;

let direction = cx.theme().text_direction;

Text Alignment

let align = rtl::text_align_start(direction); // LTR: Left,  RTL: Right
let align = rtl::text_align_end(direction);   // LTR: Right, RTL: Left

Flex Direction

let flex = rtl::flex_row(direction); // LTR: Row, RTL: RowReverse
let flex = rtl::flex_col(direction); // Column (with ColumnReverse for vertical flip)

Justification

let justify = rtl::justify_start(); // JustifyContent::FlexStart
let justify = rtl::justify_end();   // JustifyContent::FlexEnd

Padding / Margin

rtl::padding_start(&mut style, direction, px(8.));  // LTR: left,  RTL: right
rtl::padding_end(&mut style, direction, px(8.));    // LTR: right, RTL: left
rtl::margin_start(&mut style, direction, px(8.));   // LTR: left,  RTL: right
rtl::margin_end(&mut style, direction, px(8.));     // LTR: right, RTL: left

Absolute Positioning (Inset)

rtl::place_start(&mut style, direction, px(10.)); // LTR: inset.left,  RTL: inset.right
rtl::place_end(&mut style, direction, px(10.));   // LTR: inset.right, RTL: inset.left
rtl::place_start_0(&mut style, direction);        // Set logical start to 0
rtl::place_end_0(&mut style, direction);          // Set logical end to 0

Corner Radius

rtl::corner_radius_start(&mut style, direction, px(4.));     // LTR: left corners,  RTL: right corners
rtl::corner_radius_end(&mut style, direction, px(4.));       // LTR: right corners, RTL: left corners
rtl::corner_radius_start_none(&mut style, direction);        // Remove logical start radius
rtl::corner_radius_end_none(&mut style, direction);          // Remove logical end radius

Icon / Value Flipping

let arrow = rtl::flip_left_right(direction, ArrowDirection::Right, ArrowDirection::Left);

RTL-Aware Components

The following components automatically handle RTL layout:

Text & Display: label, heading, badge, text, tooltip

Buttons & Actions: button, button_group, icon_button, toggle_button, split_button, tag

Inputs: text_input, text_area, password_input, search_input, number_input, file_path_input, keybinding_input, combo_box, select

Lists & Trees: list_item, tree_item, tree_node, virtual_row

Form: form, form_row, inline_error, validation_icon

Selection: checkbox, radio, radio_group, switch, slider, dropdown_menu

Feedback: toast, progress, skeleton, empty_state

Overlays: modal, popover, notification_host

Widgets: titlebar, disclosure, avatar

RTL Layout Principles

When RTL mode is enabled, the UI follows these principles:

  1. Horizontal Layout Mirroring

    • Row flex containers use RowReverse
    • Logical start maps to physical right, end maps to physical left
    • Padding, margin, and insets operate on logical sides
  2. Text Alignment

    • Default alignment follows text_align_start(direction)
    • Label, heading, and badge default to right-aligned in RTL
  3. Icon Flipping

    • Disclosure arrows point left when collapsed in RTL
    • Directional icons should use flip_left_right()
  4. Input & Cursor

    • Text inputs paint from the right edge in RTL
    • Mouse click-to-cursor mapping accounts for RTL origin
  5. Component-Specific Behavior

    • Slider: fill grows from the right, knob positioned from the right
    • Switch: checked knob appears on the left (logical "on" side)
    • Progress: indeterminate animation and fill anchored to right
    • Split button: dropdown menu opens to the left, corners adjusted
    • Avatar: status dot positioned on the left in RTL
    • Titlebar: navigator slider animates from the right edge
    • Notifications: host positioned at top-left in RTL

Modal Actions Row

The modal_actions_row helper requires the text direction to ensure correct button ordering:

use yororen_ui::component::modal_actions_row;
use yororen_ui::theme::ActiveTheme;

.modal_actions_row(
    cx.theme().text_direction,
    [
        button().child("Cancel").into_any_element(),
        button().child("Confirm").into_any_element(),
    ],
)

Detecting RTL at Runtime

use yororen_ui::theme::ActiveTheme;

// Check if currently in RTL mode
let is_rtl = cx.theme().is_rtl();

// Get text direction
let direction = cx.theme().text_direction;

Component Integration

Using the localized() Method

Components with i18n support provide a .localized() method that automatically uses translation placeholders:

use yororen_ui::component::{select, combo_box, file_path_input, keybinding_input};

// Select component
select()
    .id("my-select")
    .options(vec![...])
    .localized()  // Use translation placeholders

// ComboBox component
combo_box()
    .id("my-combobox")
    .options(vec![...])
    .localized()

// FilePathInput
file_path_input()
    .id("my-file")
    .localized()

// KeybindingInput
keybinding_input()
    .id("my-keybinding")
    .localized()

Runtime Language Switching

use yororen_ui::i18n::{Locale, I18nContext};

// Switch language
cx.i18n().set_locale(Locale::new("zh-CN").unwrap());

// Get current language info
let locale = cx.i18n().locale();
let is_rtl = cx.i18n().is_rtl();

Translation Resource Files

File Location (v0.3)

The embedded translation files shipped inside yororen-ui-core cover the three languages the core team maintains:

crates/yororen-ui-core/locales/
├── en.json    # English (default)
├── zh-CN.json # Simplified Chinese
└── ar.json    # Arabic (RTL)

Additional languages can be loaded at runtime via I18n::load_translations(locale, map) (or by depending on a separate yororen-ui-locale-* crate that does this for you). The full list of language codes the runtime understands is SupportedLocale in yororen_ui::i18n.

v0.2 → v0.3 note: Earlier versions embedded nine JSON files (en, zh-CN, zh-TW, ja, ko, fr, de, es, ar, he) inside the single yororen-ui crate. v0.3 narrowed this to three in yororen-ui-core; add more by depending on a locale package or shipping your own JSON.

The legacy 9-language hardcoded placeholder table still lives in yororen-ui-core::i18n::defaults::DefaultPlaceholders for backwards compatibility, but is treated as deprecated. Prefer .placeholder("…") on each component.

Translation File Format

{
  "select": {
    "placeholder": "Select...",
    "no_results": "No results found"
  },
  "combobox": {
    "placeholder": "Select...",
    "search_placeholder": "Search...",
    "no_results": "No results"
  },
  "common": {
    "ok": "OK",
    "cancel": "Cancel",
    "close": "Close",
    "save": "Save",
    "delete": "Delete"
  },
  "validation": {
    "required": "This field is required",
    "invalid_email": "Invalid email address",
    "min_length": "Must be at least {min} characters"
  },
  "items": {
    "one": "{count} item",
    "other": "{count} items"
  }
}

Nested Structure

Multi-level nested structures are supported:

{
  "dialog": {
    "confirm": {
      "title": "Confirm Action",
      "message": "Are you sure you want to proceed?",
      "yes": "Yes, proceed",
      "no": "No, cancel"
    }
  }
}

Usage:

cx.t("dialog.confirm.title");     // "Confirm Action"
cx.t("dialog.confirm.message");   // "Are you sure..."
cx.t("dialog.confirm.yes");       // "Yes, proceed"

Supported Languages

Currently supported languages and their features:

Code Language RTL Number Format Date Format
en English No 1,234.56 2024-01-01
en-US English (US) No 1,234.56 01/01/2024
en-GB English (UK) No 1,234.56 01/01/2024
zh-CN Simplified Chinese No 1,234.56 2024年01月01日
zh-TW Traditional Chinese No 1,234.56 2024年01月01日
ja Japanese No 1234.56 2024年01月01日
ko Korean No 1,234.56 2024-01-01
ar Arabic Yes ١٬٢٣٤٫٥٦ 01/01/2024
he Hebrew Yes 1,234.56 01/01/2024
fr French No 1 234,56 01/01/2024
de German No 1.234,56 01.01.2024
es Spanish No 1.234,56 01/01/2024
ru Russian No 1 234,56 01.01.2024

Related Topics

Clone this wiki locally