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. 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
Latincs, 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
Cyrillicru, uk
Greekel
Right-to-leftar, fa, he
Indichi
Thaith
CJKja, 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. 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-419/messages.ftl
es-ES/messages.ftl
(one directory per shipped tag)
is/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.

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.

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.

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. 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.

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