i18n
tauri-plugin-i18n-vsk#
Reads the language catalogs from a folder and answers the text of a key in the active language. A Vue composable keeps the interface up to date and switches language without reloading the window.
- Crate:
tauri-plugin-i18n-vsk - JS package:
@vasakgroup/tauri-plugin-i18n - Platform: any (Linux in VasakOS applications)
The crate is published as tauri-plugin-i18n-vsk —the suffix avoids the name
already taken on crates.io—, but the plugin is named i18n: that is where the
permissions (i18n:…) and the commands the interface invokes (plugin:i18n|…)
come from.
Installation#
| |
| |
| |
init takes the initial language (Option<String>); with None it starts in
en. The plugin does not read the session language: the application decides
that. VasakOS applications look at LC_ALL, LC_MESSAGES and LANG, take the
first part (es_AR.UTF-8 → es) and pass es as the initial language, with
en as the only exception.
With init_with_path(language, Some(path)) you point at the translations folder:
| |
Without a path, the plugin looks for it on its own: locales/ and
src-tauri/locales/ under the working directory (only if the binary is not
installed), ../locales and ../../locales relative to the executable,
/usr/share/<binary-name>/locales —the installed-binary case, which is the one
that used to fail— and macOS’s Bundle/Resources/locales folder. It keeps the
first one that exists.
Language catalogs#
One file per language, named after the language (es.yml, en.yml,
pt-BR.json); the language is read from the last dot of the file name, so
es.yml is es. .yml, .yaml, .json and .toml are accepted.
The usual format is a nested catalog, all of it in one language:
| |
And the keys are requested flattened with dots: vistas.inicio.titulo.
The other format spreads the languages inside the same file and is declared with
_version: 2. In that case the file name does not matter:
| |
Keys missing on disk still come from the translations bundled inside the binary:
the plugin’s loader ships an empty bundled_locales.rs by default, and each
application may replace it with its own before compiling. In VasakOS
applications the texts come from disk, and the disk catalog wins key by key, so
a string can be fixed in /usr/share without recompiling.
Permissions#
i18n:default, in the capabilities file:
| |
The default permission enables all five commands. There are also individual
ones: i18n:allow-load-translations, i18n:allow-translate,
i18n:allow-get-locale, i18n:allow-set-locale,
i18n:allow-get-available-locales and their matching deny- counterparts.
An application that translates in JavaScript —the normal use— needs four:
i18n:allow-load-translations, i18n:allow-get-locale,
i18n:allow-set-locale and, only if it calls I18n.getAvailableLocales(),
i18n:allow-get-available-locales. i18n:allow-translate is for those that ask
the backend for the text instead of translating it in the WebView.
JavaScript API#
useI18n()#
The Vue composable. It returns the state and the functions components use:
t(key): string— The text of the key in the active language. If the key is missing it returns the key itself (views.home.title), which is what makes someone add it to the catalog.locale— The active language, reactive.setLocale(language): Promise<void>— Switches the language.availableLocales— The languages with a loaded catalog.isLoaded— Whether the catalogs have arrived.reload(): Promise<void>— Reads the catalogs from disk again.
It can be called from outside a component (main.ts, before mounting), which is
the recommended use.
I18n.getInstance(): I18n#
The class that holds the catalogs and the language, in a single instance per window. It is the only place where that state lives: the composable reflects it, it does not copy it.
load(): Promise<void>#
Loads the catalogs and subscribes to language changes. It can be called as many times as needed: the first call does the work and the others wait for that same one.
reload(): Promise<void>#
Asks for the catalogs again even when they are already loaded. It is useful when the language files changed on disk; it does not subscribe to language changes again.
translate(key): string#
The text of the key, read from the local state —without going to the backend—.
Like t(), it returns the key when it cannot find it.
bind(element, key): void#
Writes the text into a DOM element and keeps it noted so it can be written again when the language changes.
subscribe(listener): () => void#
Notifies when the catalogs or the language change. It returns how to unsubscribe; this is what the composable uses to reflect this state.
destroy(): void#
Cancels the subscription to language changes and forgets the load, so that a
later load() performs it again. Listeners registered with subscribe() are
kept.
I18n.setLocale(language): Promise<void>#
Switches the language and notifies the interface, even before the first load. A language without a catalog leaves the keys raw.
I18n.getLocale(): Promise<string>#
The active language, asked of the backend.
I18n.getAvailableLocales(): Promise<string[]>#
Every language with a catalog.
Rust commands#
load_translations()— All the catalogs, as{ language: { key: text } }.translate(key)— The text of the key in the active language;Noneif missing.set_locale(locale)— Switches the language and emits the notification to the frontend.get_locale()— The active language.get_available_locales()— The languages with a catalog.
Events#
| Event | When it fires | Payload |
|---|---|---|
i18n:locale_changed | The application language changes | The new language (string) |
| |
The event reaches every window: a language change requested in one is seen in
the others. The I18n class already listens for it, so an interface that uses
useI18n() or subscribe() does not need to subscribe on its own.
Example#
Loading the catalogs before mounting the application is the right move, so the first screen does not show raw keys:
| |
| |
Implementation details#
- A single state. The
I18nclass is a singleton and the composable subscribes to it. Loading before mounting leaves the texts in place for every component. - A single load. An application with fifty components using
useI18n()makes one call to the backend and one subscription to the language-change event, not fifty of each. - A single subscription, retryable. If subscribing to language changes fails, it is not marked as done: the next load tries again.
- The last load requested wins, not the last one to answer: a
reload()on top of a slow load does not leave the old catalogs, and a language change in the middle of a load is not undone. - A broken catalog takes nothing down. An unreadable, non-UTF-8 or malformed file is skipped and the rest still loads; the application opens with the keys in view instead of staying without a window.
- Arrays are not stored as text: the key is missing and shows raw, instead of
leaving a hole. A
nullvalue is an empty string on purpose, and numbers and booleans are shown as text. - If no catalog folder is found, the application still opens and the keys show
raw; the reason is written to
stderr, with the paths that were tried. - A binary in
/usr,/optor/snapcounts as installed: it does not look at the paths relative to the working directory, which belongs to whoever launched it and not to the application. Without that guard, an application opened from the menu loaded another application’s catalogs and showed the whole interface raw. - With
RUST_I18N_DEBUG=1the loader prints tocargo’s output which paths and which files it read. - The
set_localecommand emits the notification and the plugin state emits it again, so a single language change fires the event twice. Receiving the same language twice changes nothing.