Lightweight i18n using DataScript

A lightweight ;-) i18n solution for ClojureScript that uses a DataScript database. It's inspired by Tower, a Clojure/Script i18n & L10n library.

Update: later found the Tongue library (by Nikita Prokopov).

Database example code

 1(ns yournamespace.dst
 2  (:require [datascript.core :as d]))
 3
 4;; Database schema (only type ref entities need be specified).
 5(def schema {:i18n/dictionary {:db/valueType :db.type/ref}
 6             :dictionary/en-US {:db/valueType :db.type/ref}
 7             :dictionary/nl-NL {:db/valueType :db.type/ref}})
 8
 9;; Database connection.
10(def conn (d/create-conn schema))
11
12;; Log database transactions for debug purposes. BEWARE: nil as a value
13;; is not allowed and should not show up in logs!
14#_(d/listen! conn :log
15           (fn [tx-report]
16             (println (str "DST: " (:tx-data tx-report)))))
17
18;; Initial contents of (in-memory) database.
19(defn init!
20  "Initializes database contents."
21  []
22  {:post [(not (nil?  %))]}
23
24  (d/transact! conn
25               [{:db/id -1
26                 :i18n/key :i18n
27                 :i18n/fallback-locale :dictionary/en-US
28                 :i18n/dictionary
29                 {:db/id -10
30                  :dictionary/en-US {:db/id -100
31                          :main-title "Title"
32                          :main-subtitle "Subtitle"
33                  :dictionary/nl-NL {:db/id -101
34                          :main-title "Titel"
35                          :main-subtitle "Subtitel"
36                          }}}]))
37
38;;---------------------
39;; Initialize database.
40(init!)

The i18n code

 1(ns yournamespace.i18n
 2  (:require [datascript.core :as d]
 3            [yournamespace.dst :as dst]))
 4
 5(defn ^:private _get-lc
 6  "Gets locale if it exists, otherwise fallback locale"
 7  [lc]
 8  {:pre  [(not (nil? lc))]
 9   :post [(not (nil?  %))]}
10
11  (let [lc-is-present?(not
12                       (nil?
13                        (d/q '[:find ?lc .
14                               :in $ ?lci
15                               :where [?e :i18n/key] [?e :i18n/dictionary ?d]
16                               [?d ?lci ?lc]]
17                             @dst/conn lc)))]
18    (if lc-is-present?
19      lc
20      (d/q '[:find ?fl .
21             :in $
22             :where [?e :i18n/key] [?e :i18n/fallback-locale ?fl]]
23           @dst/conn))))
24
25(def ^:private get-lc (memoize _get-lc))
26
27(defn ^:private _t
28  "Returns translation for key based on supplied locale (lc)"
29  [lc key]
30  {:pre  [(not (nil? lc)), (not (nil? key))]
31   :post [(not (nil?  %))]}
32
33  (let [active-lc (get-lc lc)]
34    (if (nil? active-lc)
35      "***i18n - locale error***"
36      (d/q '[:find ?txt .
37             :in $ ?lc ?key
38             :where [?e :i18n/key] [?e :i18n/dictionary ?dic]
39             [?dic ?lc ?dl]
40             [?dl ?key ?txt]]
41           @dst/conn active-lc key))))
42
43(def t (memoize _t))

Caller code example

1(ns yournamespace.caller
2  (:require [yournamespace.i18n :as i18n]))
3
4(let [lc :dictionary/nl-NL]
5  (println (str "Title: " (i18n/t lc :main-title)))
6  (println (str "Subtitle: " (i18n/t lc :main-subtitle))))

Posts in this Series

Translations: