# fluent-tooling next Tree-sitter grammar, linter, and editor tooling for the Fluent localization system ## Changelog # Changelog All notable changes to fluent-tooling are documented here. ## 0.0.1 - 2026-07-13 Initial release of the Fluent grammar, linter, and Emacs tooling. ## Development # Development The repository provides a reproducible Nix shell with Rust, Tree-sitter, Babashka, Clang, Emacs, and Flycheck. ## Enter the development shell ```bash nix develop ``` All commands below assume this shell unless shown with `nix develop --command`. ## Repository layout | Path | Contents | | --- | --- | | `grammar.js`, `src/`, `queries/` | Grammar definition, generated parser, scanner, and queries | | `crates/tree-sitter-fluent/` | Rust grammar bindings and parser tests | | `crates/fluent-lint/` | Linter library, CLI, and tests | | `editors/emacs/` | Major mode, Flymake adapter, Flycheck checker, and ERT tests | | `test/fixtures/` | Upstream and project regression fixtures | ## Quality tasks Babashka exposes the repository’s development tasks: | Command | Action | | --- | --- | | `bb fmt` | Format Rust and grammar metadata sources | | `bb fmt:check` | Check formatting without changing files | | `bb lint` | Run Clippy with warnings denied | | `bb test` | Run all Rust tests | | `bb test:emacs` | Compile the grammar and run all ERT tests | | `bb ci` | Run formatting, Clippy, Rust tests, and ERT tests | Run the complete quality gate with: ```bash bb ci ``` From outside the shell, use: ```bash nix develop --command bb ci ``` ## Regenerate the parser After editing `grammar.js`, regenerate the checked-in Tree-sitter output: ```bash bb generate ``` Review changes to `src/parser.c`, `src/grammar.json`, and `src/node-types.json` with the grammar change. Run `bb ci` after generation. ## Run the linter from Cargo ```bash cargo run -p fluent-lint --bin ftl-lint -- \ test/fixtures/regressions/broken.ftl ``` ## Test fixtures The test suite adopts Project Fluent, Fluent.js, and fluent-rs fixtures in addition to project regressions. Imported fixtures retain exact line endings, trailing whitespace, and missing final newlines because those properties test parser behavior. Record the source, revision, license, and local normalization of imported fixtures in [`test/fixtures/UPSTREAM.md`](https://github.com/outskirtslabs/fluent-tooling/blob/main/test/fixtures/UPSTREAM.md). Grammar and fixture license provenance belongs in [`NOTICE.md`](https://github.com/outskirtslabs/fluent-tooling/blob/main/NOTICE.md). ## Nix validation Validate both packages and every flake check before release: ```bash nix build nix build .#tree-sitter-fluent nix flake check ``` ## Emacs integration # Emacs integration `fluent-ts-mode` is a native Tree-sitter major mode for Fluent FTL resources. It requires Emacs 29.1 or newer with built-in Tree-sitter support. The mode registers `.ftl` files and provides syntax highlighting, four-space indentation, comments, Imenu, defun navigation, and syntax navigation. It can check unsaved buffer contents with built-in Flymake or optional Flycheck support through `ftl-lint`. The Emacs package does not bundle a compiled grammar. Install the package and then install or expose the grammar explicitly. Opening an `.ftl` file never downloads or installs software. ## Install the package ### Vanilla Emacs with `package-vc` Emacs 29.1 and newer can install the package directly from this repository: ```elisp (package-vc-install '(fluent-ts-mode :url "https://github.com/outskirtslabs/fluent-tooling" :lisp-dir "editors/emacs" :main-file "fluent-ts-mode.el")) (require 'fluent-ts-mode) ``` ### straight.el ```elisp (straight-use-package '(fluent-ts-mode :type git :host github :repo "outskirtslabs/fluent-tooling" :files ("editors/emacs/*.el"))) ``` ### Doom Emacs Add the package recipe to `packages.el`: ```elisp (package! fluent-ts-mode :recipe (:host github :repo "outskirtslabs/fluent-tooling" :files ("editors/emacs/*.el"))) ``` Run `doom sync`, then load and configure the mode from `config.el` or a language module: ```elisp (require 'fluent-ts-mode) (setq fluent-ts-mode-checker 'auto) ``` ### Local checkout Load the package directly while developing it: ```elisp (add-to-list 'load-path "/absolute/path/to/fluent-tooling/editors/emacs") (require 'fluent-ts-mode) ``` ## Install the grammar Choose one of the following methods. Install the grammar before opening an FTL file with `fluent-ts-mode`. ### Build from the public source recipe Run `M-x fluent-ts-mode-install-grammar`. The command asks Emacs’s built-in Tree-sitter installer to clone this repository, compile `src/parser.c` and `src/scanner.c`, and install the resulting shared library. This command is the only part of `fluent-ts-mode` that may access the network. With a prefix argument, the command prompts for an installation directory: ```text C-u M-x fluent-ts-mode-install-grammar ``` ### Build from a local checkout Replace the public recipe after loading the mode, then run the installation command: ```elisp (require 'fluent-ts-mode) (setf (alist-get 'fluent treesit-language-source-alist) '("/absolute/path/to/fluent-tooling")) ``` ### Use the Nix grammar package Build the grammar and add its library directory to Emacs’s search path: ```bash nix build .#tree-sitter-fluent ``` ```elisp (require 'treesit) (add-to-list 'treesit-extra-load-path "/absolute/path/to/fluent-tooling/result/lib") (require 'fluent-ts-mode) ``` Keep the `result` link available, or use the grammar’s immutable Nix store path. Evaluate `(treesit-language-available-p 'fluent)` to confirm that Emacs can load it. ## Configure diagnostics `fluent-ts-mode-checker` controls diagnostics: | Value | Behavior | | --- | --- | | `auto` | Use Flycheck when it is already loaded; otherwise use Flymake | | `flymake` | Use built-in Flymake | | `flycheck` | Require and use Flycheck | | `nil` | Disable diagnostics | The default is `auto`. Load Flycheck before opening an FTL buffer when you want automatic selection to choose it: ```elisp (require 'flycheck) (setq fluent-ts-mode-checker 'auto) ``` Both adapters send unsaved buffer contents to `ftl-lint --format json -`. Install `ftl-lint` on Emacs’s `exec-path`, or set its absolute path: ```elisp (setq fluent-ts-mode-linter-executable "/absolute/path/to/ftl-lint") ``` Select a frontend explicitly when needed: ```elisp (setq fluent-ts-mode-checker 'flymake) ;; or (setq fluent-ts-mode-checker 'flycheck) ``` The adapters consume the stable [`ftl-lint` JSON diagnostics protocol](json-diagnostics.adoc). ## Customize indentation Fluent continuation lines use four spaces by default: ```elisp (setq fluent-ts-mode-indent-offset 4) ``` ## Troubleshooting * If mode activation reports that the grammar is unavailable, install it or add its library directory to `treesit-extra-load-path` before opening the file. * If diagnostics report that `ftl-lint` cannot be started, check `(executable-find "ftl-lint")` or set `fluent-ts-mode-linter-executable` explicitly. * If `auto` selects Flymake, load Flycheck before activating the FTL buffer or set `fluent-ts-mode-checker` to `flycheck`. ## ftl-lint # `ftl-lint` `ftl-lint` checks Fluent FTL resources for syntax and structural problems. It uses the repository’s error-tolerant Tree-sitter grammar and reports source labels, Fluent error codes, notes, and actionable help. ## Build and run Build the linter with Nix: ```bash nix build .#ftl-lint ./result/bin/ftl-lint path/to/messages.ftl ``` Run it without creating a result link: ```bash nix run .#ftl-lint -- path/to/messages.ftl ``` Build it directly with Cargo from this checkout: ```bash cargo build --release -p fluent-lint --bin ftl-lint ./target/release/ftl-lint path/to/messages.ftl ``` ## Inputs Pass one or more files as separate arguments: ```bash ftl-lint locales/en/messages.ftl locales/pl/messages.ftl ``` Use `-` once to read an FTL resource from standard input: ```bash ftl-lint - < locales/en/messages.ftl ``` ## Output Human-readable diagnostics are the default and go to standard error. Color is enabled when standard error is a terminal, unless `NO_COLOR` is set. Override the choice with `--color always`, `--color never`, or `--no-color`. Use `--format json` for editor and tool integration. JSON diagnostics go to standard output, while invocation and I/O errors remain on standard error. The [JSON diagnostics protocol](json-diagnostics.adoc) defines the stable integration interface. ## Options | Option | Meaning | | --- | --- | | `--format ` | Select `human` or `json` output; defaults to `human` | | `--color ` | Select `auto`, `always`, or `never`; defaults to `auto` | | `--no-color` | Alias for `--color never` | | `-h`, `--help` | Print command help | | `-V`, `--version` | Print the version | ## Exit status | Status | Meaning | | --- | --- | | `0` | Every input is clean | | `1` | At least one diagnostic was reported | | `2` | Invocation or I/O failure | An I/O failure takes precedence over diagnostics from other inputs, so a multi-file run returns status 2 if any file cannot be read. ## Diagnostic codes The linter emits these Project Fluent syntax codes where applicable: | Code | Meaning | | --- | --- | | `E0002` | Invalid entry or syntax | | `E0003` | Expected token | | `E0005` | Message requires a value or attributes | | `E0006` | Term requires a value | | `E0010` | Missing default variant | | `E0012` | Missing attribute value | | `E0015` | Duplicate default variant | | `E0019` | Term attribute used as a placeable | | `E0020` | Unterminated string | | `E0027` | Unbalanced closing brace | `W1001` is a project lint that replaces redundant `PLURAL(...)` wrappers with native Fluent selection. ## fluent-tooling # fluent-tooling > Tree-sitter grammar, linter, and editor tooling for the [Fluent localization system](https://projectfluent.org/) from Mozilla. ![doc](https://img.shields.io/badge/doc-outskirtslabs-orange.svg) ![status: experimental](https://img.shields.io/badge/status-experimental-red.svg) ![alt=nixbot](https://ci.outskirtslabs.com/repos/github/outskirtslabs/fluent-tooling/badge.svg) This repository contains: * [`ftl-lint`](https://docs.outskirtslabs.com/fluent-tooling/next/ftl-lint), a command-line linter with compiler-style diagnostics; * [`tree-sitter-fluent`](https://docs.outskirtslabs.com/fluent-tooling/next/tree-sitter), an error-tolerant Tree-sitter grammar with Rust bindings; and * [`fluent-ts-mode`](https://docs.outskirtslabs.com/fluent-tooling/next/emacs), a native Tree-sitter major mode for Emacs. Project status: **[Experimental](https://docs.outskirtslabs.com/open-source-vital-signs#experimental)**. ## Quick start Run the linter from this checkout: ```bash nix run . -- path/to/messages.ftl ``` See the [`ftl-lint` guide](https://docs.outskirtslabs.com/fluent-tooling/next/ftl-lint) for installation, command-line options, diagnostic codes, and editor integration. ## Documentation * [Docs](https://docs.outskirtslabs.com/fluent-tooling/next/) * [API Reference](https://docs.outskirtslabs.com/fluent-tooling/next/api) * [Support via GitHub Issues](https://github.com/outskirtslabs/fluent-tooling/issues) ## Nix packages The flake exports two named packages and selects `ftl-lint` by default: | Command | Package | | --- | --- | | `nix build` or `nix build .#ftl-lint` | The `ftl-lint` executable | | `nix build .#tree-sitter-fluent` | The compiled Tree-sitter grammar | See the [`tree-sitter-fluent` guide](https://docs.outskirtslabs.com/fluent-tooling/next/tree-sitter) for the grammar package’s layout and integration options. ## Development Enter the reproducible development shell and run the complete quality gate: ```bash nix develop bb ci ``` The [development guide](https://docs.outskirtslabs.com/fluent-tooling/next/development) covers repository structure, parser generation, individual checks, and fixture provenance. ## Licensing Copyright © 2017-present David Rios for the original Tree-sitter grammar. Copyright © 2026 mailto:casey@outskirtslabs.com[Casey Link]. Distributed under the [MIT](https://spdx.org/licenses/MIT.html) license. Copied grammar provenance and third-party fixture licenses are documented in [`NOTICE.md`](https://github.com/outskirtslabs/fluent-tooling/blob/main/NOTICE.md) and [`test/fixtures/UPSTREAM.md`](https://github.com/outskirtslabs/fluent-tooling/blob/main/test/fixtures/UPSTREAM.md). ## JSON diagnostics protocol # JSON diagnostics protocol `ftl-lint --format json` is the stable interface for editor and tool integration. Human-readable output may evolve independently; integrations should consume this protocol instead. ## Invocation and streams Pass files or standard input exactly as in human mode: ```bash ftl-lint --format json locales/pl/messages.ftl ftl-lint --format json - < locales/pl/messages.ftl ``` A valid invocation writes one JSON document to standard output. Invocation and I/O errors go to standard error. The JSON document is compact; examples in this guide are formatted for readability. ## Schema version 1 ```json { "schema_version": 1, "diagnostics": [ { "path": "locales/pl/messages.ftl", "severity": "error", "code": "E0003", "message": "expected token `}`", "labels": [ { "primary": true, "message": "unexpected `?` inside the placeable", "span": { "start": { "byte": 30, "line": 0, "column": 26 }, "end": { "byte": 31, "line": 0, "column": 27 } } } ], "notes": [], "help": ["close the placeable before `?`"] } ] } ``` The top-level object contains: | Field | Type | Meaning | | --- | --- | --- | | `schema_version` | integer | Protocol version; currently `1` | | `diagnostics` | array | Zero or more diagnostic objects | Each diagnostic contains: | Field | Type | Meaning | | --- | --- | --- | | `path` | string | Input path, or `` for standard input | | `severity` | string | `error` or `warning` | | `code` | string | Stable Fluent or project lint code | | `message` | string | Diagnostic summary | | `labels` | array | Labeled source spans | | `notes` | array of strings | Supporting context | | `help` | array of strings | Suggested actions | Every label contains: | Field | Type | Meaning | | --- | --- | --- | | `primary` | boolean | Whether this is the diagnostic’s primary label | | `message` | string | Explanation attached to the span | | `span` | object | Half-open source range with `start` and `end` positions | The `diagnostics`, `labels`, `notes`, and `help` arrays are always present, including when they are empty. ## Source positions Each position contains three zero-based values: | Field | Meaning | | --- | --- | | `byte` | UTF-8 byte offset from the beginning of the input | | `line` | Line number | | `column` | Unicode scalar-value offset from the beginning of the line | Spans are half-open: `start` is inclusive and `end` is exclusive. Empty spans have identical start and end positions. LF, CRLF, and lone CR each advance the line once and reset the column to zero. Columns count Unicode scalar values, not bytes, UTF-16 code units, grapheme clusters, or display cells. Consumers that index UTF-8 source can use `byte`; editors should translate `line` and `column` into their native position type. ## Exit and error behavior The JSON format preserves the command’s normal exit statuses: | Result | Standard output | Standard error | Status | | --- | --- | --- | --- | | All inputs clean | Empty diagnostics document | Empty | `0` | | Diagnostics found | Diagnostics document | Empty | `1` | | An input cannot be read | Diagnostics from successfully read inputs | I/O error | `2` | | Invalid invocation | No JSON document | Error and usage | `2` | An I/O failure may therefore produce an empty or partial diagnostics document. Consumers must check the process status and standard error as well as parse standard output. ## Compatibility Consumers should reject documents whose `schema_version` they do not support. Breaking field or position changes require a new schema version. Consumers should ignore unknown fields within a supported version so compatible fields can be added later. ## tree-sitter-fluent # `tree-sitter-fluent` `tree-sitter-fluent` is an error-tolerant Tree-sitter grammar for Project Fluent FTL. It supports editors, linting, incremental parsing, and source-preserving tools. ## Build the grammar with Nix ```bash nix build .#tree-sitter-fluent ``` The package contains: ```text result/ ├── parser ├── lib/ │ └── libtree-sitter-fluent.so -> ../parser ├── queries/ │ └── highlights.scm └── tree-sitter.json ``` `parser` is the compiled shared library in the layout used by Nixpkgs Tree-sitter grammar packages. The `lib` link uses the platform’s shared-library extension—`.so` on Linux—and lets Emacs find the same library directly. ## Use the Rust bindings The workspace crate exports a `tree-sitter-language` function and the generated node and highlight metadata: ```rust use tree_sitter::Parser; let mut parser = Parser::new(); let language = tree_sitter_fluent::LANGUAGE.into(); parser.set_language(&language)?; ``` The public constants are: | Constant | Contents | | --- | --- | | `LANGUAGE` | Language function for `Parser::set_language` | | `NODE_TYPES` | Generated `src/node-types.json` | | `HIGHLIGHTS_QUERY` | `queries/highlights.scm` | An external Cargo project can depend on the repository directly: ```toml [dependencies] tree-sitter = "0.25" tree-sitter-fluent = { git = "https://github.com/outskirtslabs/fluent-tooling" } ``` ## Grammar behavior The CST exposes default variants as `default_variant` nodes, retains malformed regions as Tree-sitter errors, and recovers later valid entries. The grammar accepts LF, CRLF, and lone CR line endings, negative numeric literals, EOF comments, unindented block placeables, and deeply nested selectors. The highlight query covers the syntax used by `fluent-ts-mode` and other Tree-sitter consumers. Tests compile the query against the generated grammar to keep captures synchronized with node types. ## Grammar sources The repository keeps generated C sources under `src/` so consumers can compile the grammar without running the Tree-sitter generator: | Path | Purpose | | --- | --- | | `grammar.js` | Authoritative grammar definition | | `tree-sitter.json` | Grammar name, version, file types, and repository metadata | | `src/parser.c` | Parser generated from `grammar.js` | | `src/scanner.c` | Handwritten external scanner | | `src/node-types.json` | Generated named-node metadata | | `queries/highlights.scm` | Syntax-highlight captures | Tree-sitter build tools conventionally look for `src/parser.c` and optionally `src/scanner.c` or `src/scanner.cc`. Emacs’s built-in installer follows that layout when it builds this repository’s public grammar recipe. After editing `grammar.js`, regenerate the checked-in parser as described in the [development guide](development.adoc).