356 lines
9.8 KiB
Markdown
356 lines
9.8 KiB
Markdown
![]() |
# nu_plugin_fluent
|
||
|
|
||
|
A [Nushell](https://nushell.sh/) plugin for [Fluent](https://projectfluent.org/) 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](https://nushell.sh/) alongside this plugin or change dependencies in [Cargo.toml](Cargo.toml)
|
||
|
|
||
|
This plugin is also included as submodule in [nushell-plugins](https://repo.jesusperez.pro/jesus/nushell-plugins)
|
||
|
as part of plugins collection for [Provisioning project](https://rlung.librecloud.online/jesus/provisioning)
|
||
|
|
||
|
Build from source
|
||
|
|
||
|
```nushell
|
||
|
> cd nu_plugin_fluent
|
||
|
> cargo install --path .
|
||
|
```
|
||
|
|
||
|
### Nushell
|
||
|
|
||
|
In a [Nushell](https://nushell.sh/)
|
||
|
|
||
|
```nushell
|
||
|
> plugin add ~/.cargo/bin/nu_plugin_fluent
|
||
|
```
|
||
|
|
||
|
## Commands
|
||
|
|
||
|
### `fluent-parse`
|
||
|
|
||
|
Parse a Fluent Translation List (`.ftl`) file and extract its message structure.
|
||
|
|
||
|
```nushell
|
||
|
> fluent-parse <file>
|
||
|
```
|
||
|
|
||
|
**Parameters:**
|
||
|
- **file** `<path>`: FTL file to parse
|
||
|
|
||
|
**Example:**
|
||
|
```nushell
|
||
|
> 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.
|
||
|
|
||
|
```nushell
|
||
|
> 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:
|
||
|
```nushell
|
||
|
> fluent-localize welcome-message en-US --files [locales/en-US/main.ftl]
|
||
|
"Welcome to our application!"
|
||
|
```
|
||
|
|
||
|
With arguments:
|
||
|
```nushell
|
||
|
> fluent-localize user-greeting en-US --files [locales/en-US/main.ftl] --args {name: "Alice"}
|
||
|
"Hello, Alice! Welcome back."
|
||
|
```
|
||
|
|
||
|
With fallback:
|
||
|
```nushell
|
||
|
> 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.
|
||
|
|
||
|
```nushell
|
||
|
> fluent-validate <file>
|
||
|
```
|
||
|
|
||
|
**Parameters:**
|
||
|
- **file** `<path>`: FTL file to validate
|
||
|
|
||
|
**Example:**
|
||
|
```nushell
|
||
|
> 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.
|
||
|
|
||
|
```nushell
|
||
|
> fluent-extract <file>
|
||
|
```
|
||
|
|
||
|
**Parameters:**
|
||
|
- **file** `<path>`: FTL file to extract messages from
|
||
|
|
||
|
**Example:**
|
||
|
```nushell
|
||
|
> 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.
|
||
|
|
||
|
```nushell
|
||
|
> fluent-list-locales <directory>
|
||
|
```
|
||
|
|
||
|
**Parameters:**
|
||
|
- **directory** `<path>`: Directory containing locale folders
|
||
|
|
||
|
**Example:**
|
||
|
```nushell
|
||
|
> 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.
|
||
|
|
||
|
```nushell
|
||
|
> 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:
|
||
|
```nushell
|
||
|
> fluent-create-bundle en-US --global [global/en-US/common.ftl] --page [pages/blog/en-US/blog.ftl]
|
||
|
```
|
||
|
|
||
|
Create bundle with fallback support:
|
||
|
```nushell
|
||
|
> 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
|
||
|
```
|
||
|
|
||
|
2. **List available locales:**
|
||
|
```nushell
|
||
|
> fluent-list-locales ./locales
|
||
|
```
|
||
|
|
||
|
3. **Validate all locale files:**
|
||
|
```nushell
|
||
|
> ls locales/**/*.ftl | each { |file| fluent-validate $file.name }
|
||
|
```
|
||
|
|
||
|
4. **Extract all message IDs for translation coverage:**
|
||
|
```nushell
|
||
|
> ls locales/en-US/*.ftl | each { |file|
|
||
|
fluent-extract $file.name | wrap messages | insert file $file.name
|
||
|
} | flatten
|
||
|
```
|
||
|
|
||
|
5. **Create a localization function:**
|
||
|
```nushell
|
||
|
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
|
||
|
}
|
||
|
```
|
||
|
|
||
|
6. **Use the localization function:**
|
||
|
```nushell
|
||
|
> localize "welcome-message" "es-ES"
|
||
|
"¡Bienvenido a nuestra aplicación!"
|
||
|
```
|
||
|
|
||
|
### Quality Assurance Workflow
|
||
|
|
||
|
Check for missing translations across locales:
|
||
|
|
||
|
```nushell
|
||
|
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**
|
||
|
```fluent
|
||
|
# 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:
|
||
|
|
||
|
```nushell
|
||
|
# 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](https://projectfluent.org/) - Mozilla's localization system
|
||
|
- [Nushell](https://nushell.sh/) - A new type of shell
|
||
|
- [nu_plugin_tera](../nu_plugin_tera/) - Tera templating plugin for Nushell
|