fluent.js: Project Fluent for JavaScript, Split Into Seven Packages
JavaScript implementation of Project Fluent
At a glance
- What is it?
- Fluent.js is the JavaScript implementation of Project Fluent, a localization framework whose translation format, FTL, is designed to be read like natural language while still expressing gender, plurals and conjugations. It ships as seven independent packages, from the parser and bundle through language negotiation to React bindings and a DOM overlay.
- Who is it for?
- fluent.js fits a project whose translators are expected to reason about grammar rather than swap strings, and whose user interface has gender and plural rules that a simple key and value file cannot express, and it does not fit a project that wants one dependency and a flat dictionary of strings.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 154 days ago.
- What is it written in?
- Mainly JavaScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
A Translation Format Meant to Be Read
Fluent.js is a JavaScript implementation of Project Fluent, a localization framework designed to unleash the expressive power of the natural language, and the design goal is stated as keeping simple things simple and making complex things possible.
The syntax used for describing translations is easy to read and understand, and at the same time it allows, when necessary, to represent complex concepts from natural languages like gender, plurals, conjugations and others. That sentence is the whole pitch, and it is worth sitting with, because it is a different goal from the one most translation libraries set.
A typical message file is a map from a key to a string. Fluent's FTL, which stands for Fluent Translation List, is a list of messages written as sentences with placeholders:
hello-user = Hello, { $username }!The difference from a key and value file is that the key and the text are the same thing. The message identifier is the beginning of the sentence, and the translator edits the sentence, which means a translator who does not read the source code still knows exactly which string they are changing, because the identifier is a prefix of it.
The format is specified formally, with an EBNF grammar in the specification repository, and the syntax guide is the document to send a translator.
Seven Packages You Install Separately
Fluent.js consists of a set of packages which have different use cases and can be installed independently of each other, and the README lists them with a link to each directory in the repository:
- @fluent/bundle - @fluent/dedent - @fluent/dom - @fluent/langneg - @fluent/react - @fluent/sequence - @fluent/syntax
Each is installed from npm on its own, for example npm install @fluent/react.
The split is the point rather than an accident of packaging, and the names make the layering fairly clear. The syntax package is the parser for FTL, the bundle package is the runtime that holds parsed resources and formats messages, dedent handles the indentation rules that FTL uses for multi-line values, sequence provides the ordered list of bundles used when several locales are loaded, langneg is the language negotiation component, react is the binding for React applications, and dom is the overlay that renders a translation on top of the page without changing the underlying DOM structure.
So a typical application installs the react package and the bundle package, a server side renderer needs the syntax and bundle packages, and a tool that only wants to parse a file needs the syntax package alone. Nothing forces a dependency on the whole framework.
What the FTL Syntax Is Meant to Express
The README gives the short version of what the format can carry, and it is a good guide to what a project commits to when it picks Fluent.
The simple case is a message with a variable, as in the hello-user example. Then there are the concepts the format exists for: gender, plurals and conjugations. Those are the cases where a key and value system breaks down, because the grammatical form of the output depends on properties of the data rather than on which string you picked, and usually on several properties at once.
A plural rule is the clearest example. The number of items decides which form of the noun phrase is correct, and in a language with more than two forms the choice is not a simple if. A gender rule is worse, because the grammatical gender is a property of the subject, and a translator has to know that the sentence they are writing will agree with a word chosen elsewhere.
The design consequence is that the translator is working with the rules, not with a lookup table. The README frames the invitation in those terms, and the syntax guide and the formal grammar are the reference material for that work.
The other side of the format is that the base syntax stays readable. A file of plain sentences with placeholders is still a file of plain sentences, and that is what keeps the simple cases simple.
Language Negotiation and the Bundle Sequence
Two of the packages exist because localisation is more than picking one language file.
The sequence package provides the ordered list of bundles that the runtime consults. That is the mechanism for fallback: the user requests a locale, the application has a bundle for it, and when a message is missing the runtime falls back through the sequence to a less specific bundle, typically the default language. Without an explicit sequence, a missing message is either an error or an empty string, and both are worse than a fallback that a reader will never notice.
The langneg package is the language negotiation component, which is the part that decides which of the available bundles to use given a list of requested languages and the locales the user actually has. That is a separate problem from fallback, and the separation is the useful part, because a browser sends an Accept-Language header that is a preference list, while a user in the interface may have explicitly chosen a language that is not the browser's first choice.
Together they cover the two cases that most often go wrong in a localised application: a language the user wants that the application does not have, and a message that exists only in the default language. The bundle package is the runtime that both of those feed into, and it is also where a parsed resource becomes a formatted message with arguments substituted.
The React Binding and the DOM Overlay
The two packages a browser application actually uses are the React binding and the DOM overlay, and they solve the same problem in different ways.
The DOM package overlays a translation on top of the page without changing the underlying DOM structure, which is what makes it usable on a page whose markup you do not control. That is the situation in a content management system, in an extension, or in any page assembled from third party templates, where wrapping every string in a component is not an option.
The React package is the conventional path for an application you do control. Components render a message by identifier and take the arguments as props, and the component takes care of looking the message up in the current bundle and of falling back when it is missing.
The distinction between the two is the interesting part for anyone choosing. The React package is the better fit when you own the markup, and the DOM package is the escape hatch for when you do not, and the fact that both exist in the same project means the choice is per surface rather than per project.
That split is also why the package list is longer than a typical i18n library would be. The parser, the runtime, the ordering, the negotiation, the framework binding and the overlay are separate concerns, and a project that needs only the first two does not pay for the rest.
The Monorepo Behind the Packages
The repository is an npm workspaces monorepo, and the root package.json is private, so nothing is published from the root. The workspace list names each package directory, and the root scripts orchestrate the whole set.
Building and testing all packages at once is npm run dist, and the README is careful to expand it, because the alias hides a lot: it is equivalent to running npm run clean, then npm run build --workspaces, then npm run lint, then npm run test, then npm run docs. There are pre- and post- hooks around it, with predist running the clean and postdist running the lint, the tests and the docs generation.
That expansion is the interesting part, because it means a full build is a gate rather than a compile. The lint runs with --max-warnings 0, so a warning fails the build, and the docs are regenerated as part of the same command. Each package can also be built separately by running npm run build in its directory.
The tooling around that is modern and mostly TypeScript. The build uses Rollup, the tests use Vitest, the API documentation is generated with TypeDoc and published to gh-pages, the linting is ESLint 9 with a flat config, and the formatting is Prettier with a root configuration. There is also a fuzzer in the dev dependencies, which for a parser is exactly the right thing to have, and a jsdom dependency for the DOM package tests.
The development requirements are explicit. You need at least Node.js 20.19, and older versions are not supported, and the dependencies are installed once at the root because they are managed as npm workspaces.
Where the Packages Stand and What Is Still Moving
The release history is per package rather than for the repository as a whole, which is the natural consequence of independent packages, and the recent entries show that. The dom package is at 0.10.2 from 2025-07-18, with 0.10.1 before it in March 2025, and the bundle package is at 0.19.1 from 2025-04-02.
Two things follow. First, the versions are not comparable across packages, so a dependency on @fluent/react and a dependency on @fluent/bundle resolve independently, and an update to one does not drag the other along. Second, each package is still on a 0.x line, which is the semver convention for a package that is not yet promising interface stability.
The project is licensed under the Apache License, Version 2.0, and the community is on Discourse at discourse.mozilla.org under the fluent category, which reflects the project's Mozilla roots. The README also invites three kinds of contributor, a localizer who wants a better way to express themselves in their language, a developer trying to make an app localizable and multilingual, and a hacker looking for a project to contribute to.
The repository is not archived, and the last push was on 2026-05-22, so the tree is still being worked on even though the individual packages have not been republished in a while.
Editorial conclusion
fluent.js fits a project whose translators are expected to reason about grammar rather than swap strings, and whose user interface has gender and plural rules that a simple key and value file cannot express, and it does not fit a project that wants one dependency and a flat dictionary of strings. Before adopting it, decide which packages you actually need, because each is installed independently and only @fluent/react and @fluent/dom are for a typical application, and note that the packages move at their own pace, with the dom package at 0.10.2 and the bundle package at 0.19.1 in the recent release history. The project is Apache-2.0 licensed, requires Node.js 20.19 or newer for development, and the last push was on 2026-05-22.
Frequently asked questions
What is Fluent.js and what is FTL?
Fluent.js is the JavaScript implementation of Project Fluent, a localization framework whose syntax is easy to read while still allowing complex concepts from natural languages like gender, plurals and conjugations. FTL stands for Fluent Translation List, the file format used for describing translation resources.
Which packages does Fluent.js publish?
Seven, each installable independently: @fluent/bundle, @fluent/dedent, @fluent/dom, @fluent/langneg, @fluent/react, @fluent/sequence and @fluent/syntax. A typical application installs @fluent/react and @fluent/bundle, and each package is published separately with its own version.
How do I build Fluent.js locally?
Clone the repository, install the dependencies with npm install, and run npm run dist, which is equivalent to npm run clean, npm run build --workspaces, npm run lint, npm run test and npm run docs. The dependencies are managed as npm workspaces, and you need at least Node.js 20.19.
What does the @fluent/langneg package do?
It is the language negotiation component, and @fluent/sequence provides the ordered list of bundles the runtime consults for fallback. Together they cover a user requesting a language the application does not have and a message that exists only in the default language.
When should I use @fluent/dom instead of @fluent/react?
The DOM package overlays a translation on top of the page without changing the underlying DOM structure, which suits pages whose markup you do not control. The React binding is the conventional path for applications where you own the markup and can render messages from components.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/projectfluent-fluent-js)Was this useful?