Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,12 @@ be installed independently of each other.
- [@fluent/dedent](https://github.com/projectfluent/fluent.js/tree/main/fluent-dedent)
- [@fluent/dom](https://github.com/projectfluent/fluent.js/tree/main/fluent-dom)
- [@fluent/langneg](https://github.com/projectfluent/fluent.js/tree/main/fluent-langneg)
- [@fluent/prettier-plugin](https://github.com/projectfluent/fluent.js/tree/main/fluent-prettier-plugin)
- [@fluent/react](https://github.com/projectfluent/fluent.js/tree/main/fluent-react)
- [@fluent/sequence](https://github.com/projectfluent/fluent.js/tree/main/fluent-sequence)
- [@fluent/syntax](https://github.com/projectfluent/fluent.js/tree/main/fluent-syntax)

You can install each of the above packages via `npm`, e.g. `npm install @fluent/react`.
You can install each of the above packages via `npm`, e.g. `npm install @fluent/react`.
See the end of this `README` for instructions on how to build `fluent.js` locally.

## Learn the FTL syntax
Expand Down
3 changes: 3 additions & 0 deletions fluent-prettier-plugin/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
esm/*
!esm/package.json
/index.js
6 changes: 6 additions & 0 deletions fluent-prettier-plugin/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.nyc_output
coverage
esm/.compiled
src
test
tsconfig.json
5 changes: 5 additions & 0 deletions fluent-prettier-plugin/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Changelog

## @fluent/prettier-plugin 0.1.0

- Initial release
88 changes: 88 additions & 0 deletions fluent-prettier-plugin/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# @fluent/prettier-plugin ![](https://github.com/projectfluent/fluent.js/workflows/test/badge.svg)

`@fluent/prettier-plugin` is a [Prettier](https://prettier.io/) plugin
built on top of `@fluent/syntax` to format Project Fluent `.ftl`
files. It's part of [Project Fluent][].

[project fluent]: https://projectfluent.org

The formatter normalizes valid Fluent syntax, such as indentation,
newlines, and spacing within placeables and functions. Invalid input
is rejected with an error.

Terms and messages sort alphabetically, giving deterministic output
that is easy to read and helps minimize merge conflicts. Example:

```fluent
account = Account
-brand-name = Foo 3000
welcome = Welcome, { $name }, to { -brand-name }!
```

Both stand-alone comments and comments bound to messages (see the
[syntax guide](https://projectfluent.org/fluent/guide/comments.html))
are preserved, and stand-alone comments keep their original order.
Groups of messages and terms delineated by stand-alone comments are
sorted separately.

See the unit tests for more formatting examples.

## Installation

```sh
npm install --save-dev prettier @fluent/prettier-plugin
```

## How to use

```sh
npx prettier --plugin=@fluent/prettier-plugin --write "**/*.ftl"
```

Add the plugin to the project Prettier config to automatically use it:

```json
{
"plugins": ["@fluent/prettier-plugin"]
}
```

Individual files can opt out of formatting via a `@noformat` or
`@noprettier` ‘pragma’ comment at the top of the file. Example:

```fluent
# @noformat
beta=Beta
alpha=Alpha
```

## Vue support

When using [fluent-vue](https://github.com/fluent-vue/fluent-vue) and
per-component messages in Vue single-file components (SFC), add a
`lang="fluent"` attribute on `<fluent>` custom blocks to tell Prettier which
formatter to use:

```vue
<fluent locale="en" lang="fluent">
account = Account
greeting = Hello, { $name }
</fluent>
```

The [eslint-plugin-vue](https://eslint.vuejs.org/)
[`vue/block-lang`](https://eslint.vuejs.org/rules/block-lang) rule can
be used to enforce this:

```json
{
"vue/block-lang": [
"error",
{
"fluent": {
"lang": "fluent"
}
}
]
}
```
3 changes: 3 additions & 0 deletions fluent-prettier-plugin/esm/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"type": "module"
}
49 changes: 49 additions & 0 deletions fluent-prettier-plugin/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
{
"name": "@fluent/prettier-plugin",
"description": "Prettier plugin for Fluent files",
"version": "0.1.0",
"homepage": "https://projectfluent.org",
"author": "Mozilla <l10n-drivers@mozilla.org>",
"license": "Apache-2.0",
"contributors": [
{
"name": "wouter bolsterlee",
"email": "wouter@bolsterl.ee"
}
],
"main": "./index.js",
"module": "./esm/index.js",
"types": "./esm/index.d.ts",
"repository": {
"type": "git",
"url": "https://github.com/projectfluent/fluent.js.git"
},
"keywords": [
"fluent",
"format",
"ftl",
"i18n",
"internationalization",
"l10n",
"localization",
"prettier",
"prettier-plugin",
"translation"
],
"scripts": {
"build": "tsc",
"postbuild": "rollup -c ../rollup.config.mjs --globals @fluent/syntax:FluentSyntax"
},
"engines": {
"node": "^20.19 || ^22.12 || >=24"
},
"dependencies": {
"@fluent/syntax": "^0.19.0"
},
"devDependencies": {
"@fluent/dedent": "^0.5.0"
},
"peerDependencies": {
"prettier": "^3.0.0"
}
}
93 changes: 93 additions & 0 deletions fluent-prettier-plugin/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import * as fluentSyntax from "@fluent/syntax";
import type { Plugin } from "prettier";

const plugin: Plugin<fluentSyntax.Resource> = {
languages: [
{
name: "Fluent",
parsers: ["fluent"],
extensions: [".ftl"],
linguistLanguageId: 206353404, // see https://github.com/github-linguist/linguist/blob/main/lib/linguist/languages.yml
},
],
parsers: {
fluent: {
parse(text) {
const resource = fluentSyntax.parse(text, { withSpans: true });
const firstJunkEntry = resource.body.find(
entry => entry instanceof fluentSyntax.Junk
);
if (firstJunkEntry) {
throw createParseError(text, firstJunkEntry);
}
return resource;
},
astFormat: "fluent-ast",
hasIgnorePragma(text) {
return Boolean(
text.trimStart().match(/^#{1,3}\s*(?:@noformat|@noprettier)\b/)
);
},
locStart(node) {
return node.span?.start ?? 0;
},
locEnd(node) {
return node.span?.end ?? 0;
},
},
},
printers: {
"fluent-ast": {
print(path) {
return fluentSyntax.serialize(sortResource(path.node), {});
},
},
},
};
export default plugin;

function sortResource(resource: fluentSyntax.Resource): fluentSyntax.Resource {
type SortableEntry = fluentSyntax.Message | fluentSyntax.Term;
function compare(a: SortableEntry, b: SortableEntry): number {
return a.id.name.localeCompare(b.id.name);
}
const entries: fluentSyntax.Entry[] = [];
const pending: SortableEntry[] = [];
for (const entry of resource.body) {
if (
entry instanceof fluentSyntax.Message ||
entry instanceof fluentSyntax.Term
) {
pending.push(entry);
continue;
}
if (pending.length) {
entries.push(...pending.sort(compare));
pending.length = 0;
}
entries.push(entry);
}
entries.push(...pending.sort(compare));
return new fluentSyntax.Resource(entries);
}

type FluentParseError = Error & {
// Optional, but Prettier gives nicer error messages when set.
loc?: { start: { line: number; column: number } };
};

function createParseError(
text: string,
junk: fluentSyntax.Junk
): FluentParseError {
const annotation = junk.annotations[0];
const offset = annotation?.span?.start ?? junk.span?.start ?? 0;
const line = fluentSyntax.lineOffset(text, offset) + 1;
const column = fluentSyntax.columnOffset(text, offset) + 1;
const details = annotation
? `${annotation.code}: ${annotation.message}`
: "Invalid Fluent syntax";
const error: FluentParseError = new Error(`${details} (${line}:${column})`);
error.loc = { start: { line, column } };
return error;
}
Loading