Skip to content

Externalize Fluent localization; Windows test context refactor + audit#260

Merged
leynos merged 28 commits intomainfrom
terragon/externalize-user-strings-ayqyhg
Jan 28, 2026
Merged

Externalize Fluent localization; Windows test context refactor + audit#260
leynos merged 28 commits intomainfrom
terragon/externalize-user-strings-ayqyhg

Conversation

@leynos
Copy link
Copy Markdown
Owner

@leynos leynos commented Jan 17, 2026

Summary

  • Refactored Windows test setup to centralize setup/teardown via a WindowsSetupContext and align test scaffolding with the broader localization changes.
  • Implemented Fluent-based localization infrastructure across the codebase, including a centralized key registry and a Localizer wrapper to render localized messages.
  • Added a compile-time localization audit (build_l10n_audit.rs) and wired it into build.rs to fail CI if declared keys are missing from Fluent bundles.
  • Wired CLI/runtime messages through the Localizer with English as the fallback and es-ES as the example locale; added tests/docs to cover localization rendering and audits.
  • New localization assets: locales/en-US/messages.ftl and locales/es-ES/messages.ftl.
  • Localization plumbing and key registry placed under src/localization (mod.rs and keys.rs).
  • Updated build tooling and tests to support localization auditing and rendering.
  • Documentation and roadmap updated to reflect localization work and the Windows test context refactor.
  • Revision note: 2026-01-23: Post-completion fixes including Windows test setup refactor and localisation audit integration.

📎 Task references preserved in existing plan and task notes.

Changes

  • New files:
    • src/localization/mod.rs
    • src/localization/keys.rs
    • build_l10n_audit.rs
    • tests/localization_tests.rs (and related test fixtures)
  • Localization assets:
    • locales/en-US/messages.ftl
    • locales/es-ES/messages.ftl
  • Runtime localization plumbing:
    • src/localization/mod.rs (Localizer wrapper, LocalizedMessage, helper plumbing)
    • src/localization/keys.rs (centralized Fluent key constants)
  • CLI/runtime integration:
    • Updated CLI plumbing to route help/error messages through the Localizer
    • Integrated Localizer into main error paths and diagnostic rendering
  • Build tooling:
    • build.rs: expanded to audit localization keys against Fluent bundles and fail CI on missing keys
  • Test support:
    • test_support/src/localizer.rs and related test helpers
  • Documentation:
    • docs/roadmap.md updated to mark 3.7.1 done
    • docs/netsuke-design.md and docs/users-guide.md updated to reflect localisation behavior
  • Tests:
    • Localization tests added to cover English/Spanish rendering and key audits

Goals and outcomes

  • All user-facing text (CLI help, status, warnings, errors) sourced from Fluent bundles under locales/ and rendered via ortho_config::Localizer.
  • Compile-time localization audit fails the build if a referenced Fluent key is missing.
  • es-ES translations exist for the new keys; en-US remains the fallback.
  • Tests validate localization rendering for English and Spanish outputs; docs describe localisation behavior.

Plan of work (Stage A–D)

  • Stage A: Inventory and key taxonomy (completed)
  • Stage B: Localization infrastructure and key audit (completed)
  • Stage C: Externalize user-facing strings (completed)
  • Stage D: Tests, docs, and finalisation (completed)

Validation and acceptance criteria

  • All user-facing strings come from Fluent bundles under locales/.
  • A compile-time audit fails CI if a referenced Fluent key is missing.
  • Spanish (es-ES) translations exist for every new key used by the CLI.
  • Localization tests cover English/Spanish rendering; docs describe localization behavior.
  • Roadmap item 3.7.1 is marked as done.
  • Fmt, lint, and test gates pass.

Interfaces and dependencies

  • Localization module: src/localization/mod.rs (and keys.rs)
  • Compile-time audit: build.rs and build_l10n_audit.rs validate keys against bundles
  • Fluent resources: locales/en-US/messages.ftl and locales/es-ES/messages.ftl with new keys
  • CLI output: main/CLI paths render diagnostics using the localizer; errors fall back to English if missing
  • Tests: localisation tests added; test_support/helpers support localizer usage

Revision note

  • 2026-01-23: Post-completion fixes: updated ManifestError::Parse doc example to use MietteDiagnostic, refactored windows_command_setup with WindowsSetupContext struct, fixed redaction.rs doc tests (marked as ignore). Quality gates pass.

References

…o Fluent

Add a detailed ExecPlan document (3-7-1-externalize-user-facing-strings.md) outlining the roadmap for migrating all user-facing strings to Fluent localization bundles. The plan covers scope, constraints, risks, progress steps, and validation criteria for implementing Fluent key audits, Spanish translations, localization infrastructure, testing, and documentation updates for roadmap item 3.7.1.

Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jan 17, 2026

Summary by CodeRabbit

  • New Features

    • Added comprehensive Spanish (es-ES) localization for all user-facing messages and runtime diagnostics
    • Enhanced error messages with improved context and clarity for manifest parsing, builds, and network operations
    • Extended localisation coverage to include runtime diagnostics, not just CLI help text
  • Documentation

    • Updated design documentation and user guide to reflect localisation capabilities

✏️ Tip: You can customize this high-level summary in your review settings.

Walkthrough

Add a localisation subsystem and export Fluent keys; wire Arc‑shared localisers into CLI and runtime; replace many hard‑coded messages with LocalizedMessage; attach localized messages to multiple error/diagnostic variants; add a build‑time Fluent key auditor; and update tests and test‑support to use localisation fixtures and expectations.

Changes

