Skip to content

fosskers/fluent

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

41 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Fluent

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.

Table of Contents

Compatibility

CompilerStatus
SBCL
ECL
CMUCL
ABCL
Clasp
CCL
Clisp
Allegro
LispWorks

Usage

The examples below use (in-package :fluent) for brevity, but it’s assumed you’ll use a nickname in your own code, perhaps f.

Reading localisations from disk

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.

Localisation lookups

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 and days args above).

Fallback

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.

Limitations

  • Gap lines in multiline text are not supported.
  • Preservation of clever indenting in multiline text is not supported.
  • For the NUMBER function, only the minimumFractionDigits, maximumFractionDigits, and type 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.
    }