Translating Netsuke
Every user-facing message in Netsuke lives in a Project Fluent resource file, not in Rust source. Translating the whole CLI means editing one file per locale — this guide covers the format, the conventions, and the checks that keep translations honest.
How localization works
Fluent handles the hard parts of natural language — plurals, variables, and grammatical agreement — while keeping translation files readable.
Two locales ship today: English (en-US, the source locale) and Spanish (es-ES, the reference translation).
Unsupported locales fall back to English.
Locale precedence
Highest wins:
-
1
CLI flag:
--locale es-ES -
2
Environment:
NETSUKE_LOCALE=es-ES -
3
Config file:
locale = "es-ES" - 4 System default locale
-
5
Fallback to
en-US
What gets translated
- CLI help text, flag descriptions, and validation messages
- Command-line parser errors
- Manifest parse, template, and intermediate representation (IR) diagnostics
- Standard-library and network-policy errors
- Ninja-generation failures
File structure
One directory per locale, one messages.ftl file per directory. That is the whole layout.
# Comment explaining the message context.
message-key = The translated message text.
# Message with a variable.
greeting = Hello, { $name }!
Key rules
Message keys use lowercase with hyphens or dots. Comments start with # and give translators context.
Lines starting with . are attributes (sub-messages); lines starting with - are terms (reusable fragments).
Never rename a key — key names are referenced by the Rust code and must stay identical.
Message keys
Keys use hierarchical dot-notation, organized by domain: domain.subdomain.specific_message.
The corresponding Rust constants live in src/localization/keys.rs in UPPER_SNAKE_CASE
(CLI_FLAG_FILE_HELP maps to cli.flag.file.help).
| Domain | Purpose | Example |
|---|---|---|
| cli.* | CLI help text and validation | cli.flag.file.help |
| clap-error-* | Command-line parser errors | clap-error-missing-argument |
| runner.* | Manifest loading and I/O | runner.manifest.not_found |
| manifest.* | YAML parse and template errors | manifest.yaml.parse |
| ir.* | Intermediate representation errors | ir.rule_not_found |
| ninja_gen.* | Ninja file generation | ninja_gen.missing_action |
| stdlib.* | Standard library helpers | stdlib.fetch.url_invalid |
| host_pattern.* | Network host validation | host_pattern.empty |
| network_policy.* | Network access control | network_policy.host.blocked |
Variables & plurals
Variables are placeholders replaced at runtime. Keep the same names and the same count as the English source — the quality checks compare them. Plural forms use Common Locale Data Repository (CLDR) categories, which differ per language.
# Basic variable substitution.
error-at-path = Error at { $path }: { $details }
# Variables can appear multiple times.
range-error = Value { $value } must be
between { $min } and { $max }.
# en-US/messages.ftl
example.files_processed = { $count ->
[one] Processed { $count } file.
*[other] Processed { $count } files.
}
# es-ES/messages.ftl
example.files_processed = { $count ->
[one] Se procesó { $count } archivo.
*[other] Se procesaron { $count } archivos.
}
CLDR categories by language
- English, Spanish, French —
one,other - Russian —
one,few,many,other - Arabic —
zero,one,two,few,many,other - Japanese —
otheronly
The * marks the required default variant. Explicit numeric matches like [0] handle special cases.
Current limitation
The localization API currently passes all arguments as strings, so CLDR selectors like [one] will not match —
messages resolve to the default *[other] variant. Write correct plural syntax anyway: it becomes active when numeric
argument support lands, with no retranslation needed.
Adding a new locale
Six steps take a locale from nothing to a passing build. French (fr-FR) stands in for your language below.
# 1. Create the locale directory
$ mkdir -p locales/fr-FR
# 2. Copy the English source file
$ cp locales/en-US/messages.ftl locales/fr-FR/messages.ftl
# 3. Translate values, keep keys. For example:
# cli.about = Netsuke compile les manifestes YAML + Jinja
# en plans Ninja.
Step 4: register the locale
Edit src/cli_localization.rs:
const NETSUKE_FR_FR: &str =
include_str!("../locales/fr-FR/messages.ftl");
Then update build_localizer() to handle the new locale tag.
Steps 5–6: build and test
$ cargo build
$ cargo run -- --locale fr-FR --help
The compile-time audit verifies every key is present; missing or orphaned keys fail the build with a detailed error. The second command should print help text in French.
Quality & validation
Translations are validated twice: at compile time by build_l10n_audit.rs, and at test time by
localization unit tests, smoke tests for secondary locales, and fallback tests. Run make test before opening a pull request.
Pre-submission checklist
- All keys from
en-US/messages.ftlare present - No extra (orphaned) keys exist
- Variables match the English source (names and count)
- Plural forms use correct CLDR categories
- Comments are translated or preserved for context
-
cargo buildpasses -
netsuke --locale <tag> --helprenders correctly