# 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 ``` **Parameters:** - **file** ``: 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 [--files] [--bundle] [--args] [--fallback] ``` **Parameters:** - **message_id** ``: Message ID to localize - **locale** ``: Locale code (e.g., en-US, es-ES) **Flags:** - **--files** `-f` ``: FTL files to load - **--bundle** `-b` ``: Pre-loaded message bundle - **--args** `-a` ``: 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 ``` **Parameters:** - **file** ``: 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 ``` **Parameters:** - **file** ``: 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 ``` **Parameters:** - **directory** ``: 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 [--global] [--page] [--fallback] [--override] ``` **Parameters:** - **locale** ``: Locale code (e.g., en-US) **Flags:** - **--global** `-g` ``: Global FTL files to include - **--page** `-p` ``: Page-specific FTL files to include - **--fallback** `-f` ``: 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