Cohort / File(s) Summary
Localization core
src/localization/mod.rs, src/localization/keys.rs, src/lib.rs
Add public localisation module and API: global Arc<dyn Localizer> with guards, LocalizedMessage type with rendering, and a large set of generated Fluent key constants plus ALL_KEYS; export pub mod localization.
Build‑time audit
build.rs, build_l10n_audit.rs, Cargo.toml
Add build script audit that reads declared keys vs .ftl bundles (locales/en-US, locales/es-ES), add cargo:rerun-if-changed directives for keys and locales, and add tracing dependency.
Fluent bundles
locales/en-US/messages.ftl, locales/es-ES/messages.ftl
Add comprehensive English and Spanish message sets covering CLI, manifest, stdlib, network, which, time, etc.
CLI & parsing
src/cli/mod.rs, src/cli/parsing.rs, src/cli_l10n.rs
Introduce LocalizedValueParser and parsing helpers; centralise flag/key usage; attach localized validators and helpers; update localization lookups and fallbacks.
Main initialisation
src/main.rs, src/cli/*
Initialise initial and runtime localisers as Arc, set global localizer at runtime, and reconfigure CLI validation parsers with runtime localizer.
IR, codegen & ninja
src/ir/graph.rs, src/ir/from_manifest.rs, src/ir/cmd_interpolate.rs, src/ninja_gen.rs
Add LocalizedMessage fields to many IrGenError and NinjaGenError variants; convert some variants to struct form; update Display/source wiring — public error variant shapes changed.
Manifest diagnostics & YAML
src/manifest/diagnostics/mod.rs, src/manifest/diagnostics/yaml.rs, src/manifest/*, src/manifest/hints.rs
Convert diagnostic labels/messages to LocalizedMessage, use keys for YAML hints and manifest errors, update map helpers and tests; ManifestError::Parse now carries a LocalizedMessage.
Runner & I/O
src/runner/mod.rs, src/runner/error.rs, src/runner/process/file_io.rs
Localise runner contexts and I/O messages; extend RunnerError::ManifestNotFound to carry message and help LocalizedMessage fields (public enum payload changed).
Host pattern & validation
src/host_pattern.rs
Convert HostPatternError variants to carry LocalizedMessage; construct messages via keys and update tests for wording changes.
Stdlib IO helpers & wiring
src/stdlib/io_helpers.rs, src/stdlib/mod.rs, src/stdlib/**
Add io_helpers module that maps IO errors into localized diagnostics; replace numerous hard‑coded strings across stdlib (command, path, which, network, time, collections); update internal helper signatures (e.g., io_to_error).
Network policy
src/stdlib/network/policy/mod.rs, src/stdlib/network/policy/tests.rs
Add LocalizedMessage fields to policy error/violation variants; adjust constructors and tests (use pattern matches that tolerate extra fields).
Tests & fixtures
tests/**, tests/localization_tests.rs, tests/manifest_glob_tests.rs
Add test localiser lock and helpers; add localization fixtures and en guard; update many tests to construct expected messages from keys or use more tolerant pattern checks; reorganise std‑filter tests into modular suites.
Test support
test_support/src/localizer.rs, test_support/src/manifest.rs, test_support/src/lib.rs, test_support/Cargo.toml
Add test helpers for a process‑wide localization lock and setting an English localiser; add/adjust ensure_manifest_exists helper and rework public re‑exports; remove legacy manifest helpers in some locations.
Docs & plans
docs/*, docs/execplans/*, docs/roadmap.md, docs/users-guide.md
Mark i18n task done; document localisation flow and build‑time audit; update CLI path references to src/cli/mod.rs; add exec plans and roadmap updates.

Sequence Diagram(s)

sequenceDiagram
    rect rgba(120,150,200,0.5)
    participant Build as Build Script
    participant Keys as Keys Parser
    participant FTL_en as FTL Parser (en‑US)
    participant FTL_es as FTL Parser (es‑ES)
    participant Auditor as Auditor
    participant Cargo as Cargo
    end

    Build->>Keys: Read src/localization/keys.rs
    Keys->>Keys: Extract define_keys! body and parse string literals
    Keys-->>Build: Declared keys set

    Build->>FTL_en: Read locales/en‑US/messages.ftl
    FTL_en->>FTL_en: Parse message identifiers
    FTL_en-->>Build: en‑US keys set

    Build->>FTL_es: Read locales/es‑ES/messages.ftl
    FTL_es->>FTL_es: Parse message identifiers
    FTL_es-->>Build: es‑ES keys set

    Build->>Auditor: Compare declared vs bundles
    Auditor->>Auditor: Compute missing / orphaned keys
    alt Issues found
        Auditor->>Cargo: Emit formatted error and fail build
    else
        Auditor->>Cargo: Continue build
    end
Loading
sequenceDiagram
    rect rgba(180,200,150,0.5)
    participant Main as main.rs
    participant CLI as CLI parser
    participant Localizer as Global Localizer
    participant Config as Config Merge
    participant RuntimeLocalizer as Runtime Localizer Builder
    end

    Main->>Localizer: Set initial Arc<Localizer>
    Main->>CLI: Configure validation parsers with Arc localizer

    CLI->>Localizer: Request validation messages during parse
    Localizer-->>CLI: Localized values / error messages
    CLI-->>Main: Parsed CLI and config

    Main->>Config: Merge configs and determine runtime locale
    Main->>RuntimeLocalizer: build_localizer(runtime_locale)
    RuntimeLocalizer-->>Main: Arc localiser for runtime
    Main->>Localizer: set_localizer(runtime Arc)
    Main->>CLI: Reconfigure validation parsers with runtime Arc

    AnyError->>Localizer: Render LocalizedMessage via Display
    Localizer-->>AnyError: Formatted text for display
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Poem

✨ Audit the keys and let them sing,
Localiser Arc makes messages ring.
Tests hold the lock, bundles align,
Errors now speak in every line.
Netsuke sings in English and Spanish rhyme.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed Title accurately summarises the main changes: externalizing Fluent localization, Windows test refactoring, and adding a compile-time audit.
Description check ✅ Passed Description comprehensively details the localisation infrastructure, compile-time audit, CLI integration, new files, tests, and documentation updates related to the changeset.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch terragon/externalize-user-strings-ayqyhg

Comment @coderabbitai help to get the list of available commands and usage tips.

@sourcery-ai
Copy link
Copy Markdown
Contributor

sourcery-ai Bot commented Jan 17, 2026

Reviewer's Guide

Implements a centralized Fluent-based localization framework (with English fallback and Spanish translations) across CLI, runtime, stdlib, and diagnostics, adds a build-time key audit to enforce key coverage, and updates tests and docs to assert localized behavior and mark roadmap item 3.7.1 as complete.

Sequence diagram for CLI locale selection and localised error rendering

sequenceDiagram
    actor User
    participant Shell
    participant Main as main_rs
    participant CliLoc as cli_localization
    participant LocMod as localization_module
    participant Cli as cli_rs
    participant Runner as runner_mod

    User->>Shell: run `netsuke --locale es-ES build`
    Shell->>Main: start process with args

    Main->>CliLoc: build_localizer(locale_hint_or_env)
    CliLoc-->>Main: FluentLocalizer(en_US+es_ES)

    Main->>LocMod: set_localizer(Arc<FluentLocalizer>)
    Main->>Cli: set_validation_localizer(Arc<FluentLocalizer>)

    Main->>Cli: parse_with_localizer_from(args, localizer)
    Cli-->>Main: Cli struct, ArgMatches

    Main->>Runner: generate_ninja(&Cli)
    Runner->>LocMod: message(RUNNER_MANIFEST_NOT_FOUND)
    LocMod->>LocMod: build LocalizedMessage with args(manifest_name,directory)
    LocMod-->>Runner: formatted Spanish string
    Runner-->>Main: Err(RunnerError::ManifestNotFound { message, help })

    Main->>LocMod: localizer() for rendering Diagnostic
    Main-->>Shell: print Spanish error and hint to stderr
Loading

Class diagram for localisation core types and error integration

classDiagram
    class Localizer~trait~ {
        <<external>>
        +message(key: &str, args: Option~LocalizationArgs~, fallback: &str) String
    }

    class LocalizationArgs {
        <<external>>
    }

    class localization_module {
        <<module>>
        +set_localizer(localizer: Arc~Localizer~) void
        +localizer() Arc~Localizer~
        +set_localizer_for_tests(localizer: Arc~Localizer~) LocalizerGuard
        +message(key: &'static str) LocalizedMessage
    }

    class LocalizerGuard {
        -previous: Arc~Localizer~
        +drop() void
    }

    class LocalizedMessage {
        +key: &'static str
        +args: Vec~(&'static str, String)~
        +new(key: &'static str) LocalizedMessage
        +with_arg(name: &'static str, value: ToString) LocalizedMessage
        +to_string() String
    }

    class localization_keys {
        <<module>>
        +CLI_USAGE: &str
        +RUNNER_MANIFEST_NOT_FOUND: &str
        +MANIFEST_PARSE: &str
        +IR_RULE_NOT_FOUND: &str
        +NINJA_GEN_MISSING_ACTION: &str
        +HOST_PATTERN_EMPTY: &str
        +NETWORK_POLICY_SCHEME_EMPTY: &str
        +STDLIB_FETCH_URL_INVALID: &str
        +STDLIB_PATH_IO_NOT_FOUND: &str
        +STDLIB_WHICH_NOT_FOUND: &str
        +ALL_KEYS: &'static [&'static str]
    }

    class RunnerError {
        <<enum>>
        ManifestNotFound
        -manifest_name: String
        -directory: String
        -path: PathBuf
        -message: LocalizedMessage
        -help: LocalizedMessage
    }

    class ManifestError {
        <<enum>>
        Parse
        -source: Box~Diagnostic~
        -message: LocalizedMessage
    }

    class IrGenError {
        <<enum>>
        +RuleNotFound
        +MultipleRules
        +EmptyRule
        +DuplicateOutput
        +CircularDependency
        +ActionSerialisation
        +InvalidCommand
        -message: LocalizedMessage
    }

    class NinjaGenError {
        <<enum>>
        +MissingAction
        +Format
        -message: LocalizedMessage
    }

    class NetworkPolicyConfigError {
        <<enum>>
        +EmptyScheme
        +InvalidScheme
        +EmptyAllowlist
        -message: LocalizedMessage
    }

    class NetworkPolicyViolation {
        <<enum>>
        +SchemeNotAllowed
        +MissingHost
        +HostNotAllowlisted
        +HostBlocked
        -message: LocalizedMessage
    }

    class HostPatternError {
        <<enum>>
        +Empty
        +ContainsScheme
        +ContainsSlash
        +MissingSuffix
        +EmptyLabel
        +InvalidCharacters
        +InvalidLabelEdge
        +LabelTooLong
        +HostTooLong
        -message: LocalizedMessage
    }

    localization_module --> LocalizedMessage : builds
    localization_module --> Localizer : uses
    localization_module --> localization_keys : keys_*
    LocalizerGuard --> localization_module : restores
    LocalizedMessage --> LocalizationArgs : builds args

    RunnerError --> LocalizedMessage : message,help
    ManifestError --> LocalizedMessage : message
    IrGenError --> LocalizedMessage : message
    NinjaGenError --> LocalizedMessage : message
    NetworkPolicyConfigError --> LocalizedMessage : message
    NetworkPolicyViolation --> LocalizedMessage : message
    HostPatternError --> LocalizedMessage : message

    class cli_module {
        <<module>>
        +set_validation_localizer(localizer: Arc~Localizer~) void
        -validation_message(key: &str, args: Option~LocalizationArgs~, fallback: &str) String
    }

    cli_module --> Localizer : uses
    cli_module --> localization_keys : uses
Loading

Flow diagram for build-time Fluent key audit

flowchart TD
    A_Start[Start build.rs] --> B_ReadKeys
    B_ReadKeys[extract_key_constants
from src/localization/keys.rs] --> C_ReadEn
    C_ReadEn[extract_ftl_keys
from locales/en-US/messages.ftl] --> D_ReadEs
    D_ReadEs[extract_ftl_keys
from locales/es-ES/messages.ftl] --> E_Compare

    E_Compare[Compute differences
between declared and en-US/es-ES keys] --> F_CheckMissing

    F_CheckMissing{Any missing
keys?} -->|No| G_Success[Audit passes
continue build]
    F_CheckMissing -->|Yes| H_Error[Emit error message
with missing keys per locale
and fail build]

    H_Error --> I_End[Build script returns Err]
    G_Success --> I_End
Loading

File-Level Changes

Change Details Files
Centralize localization infrastructure and Fluent key definitions, and wire a global Localizer used by CLI, runtime, and tests.
  • Introduce src/localization/mod.rs with a global Localizer, LocalizedMessage helper, and test guard APIs for overriding the active localizer.
  • Add src/localization/keys.rs as the single source of truth for Fluent message keys and expose ALL_KEYS for auditing.
  • Update main.rs to construct a Fluent-backed Localizer once, install it as the global localizer, and share it with CLI validation paths.
  • Add dedicated localization tests to verify Spanish and English fallback behavior via the global Localizer.
src/localization/mod.rs
src/localization/keys.rs
src/main.rs
tests/localization_tests.rs
src/lib.rs
Add compile-time localization key audit and integrate Fluent keys into the build process.
  • Extend build.rs to parse src/localization/keys.rs and locales/*/messages.ftl, extract keys, and fail the build when any declared keys are missing from en-US or es-ES bundles.
  • Register build script dependencies on the keys file and both .ftl files via cargo:rerun-if-changed directives.
  • Exercise CLI, localization, and host pattern symbols in build.rs to keep shared modules linked when compiling the build script.
