Liking cljdoc? Tell your friends :D

fluent-clj

Clojars Project cljdoc badge

Project Fluent is very cool. The available Java and Javascript packages are relatively easy to use through interop, but without a unified interface, it's hard to write consistent and testable code.

This library aims to smooth over those differences, making it easy to build your own translation system.

Example

(require '[noahtheduke.fluent :as i18n])

;; A resource is a string of Fluent messages, terms, etc.
(def sample-resource
  "
hello = Hello world!
welcome = Welcome, {$user}!
email-cnt = {$cnt ->
    [one] {$cnt} email
    *[other] {$cnt} emails
}")
=> #'sample-resource

;; Bundles are native objects that hold the processed Fluent strings. They can be interacted with through interop but generally you only need the provided api functions.
(def bundle (i18n/build "en" simple-resource))
=> #'bundle

;; Message ids can be specified with strings, keywords, or symbols
(i18n/format bundle :hello)
=> "Hello world!"

;; Argument maps are just plain clojure maps
(i18n/format bundle "welcome" {:user "Noah"})
=> "Welcome, Noah!"

;; And their keys can be strings, keywords, or symbols as well
(i18n/format bundle :email-cnt {"cnt" 1})
=> "1 email"

(i18n/format bundle "email-cnt" {:cnt 2})
=> "2 emails"

Usage in an actual application

I built this library for a website that uses Reagent, so I'll share how we do it there.

The translations are stored as both raw text and fluent bundles. During app start, (load-dictionary! "resources/public/i18n") is called to load all of the Fluent files. Then on app load, the client sets a GET request to the server for the desired translation, and stores it locally with insert-lang!. The function tr (below) is modeled after Tempura's api, where a fallback value can be passed in with the desired translation: (i18n/tr :hello) without fallback, (i18n/tr [:hello "sup nerd"]) with fallback.

Done in a .cljc like this, translations can be tested in a normal clojure repl.

(ns example.i18n
  (:require
   [noahtheduke.fluent :as fluent]
   #?(:cljs
     [reagent.core :as r])))

(defonce fluent-dictionary
  #?(:clj (atom nil)
     :cljs (r/atom {})))

(defn insert-lang! [lang content]
  (swap! fluent-dictionary assoc lang {:content content
                                       :ftl (fluent/build lang content)}))

#?(:clj
   (defn load-dictionary!
     [dir]
     (let [langs (->> (io/file dir)
                      (file-seq)
                      (filter #(.isFile ^java.io.File %))
                      (filter #(str/ends-with? (str %) ".ftl"))
                      (map (fn [^java.io.File f]
                             (let [n (str/replace (.getName f) ".ftl" "")
                                   content (slurp f)]
                               [n content]))))
           errors (volatile! [])]
       (doseq [[lang content] langs]
         (try (insert-lang! lang content)
              (catch Throwable t
                (println "Error inserting i18n data for" lang)
                (println (ex-message t))
                (vswap! errors conj lang))))
       @errors)))

(defn get-content
  [lang]
  (get-in @fluent-dictionary [lang :content]))

(defn get-bundle
  [lang]
  (get-in @fluent-dictionary [lang :ftl]))

(defn get-translation
  [bundle id params]
  (when bundle
    (fluent/format bundle id params)))

(defn tr
  ([lang resource] (tr lang resource nil))
  ([lang resource params]
   (let [resource (if (vector? resource) resource [resource])
         [id fallback] resource]
     (or (get-translation (get-bundle lang) id params)
         ;; You can choose to use the fallback directly or use a translation from a different language.
         ;; Project Fluent's javascript implementation has language negotiation libraries already so those can be used directly as desired.
         fallback
         (get-translation (get-bundle "en") id params)))))

Can you improve this documentation?Edit on GitHub

cljdoc is a website building & hosting documentation for Clojure/Script libraries

× close