This is a Common Lisp implementation of Fluent, a modern localisation system.
See also Fluent’s Syntax Guide.
With Fluent, localisations are defined in per-language .ftl files as key-value
pairs. Many localisations are just simple lookups:
check-start = Validating your system.
But Fluent’s strength is in the ability to inject values into the line, as well as perform “selections” based on grammatical rules and plural categories:
check-pconf-pacnew-old = { $path } is older than its .pacnew by { $days ->
[one] 1 day.
*[many] {$days} days.
}
(in-package :fluent)
(let* ((loc (parse (uiop:read-file-string "tests/data/aura.ftl")))
(ctx (localisation->fluent loc :en)))
(resolve ctx "check-pconf-pacnew-old" :path "pacman.conf" :days 1))pacman.conf is older than its .pacnew by 1 day.
Per-locale plural rules are provided by the plurals library.
| Compiler | Status |
|---|---|
| SBCL | ✅ |
| ECL | ✅ |
| CMUCL | ✅ |
| ABCL | ✅ |
| Clasp | ✅ |
| CCL | ✅ |
| Clisp | ❌ |
| Allegro | ✅ |
| LispWorks | ❓ |
The examples below use (in-package :fluent) for brevity, but it’s assumed you’ll
use a nickname in your own code, perhaps f.
Your localisation files must have the extension .ftl and be separated into
different subdirectories by their locale:
i18n ├── ar-SA │ └── your-project.ftl ├── bn-BD │ └── your-project.ftl ├── cs-CZ │ └── your-project.ftl ├── de-DE │ └── your-project.ftl ├── en-US │ └── your-project.ftl
Each subdirectory can contain as many .ftl files as is convenient to you; their
contents will be fused when read.
(in-package :fluent)
(fluent (read-all-localisations #p"i18n"))#S(FLUENT
:LOCALE :EN-US
:LOCALE-LANG :EN
:FALLBACK :EN-US
:FALLBACK-LANG :EN
:LOCS #<HASH-TABLE :TEST EQ :COUNT 5 {120DD70B23}>)
As you can see, you pass a parent directory (i18n/ here), and all .ftl files are
automatically detected.
Once you have a fully formed fluent context, you can perform localisation
lookups. Input args into the localisation line are passed as keyword arguments.
For example, the following message:
check-pconf-pacnew-old = { $path } is older than its .pacnew by { $days ->
[one] 1 day.
*[many] {$days} days.
}
can be resolved like so:
(in-package :fluent)
(let* ((ctx (fluent (read-all-localisations #p"tests"))))
(resolve ctx "check-pconf-pacnew-old" :path "pacman.conf" :days 1))pacman.conf is older than its .pacnew by 1 day.
A condition will be raised if:
- The requested locale doesn’t exist in the
fluentcontext. - The requested localisation line doesn’t exist in the locale.
- Expected line inputs were missing (e.g. the
pathanddaysargs above).
A “fallback locale” was mentioned above, which can be set when you first create
a fluent context:
(in-package :fluent)
(let* ((ctx (fluent (read-all-localisations #p"tests") :fallback :ja-jp)))
(resolve ctx "sonzai-shinai"))大変!
In this case, the line sonzai-shinai had no localisation within the default
:en-us locale, so it defaulted to looking within the Japanese locale. More than
likely English will be your fallback, with your initial :locale being some other
localisation target, as in:
(in-package :fluent)
(fluent (read-all-localisations #p"tests") :locale :ja-jp :fallback :en-us)#S(FLUENT
:LOCALE :JA-JP
:LOCALE-LANG :JA
:FALLBACK :EN-US
:FALLBACK-LANG :EN
:LOCS #<HASH-TABLE :TEST EQ :COUNT 2 {12078F9753}>)
You are free to mutate this fluent struct at runtime or call resolve-with
directly to match a user’s locale settings in a more dynamic way. For instance,
if they change language settings within your app after opening it.
- Gap lines in multiline text are not supported.
- Preservation of clever indenting in multiline text is not supported.
- For the
NUMBERfunction, only theminimumFractionDigits,maximumFractionDigits, andtypearguments are supported. - The
DATETIMEfunction has not been implementation. - Attributes are not available, so the following is not possible:
-brand-name = Aurora
.gender = feminine
update-successful =
{ -brand-name.gender ->
[masculine] { -brand-name } został zaktualizowany.
[feminine] { -brand-name } została zaktualizowana.
*[other] Program { -brand-name } został zaktualizowany.
}