build.rs
Expand and align Fluent resources for English and Spanish, covering CLI validation, runner, manifest, IR, Ninja generation, stdlib, and path/network diagnostics.
  • Add many new message IDs for CLI validation errors, runner I/O contexts, manifest diagnostics, IR/Ninja errors, host pattern and network policy messages, stdlib command/path/fetch/time/which/collections diagnostics, and stdlib registration.
  • Provide parallel Spanish translations for all new keys, ensuring variable placeholders match the English bundles.
  • Adjust tests and feature files that asserted on raw English strings to instead assert on localized wording or key phrases (often capitalized or rephrased).
locales/en-US/messages.ftl
locales/es-ES/messages.ftl
tests/features/stdlib.feature
tests/features/missing_manifest.feature
tests/features/cli.feature
tests/features/manifest_subcommand.feature
tests/yaml_error_tests.rs
tests/std_filter_tests/command_filters.rs
tests/std_filter_tests/network_functions.rs
tests/command_escaping_tests.rs
tests/logging_stderr_tests.rs
tests/manifest_glob_tests.rs
Route CLI validation and help text through Fluent keys, including argument help, subcommand descriptions, and config validation errors.
  • Introduce a static VALIDATION_LOCALIZER with setter and helper to localize CLI validation error messages and allow tests to override it.
  • Refactor parse_jobs, parse_scheme, parse_locale, and cli_overrides_from_matches to use Fluent keys (via validation_message and LocalizationArgs) instead of hard-coded English strings.
  • Refactor CLI help localization logic in cli_l10n.rs to use symbolic key constants and helper functions for flags and subcommand about/long_about fields, replacing stringly-typed key construction.
src/cli.rs
src/cli_l10n.rs
Localize runner errors, I/O paths, manifest loading, and stdlib configuration errors using LocalizedMessage and Fluent keys.
  • Change RunnerError::ManifestNotFound to carry LocalizedMessage message and help fields, and build them via Fluent keys when manifest discovery fails.
  • Refactor manifest path resolution, workspace resolution, and manifest file reading to use localized context strings for UTF-8 problems, workspace root resolution, and read failures.
  • Convert StdlibConfig validation and builder methods (cache path, which cache, limits, skip dirs) to use Fluent messages for all failure reasons and adjust tests to assert new messages.
  • Localize runner file I/O helpers for Ninja temp files, file creation, stdout writing, and path derivation, updating error contexts accordingly.
src/runner/mod.rs
src/runner/error.rs
src/runner/process/file_io.rs
src/manifest/mod.rs
src/manifest/workspace.rs
src/stdlib/config.rs
src/stdlib/network/tests.rs
src/stdlib/network/mod.rs
Localize stdlib command, path, hash, time, collections, and which diagnostics, and update their associated tests.
  • Replace literal error strings in stdlib command error.rs, filters.rs, quote.rs, context.rs, and config.rs with calls to localization::message plus appropriate arguments, including output limit and timeout messages.
  • Introduce localized labels and action keys for stdlib path I/O helpers, fs_utils, path_utils, hash_utils, and io_helpers, and update tests to assert against key-based messaging instead of hard-coded text.
  • Update stdlib collections, time, and which modules to emit localized diagnostics for flatten/group_by, timedelta overflow, offset parsing, PATH/working-directory issues, and which-level argument errors.
  • Adjust various tests (unit, BDD, and step definitions) to assert on new localized phrases or to derive expected messages via the localization module rather than raw literals.
src/stdlib/command/error.rs
src/stdlib/command/filters.rs
src/stdlib/command/quote.rs
src/stdlib/command/context.rs
src/stdlib/command/config.rs
src/stdlib/path/io_helpers.rs
src/stdlib/path/fs_utils.rs
src/stdlib/path/path_utils.rs
src/stdlib/path/hash_utils.rs
src/stdlib/path/filters.rs
src/stdlib/collections.rs
src/stdlib/time/mod.rs
src/stdlib/which/error.rs
src/stdlib/which/env.rs
src/stdlib/which/options.rs
src/stdlib/which/mod.rs
src/stdlib/which/lookup/mod.rs
src/stdlib/which/lookup/tests.rs
src/stdlib/which/lookup/workspace/posix.rs
src/stdlib/which/lookup/workspace/windows.rs
src/stdlib/network/tests.rs
src/stdlib/register.rs
Localize manifest diagnostics (YAML and structural), glob errors, Jinja macros, and foreach/when expansion logic.
  • Change ManifestError::Parse, DataDiagnostic, and YAML mapping to carry LocalizedMessage fields and use Fluent keys for parse/structure errors, YAML labels, and hints (tabs, tokens, mapping issues).
  • Localize manifest glob error creation for unmatched braces, invalid patterns, and IO errors, and adjust glob-related tests to assert against generalized or localized fragments.
  • Refactor manifest expand.rs and Jinja macro modules to use localized messages for foreach/when validation, vars shape, expression parse/eval errors, macro signature/sequence issues, macro template loading/init, missing macros, and macro caller type mismatches.
  • Update related tests to assert new messages or use localization helpers to compute expected text.
