Skip to content

Instantly share code, notes, and snippets.

@isaksky
Created November 5, 2019 05:05
Show Gist options
  • Save isaksky/d1a5f3a1873dc853f04821392118b756 to your computer and use it in GitHub Desktop.
Save isaksky/d1a5f3a1873dc853f04821392118b756 to your computer and use it in GitHub Desktop.
Handling translations in ClojureScript
(ns front.utilities.i18n-impl
(:require [front.i18n]))
(defn build-domain-index [text-vec]
(let [by-msgid (atom (transient {}))
by-msg (atom (transient {}))]
(doseq [{:keys [s_message s_message_id] :as text} text-vec]
(when-not (clojure.string/blank? s_message_id)
(swap! by-msgid assoc! s_message_id text))
(when-not (clojure.string/blank? s_message)
(swap! by-msg assoc! s_message text)))
{:msgid->text (persistent! @by-msgid)
:msg->text (persistent! @by-msg)}))
(defn build-text-index [text-ary]
(let [text-vec (js->clj text-ary :keywordize-keys true)
domain->texts (group-by :s_domain text-vec)]
(into
{}
(for [[k v] domain->texts]
[k (build-domain-index v)]))))
(defn interpolate-vars [text-str args]
(reduce-kv
(fn [acc k v]
(cond
(keyword? k)
(.replace acc (str "{" (name k) "}") (str v))
:else acc))
text-str
args))
(defn interpret-text [text msg args]
(let [count-arg (get args :count)
base-text-str
(cond
count-arg
(let [text-i18n (get text :texts_i18n)]
(case count-arg
;; FIXME - this assumes the locale uses Plural rule 1, like English.
;; See: https://developer.mozilla.org/en-US/docs/Mozilla/Localization/Localization_and_Plurals#List_of_Plural_Rules
1 (or (first text-i18n) (:s_message text) (first msg))
(or (nth text-i18n 1) (:s_message_plural text) (last msg))))
:else
(get-in
text
[:texts_i18n 0]
(get text :s_message (first msg))))]
(interpolate-vars (or base-text-str "") args)))
(defn get-text [text-index msg args]
(let [e (nth msg 0)
msg (if (qualified-keyword? e)
(subvec msg 1)
msg)
text-entry (cond
(qualified-keyword? e)
(get-in text-index [(namespace e) :msgid->text (name e)])
:else
(get-in text-index ["" :msg->text e]))]
(interpret-text text-entry msg args)))
(defn create-translator [text-ary]
(let [text-index (build-text-index text-ary)]
(reify front.i18n/ITranslate
(-get-text [_ msg args]
(get-text text-index msg args)))))
(ns front.i18n
(:require
[cljs.env :as env]
[cljs.analyzer :as ana]
[cljs.tagged-literals :as tlit]))
;; Adapted from:: https://github.com/thheller/cljs-i18n-api/blob/master/src/cljs/i18n.clj
(def vec-conj (fnil conj []))
(defmacro tr
"Macro that makes strings available for translation (added to icx_master as a compile step).
msg is a literal string or vector.
args are :key/value pairs (optional) to inject into the translated string
if the the argument count is uneven the last argument will be treated as a dynamic map
Examples:
Plain string. Use this form if no context/domain is needed to translate.
(tr \"Ok\")
A vector, where the first member is a qualified keyword. The namespace of the keyword
will be set as the domain, and the message_id will be the name, giving some context
to the translator. It will also be stable over time, even if the string (last arg)
changes.
(tr [:employee.pto-request/approve \"Approve\"])
;; Pluralization, interpolation: (the :count keyword is special)
(tr [:common/pony-brag-message \"I have {count} pony\" \"I have {count} ponies\"] :count 2)
"
[msg & args]
(let [msg (if (or (string? msg) (not (seqable? msg)))
[msg]
msg)
_ (when-not
(every?
#(or (string? %1) (qualified-keyword? %1))
msg)
(throw (ex-info "Only string literals or qualified keywords supported for the translation string."
{:translation-string msg})))
kv-args
(if (even? (count args))
args
(butlast args))
map-arg
(when (odd? (count args))
(last args))
static-args
(-> (apply hash-map kv-args)
(cond->
(map? map-arg)
(merge map-arg)))
current-ns
(-> &env :ns :name)
string-data
{:msg msg
:ns current-ns
:args (into [] (keys static-args))
:line line
:column column}]
;; thheller recommends storing it like this (and not globally),
;; or it will break with caching enabled for the build.
(when env/*compiler*
(swap! env/*compiler* update-in [::ana/namespaces current-ns ::strings] vec-conj string-data))
`(tr*
~msg
~(cond
(or (nil? map-arg)
(= map-arg static-args))
static-args
(and (empty? static-args)
(some? map-arg))
map-arg
:else
`(merge ~static-args ~map-arg)))))
(ns front.i18n
(:require-macros [front.i18n]))
(defprotocol ITranslate
(-get-text [this msg args]))
(defonce translate-ref (atom nil))
(defn set-translator! [tx]
{:pre [(satisfies? ITranslate tx)]}
(reset! translate-ref tx))
(defn tr*
"do not use directly, accesse via the `tr` macro"
[msg args]
(if-some [tx @translate-ref]
(-get-text tx msg args)
(some #(when (string? %1) %1) msg)))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment