Elyra
Elyra The coding agent eTerm The terminal that knows where each command ends Starf An activity monitor for Apple silicon that never invents a number Litr A small, native web browser for macOS e The native code editor Elyra Grove Native local development environment Askr The real server for Laravel & PHP Elyra Framework Rust + Svelte 5 framework for desktop apps Elyra Conductor Local project conductor Elyra SQL Server MySQL-compatible SQL server in Rust Elyra Félagi Agents as teammates on one board Elyra SQL Client Native desktop SQL workbench Elyra SQL Anywhere Replication-ready SQL engine Elyra Sjá SEO & GEO workspace for macOS Elyra DataGrid Server-driven data grid for Laravel
Release notes
Changelog
Elyra
Translations (i18n)

Translations (i18n)

One set of translation files for both halves of the app — Laravel's __() and trans_choice() in Rust, $t / $tc in Svelte.

Files

One JSON file per locale, nested or flat:

// lang/en.json
{
  "welcome": "Hello, :name!",
  "files": "{0} No files|{1} One file|[2,*] :count files",
  "nav": { "home": "Home", "settings": "Settings" }
}
// lang/nb.json
{
  "welcome": "Hei, :name!",
  "files": "{0} Ingen filer|{1} Én fil|[2,*] :count filer",
  "nav": { "home": "Hjem", "settings": "Innstillinger" }
}

Embed them so they ship inside the binary:

#[derive(rust_embed::RustEmbed)]
#[folder = "lang/"]
struct Lang;

App::new().provider(I18nProvider::embedded::<Lang>().fallback("en"))

(I18nProvider::from_dir("lang") reads them from disk instead — handy while editing them.)

In Rust

let t = ctx.get::<Translator>();
t.get("welcome", &[("name", "Ada")]);   // "Hei, Ada!"
t.get("nav.home", &[]);                 // nested keys use dots
t.choice("files", 3, &[]);              // "3 filer"
t.set_locale("nb");

In Svelte

<script>
  import { t, tc, locale, setLocale } from "@elyra/runtime";
</script>

<h1>{$t("welcome", { name })}</h1>
<p>{$tc("files", count)}</p>
<button on:click={() => setLocale("nb")}>Norsk ({$locale})</button>

$t and $tc are stores: markup re-renders when the catalog loads and when the locale changes — from setLocale, from Rust (Translator::set_locale), or in another window. Outside a component, translate(key, params) and choice(key, count, params) do the same once loadTranslations() has resolved.

Which locale

At startup: an explicit I18nProvider::locale("nb"), else the locale the user last chose (set_locale saves it in the Store), else the OS language (read properly on macOS and Windows — $LANG is unset for an app launched from the Dock), else the fallback.

A lookup tries the exact locale, then its language, then the fallback: nb-NOnben. A key found nowhere returns the key itself, so a missing translation shows up instead of rendering blank.

Placeholders

:name is replaced by the value; :Name capitalises it and :NAME upper-cases it. Longer names are filled first, so :name never eats the start of :namespace.

Plurals

Segments separated by |. A segment may start with an exact count {0} or a range [1,19] / [2,*], and the first matching one wins. Otherwise the locale's plural rule chooses among the segments — the same table Laravel uses: two forms for English, Norwegian and most European languages, 0 counting as singular in French, one form in Japanese or Chinese, three in Russian or Polish.

"apples":  "apple|apples",                              // rule-picked
"files":   "{0} No files|{1} One file|[2,*] :count files" // conditions

Mixing the two needs care, exactly as in Laravel: once no condition matches, the rule counts every segment, so "{0} none|file|files" picks none for 1. Give each segment its condition, or none.

The rules exist on both sides (Rust and the runtime) so $tc stays synchronous; tests pin them to the same cases.

Testing

Swap in a hand-built translator and an in-memory store, so a test neither reads the OS language nor writes the real settings file:

let t = Translator::new("en").add_json("en", r#"{ "welcome": "Hello, :name!" }"#);
TestApp::new(
    App::new()
        .provider(I18nProvider::with_translator(t).locale("en"))
        .swap(Store::fake()),
)

Related