nushell-plugins/nu_plugin_fluent

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.