src/manifest/diagnostics/mod.rs
src/manifest/diagnostics/yaml.rs
src/manifest/glob/errors.rs
src/manifest/glob/tests.rs
src/manifest/expand.rs
src/manifest/jinja_macros/mod.rs
src/manifest/jinja_macros/cache.rs
src/manifest/tests/macros.rs
src/manifest/tests/workspace.rs
Localize IR generation and Ninja graph errors and adjust tests to reflect the additional message fields.
  • Refactor IrGenError variants (rule-not-found, multiple-rules, empty-rule, duplicate-output, circular-dependency, action-serialisation, invalid-command) to store LocalizedMessage alongside structured data and use those messages for Display.
  • Update IR-from-manifest logic to construct localized IrGenError values using Fluent keys and arguments for target names, rule sets, outputs, cycles, and action serialisation details.
  • Adjust IR-related tests to ignore the new message field where not relevant, or to assert on localized error phrasing where previously checking raw strings.
src/ir/graph.rs
src/ir/from_manifest.rs
src/ir/cmd_interpolate.rs
tests/ir_from_manifest_tests.rs
tests/command_escaping_tests.rs
Localize host pattern validation and network policy configuration/violation errors.
  • Augment HostPatternError variants to carry LocalizedMessage fields and construct them via Fluent keys in normalise_host_pattern and ValidationContext::validate_label, covering empty patterns, schemes, slashes, suffixes, empty labels, invalid characters, label edges/length, and overall host length.
  • Extend NetworkPolicyConfigError and NetworkPolicyViolation enums to carry LocalizedMessage, and build them using Fluent messages for empty/invalid schemes, empty allowlists, missing hosts, blocked hosts, and not-allowlisted hosts.
  • Update host pattern and network policy tests to match on the new enum shapes while focusing on the structured fields, and adjust feature tests to assert against localized wording (for example, 'blocked by policy', 'not on the allowlist').
src/host_pattern.rs
src/stdlib/network/policy/mod.rs
src/stdlib/network/policy/tests.rs
Document localisation behavior and mark roadmap item 3.7.1 as complete.
  • Create docs/execplans/3-7-1-externalize-user-facing-strings.md describing scope, risks, plan, and outcomes for localisation work.
  • Update docs/netsuke-design.md to explain Fluent-backed localisation, the global localiser, and the compile-time key audit.
  • Extend docs/users-guide.md to describe how locale selection affects both CLI copy and runtime diagnostics and to mention Spanish as the bundled example locale.
  • Mark roadmap item 3.7.1 as done in docs/roadmap.md and fix minor formatting/word-wrap issues in the 3.6.1 ExecPlan.
docs/execplans/3-7-1-externalize-user-facing-strings.md
docs/netsuke-design.md
docs/users-guide.md
docs/roadmap.md
docs/execplans/3-6-1-ensure-default-subcommand-builds-manifest-defaults.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

…ndles

- Extract all user-facing strings into Fluent `.ftl` catalogs for English and Spanish.
- Introduce `src/localization/keys.rs` to centralize Fluent message keys as source of truth.
- Implement a compile-time audit in `build.rs` that validates presence of Fluent keys across catalogs, failing CI on missing keys.
- Update numerous modules (`cli`, `runner`, `manifest`, `stdlib`, etc.) to use localized messages instead of hardcoded strings.
- Add support in CLI for localized validation messages and help texts with snake_case keys.
- Integrate localization with error types to display translated diagnostics messages and hints.
- Introduce helper types and APIs in `src/localization/mod.rs` for global localizer management and rendering.
- Add Spanish translations alongside English, used as reference language.

This change enables full localization support, ensures message key consistency, and improves user-facing error and help output internationalization.

Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.com>
@leynos leynos changed the title Plan 3.7.1: Externalize user-facing strings Plan 3.7.1: Externalize user-facing strings via Fluent Jan 17, 2026
codescene-delta-analysis[bot]

This comment was marked as outdated.

@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Jan 18, 2026

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix:

build.rs

Comment on file

    let mut keys = BTreeSet::new();
    for line in source.lines() {
        let trimmed = line.trim_start();
        if trimmed.is_empty() || trimmed.starts_with('#') || trimmed.starts_with('.') {

❌ New issue: Complex Conditional
extract_ftl_keys has 1 complex conditionals with 2 branches, threshold = 2

@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Jan 18, 2026

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix:

src/cli_l10n.rs

Comment on lines +74 to +80

        if let Some(about_key) = subcommand_about_key(&name) {
            if let Some(about) = updated.get_about().map(ToString::to_string) {
                let message = localizer.message(about_key, None, &about);
                updated = updated.about(message);
            } else if let Some(message) = localizer.lookup(about_key, None) {
                updated = updated.about(message);
            }

❌ New issue: Bumpy Road Ahead
localize_subcommands has 2 blocks with nested conditional logic. Any nesting of 2 or deeper is considered. Threshold is 2 blocks per function

@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Jan 18, 2026

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix:

src/stdlib/path/fs_utils.rs

Comment on lines +73 to +79

        .map_err(|err| {
            io_to_error(
                path,
                &localization::message(keys::STDLIB_PATH_ACTION_STAT),
                err,
            )
        })

❌ New issue: Code Duplication
The module contains 3 functions with similar structure: file_size,open_file,read_utf8

@coderabbitai

This comment was marked as resolved.

@coderabbitai

This comment was marked as resolved.

@coderabbitai

This comment was marked as resolved.

…dir helper

- Introduced with_parent_dir helper to encapsulate opening parent directory and error mapping.
- Refactored file_size, read_utf8, and open_file functions to use the new helper.
- In cli_l10n, extracted localize_field function to reduce duplication when localizing command fields.
- In build.rs, extracted should_skip_ftl_line function to clean up conditionals in extract_ftl_keys.

Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.com>
@leynos leynos changed the title Plan 3.7.1: Externalize user-facing strings via Fluent 3.7.1: Externalize user-facing strings via Fluent Jan 18, 2026
Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.com>
@leynos leynos changed the title 3.7.1: Externalize user-facing strings via Fluent 3.7.1: Externalize user-facing strings via Fluent (DONE) Jan 18, 2026
…or clarity

Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.com>
@leynos leynos changed the title 3.7.1: Externalize user-facing strings via Fluent (DONE) 3.7.1: Externalize strings via Fluent; add localisation audit Jan 18, 2026
Changed the type of the action_key parameter in with_parent_dir function from &str to &'static str for improved clarity and potential lifetime benefits.

Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.com>
@leynos leynos changed the title 3.7.1: Externalize strings via Fluent; add localisation audit 3.7.1: Externalize strings with Fluent; localisation audit complete Jan 18, 2026
…positives

Add module-level suppressions of clippy and unused_assignments lints due to false positives from miette/thiserror macros that vary across Rust versions. This workaround prevents lint failures until upstream fixes are available.

Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.com>
@leynos leynos marked this pull request as ready for review January 18, 2026 16:32
sourcery-ai[bot]

This comment was marked as resolved.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 26

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (12)
tests/std_filter_tests/command_filters.rs (1)

1-12: Add a module-level doc comment and decompose this file.

This file lacks the required //! doc comment explaining its purpose. Additionally, at 729 lines, it exceeds the 400-line limit.

Split the tests into separate submodules by concern (e.g., shell_filter_tests.rs, grep_filter_tests.rs, windows_filter_tests.rs) and add a module doc to each. As per coding guidelines, large modules must be decomposed.

src/stdlib/config.rs (2)

427-463: Replace hard-coded limit errors with localization keys.

Avoid brittle English literals by deriving expected messages from keys.

♻️ Proposed refactor
-    #[case::output(CommandLimitCase {
-        builder: StdlibConfig::with_command_max_output_bytes,
-        accessor: |cfg: &StdlibConfig| cfg.command_max_output_bytes,
-        default_value: DEFAULT_COMMAND_MAX_OUTPUT_BYTES,
-        updated: 2_048,
-        zero_err: "Command output capture limit must be positive.",
-    })]
+    #[case::output(CommandLimitCase {
+        builder: StdlibConfig::with_command_max_output_bytes,
+        accessor: |cfg: &StdlibConfig| cfg.command_max_output_bytes,
+        default_value: DEFAULT_COMMAND_MAX_OUTPUT_BYTES,
+        updated: 2_048,
+        zero_err_key: keys::STDLIB_COMMAND_OUTPUT_LIMIT_POSITIVE,
+    })]
@@
-    #[case::stream(CommandLimitCase {
-        builder: StdlibConfig::with_command_max_stream_bytes,
-        accessor: |cfg: &StdlibConfig| cfg.command_max_stream_bytes,
-        default_value: DEFAULT_COMMAND_MAX_STREAM_BYTES,
-        updated: 65_536,
-        zero_err: "Command stream limit must be positive.",
-    })]
+    #[case::stream(CommandLimitCase {
+        builder: StdlibConfig::with_command_max_stream_bytes,
+        accessor: |cfg: &StdlibConfig| cfg.command_max_stream_bytes,
+        default_value: DEFAULT_COMMAND_MAX_STREAM_BYTES,
+        updated: 65_536,
+        zero_err_key: keys::STDLIB_COMMAND_STREAM_LIMIT_POSITIVE,
+    })]
@@
-        let err = (case.builder)(base_config, 0).expect_err("zero-byte limits must be rejected");
-        assert_eq!(err.to_string(), case.zero_err);
+        let err = (case.builder)(base_config, 0).expect_err("zero-byte limits must be rejected");
+        let expected = localization::message(case.zero_err_key).to_string();
+        assert_eq!(err.to_string(), expected);
@@
-    zero_err: &'static str,
+    zero_err_key: &'static str,

