-
Notifications
You must be signed in to change notification settings - Fork 9
Design Internationalization
v0.3 split:
yororen-uikeeps the i18n mechanism in core (I18n,Locale,TextDirection,Translate,cx.t(), formatters, embeddeden/zh-CN/arJSON files) but the 9-language hardcodedDefaultPlaceholderstable is now considered legacy — the recommended way to set placeholders is to pass them explicitly via.placeholder(...)on each component (or via a user-suppliedyororen-ui-locale-*package). SeeMIGRATION.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.
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 languageThe Locale type automatically parses:
-
Language code:
en,zh,ar,fr, etc. -
Region code:
US,CN,GB, etc. -
Variant:
Hans(Simplified),Hant(Traditional), etc.
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).
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 via I18nContext in the App context:
use yororen_ui::i18n::I18nContext;
// Get translated string
let text = cx.t("select.placeholder");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"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"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"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)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 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)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 $"| 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 | ١٠٠ ر.س |
let formatter = NumberFormatter::new(Locale::new("en").unwrap());
formatter.format_percent(0.75); // "75%"
formatter.format_percent(0.123); // "12.3%"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: "١٬٢٣٤٫٥٦٧٫٨٩"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"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"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"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.
Components should read the text direction from the theme for consistency:
let direction = cx.theme().text_direction;
let is_rtl = direction.is_rtl();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;let align = rtl::text_align_start(direction); // LTR: Left, RTL: Right
let align = rtl::text_align_end(direction); // LTR: Right, RTL: Leftlet flex = rtl::flex_row(direction); // LTR: Row, RTL: RowReverse
let flex = rtl::flex_col(direction); // Column (with ColumnReverse for vertical flip)let justify = rtl::justify_start(); // JustifyContent::FlexStart
let justify = rtl::justify_end(); // JustifyContent::FlexEndrtl::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: leftrtl::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 0rtl::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 radiuslet arrow = rtl::flip_left_right(direction, ArrowDirection::Right, ArrowDirection::Left);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
When RTL mode is enabled, the UI follows these principles:
-
Horizontal Layout Mirroring
- Row flex containers use
RowReverse - Logical
startmaps to physicalright,endmaps to physicalleft - Padding, margin, and insets operate on logical sides
- Row flex containers use
-
Text Alignment
- Default alignment follows
text_align_start(direction) - Label, heading, and badge default to right-aligned in RTL
- Default alignment follows
-
Icon Flipping
- Disclosure arrows point left when collapsed in RTL
- Directional icons should use
flip_left_right()
-
Input & Cursor
- Text inputs paint from the right edge in RTL
- Mouse click-to-cursor mapping accounts for RTL origin
-
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
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(),
],
)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;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()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();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 singleyororen-uicrate. v0.3 narrowed this to three inyororen-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::DefaultPlaceholdersfor backwards compatibility, but is treated as deprecated. Prefer.placeholder("…")on each component.
{
"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"
}
}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"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 |
- Theming - Theme system
- Accessibility - Accessibility
Yororen UI v0.3.0 · repository · Apache-2.0 · This wiki documents Yororen UI v0.3.0.
This wiki documents Yororen UI v0.3.0 — the headless-core, swappable-renderer build.