Localization
Renzora ships a single, process-wide translation table that every crate, the editor bundle, and every dlopen'd plugin read through one function:
renzora::lang::t("menu.file") // → "File", "Fichier", "ファイル", …
The table lives in the renzora contract dylib, so there is exactly one copy
across the plugin boundary. The renzora_lang runtime plugin fills it
from three sources and keeps the active language in sync.
How strings resolve
t(key) resolves in this order and never fails:
- The active language, if it has the key.
- English (
en), the guaranteed-complete fallback. - The key itself (e.g.
"menu.file").
Because of the fallback chain, converting a hardcoded literal to t("…") is
always safe: an untranslated key shows readable English, and a key with no
entry at all shows the key. The UI is never blank.
For runtime values, interpolate {name} placeholders:
// "Saved {file}" → "Saved player.scene"
renzora::lang::t_args("status.saved", &[("file", &name)]);
Where languages come from
- Built-in packs are compiled into the binary from the repo-root
languages/directory, so an exported game is fully localized with no extra files. - External packs — any
languages/*.tomlnext to the executable or in the working directory — are loaded on top and override a built-in of the samecode, key for key. The folder is re-scanned every couple of seconds, so editing a pack updates the running editor live. This is the install path a marketplace language pack will use. - Plugin contributions — see below.
Pack format
A language pack is a TOML file with a [meta] header and a flat [strings]
table of key = "value" pairs:
[meta]
name = "Français" # native name shown in the picker
code = "fr" # BCP-47-ish code: "fr", "pt-BR", "zh", "zh-TW"
author = "Renzora Team"
version = "1.0"
[strings]
"common.ok" = "OK"
"menu.file" = "Fichier"
"menu.edit" = "Édition"
Keys are dotted, grouped by area (common.*, menu.*, inspector.*, one
section per panel). English (languages/en.toml) is the source of truth for
which keys exist.
Adding translations from a plugin
Any renzora_* plugin can localize its own UI from its build(). The
registration API is on the contract crate, so a plugin needs no dependency
on renzora_lang:
impl Plugin for MyPlugin {
fn build(&self, app: &mut App) {
// From an embedded pack (same [meta]/[strings] format):
let _ = renzora::lang::register_pack_str(
include_str!("../languages/de.toml"),
);
// …or inline, for a handful of keys (no TOML file needed):
renzora::lang::register_translations("en", [
("myplugin.title", "My Plugin"),
("myplugin.run", "Run"),
]);
}
}
Registration order doesn't matter — the table accumulates every contribution and
t() resolves against the active language at call time. Later writes to the
same (code, key) win, so a user pack can override your plugin's strings too.
Switching language
renzora::lang::set_active("ja"); // change active language
renzora::lang::active_code(); // "ja"
renzora::lang::available(); // Vec<LocaleMeta> for a picker
Setting an as-yet-unknown code is allowed: install the pack later and it lights
up without another call. On launch the active language defaults to English (or
the RENZORA_LANG environment override).
Reacting to changes
When the active language changes or packs (re)load, the plugin fires a
LanguageChanged message and bumps a global revision counter:
// Event-driven rebuild:
fn relocalize(mut events: MessageReader<renzora::lang::LanguageChanged>) {
for _ in events.read() { /* rebuild cached text */ }
}
// …or gate a reactive panel on the counter:
renzora::lang::revision(); // monotonic; compare to a stored value
Scripting
Scripts get a tr(key) function:
set_text(label, tr("hud.score"))