492-509: Drive remaining test expectations from localization keys.

Remove hard-coded strings in which-cache and skip-dir tests by building expected messages from keys.

♻️ Proposed refactor
-        assert_eq!(err.to_string(), "Which cache capacity must be positive.");
+        let expected =
+            localization::message(keys::STDLIB_WHICH_CACHE_CAPACITY_POSITIVE).to_string();
+        assert_eq!(err.to_string(), expected);
@@
-    #[case(vec![""], "Skip directory entries must not be empty.")]
-    #[case(vec!["."], "Skip directory entries must not contain '..'.")]
-    #[case(vec![".."], "Skip directory entries must not contain '..'.")]
-    #[case(vec!["dir/name"], "Skip directory entries must not contain path separators.")]
-    #[case(vec!["dir\\name"], "Skip directory entries must not contain path separators.")]
+    #[case(vec![""], keys::STDLIB_SKIP_DIR_EMPTY)]
+    #[case(vec!["."], keys::STDLIB_SKIP_DIR_NAVIGATION)]
+    #[case(vec![".."], keys::STDLIB_SKIP_DIR_NAVIGATION)]
+    #[case(vec!["dir/name"], keys::STDLIB_SKIP_DIR_SEPARATOR)]
+    #[case(vec!["dir\\name"], keys::STDLIB_SKIP_DIR_SEPARATOR)]
     fn workspace_skip_dirs_validate_inputs(
         base_config: StdlibConfig,
         #[case] entries: Vec<&str>,
-        #[case] message: &str,
+        #[case] message_key: &'static str,
     ) {
         let err = base_config
             .with_workspace_skip_dirs(entries)
             .expect_err("invalid skip entries should error");
-        assert_eq!(err.to_string(), message);
+        let expected = localization::message(message_key).to_string();
+        assert_eq!(err.to_string(), expected);
     }
src/cli.rs (1)

1-4: File exceeds 400 line limit.

At 437 lines, this file exceeds the 400 line guideline. Decompose by extracting the validation localizer infrastructure (lines 33-64) and/or the parsing functions (parse_jobs, parse_scheme, parse_locale) into a separate module. As per coding guidelines, large modules must be decomposed.

src/ir/graph.rs (7)

70-89: Update doc examples to include the message field.

The doc example constructs IrGenError::EmptyRule without the newly required message field. This will cause cargo test --doc to fail.

🐛 Proposed fix
 /// fn describe(err: IrGenError) -> String {
 ///     match err {
-///         IrGenError::EmptyRule { target_name } => {
+///         IrGenError::EmptyRule { target_name, .. } => {
 ///             format!("{target_name} missing rule")
 ///         },
 ///         other => other.to_string(),
 ///     }
 /// }
 ///
+/// use netsuke::localization;
 /// assert_eq!(
 ///     describe(IrGenError::EmptyRule {
 ///         target_name: "app".into(),
+///         message: localization::message("ir-empty-rule"),
 ///     }),
 ///     "app missing rule"
 /// );

94-117: Update doc example to include the message field.

The construction at lines 99-102 omits the required message field. Add the field or use a helper to construct the error.

🐛 Proposed fix
+/// use netsuke::localization;
 /// let err = IrGenError::RuleNotFound {
 ///     target_name: "app".into(),
 ///     rule_name: "compile".into(),
+///     message: localization::message("ir-rule-not-found"),
 /// };

119-144: Update doc example to include the message field.

The construction at lines 125-128 omits the required message field.

🐛 Proposed fix
+/// use netsuke::localization;
 /// let err = IrGenError::MultipleRules {
 ///     target_name: "lib".into(),
 ///     rules: vec!["c".into(), "cpp".into()],
+///     message: localization::message("ir-multiple-rules"),
 /// };

146-164: Update doc example to include the message field and fix assertion.

The construction omits the required message field. Additionally, the assertion at line 154-156 expects a hardcoded string, but the display now uses the localized message.

🐛 Proposed fix
+/// use netsuke::localization;
-/// let err = IrGenError::EmptyRule { target_name: "docs".into() };
-/// assert_eq!(
-///     err.to_string(),
-///     "No rules specified for target docs"
-/// );
+/// let msg = localization::message("ir-empty-rule").with_arg("target", "docs");
+/// let err = IrGenError::EmptyRule {
+///     target_name: "docs".into(),
+///     message: msg.clone(),
+/// };
+/// assert_eq!(err.to_string(), msg.to_string());

166-188: Update doc example to include the message field.

The construction at lines 172-174 omits the required message field, and the pattern match at line 175 should use .. to ignore additional fields.

🐛 Proposed fix
+/// use netsuke::localization;
 /// let err = IrGenError::DuplicateOutput {
 ///     outputs: vec!["obj.o".into()],
+///     message: localization::message("ir-duplicate-outputs"),
 /// };
-/// if let IrGenError::DuplicateOutput { outputs } = err {
+/// if let IrGenError::DuplicateOutput { outputs, .. } = err {

190-216: Update doc example to include the message field.

The construction at lines 197-200 omits the required message field.

🐛 Proposed fix
+/// use netsuke::localization;
 /// let err = IrGenError::CircularDependency {
 ///     cycle: vec![Utf8PathBuf::from("a"), Utf8PathBuf::from("a")],
 ///     missing_dependencies: Vec::new(),
+///     message: localization::message("ir-circular-dependency"),
 /// };

218-260: Update doc examples to match struct variant signatures.

The ActionSerialisation example at line 225 uses tuple syntax but the variant is now a struct with source and message fields. The InvalidCommand example omits the message field and the assertion expects a hardcoded string.

🐛 Proposed fix for ActionSerialisation
+/// use netsuke::localization;
 /// let source = serde_json::Error::custom("invalid action");
-/// let err = IrGenError::ActionSerialisation(source);
+/// let err = IrGenError::ActionSerialisation {
+///     source,
+///     message: localization::message("ir-action-serialisation"),
+/// };
 /// assert!(err.to_string().contains("invalid action"));
