We are happy to announce that Project Fluent has finally come to Haskell! ![]()
Fluent is a localisation system for natural-sounding translations. It keeps simple messages simple and lets translators express plurals, gender and other grammar when a language needs it.
Core:
- fluent-syntax
The FTL syntax tree and parser, as well as a quasi-quoter to construct resources at compile time. Tested against the upstream reference fixtures from the Fluent project. - fluent
Translation API comprised of Bundle, Locale, and Translate. This library does not provide any concrete instances of Locale, and is thus not intended to be used directly for translating messages. - fluent-effectful
A Fluent effect for the effectful effect system. Can be either single or multi language. Just like fluent, it does not contain any concrete instances of Locale.
Backends:
- fluent-icu
A Locale backed by text-icu. This library re-exports everything needed to translate messages. - miso-fluent
In-browser localisation for miso apps, backed by the browserβs Intl API.
All five packages are version 1.0.0 and licensed under EUPL-1.2.
For usage examples, please have a look at the module documentation on Hackage.
These libraries power our production systems, so we are committed to long-term, responsible maintenance. We are very much looking forward to your feedback, feature requests, and bug reports. ![]()
Why Fluent?
Most localisation tools map each translation one-to-one onto the English string, so English grammar limits every other language.
Letβs take an example from Firefox, adapted from Mozillaβs Fluent 1.0 announcement[1].
We add one line to en.ftl:
tabs-closing = You are about to close { $count } tabs.
In Czech, βtabβ is panely for counts of 2-4 and panelΕ― otherwise. The Czech translator adds the following to cz.ftl:
tabs-closing = { $count ->
[few] ChystΓ‘te se zavΕΓt { $count } panely.
*[other] ChystΓ‘te se zavΕΓt { $count } panelΕ―.
}
The Haskell code is the same for every language:
translate "tabs-closing" ("count", 3 :: Int)
-- Right "ChystΓ‘te se zavΕΓt 3 panely."
The developer only supplies an identifier and variables, while the translator is free to decide the complexity of the translation for the given language. Complexity (or lack thereof) in one language does not affect the code or other languages. Itβs nice not having to glue together strings and conditionals in code.
Mozilla calls this asymmetric localisation. It also covers grammatical case, gender, and more.
Haskell can have nice things, too!
Mozillaβs official Rust implementation, fluent-bundle[2], resolves numbers without locale formatting[3].
On the Haskell side of things, fluent-icu and miso-fluent format numbers, currencies, units and dates for the locale:
translate "price" ("amount", 1234.5 :: Double) enGB
-- Right "1,234.50 euros"
translate "price" ("amount", 1234.5 :: Double) deCH
-- Right "1'234.50 Euro"
This makes our Haskell implementation more feature-complete than the official Rust one. ![]()
Is this LLM slop?
No! Where would be the fun in that? We take great pride in our work[4]. We do use LLMs sometimes[5], but every line of code in our codebase is thoroughly reviewed, rewritten, argued about, rewritten again β¦ by real humans.
Speaking of real humans, Iβd like to take this opportunity to thank the following ones for all the reviewing and arguing and rewriting over the months, and without whom these libraries would never have seen the light of day:
Fluent 1.0: a localization system for natural-sounding translations - Mozilla Hacks - the Web developer blog β©οΈ
In
types/number.rs,FluentNumber::as_stringisself.value.to_string()plus zero-padding forminimumFractionDigits.style,currency, andcurrencyDisplayare parsed and stored inFluentNumberOptions, but nothing reads them when formatting. Thereβs no locale-aware digit grouping and noDATETIMEbuiltin. The same file has a// XXX: Add support for other options.comment on the plural side. β©οΈor maybe we just like pain and have too much time to spare β©οΈ
after all, how else can you complain about them β©οΈ