Adding i18n strings
User-facing text — in the server, the CLI and the web UI, error messages included — is never hard-coded; it is a Fluent key. Strings are handled by the wpg-i18n crate, which embeds the Fluent files locales/<lang>/main.ftl with include_str! (no file I/O, so it builds for the browser). Eight languages are supported: Japanese (ja), English (en), Simplified Chinese (zh-CN), Traditional Chinese (zh-TW), Korean (ko), German (de), French (fr) and Spanish (es).
The manual (mdBook), the references generated by wpg docs gen and the descriptions of the bundled templates (meta.descriptions) exist in Japanese and English only. When a template has no description in the UI language, the English one is shown.
Add a key
-
Add one line with the same key to every
locales/*/main.ftl(all eight languages). English is the base and the fallback. Keep placeholders ({ $name }) and backquoted code and ids untranslated.# locales/en/main.ftl send-done = Sent { $packets } packets in { $elapsed } # locales/ja/main.ftl send-done = { $packets } パケットを { $elapsed } で送信しました # locales/de/main.ftl (likewise zh-CN / zh-TW / ko / fr / es) send-done = { $packets } Pakete in { $elapsed } gesendet -
Reference it from code:
twhen there are no arguments,t_argswhen there are.#![allow(unused)] fn main() { use wpg_i18n::{t, t_args, FluentArgs, Locale}; let s = t(locale, "error-not-found"); let mut args = FluentArgs::new(); args.set("packets", 1022); args.set("elapsed", "0.5s"); let s = t_args(locale, "send-done", &args); }
A missing key falls back to English, then to the key name itself. The { $name } placeholder names are a contract with the caller — do not rename them in a translation.
Keep the key sets equal
The .ftl files of all languages must hold the same set of keys. A wpg-i18n unit test checks this and fails if a language lacks a key or has a key English does not have.
cargo test -p wpg-i18n
This is the “i18n parity lint” of the specification (docs/spec/11-risks.md); there is no separate wpg subcommand for it. The same test run also checks that every field-description key of the protocol catalog exists, and that no key is dead (no_unused_keys: a key that no source file mentions must be removed; keys built at run time from a prefix, docgen-* and rtcp-*, are exempt). In CI it runs as part of cargo test --workspace; the GUI end-to-end tests (just e2e, e2e/README.md) additionally switch the language in the browser.
Protocol names and field descriptions
The desc = "key" attached to a field in a protocol definition (wpg-protocols) is a Fluent key. When you add a protocol, add its desc keys to the .ftl files of every language (see “Adding a protocol” in CONTRIBUTING.md and Adding a protocol).
#![allow(unused)]
fn main() {
// crates/wpg-protocols/src/app.rs
op: enum(8) { 1 => boot_request, 2 => boot_reply } = 1, desc = "dhcp-op";
}
# locales/*/main.ftl
dhcp-op = ...
Diagnostic (diag-*) and lint (lint-*) messages work the same way. Their argument names ({ $field } and so on) are a contract with wpg-core’s diag::keys.
Add a language
- Create
locales/<tag>/main.ftland translate every key (<tag>is a BCP 47 language tag, e.g.pt-BR). - Add a variant to
wpg-i18n’sLocaleand add it toLocale::ALL,as_str(),source()andfrom_langid(detection fromAccept-LanguageorLANG); see the doc comment incrates/wpg-i18n/src/lib.rs. - Add the language-name key
ui-lang-<tag>(the language’s name in itself, e.g.ui-lang-pt-br = Português (Brasil)) to every.ftl, and map it inlocale_label_keyincrates/wpg-web/src/i18n.rs. The language menus of the header and of the settings page list it automatically. - Run
cargo test -p wpg-i18n -p wpg-webto confirm no key is missing.
The language is picked from the saved preference, else the browser’s navigator.languages in the web UI; from Accept-Language in the API; and from LC_ALL / LC_MESSAGES / LANG in the CLI. Chinese is split by script and region: zh-Hant, zh-TW, zh-HK and zh-MO give Traditional Chinese, any other zh Simplified Chinese. Unsupported languages get English.