🐛 Proposed fix for InvalidCommand
+/// use netsuke::localization;
+/// let msg = localization::message("ir-invalid-command")
+///     .with_arg("snippet", "echo $in");
 /// let err = IrGenError::InvalidCommand {
 ///     command: "echo $in".into(),
 ///     snippet: "echo $in".into(),
+///     message: msg.clone(),
 /// };
-/// assert_eq!(
-///     err.to_string(),
-///     "command is not a valid shell command: echo $in"
-/// );
+/// assert_eq!(err.to_string(), msg.to_string());
src/manifest/diagnostics/mod.rs (1)

126-135: Update doc example to include the message field.

The doc example constructs ManifestError::Parse without the newly required message field. This will cause cargo test --doc to fail.

🐛 Proposed fix
+/// use netsuke::localization;
-/// let err = ManifestError::Parse { source: Box::new(miette!("bad manifest")) };
-/// assert_eq!(format!("{err}"), "manifest parse error");
+/// let msg = localization::message("manifest-parse");
+/// let err = ManifestError::Parse {
+///     source: Box::new(miette!("bad manifest")),
+///     message: msg.clone(),
+/// };
+/// assert_eq!(format!("{err}"), msg.to_string());
🤖 Fix all issues with AI agents
In `@build.rs`:
- Around line 104-125: Add a doc comment above the extract_ftl_keys function
that succinctly explains what FTL structures it expects and which constructs it
intentionally skips: mention it reads a Fluent .ftl file, ignores
comment/metadata lines (via should_skip_ftl_line), skips attribute lines and
entries without '=' or with empty IDs, treats lines starting with '-' as
terms/invalid keys, and returns a sorted set of message IDs or an error if none
found; reference the function name extract_ftl_keys and helper
should_skip_ftl_line in the comment so future readers understand the parsing
assumptions and edge cases.
- Around line 81-98: Add a brief explanatory comment above the
extract_key_constants function describing the expected keys.rs format: lines
must include the pattern => "key" (i.e., a map entry where the RHS is a quoted
key) because the parser uses trimmed.split_once("=> \"") and
rest.split_once('"') to extract keys; mention that other formats or additional
whitespace/escaping may break extraction and that the function returns an error
if no keys found.

In `@docs/execplans/3-7-1-externalize-user-facing-strings.md`:
- Around line 92-96: Update the document text to standardise spelling to
en-GB-oxendict forms by replacing all occurrences of "localisation" and
"localiser" (and their capitalised variants) with "localization" and "localizer"
respectively; ensure the decision line references ortho_config::Localizer and
the Fluent resources under "locales/" use the new spelling, and scan the
surrounding prose to update any related phrases so all mentions consistently use
"localization"/"localizer".
- Around line 108-110: Update the two outcome bullets so they correctly state
that English is the reference locale and Spanish is used only as an example
locale: change the first bullet to say "Fluent keys now cover CLI copy,
diagnostics, and stdlib errors with English as the reference translation and
Spanish as an example locale (with English fallback)," and update the second
bullet to explicitly note that "The compile-time Fluent key audit prevents
missing-key regressions in CI by validating against the English reference."
Target the two outcome lines that start with "Fluent keys now cover..." and "The
compile-time Fluent key audit..." in the document.
- Around line 13-15: Update the opening list in the document so it uses the
Oxford comma: change "help, status, warnings, errors" to "help, status,
warnings, and errors" and scan the rest of the file (including the sentence
starting "Netsuke should render all user-facing text") to ensure all
comma-separated lists consistently include the Oxford comma (e.g., before the
final conjunction).

In `@docs/roadmap.md`:
- Around line 231-233: The roadmap shows a contradictory status: the parent item
"3.7.1. Externalize user-facing strings into Fluent `.ftl` bundles." is checked
but its subtask "Implement compile-time audit that fails CI on missing message
keys." is unchecked; update docs to reflect shipped state by marking the subtask
complete if the compile-time audit has been delivered, or revert the parent
checkbox on "3.7.1." to unchecked if the audit hasn’t shipped, and ensure
adjacent item "3.7.2. Implement locale resolution." remains unchanged.

In `@docs/users-guide.md`:
- Line 543: Update the heading text "Configuration and Localisation" to use
Oxford English "-ization" spelling as "Configuration and Localization" and
replace any other occurrences of the word "Localisation" in this document with
"Localization" to conform to en-GB-oxendict; look for the exact heading string
"Configuration and Localisation" and the token "Localisation" to locate and
change them.
- Around line 559-563: Change the British spelling "localised" to Oxford English
"localized" in the CLI docs string that describes --locale <LOCALE> and
NETSUKE_LOCALE (e.g., the sentence "Use `--locale <LOCALE>` or `NETSUKE_LOCALE`
to select localised CLI copy and error messages."). Update any other occurrences
in the same paragraph (including "user-facing runtime diagnostics" sentence) so
all instances use "localized" / "localized" consistently.

In `@locales/en-US/messages.ftl`:
- Line 205: Remove the two leading spaces from the message value for the key
stdlib.command.closed_input_early so it reads "Input closed early while writing
to the command." (locate the string for stdlib.command.closed_input_early and
trim the leading whitespace; do not change surrounding lines or spacing for
other concatenated messages.)

In `@locales/es-ES/messages.ftl`:
- Around line 205-212: The translated message values for the following
keys—stdlib.command.closed_input_early, stdlib.command.broken_pipe,
stdlib.command.terminated_by_signal, stdlib.command.exited_with_status,
stdlib.command.output_limit_exceeded, stdlib.command.timeout,
stdlib.command.exit_status_suffix, and stdlib.command.signal_suffix—have
unintended leading spaces; edit the values to remove the leading whitespace at
the start of each string so user-facing output does not include a prefixed space
while preserving placeholders (e.g., { $location }, { $status }, { $mode }, {
$stream }, { $limit }, { $seconds }) and the intended spacing around
parentheses.

In `@src/cli.rs`:
- Line 56: Add a brief inline comment above the const assertion to explain that
the line "const _: fn(Arc<dyn Localizer>) = set_validation_localizer;" is a
compile-time signature check: it ensures the function set_validation_localizer
has the exact type fn(Arc<dyn Localizer>) and will fail to compile if the
signature changes; mention both the const trick and the expected signature
(Arc<dyn Localizer>) so future readers understand its intent.

In `@src/localization/mod.rs`:
- Around line 64-65: Add a one-line comment immediately above the two const
assertions explaining they are compile-time signature checks that assert the
functions set_localizer and set_localizer_for_tests have the expected types
(fn(Arc<dyn Localizer>) and fn(Arc<dyn Localizer>) -> LocalizerGuard
respectively), so future readers understand these lines are intentional
type-level tests rather than unused code. Reference the symbols set_localizer
and set_localizer_for_tests in the comment for clarity.

