Elyra
Elyra The coding agent eTerm The terminal that knows where each command ends Huskarl AI colleagues on your own Mac Starf An activity monitor for Apple silicon that never invents a number etrans An SSH and SFTP client for macOS Elyra VM Virtual machines for macOS, Linux and Windows on your Mac Litr A small, native web browser for macOS Notr A notebook 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 Refr Local-first PDF workspace for macOS Elyra Workspace A desktop workspace for coding agents 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.

Typed keys

rata codegen types $t / $tc against your catalog. Import them from the generated bindings instead of @elyra/runtime:

<script>
  import { t, tc } from "./bindings";
</script>

{$t("welcome", { name })}      <!-- ok -->
{$t("welcom", { name })}       <!-- error: not a key -->
{$t("welcome")}                <!-- error: `name` is required -->
{$tc("files", count, { dir })} <!-- `:count` comes from `count` -->

The key list comes from the fallback locale (the canonical catalog), and each key's parameters from the :placeholders in its message (:name, :Name and :NAME all mean name; a time like 10:30 isn't one). The bindings also export Translations, TranslationKey, and typed translate / choice. Re-run rata codegen after editing the fallback file.

In .svelte files the check runs through svelte-check (or your editor's Svelte language server); in .ts files, through tsc.

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-NO → nb → en. 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.

Validation messages

Validator::translator(&t) renders validation errors from validation.<key> and names fields from validation.attributes.<field> — see validation for the full key list.

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