nushell-plugins/nu_plugin_fluent
Jesús Pérez 2b3e574e3d # Summary
fix: help system integration, build process optimization, and plugin rebuild efficiency

## Detailed Description

This commit addresses critical issues in the help system discoverability, build process robustness, and plugin rebuild efficiency.

### 1. Help System Integration (New Feature)

**Issue**: Version-update module recipes were not discoverable
- Not shown in `just help modules`
- Not referenced in `just help`
- Not included in help navigation system
- Users had to manually run `just --list` to find update commands

**Solution**:
- Added version-update module to all help outputs
- Updated `justfiles/help.just` to document all 30+ version-update recipes
- Created new `just commands` recipe as discoverable alias for `just --list`
- Integrated version-update into help-all workflow

**Impact**:
- Version-update commands now fully discoverable via help system
- Users can find update commands with: `just help modules`, `just help`, `just commands`
- Improved overall help system navigation

**Files Modified**:
- `justfiles/help.just` (+23 lines)
  - Added version-update module to help sections
  - Added to modules list
  - Added to help-all workflow
  - New `commands` recipe showing all recipes by group

### 2. Build Process Fixes (Phase 3: Bin Archives)

#### 2a. Plugin Archive Collection Bug

**Issue**: "No plugins found to package" warning in Phase 3
- Collected 26 plugin binaries but reported 0
- Archive creation skipped because count was wrong

**Root Cause**: `each` command returns null, so `| length` returned 0
```nushell
#  OLD - each returns null
let plugin_count = (ls nu_plugin_*/target/release/nu_plugin_* | each {|p|
    cp $p.name $"($temp_dir)/"
} | length)  # Returns 0!
```

**Solution**: Separated counting from copying with proper filtering
```nushell
#  NEW - count before operations
let plugins_to_copy = (ls nu_plugin_*/target/release/nu_plugin_* | where type == "file")
let plugin_count = ($plugins_to_copy | length)
```

**Impact**:
- Now correctly collects and reports 26 plugins
- Filters out .d dependency files automatically
- Warning eliminated

#### 2b. Tar Archive Path Handling

**Issue**: Tar command failing silently with relative paths in subshell
- `cd $temp_dir` changes context unpredictably
- Relative path `../$archive_name` fails in subshell
- Archive file not created despite exit code 0

**Root Cause**: Shell context and relative path issues in Nushell `do` block

**Solution**: Used `tar -C` with absolute paths instead of `cd`
```nushell
#  OLD - unreliable context switching
do {
    cd $temp_dir
    tar -czf ../$archive_name .
}

#  NEW - absolute paths, no context switching
tar -C $temp_dir -czf $archive_path .
```

**Additional Improvements**:
- Absolute path construction using `pwd | path join`
- Better error diagnostics with exit code and stderr output
- File verification after creation

**Impact**:
- Tar archives now created successfully
- Robust path handling across platforms
- Clear error messages for debugging

#### 2c. File Size Calculation Type Error

**Issue**: Runtime error when calculating archive size
```
Error: The '/' operator does not work on values of type 'list<filesize>'
```

**Root Cause**: `ls` returns list of records, so `.size` was a list
```nushell
#  OLD - returns list<filesize>
(ls $archive_path).size / 1024 / 1024

#  NEW - returns filesize
(ls $archive_path | get 0.size) / 1024 / 1024
```

**Impact**:
- Proper file size calculation in MB
- No more type errors

**Files Modified**:
- `scripts/create_full_distribution.nu` (+58 lines, refactored plugin collection)
  - Fixed plugin counting logic
  - Improved path handling with absolute paths
  - Enhanced error diagnostics

### 3. Plugin Rebuild Optimization

**Issue**: All plugins marked for rebuild even when dependencies unchanged
- Step 4 (`update_all_plugins.nu`) touched all Cargo.toml files at 01:00:32
- Step 5 saw all files as "newer" than binaries
- Marked ALL plugins for rebuild, though cargo only rebuilt changed ones

**Root Cause**: Script always saved files, even when no changes made
```nushell
#  OLD - always saves, touching file timestamp
$updated_content | to toml | save -f $cargo_toml
```

**Solution**: Only save if content actually changed
```nushell
#  NEW - compare before writing
let original_toml = $content | to toml
let new_toml = $updated_content | to toml

if $original_toml != $new_toml {
    $updated_content | to toml | save -f $cargo_toml
}
```

**Impact**:
- Unchanged files preserve original timestamps
- Only plugins with actual dependency changes are rebuilt
- Efficient rebuild process with accurate file modification detection

**Files Modified**:
- `scripts/update_all_plugins.nu` (+12 lines, added content comparison)
  - Only touches files with real changes
  - Preserves timestamps for efficiency
  - Clearer logic and comments

### 4. Documentation

**Files Modified**:
- `CHANGELOG.md` (+56 lines)
  - Added comprehensive 2025-10-19 entry
  - Documented all fixes with root causes
  - Listed files modified and impact summary

## Technical Details

### Nushell Patterns Used

