Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

  1. 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
    
  2. Reference it from code: t when there are no arguments, t_args when 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

  1. Create locales/<tag>/main.ftl and translate every key (<tag> is a BCP 47 language tag, e.g. pt-BR).
  2. Add a variant to wpg-i18n’s Locale and add it to Locale::ALL, as_str(), source() and from_langid (detection from Accept-Language or LANG); see the doc comment in crates/wpg-i18n/src/lib.rs.
  3. 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 in locale_label_key in crates/wpg-web/src/i18n.rs. The language menus of the header and of the settings page list it automatically.
  4. Run cargo test -p wpg-i18n -p wpg-web to 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.