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
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
- 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
- List available locales:
> fluent-list-locales ./locales
- Validate all locale files:
> ls locales/**/*.ftl | each { |file| fluent-validate $file.name }
- 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
- 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
}
- 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.
Related Projects
- Fluent - Mozilla's localization system
- Nushell - A new type of shell
- nu_plugin_tera - Tera templating plugin for Nushell