1. **Proper List Handling**:
   - `ls` returns list of records, access with `| get 0.size`
   - Filter with `where type == "file"` to exclude metadata

2. **Absolute Path Construction**:
   - `pwd | append "path" | path join` for cross-platform paths
   - Safer than string concatenation with `/`

3. **Content Comparison**:
   - Compare TOML string representation before saving
   - Preserves file timestamps for efficiency

4. **Error Diagnostics**:
   - Capture `stderr` from commands
   - Report exit codes and error messages separately

## Testing

- [x] Help system shows version-update module
- [x] `just commands` displays all recipes by group
- [x] Phase 3 bin archive creation works
- [x] Plugin collection reports correct count (26)
- [x] Tar archives created successfully
- [x] File size calculated correctly
- [x] Plugin rebuild only touches changed files
- [x] CHANGELOG updated with all changes

## Files Changed

```
38 files changed, 2721 insertions(+), 2548 deletions(-)

Core Changes:
- justfiles/help.just                  (+23)  Help system integration
- scripts/create_full_distribution.nu  (+58)  Build process fixes
- scripts/update_all_plugins.nu        (+12)  Rebuild optimization
- CHANGELOG.md                         (+56)  Documentation

Dependency Updates:
- All plugin Cargo.toml and Cargo.lock files (version consistency)
```

## Breaking Changes

None. These are bug fixes and optimizations that maintain backward compatibility.

## Migration Notes

No migration needed. Improvements are transparent to users.

## Related Issues

- Help system discoverability
- Build process Phase 3 failures
- Unnecessary plugin rebuilds
- Build process reliability

## Checklist

- [x] Changes follow Rust/Nushell idioms
- [x] Code is well-commented
- [x] Error handling is comprehensive
- [x] Documentation is updated
- [x] All changes tested
- [x] No breaking changes introduced
2025-10-19 01:17:13 +01:00
..
2025-10-19 01:17:13 +01:00
2025-10-19 01:17:13 +01:00

nu_plugin_fluent

A Nushell plugin for Fluent internationalization (i18n) and localization (l10n) workflows.

Overview

This plugin provides powerful tools for managing multilingual applications using Mozilla's Fluent localization system. It enables you to parse, validate, localize, and manage Fluent Translation List (.ftl) files directly from Nushell.

Installing

Clone this repository

Warning

nu_plugin_fluent has dependencies to nushell source via local path in Cargo.toml Nushell and plugins require to be sync with same version

Clone Nushell alongside this plugin or change dependencies in Cargo.toml

This plugin is also included as submodule in nushell-plugins as part of plugins collection for Provisioning project

Build from source

> cd nu_plugin_fluent
> cargo install --path .

Nushell

In a Nushell

> plugin add ~/.cargo/bin/nu_plugin_fluent

Commands

fluent-parse

Parse a Fluent Translation List (.ftl) file and extract its message structure.

> fluent-parse <file>

Parameters:

  • file <path>: FTL file to parse

Example:

> fluent-parse locales/en-US/main.ftl
╭───────────────┬─────────────────────────────╮
│ file          │ locales/en-US/main.ftl      │
│ message_count │ 3                           │
│ messages      │ [list of parsed messages]   │
╰───────────────┴─────────────────────────────╯

fluent-localize

Localize a message using the Fluent translation system with hierarchical fallback support.

> fluent-localize <message_id> <locale> [--files] [--bundle] [--args] [--fallback]

Parameters:

  • message_id <string>: Message ID to localize
  • locale <string>: Locale code (e.g., en-US, es-ES)

Flags:

  • --files -f <list>: FTL files to load
  • --bundle -b <record>: Pre-loaded message bundle
  • --args -a <record>: Arguments for message interpolation
  • --fallback -F: Return message ID if translation not found

Examples:

Basic localization:

> fluent-localize welcome-message en-US --files [locales/en-US/main.ftl]
"Welcome to our application!"

With arguments:

> fluent-localize user-greeting en-US --files [locales/en-US/main.ftl] --args {name: "Alice"}
"Hello, Alice! Welcome back."

With fallback:

> fluent-localize missing-message es-ES --files [locales/es-ES/main.ftl] --fallback
"[[missing-message]]"

fluent-validate

Validate the syntax of a Fluent Translation List (.ftl) file.

> fluent-validate <file>

Parameters:

  • file <path>: FTL file to validate

Example:

> fluent-validate locales/en-US/main.ftl
╭────────┬─────────────────────────╮
│ valid  │ true                    │
│ file   │ locales/en-US/main.ftl  │
│ errors │ []                      │
╰────────┴─────────────────────────╯

fluent-extract

Extract message IDs from a Fluent Translation List (.ftl) file.

> fluent-extract <file>

Parameters:

  • file <path>: FTL file to extract messages from

Example:

> fluent-extract locales/en-US/main.ftl
╭───┬─────────────────╮
│ 0 │ welcome-message │
│ 1 │ user-greeting   │
│ 2 │ goodbye-message │
╰───┴─────────────────╯

fluent-list-locales

List available locales from a directory structure.

> fluent-list-locales <directory>

Parameters:

  • directory <path>: Directory containing locale folders

