356 lines
9.8 KiB
Markdown
Raw Permalink Normal View History

# 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