Permalink
Browse files

NB experimental: add support for arbitrary locale fallbacks

  • Loading branch information...
1 parent 527ff05 commit 46f21ce015d015f5ca37b8fe89a71ba48b8f821d @ptaoussanis committed May 2, 2014
Showing with 72 additions and 42 deletions.
  1. +1 −1 project.clj
  2. +32 −16 src/taoensso/tower.clj
  3. +27 −16 src/taoensso/tower.cljs
  4. +12 −9 test/taoensso/tower/tests/main.clj
View
@@ -11,7 +11,7 @@
*assert* true}
:dependencies
[[org.clojure/clojure "1.4.0"]
- [com.taoensso/encore "1.2.0"]
+ [com.taoensso/encore "1.5.0"]
[com.taoensso/timbre "3.1.6"]
[markdown-clj "0.9.41"]]
@@ -357,13 +357,26 @@
(throw (Exception. (format "Failed to load dictionary from resource: %s"
dict) e))))))
-(def ^:private loc-tree ":en-US-var1 -> [:en-US-var1 :en-US :en]"
- (memoize ; Also used runtime by `translate` fn
- (fn [loc]
- (let [loc-parts (str/split (-> loc locale-key name) #"[-_]")
- loc-tree (mapv #(keyword (str/join "-" %))
- (take-while identity (iterate butlast loc-parts)))]
- loc-tree))))
+(def ^:private loc-tree
+ ":en-US-var1 -> [:en-US-var1 :en-US :en]
+ [:en-US-var1 :fr-FR-var1 :fr] -> [:en-US-var1 :fr-FR-var1 :en-US :fr-FR :en :fr]"
+ (let [loc-tree*
+ (fn [loc]
+ (let [loc-parts (str/split (-> loc locale-key name) #"[-_]")
+ loc-tree (mapv #(keyword (str/join "-" %))
+ (take-while identity (iterate butlast loc-parts)))]
+ loc-tree))]
+ (memoize ; Also used runtime by translation fns
+ (fn [loc-or-locs]
+ (if-not (vector? loc-or-locs)
+ (loc-tree* loc-or-locs) ; Build search tree from single locale
+ (->> loc-or-locs ; Build search tree from multiple desc-preference locales
+ (mapv loc-tree*)
+ (apply encore/interleave-all) ; (reduce into)
+ (encore/distinctv)))))))
+
+(comment (map loc-tree [:en-US [:en-US] [:en-US :fr-FR :fr :en]])
+ (loc-tree ["en_GB" "en_US" "fr_FR" "en"]))
(defn- dict-inherit-parent-trs
"Merges each locale's translations over its parent locale translations."
@@ -461,34 +474,37 @@
find-scoped (fn [d k l] (some #(get-in d [(scope-fn k) %]) (loc-tree l)))
find-unscoped (fn [d k l] (some #(get-in d [ k %]) (loc-tree l)))]
- (fn new-t [loc k-or-ks & fmt-args]
- (let [dict (or dict-cached (dict-compile* dictionary)) ; Recompile (slow)
+ (fn new-t [loc-or-locs k-or-ks & fmt-args]
+ (let [l-or-ls loc-or-locs
+ dict (or dict-cached (dict-compile* dictionary)) ; Recompile (slow)
ks (if (vector? k-or-ks) k-or-ks [k-or-ks])
+ ls (if (vector? l-or-ls) l-or-ls [l-or-ls])
+ loc1 (nth ls 0) ; Preferred locale (always used for fmt)
tr
(or
- ;; Try loc & parents:
- (some #(find-scoped dict % loc) (take-while keyword? ks))
+ ;; Try locales & parents:
+ (some #(find-scoped dict % l-or-ls) (take-while keyword? ks))
(let [last-k (peek ks)]
(if-not (keyword? last-k)
last-k ; Explicit final, non-keyword fallback (may be nil)
(do
(when-let [log-f log-missing-translation-fn]
- (log-f {:locale loc :scope (scope-fn nil) :ks ks
+ (log-f {:locales ls :scope (scope-fn nil) :ks ks
:dev-mode? dev-mode? :ns (str *ns*)}))
(or
;; Try fallback-locale & parents:
(some #(find-scoped dict % fallback-locale) ks)
- ;; Try :missing in loc, parents, fallback-loc, & parents:
+ ;; Try :missing in locales, parents, fallback-loc, & parents:
(when-let [pattern
- (or (find-unscoped dict :missing loc)
+ (or (find-unscoped dict :missing l-or-ls)
(find-unscoped dict :missing fallback-locale))]
- (fmt-fn loc pattern (nstr loc) (nstr (scope-fn nil))
+ (fmt-fn loc1 pattern (nstr ls) (nstr (scope-fn nil))
(nstr ks))))))))]
(if (nil? fmt-args) tr
(if (nil? tr) (throw (Exception. "Can't format nil translation pattern"))
- (apply fmt-fn loc tr fmt-args))))))))
+ (apply fmt-fn loc1 tr fmt-args))))))))
(def ^:private make-t-cached (memoize make-t-uncached))
(defn make-t
@@ -38,13 +38,21 @@
(def my-dict-inline (tower-macros/dict-compile {:en {:a "**hello**"}}))
(def my-dict-resource (tower-macros/dict-compile "slurps/i18n/utils.clj")))
-(def loc-tree ; Crossover (direct)
- (memoize ; Also used runtime by `translate` fn
- (fn [loc]
- (let [loc-parts (str/split (-> loc locale-key name) #"[-_]")
- loc-tree (mapv #(keyword (str/join "-" %))
- (take-while identity (iterate butlast loc-parts)))]
- loc-tree))))
+(def ^:private loc-tree ; Crossover (direct)
+ (let [loc-tree*
+ (fn [loc]
+ (let [loc-parts (str/split (-> loc locale-key name) #"[-_]")
+ loc-tree (mapv #(keyword (str/join "-" %))
+ (take-while identity (iterate butlast loc-parts)))]
+ loc-tree))]
+ (memoize ; Also used runtime by translation fns
+ (fn [loc-or-locs]
+ (if-not (vector? loc-or-locs)
+ (loc-tree* loc-or-locs) ; Build search tree from single locale
+ (->> loc-or-locs ; Build search tree from multiple locales
+ (mapv loc-tree*)
+ (apply encore/interleave-all) ; (reduce into)
+ (encore/distinctv)))))))
(defn make-t ; Crossover (modified)
[tconfig] {:pre [(map? tconfig) ; (:dictionary tconfig)
@@ -69,33 +77,36 @@
find-scoped (fn [d k l] (some #(get-in d [(scope-fn k) %]) (loc-tree l)))
find-unscoped (fn [d k l] (some #(get-in d [ k %]) (loc-tree l)))]
- (fn new-t [loc k-or-ks & fmt-args]
- (let [dict (or dict-cached ; (dict-compile-uncached dictionary)
+ (fn new-t [loc-or-locs k-or-ks & fmt-args]
+ (let [l-or-ls loc-or-locs
+ dict (or dict-cached ; (dict-compile-uncached dictionary)
)
ks (if (vector? k-or-ks) k-or-ks [k-or-ks])
+ ls (if (vector? l-or-ls) l-or-ls [l-or-ls])
+ loc1 (nth ls 0) ; Preferred locale (always used for fmt)
tr
(or
- ;; Try loc & parents:
- (some #(find-scoped dict % loc) (take-while keyword? ks))
+ ;; Try locales & parents:
+ (some #(find-scoped dict % l-or-ls) (take-while keyword? ks))
(let [last-k (peek ks)]
(if-not (keyword? last-k)
last-k ; Explicit final, non-keyword fallback (may be nil)
(do
(when-let [log-f log-missing-translation-fn]
- (log-f {:locale loc :scope (scope-fn nil) :ks ks
+ (log-f {:locales ls :scope (scope-fn nil) :ks ks
:dev-mode? dev-mode? ; :ns (str *ns*)
}))
(or
;; Try fallback-locale & parents:
(some #(find-scoped dict % fallback-locale) ks)
- ;; Try :missing in loc, parents, fallback-loc, & parents:
+ ;; Try :missing in locales, parents, fallback-loc, & parents:
(when-let [pattern
- (or (find-unscoped dict :missing loc)
+ (or (find-unscoped dict :missing l-or-ls)
(find-unscoped dict :missing fallback-locale))]
- (fmt-fn loc pattern (nstr loc) (nstr (scope-fn nil))
+ (fmt-fn loc1 pattern (nstr ls) (nstr (scope-fn nil))
(nstr ks))))))))]
(if (nil? fmt-args) tr
(if (nil? tr) (throw (js/Error. "Can't format nil translation pattern."))
- (apply fmt-fn loc tr fmt-args))))))))
+ (apply fmt-fn loc1 tr fmt-args))))))))
@@ -147,20 +147,23 @@
(expect "Hello Steve, how are you?" (pt :en :example/greeting "Steve"))
(expect Exception ((tower/make-t {:dictionary {}}) :en :anything "Any arg"))
-;;; Missing keys & key fallback
-(expect "|Missing translation: [:en nil [:invalid]]|"
+;;; Missing translations
+(expect "|Missing translation: [[:en] nil [:invalid]]|"
(pt :en :invalid))
(expect nil (pt :de :invalid)) ; No locale-appropriate :missing key
-(expect "|Missing translation: [:en :whatever [:invalid]]|"
+(expect "|Missing translation: [[:en] :whatever [:invalid]]|"
(with-tscope :whatever (pt :en :invalid)))
-(expect "|Missing translation: [:en nil [:invalid]]|"
+(expect "|Missing translation: [[:en] nil [:invalid]]|"
(pt :en :invalid "arg"))
-(expect ":en :example/foo text"
- (pt :en [:invalid :example/foo]))
-(expect "|Missing translation: [:en nil [:invalid :invalid]]|"
+(expect "|Missing translation: [[:en] nil [:invalid :invalid]]|"
(pt :en [:invalid :invalid]))
-(expect "Explicit fallback" (pt :en [:invalid "Explicit fallback"]))
-(expect nil (pt :en [:invalid nil]))
+
+;;; Fallbacks
+(expect ":en :example/foo text" (pt :en [:invalid :example/foo]))
+(expect "Explicit fallback" (pt :en [:invalid "Explicit fallback"]))
+(expect nil (pt :en [:invalid nil]))
+(expect ":de :example/foo text" (pt [:zh :de] :example/foo))
+(expect ":de :example/foo text" (pt [:zh :de] [:invalid :example/foo]))
;;; Aliases
(expect "Hello Bob, how are you?" (pt :en :example/greeting-alias "Bob"))

0 comments on commit 46f21ce

Please sign in to comment.