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()),
)