Example:

> fluent-list-locales ./locales
╭───┬───────╮
│ 0 │ en-US │
│ 1 │ es-ES │
│ 2 │ fr-FR │
│ 3 │ de-DE │
╰───┴───────╯

fluent-create-bundle

Create a merged Fluent bundle from global and page-specific locale files.

> fluent-create-bundle <locale> [--global] [--page] [--fallback] [--override]

Parameters:

  • locale <string>: Locale code (e.g., en-US)

Flags:

  • --global -g <list>: Global FTL files to include
  • --page -p <list>: Page-specific FTL files to include
  • --fallback -f <list>: Fallback locales in order
  • --override -o: Allow page files to override global messages

Examples:

Create bundle with global and page-specific files:

> fluent-create-bundle en-US --global [global/en-US/common.ftl] --page [pages/blog/en-US/blog.ftl]

Create bundle with fallback support:

> fluent-create-bundle es-ES --fallback [en-US] --global [global/es-ES/common.ftl]

Workflow Examples

Complete Localization Workflow

  1. Set up your locale directory structure:
locales/
├── en-US/
│   ├── common.ftl
│   └── pages.ftl
├── es-ES/
│   ├── common.ftl
│   └── pages.ftl
└── fr-FR/
    ├── common.ftl
    └── pages.ftl
  1. List available locales:
> fluent-list-locales ./locales
  1. Validate all locale files:
> ls locales/**/*.ftl | each { |file| fluent-validate $file.name }
  1. Extract all message IDs for translation coverage:
> ls locales/en-US/*.ftl | each { |file|
    fluent-extract $file.name | wrap messages | insert file $file.name
} | flatten
  1. Create a localization function:
def localize [message_id: string, locale: string = "en-US"] {
    let files = (ls $"locales/($locale)/*.ftl" | get name)
    fluent-localize $message_id $locale --files $files --fallback
}
  1. Use the localization function:
> localize "welcome-message" "es-ES"
"¡Bienvenido a nuestra aplicación!"

Quality Assurance Workflow

Check for missing translations across locales:

def check-translation-coverage [] {
    let base_locale = "en-US"
    let base_messages = (ls $"locales/($base_locale)/*.ftl"
        | each { |file| fluent-extract $file.name }
        | flatten | uniq)

    ls locales/*/
    | get name
    | path basename
    | where $it != $base_locale
    | each { |locale|
        let locale_messages = (ls $"locales/($locale)/*.ftl"
            | each { |file| fluent-extract $file.name }
            | flatten | uniq)

        let missing = ($base_messages | where $it not-in $locale_messages)
        {locale: $locale, missing_count: ($missing | length), missing: $missing}
    }
}

> check-translation-coverage

Fluent File Format

Example .ftl file structure:

locales/en-US/common.ftl

# Simple message
welcome-message = Welcome to our application!

# Message with variables
user-greeting = Hello, { $name }! Welcome back.

# Message with attributes
login-button =
    .label = Sign In
    .aria-label = Sign in to your account
    .tooltip = Click here to access your account

# Message with variants
unread-emails = You have { $count ->
    [one] one unread email
   *[other] { $count } unread emails
}.

# Message with functions
last-login = Last login: { DATETIME($date, month: "long", day: "numeric") }

Features

  • Parse FTL files - Extract and analyze message structure
  • Validate syntax - Ensure FTL files are syntactically correct
  • Localize messages - Translate messages with variable interpolation
  • Fallback support - Hierarchical locale fallback system
  • Bundle management - Merge global and page-specific translations
  • Message extraction - List all message IDs for coverage analysis
  • Locale discovery - Auto-detect available locales
  • Quality assurance - Tools for translation completeness checking

Use Cases

  • Web Applications: Manage frontend translations with dynamic content
  • CLI Tools: Internationalize command-line applications
  • Documentation: Maintain multilingual documentation systems
  • Content Management: Handle localized content workflows
  • Quality Assurance: Automate translation coverage and validation
  • Build Systems: Integrate i18n validation into CI/CD pipelines

Integration with Nushell

This plugin leverages Nushell's powerful data processing capabilities:

# Batch validate all locale files
ls locales/**/*.ftl
| par-each { |file| fluent-validate $file.name }
| where valid == false

# Generate translation progress report
def translation-report [] {
    fluent-list-locales ./locales
    | each { |locale|
        let files = (ls $"locales/($locale)/*.ftl" | get name)
        let message_count = ($files | each { |f| fluent-extract $f } | flatten | length)
        {locale: $locale, messages: $message_count}
    }
}

# Find untranslated messages
def find-missing-translations [base_locale: string, target_locale: string] {
    let base_msgs = (ls $"locales/($base_locale)/*.ftl" | each { |f| fluent-extract $f.name } | flatten)
    let target_msgs = (ls $"locales/($target_locale)/*.ftl" | each { |f| fluent-extract $f.name } | flatten)
    $base_msgs | where $it not-in $target_msgs
}

Contributing

Contributions are welcome! Please feel free to submit issues, feature requests, or pull requests.

License

This project is licensed under the MIT License.