diff --git a/Cargo.toml b/Cargo.toml index bb2ba4a..587f0ba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -115,3 +115,7 @@ path = "examples/docs/icons.rs" [[example]] name = "docs_overlays" path = "examples/docs/overlays.rs" + +[[example]] +name = "docs_i18n" +path = "examples/docs/i18n.rs" diff --git a/examples/docs/i18n.rs b/examples/docs/i18n.rs new file mode 100644 index 0000000..eeec755 --- /dev/null +++ b/examples/docs/i18n.rs @@ -0,0 +1,86 @@ +use ely_gpui_component::{ + Assets, + buttons::Button, + i18n::{Direction, I18n}, + primitives::FocusScope, + theme::ActiveTheme, +}; +use gpui::{ + App, AppContext, Context, FocusHandle, IntoElement, ParentElement, Render, Styled, Window, + WindowOptions, div, +}; + +struct Words { + focus: FocusHandle, +} + +impl Render for Words { + fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { + let theme = cx.theme(); + let i18n = cx.global::(); + let locale = i18n.locale(); + let greeting = i18n.text("greeting", &[("name", "Ada")]); + let files = i18n.text("files", &[("count", &locale.number(1234.0, 0))]); + let next = if locale.direction == Direction::Rtl { + "en-US" + } else { + "he-IL" + }; + let row = locale + .direction + .row(div()) + .gap_4() + .child(greeting) + .child(files); + FocusScope::new(&self.focus) + .root() + .size_full() + .flex() + .flex_col() + .gap_4() + .p_8() + .bg(theme.colors.bg) + .text_color(theme.colors.fg) + .child(row) + .child( + Button::new("locale", i18n.text("switch", &[])) + .on_click(move |_, _, cx| I18n::set_locale(next, cx)), + ) + } +} + +fn main() { + gpui_platform::application() + .with_assets(Assets) + .run(|cx: &mut App| { + ely_gpui_component::init(cx); + cx.set_global( + I18n::new("en-US") + .catalog( + "en-US", + &[ + ("greeting", "Hello, {name}"), + ("files", "{count} files"), + ("switch", "עברית"), + ], + ) + .catalog( + "he-IL", + &[ + ("greeting", "שלום, {name}"), + ("files", "{count} קבצים"), + ("switch", "English"), + ], + ), + ); + cx.open_window(WindowOptions::default(), |window, cx| { + cx.new(|cx| { + let focus = cx.focus_handle(); + window.focus(&focus, cx); + Words { focus } + }) + }) + .expect("window failed to open"); + cx.activate(true); + }); +} diff --git a/frontend/src/docs/guides.ts b/frontend/src/docs/guides.ts index 1150337..fdbb2de 100644 --- a/frontend/src/docs/guides.ts +++ b/frontend/src/docs/guides.ts @@ -6,7 +6,7 @@ import { Marked, type Tokens } from "marked"; const at = (relative: string) => fileURLToPath(new URL(relative, import.meta.url)); /** The guides in reading order, by file name. */ -const ORDER = ["introduction", "installation", "getting-started", "theme", "focus", "motion", "icons", "overlays"]; +const ORDER = ["introduction", "installation", "getting-started", "theme", "focus", "motion", "icons", "overlays", "i18n"]; export interface Guide { /** The path segment; the introduction has none. */ diff --git a/frontend/src/docs/i18n.md b/frontend/src/docs/i18n.md new file mode 100644 index 0000000..22ed5be --- /dev/null +++ b/frontend/src/docs/i18n.md @@ -0,0 +1,39 @@ +# Internationalization + +`i18n::I18n` holds an app's messages in catalogs, one per locale, and the locale on show. The app sets it as a GPUI global; views read their words from it. + +## Catalogs and messages + +| Call | Does | +| --- | --- | +| `I18n::new("en-US")` | Starts with the locale on show. | +| `.catalog(tag, &[(key, message)])` | Adds a locale's messages. Every catalog holds the first one's keys, no more and no fewer, or it panics naming them. | +| `cx.set_global(i18n)` | Makes it the app's. | +| `cx.global::().text(key, &[(name, value)])` | The message for `key`, each `{name}` filled. A missing key, argument or catalog panics. | +| `I18n::set_locale(tag, cx)` | Shows another locale in every window, logs the change, and panics on a locale without a catalog. | + +Ely's components take their words from the app: a `Button`'s label, a `Dialog`'s title. Some carry English words of their own, such as a toast's Undo, the Cancel of `ConfirmDialog` and `PromptDialog`, and the Close tip on a fullscreen dialog. + +## Locales + +`LOCALES` lists the seven locales Ely formats for: en-US, en-GB, de-DE, fr-FR, ja-JP, zh-CN and he-IL. `Locale::of(tag)` returns one and panics on a tag outside the list. + +| Item | Holds | +| --- | --- | +| `tag`, `name` | `"de-DE"`, `"Deutsch"`. | +| `number(value, decimals)` | The value with the locale's separators: `number(1234.5, 1)` is `1.234,5` in de-DE. | +| `date(at, zone)` | A numeric date in CLDR's short form. | +| `direction` | `Direction::Ltr` or `Direction::Rtl`. | + +## Right to left + +He-IL reads right to left. `direction.row(element)` lays a flex row from the reading side, and `direction.align(element)` aligns text to that side. + +GPUI sets no paragraph direction, so a line runs the way its first strong letter does. `text` leads a right-to-left message with a right-to-left mark, so its words suit labels, not a field's value. `number` leads a right-to-left minus with a left-to-right mark, as CLDR does. GPUI wraps right-to-left lines in the wrong order, so keep such text on one line, an `Ellipsis`. + +## Two locales + +The example greets in English and Hebrew, formats a count by the locale, mirrors its row, and switches locale with a button. + +```rust example=i18n +```