- Guides
- Translating Netsuke
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.
Thirty-five locale catalogues ship, with English (en-US) as the source locale that defines the key set and renders any message a translation has not yet covered.
Three documents govern a translation: this guide and the upstream translator guide own the Fluent mechanics, locale registry, and fallback policy;
the localization style guide owns voice, tone, and register;
and the localization glossary is the source of truth for terminology in every locale.
Shipped locales by script family
| Script family | Locale catalogues |
|---|---|
| Latin | cs, cy, da, de, en-GB, en-US, es-419, es-ES, fi, fr, gd, hu, id, it, nb, nl, pl, pt-BR, pt-PT, ro, sv, tr, vi |
| Cyrillic | ru, uk |
| Greek | el |
| Right-to-left | ar, fa, he |
| Indic | hi |
| Thai | th |
| CJK | ja, ko, zh-Hans, zh-Hant |
A requested tag resolves to the exact catalogue first, then by a per-language script or region rule (es-MX uses es-419, pt uses pt-PT, zh-TW uses zh-Hant, no uses nb), then to the only catalogue for that language (fr-CA uses fr), and finally to en-US. Variants that differ in substance are never merged.
Locale precedence
Highest wins. Help, usage, and command-line validation errors are rendered before any configuration file is read, so for them step 3 does not apply; diagnostics, progress, and status output consult all five:
-
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.
Style, tone, and terminology
A catalogue that parses and passes the audit can still change the product. Two upstream documents keep every locale sounding like Netsuke: the localization style guide fixes the voice and explains how tone shifts by content type, and the localization glossary records the preferred, allowed, and forbidden form of each term, in English and in every shipped locale. Read both before translating a single message.
Localization style guide
- Voice is invariant: clear, calm, capable, and respectful. Messages state facts, never blame, joke, or use marketing language.
- Tone follows content type: neutral help text, direct validation errors, factual “Failed to …” runner errors, imperative hints, and compact parallel status labels.
- Register: the professional plain register of developer tools in the target locale; one address form per catalogue, never mixed; gender-neutral constructions where the language allows.
- Mechanics: placeables move with the target syntax but keep their exact names; locale punctuation applies outside frozen literals; one condition per sentence.
Localization glossary
- One record per term:
title,preferred,allowed, andforbiddencolumns, with theen-UScatalogue as the authority. - Invariant names: Netsuke,
netsuke-build, Netsukefile, Ninja, Fluent, Jinja, YAML, and DOT are never translated, transliterated, or respelt. - Distinct concepts stay distinct: manifest, target, action, rule, dependency, and order-only dependency each have a preferred form and a list of near-synonyms to avoid.
- Per-locale sections: every shipped locale has its own terminology table and register notes, so reviewers apply the same vocabulary consistently.
Frozen literals
Code, paths, identifiers, option names, command fragments, message keys, Fluent placeables such as { $path }, JSON field names, and the literal policy values
auto, always, never, on, and off pass through every locale untouched — even when they appear inside otherwise-translatable prose.
Automation parses Netsuke's output, so a translation must never alter JSON structure, exit-status conventions, or the command syntax shown for the user to run.
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. Icelandic (is) stands in for your language below. A new tag need not be a new language: a region or script variant such as pt-BR beside pt-PT is registered the same way, because the registry resolves by tag rather than by language.
# 1. Create the locale directory
$ mkdir -p locales/is
# 2. Copy the English source file
$ cp locales/en-US/messages.ftl locales/is/messages.ftl
# 3. Translate values, keep keys, follow the glossary. For example:
# cli.about = Netsuke þýðir YAML + Jinja-lýsingar
# í Ninja-áætlanir.
Step 4: register the locale
src/locale_catalogues.rs is the authoritative registry. Its define_locales! macro both declares the tag and embeds the catalogue, so a tag without a file on disk fails to compile:
define_locales! {
// …
"is" => "../locales/is/messages.ftl",
}
Two lists deliberately duplicate the registry and must be updated too: the locales array under [package.metadata.ortho_config] in Cargo.toml (the build audit fails if the two drift) and EXPECTED_SHIPPED_TAGS in tests/locale_registry_tests.rs, an independent oracle. When the language already ships a catalogue, extend its LANGUAGE_FALLBACKS rule so every region and script still resolves to exactly one variant.
Steps 5–6: build and test
$ cargo build
$ cargo run -- --locale is --help
The compile-time audit verifies every key and every { $variable } is present; missing or orphaned keys, variable mismatches, and metadata drift fail the build with a detailed error.
The second command should print help text in Icelandic.
Quality & validation
Translations are validated twice: at compile time by the build_l10n_audit crate (metadata drift, missing keys, orphaned keys, and variable mismatches), and at test time by
rendering tests for every registered locale, registry tests for each fallback rule, and catalogue tests for CLDR plural categories, right-to-left direction marks, untranslated identifiers, and the rule that a translation is not a copy of the English source. Run make test before opening a pull request.
Right-to-left catalogues
Arabic, Hebrew, and Persian messages that open with a Latin word, a bracket, or a placeable must begin with U+200F RIGHT-TO-LEFT MARK, otherwise that first token flips the whole line in a terminal. The same applies to each variant of a select expression. A catalogue test enforces this, with a short explicit list of all-Latin technical exceptions.
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
- Netsuke identifiers and literal option values are untranslated
- Terminology matches the glossary, including the locale's own section
- One address form throughout, and the voice of the style guide (no humour, idiom, or invented content)
- Right-to-left catalogues carry the direction marks
- Comments are translated or preserved for context
-
cargo buildandmake testpass -
netsuke --locale <tag> --helprenders correctly