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.
(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"
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