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
fluent
context. - The requested localisation line doesn’t exist in the locale.
- Expected line inputs were missing (e.g. the
path
anddays
args 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
NUMBER
function, only theminimumFractionDigits
,maximumFractionDigits
, andtype
arguments are supported. - The
DATETIME
function 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. }