In `@src/manifest/diagnostics/mod.rs`:
- Around line 8-18: Keep the module-level suppression block as-is but preserve
and clarify the existing comment and FIXME: retain the rationale explaining the
clippy circularity and the version-dependent thiserror/miette behavior, keep the
FIXME marker, and include a note that once upstream is fixed the module-level
#![allow(clippy::allow_attributes, clippy::allow_attributes_without_reason,
unused_assignments)] should be removed and replaced by item-level expectations
(#[expect(..., reason = "...")]) on the two Error derives referenced at lines
136 and 151; do not narrow or remove the current module-level allows now because
scoping them to items would re-trigger the clippy lints.

In `@src/manifest/tests/macros.rs`:
- Around line 47-49: Replace the hard-coded message strings in the #[case]
attributes for the test cases and use the correct localization keys that include
the SIGNATURE_ infix: change the second element of the cases to
"MANIFEST_MACRO_SIGNATURE_MISSING_PARAMS" for the "(name)" case and to
"MANIFEST_MACRO_SIGNATURE_MISSING_IDENTIFIER" for the "   " and "greet" (or
other identifier-missing) cases; locate and update the attributes in the test
macro cases (the #[case(...)] lines shown) so they reference those exact key
names instead of the incorrect keys.

In `@src/ninja_gen.rs`:
- Around line 20-35: Update the doc comments for the error variants
MissingAction and Format to use en-GB-oxendict spelling by replacing “Localised”
with “Localized” where it describes the message field; locate the MissingAction
and Format enum variants and their docs that mention LocalizedMessage and change
the comment text accordingly so it reads “Localized error message.”

In `@src/runner/error.rs`:
- Around line 37-41: Update the doc comments for the struct fields referencing
LocalizedMessage: change the word "Localised" to "Localized" in the docstrings
for the message and help fields (the fields named message and help, which use
the LocalizedMessage type and the #[help] attribute) so the comments follow the
en-GB-oxendict spelling.

In `@src/runner/process/file_io.rs`:
- Around line 85-91: The error construction inside the closure passed to
ok_or_else is unnecessarily verbose; replace the anyhow!("{}",
localization::message(keys::RUNNER_IO_NO_EXISTING_ANCESTOR).with_arg("path",
path.as_str())) pattern with a simpler form that converts the LocalizedMessage
to a String (e.g., anyhow!(localization::message(...).with_arg("path",
path.as_str()).to_string())) or use ok_or_else(||
localization::message(...).with_arg(...).to_string().into()) so the Display
implementation is used directly; update the closure referenced around
ok_or_else() where path and
localization::message(keys::RUNNER_IO_NO_EXISTING_ANCESTOR) are used.
- Around line 100-106: Replace the indirect formatting that wraps the localized
message in a format string when constructing the error for Utf8Path::from_path;
instead of anyhow!("{}",
localization::message(keys::RUNNER_IO_NON_UTF8_PATH).with_arg("path",
path.display().to_string())), call .to_string() directly on the localized
message (the result of localization::message(...).with_arg(...)) and pass that
string into anyhow! so the error construction for utf8_path is simplified and
consistent with other uses in this file.

In `@src/stdlib/command/error.rs`:
- Around line 95-103: In io_error (and the similar suffix-appending code around
lines 162–170), ensure you insert an explicit separator before appending
localized suffixes instead of relying on translation whitespace: after
constructing the base message in io_error (and the other function), append a
single space or punctuation (e.g., message.push(' ') or message.push_str(": "))
before calling message.push_str(&localization::message(...).to_string()); this
guarantees proper separation regardless of translation content and keeps
Error::new(ErrorKind::InvalidOperation, message) unchanged.

In `@src/stdlib/command/quote.rs`:
- Around line 15-23: The Display impl for QuoteError currently allocates by
calling to_string() and passing that into f.write_str; update the fmt::Display
for QuoteError (the fmt::Formatter handling in impl fmt::Display for QuoteError)
to use write!(f, "{}", localization::message(keys::COMMAND_QUOTE_LINE_BREAK))
instead of creating an intermediate String so the message is written directly to
the formatter and avoids the extra allocation.

In `@src/stdlib/network/mod.rs`:
- Around line 337-346: The io_error function duplicates logic from
io_helpers::io_to_error; refactor to reuse a single helper: either extend
io_helpers::io_to_error to accept the extra context (action_key: &str and path:
&Utf8Path or path: &str) and return the localized minijinja::Error, or replace
io_error with a small wrapper that calls io_helpers::io_to_error after
converting the parameters (e.g., map action_key to localization message and path
to &str). Update any callers in this module to use the consolidated helper and
remove the duplicated io_error implementation so the localization and io->Error
conversion logic lives only in io_helpers::io_to_error (or the new shared
helper).

In `@src/stdlib/network/policy/mod.rs`:
- Around line 41-59: The doc comments use British spelling "Localised" instead
of the project's required "-ize" spelling; update the doc comments for the enum
variants EmptyScheme, InvalidScheme (the field `message: LocalizedMessage`
comment), and EmptyAllowlist to replace "Localised error message." with
"Localized error message." and apply the same change to the other occurrences in
the file (around the other block at lines ~356-384) so all doc comments
consistently use "Localized".

In `@src/stdlib/path/fs_utils.rs`:
- Around line 44-45: Update the doc comment in src/stdlib/path/fs_utils.rs that
begins "Execute an operation on a file's parent directory handle, translating
I/O errors with the appropriate localized action message." to use en-GB
spelling: replace "localized" with "localised" in that comment (the comment
above the function that executes an operation on a file's parent directory
handle in fs_utils.rs).

In `@src/stdlib/path/io_helpers.rs`:
- Around line 10-29: In io_to_error, normalize the label before performing the
case-insensitive contains check: compute a lower-cased version of the label
(e.g., label_lower) from the already-built label String and use
detail.to_ascii_lowercase().contains(label_lower.as_str()) instead of comparing
to the original capitalised label; update the conditional that branches to
STDLIB_PATH_IO_FAILED_WITH_DETAIL to use this normalized label so capitalised
labels don't skip the intended branch and duplicate the label in the final
message (references: function io_to_error, variable label, variable detail, and
io_error_kind_label).

In `@tests/manifest_glob_tests.rs`:
- Around line 181-183: The test's substring assertions for glob error messages
are locale-sensitive; before the assertions referencing msg and using ensure!
(in tests/manifest_glob_tests.rs) set the test localizer to a fixed locale
(e.g., en-US) or replace the hardcoded substrings with calls to
localization::message(...) for the expected texts; specifically, initialize the
locale at the start of the test (or before each assertion block) so the
ensure!(msg.contains(...)) checks always compare against the English/localized
message retrieved via the localization API rather than environment-dependent
strings.

In `@tests/runner_tool_subcommands_tests.rs`:
- Line 8: Pin the test locale before constructing the expected localized strings
so assertions are deterministic: call the existing test localization helper to
set the locale to "en-US" (or equivalent) via the test helper provided in the
project (e.g., the helper that configures netsuke::localization) at the start of
the test in tests/runner_tool_subcommands_tests.rs, before you build the
expected variable that uses localization keys (netsuke::localization::{self,
keys}); repeat the same change for the other test blocks noted (around lines
113-118) so all localization-dependent assertions use the pinned locale.

Comment thread build.rs Outdated
Comment thread build.rs Outdated
Comment thread docs/execplans/3-7-1-externalize-user-facing-strings.md Outdated
Comment thread docs/execplans/3-7-1-externalize-user-facing-strings.md
Comment thread docs/execplans/3-7-1-externalize-user-facing-strings.md Outdated
Comment thread src/stdlib/network/policy/mod.rs
Comment thread src/stdlib/path/fs_utils.rs
Comment thread src/stdlib/path/io_helpers.rs Outdated
Comment thread tests/manifest_glob_tests.rs Outdated
Comment thread tests/runner_tool_subcommands_tests.rs
@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Jan 26, 2026

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix:

src/manifest/tests/macros.rs

Comment on lines +167 to +168

    let _lock = localizer_test_lock().expect("localizer test lock poisoned");
    let _guard = set_en_localizer();

❌ New issue: Code Duplication
The module contains 3 functions with similar structure: register_manifest_macros_rejects_non_string_values,register_manifest_macros_requires_body,register_manifest_macros_validates_shape

@coderabbitai

This comment was marked as resolved.

…lock in tests

- Update localizer_test_lock calls to use error propagation instead of expects.
- Introduce helper assert_macro_registration_fails to reduce code duplication in macro registration tests.
- Change test fixtures and BDD step definitions to handle poisoned localizer test lock via Result.
- Simplify localizer test lock function formatting.

These changes enhance robustness by properly handling poisoned mutex errors during tests and reduce duplicative code in macro error assertions.

Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.com>
@leynos leynos changed the title Externalize user-facing strings with Fluent; Windows tests refactor Refactor Windows tests; Fluent localization infra and key audit Jan 27, 2026
codescene-delta-analysis[bot]

This comment was marked as outdated.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@tests/bdd/steps/stdlib/config.rs`:
- Around line 34-47: The configure_localisation function currently always calls
localizer_test_lock(), causing deadlock if the same thread already holds the
guard in world.localization_lock; change it to first check whether
world.localization_lock already contains a guard and reuse it: if
world.localization_lock has a value, do not call localizer_test_lock(), just
replace the localisation guard via cli_localization::build_localizer and
localization::set_localizer_for_tests and update world.localization_guard;
otherwise acquire the lock with localizer_test_lock() and then set both
world.localization_lock and world.localization_guard as before. Ensure you keep
the existing calls to world.localization_guard.take_value() and
world.localization_lock.take_value() before setting new values, and preserve the
function's Result return type and error propagation from localizer_test_lock().

In `@tests/manifest_glob_tests.rs`:
- Around line 33-44: The map_err call in en_localizer (wrapping
localizer_test_lock()) discards the original poison error; replace the
map_err(|e| anyhow!(...)) chain with anyhow::Context by calling
.context("localizer test lock poisoned") (ensure anyhow::Context is in scope) so
the returned Result preserves the original error as a source; update the
en_localizer function to use localizer_test_lock().context("localizer test lock
poisoned") and keep the rest (cli_localization::build_localizer,
localization::set_localizer_for_tests, EnLocalizerFixture) unchanged.

In `@tests/runner_tool_subcommands_tests.rs`:
- Around line 36-38: The current use of map_err(anyhow::anyhow!("localizer test
lock poisoned: {e}")) on the Result from localizer_test_lock() discards the
original poison error chain; change that call to use anyhow::Context (e.g.,
localizer_test_lock().context("localizer test lock poisoned")?) so the original
error is preserved as the source, and apply the same change to the other
occurrence around set_en_localizer() at the later block (lines ~103-105); ensure
anyhow::Context is in scope so .context(...) can be used on the Result.

Comment thread tests/bdd/steps/stdlib/config.rs
Comment thread tests/manifest_glob_tests.rs
Comment thread tests/runner_tool_subcommands_tests.rs Outdated
@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Jan 27, 2026

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix:

tests/manifest_glob_tests.rs

Comment on lines +190 to +191

fn glob_unmatched_bracket_errors(en_localizer: Result<EnLocalizerFixture>) -> Result<()> {
    let (_lock, _guard) = en_localizer?;

❌ New issue: Code Duplication
The module contains 3 functions with similar structure: glob_unmatched_bracket_errors,glob_unmatched_closing_brace_reports_position,glob_unmatched_opening_brace_reports_position

1 similar comment
@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Jan 27, 2026

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix:

tests/manifest_glob_tests.rs

Comment on lines +190 to +191

fn glob_unmatched_bracket_errors(en_localizer: Result<EnLocalizerFixture>) -> Result<()> {
    let (_lock, _guard) = en_localizer?;

❌ New issue: Code Duplication
The module contains 3 functions with similar structure: glob_unmatched_bracket_errors,glob_unmatched_closing_brace_reports_position,glob_unmatched_opening_brace_reports_position

@coderabbitai

This comment was marked as resolved.

@coderabbitai

This comment was marked as resolved.

Expand the comment documenting the module-level `#![allow]` suppression
to better explain:

- Why `#[expect]` cannot be used (version-dependent lint behaviour)
- Why item-level suppression isn't feasible (clippy circular dependency)
- Which Error derives will need item-level expectations once upstream
  miette/thiserror fixes are released

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
codescene-delta-analysis[bot]

This comment was marked as outdated.

- Add `en_localizer` rstest fixture in `src/manifest/tests/macros.rs`
  to eliminate duplicated localizer setup across test functions
- Fix potential deadlock in `configure_localisation` BDD step by
  reusing existing lock when present instead of re-acquiring
- Improve error chaining in `tests/manifest_glob_tests.rs` and
  `tests/runner_tool_subcommands_tests.rs` using `.context()`
- Add `assert_glob_error_contains` helper function to reduce
  structural duplication in glob error tests

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@leynos leynos changed the title Refactor Windows tests; Fluent localization infra and key audit Externalize Fluent localization; Windows test context refactor + key audit Jan 27, 2026
codescene-delta-analysis[bot]

This comment was marked as outdated.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@src/manifest/tests/macros.rs`:
- Around line 33-44: Change the en_localizer fixture to return a Result instead
of panicking: update the function signature of en_localizer to return
AnyResult<EnLocalizerFixture>, remove the .expect() call on
localizer_test_lock() and use the ? operator to propagate PoisonError (i.e., let
lock = localizer_test_lock()?;), then return Ok((lock, guard)). Update all
consumers of en_localizer to propagate errors (use ? where they currently assume
non-Result) so lock poisoning is handled rather than panicking; references:
EnLocalizerFixture type, en_localizer function, and localizer_test_lock().

In `@tests/bdd/steps/stdlib/config.rs`:
- Around line 43-53: Replace the separate is_some() check and take_value() call
with a single take to atomically acquire the optional lock: call
world.localization_lock.take_value() and fall back to
Some(localizer_test_lock()?) if it returns None, then later restore the taken
value with world.localization_lock.set_value(...) if present; locate this change
around the use of world.localization_lock, localizer_test_lock(), and the
subsequent
localization::set_localizer_for_tests/cli_localization::build_localizer calls
and update the logic accordingly to remove the is_some() branch and the
conditional restoration pattern.

Comment thread src/manifest/tests/macros.rs
Comment thread tests/bdd/steps/stdlib/config.rs Outdated
…t fixtures

- Change `en_localizer` fixture in `src/manifest/tests/macros.rs` to
  return `AnyResult` instead of using `.expect()`, propagating lock
  poisoning errors properly
- Consolidate `register_manifest_macros_rejects_non_string_values` and
  `register_manifest_macros_requires_body` into a parameterized test
  `register_manifest_macros_invalid_macro_entry` to reduce code
  duplication
- Simplify lock acquisition in `configure_localisation` BDD step by
  combining the check-and-take into a single `take_value().map_or_else()`
  operation, eliminating the TOCTOU gap

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@leynos leynos changed the title Externalize Fluent localization; Windows test context refactor + key audit Externalize Fluent localization; Windows test context refactor + audit Jan 28, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@src/manifest/tests/macros.rs`:
- Around line 195-196: The assertion
`ensure!(err.to_string().contains(&expected), "{err}");` drops the expected
value from the failure message; update the failure message in this test to
include both the expected string (`expected`) and the actual error (`err`) so
failures show expected vs actual (follow the pattern used in
`parse_macro_name_errors`), i.e., change the `ensure!` invocation that
references `expected` and `err` to format both values into the message.

In `@tests/bdd/steps/stdlib/config.rs`:
- Around line 39-47: The current comment inaccurately describes the lock-taking
behaviour—update the comment above the let lock =
world.localization_lock.take_value().map_or_else(|| localizer_test_lock(), Ok)?
to say that it takes and returns an existing lock if present, otherwise calls
localizer_test_lock() to create/acquire a new lock (i.e., reuse existing lock
when Some, call localizer_test_lock() when None), and remove the reference to
"Using take_value().or_else()" so the wording matches the actual map_or_else
call.

Comment thread src/manifest/tests/macros.rs Outdated
Comment thread tests/bdd/steps/stdlib/config.rs
…ments

- Use `anyhow::Error::msg()` instead of `anyhow!("{e}")` for cleaner
  error conversion while preserving context chaining
- Improve assertion diagnostics in `assert_macro_registration_fails`
  to show expected value alongside actual error, matching the pattern
  used in `parse_macro_name_errors`
- Fix misleading comment that referenced `or_else` when code uses
  `map_or_else`

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…ob tests

Use `anyhow::Error::msg(e.to_string())` instead of `anyhow!("{e}")` for
cleaner error conversion while preserving context chaining, matching the
pattern used in src/manifest/tests/macros.rs.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Jan 28, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jan 28, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@leynos leynos merged commit 8be50fd into main Jan 28, 2026
16 checks passed
@leynos leynos deleted the terragon/externalize-user-strings-ayqyhg branch January 28, 2026 22:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant