Localization

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. 1 CLI flag: --locale es-ES
  2. 2 Environment: NETSUKE_LOCALE=es-ES
  3. 3 Config file: locale = "es-ES"
  4. 4 System default locale
  5. 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.

locales/
en-US/messages.ftl (source locale)
es-ES/messages.ftl (reference translation)
fr-FR/messages.ftl (your new locale)
FTL format in four lines
# 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.

Variables
# 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 }.
Plural forms (English vs Spanish)
# 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 — other only

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.

Steps 1–3: create, copy, translate
# 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.ftl are 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 build passes
  • netsuke --locale <tag> --help renders correctly