From ed672c14c95d0d42f5e3f8891c98af1960e98c73 Mon Sep 17 00:00:00 2001 From: Peter Taoussanis Date: Mon, 17 Oct 2022 17:21:48 +0200 Subject: [PATCH] [new] NB add `new-tr-fn` for creating `tr` partials with fn-local cache Previously the only interface to Tempura's translation was `tempura/tr`. Unfortunately this didn't allow any way to create a `tr` partial with partial-level caching. `tempura/new-tr-fn` now allows the creation of such partials. --- src/taoensso/tempura.cljc | 402 ++++++++++++++++----------------- src/taoensso/tempura/impl.cljc | 193 +++++++++------- 2 files changed, 309 insertions(+), 286 deletions(-) diff --git a/src/taoensso/tempura.cljc b/src/taoensso/tempura.cljc index 5a37d5b..32f7d07 100644 --- a/src/taoensso/tempura.cljc +++ b/src/taoensso/tempura.cljc @@ -1,20 +1,16 @@ (ns taoensso.tempura "Pure Clojure/Script i18n translations library." {:author "Peter Taoussanis (@ptaoussanis)"} - #?(:clj - (:require - [clojure.string :as str] - [taoensso.encore :as enc :refer [have have? qb]] - [taoensso.tempura.impl :as impl :refer []])) - - #?(:cljs - (:require - [clojure.string :as str] - [taoensso.encore :as enc :refer-macros [have have?]] - [taoensso.tempura.impl :as impl :refer-macros []]))) + (:require + [clojure.string :as str] + [clojure.test :as test :refer [deftest is]] + [taoensso.encore :as enc :refer [have have? qb]] + [taoensso.tempura.impl :as impl :refer []])) (enc/assert-min-encore-version [3 23 0]) +(comment (test/run-tests)) + (def ^:dynamic *tr-opts* nil) (def ^:dynamic *tr-scope* nil) @@ -52,52 +48,51 @@ (nth out 1) (do out)))) -(comment (enc/qb 1e4 (compact [:span "a" "b" [:strong "c" "d"] "e" "f"]))) +(comment (enc/qb 1e4 (compact [:span "a" "b" [:strong "c" "d"] "e" "f"]))) ; 7.16 -(def get-default-resource-compiler +(defn get-default-resource-compiler "Implementation detail. Good general-purpose resource compiler. Supports output of text, and Hiccup forms with simple Markdown styles." - (enc/fmemoize - (fn [{:keys [default-tag escape-html? experimental/compact-vectors?] - :or {default-tag :span}}] - - (let [?esc1 (if escape-html? impl/escape-html identity) - ?esc2 (if escape-html? impl/vec-escape-html-in-strs identity) - ?compact (if compact-vectors? (fn [f] (comp compact f)) identity)] - - (enc/fmemoize - (fn [res] ; -> [(fn [vargs]) -> ] - (enc/cond! ; Nb no keywords, nils, etc. - (fn? res) (-> res) ; Completely arb, full control - (string? res) (-> res ?esc1 impl/str->vargs-fn) - (vector? res) - (?compact ; [:span "foo" "bar"] -> "foobar", etc. - (-> res - (impl/vec->vtag default-tag) - impl/vec-explode-styles-in-strs - impl/vec-explode-args-in-strs - ?esc2 ; Avoid for Reactjs - impl/vec->vargs-fn ; (fn [args]) -> result - ))))))))) - -(comment + [{:keys [default-tag escape-html? experimental/compact-vectors?] + :or {default-tag :span}}] + + (let [?esc1 (if escape-html? impl/escape-html identity) + ?esc2 (if escape-html? impl/vec-escape-html-in-strs identity) + ?compact (if compact-vectors? (fn [f] (comp compact f)) identity)] + + (enc/fmemoize ; Ref. transparent, limited domain + (fn [res] ; (fn [vargs]) -> + (enc/cond! ; Nb no keywords, nils, etc. + (fn? res) (-> res) ; Completely arb, full control + (string? res) (-> res ?esc1 impl/str->vargs-fn) + (vector? res) + (?compact ; [:span "foo" "bar"] -> "foobar", etc. + (-> res + (impl/vec->vtag default-tag) + impl/vec-explode-styles-in-strs + impl/vec-explode-args-in-strs + ?esc2 ; Avoid for Reactjs + impl/vec->vargs-fn ; (fn [args]) -> result + ))))))) + +(deftest _get-default-resource-compiler (let [rc (get-default-resource-compiler {:experimental/compact-vectors? #_false true})] - [((rc "Hi %1 :-)") ["Steve"]) - ((rc "Hi **%1** :-)") ["Steve"]) - ((rc ["a **b %1 c** d %2"]) [1 2]) - ((rc ["a" "b"]) [])])) + [(is (= ((rc "Hi %1 :-)") ["Steve"]) "Hi Steve :-)")) + (is (= ((rc "Hi **%1** :-)") ["Steve"]) "Hi **Steve** :-)")) + (is (= ((rc ["a **b %1 c** d %2"]) [1 2]) [:span "a " [:strong "b 1 c"] " d 2"])) + (is (= ((rc ["a" "b"]) []) "ab"))])) (def default-tr-opts {:default-locale :en :dict {:en {:missing "[Missing tr resource]"}} :scope-fn (fn [] *tr-scope*) - :cache-dict? #?(:clj false :cljs true) - :cache-locales? #?(:clj false :cljs true) - :cache-resources? false + :cache-dict? #?(:clj false :cljs :global) + :cache-locales? #?(:clj false :cljs :global) + :cache-resources? false :resource-compiler (get-default-resource-compiler {:escape-html? false}) :missing-resource-fn nil ; Nb return nnil to use as resource @@ -105,6 +100,13 @@ (debugf "Missing tr resource: %s" [locales resource-ids]) nil)}) +(def ^:private merge-into-default-opts + (enc/fmemoize + (fn [opts dynamic-opts] + (merge default-tr-opts opts dynamic-opts)))) + +;;;; + (def example-dictionary {:en-GB ; Locale {:missing ":en-GB missing text" ; Fallback for missing resources @@ -153,58 +155,6 @@ ;;;; -(def ^:private merge-into-default-opts - (enc/fmemoize - (fn [opts dynamic-opts] - (merge default-tr-opts opts dynamic-opts)))) - -(def ^:private scoped - (enc/fmemoize - (fn [locale ?scope resid] - (enc/merge-keywords [locale ?scope resid])))) - -(defn- search-resids* - "loc1 res1 var1 var2 ... base - res2 var1 var2 ... base - ... - loc2 res1 var1 var2 ... base - res2 var1 var2 ... base - ..." - [dict locale-splits ?scope resids] - (reduce - (fn [acc locale-split] - (reduce - (fn [acc resid] - (reduce - (fn [acc lvar] - ;; (debugf "Searching: %s" (scoped lvar ?scope resid)) - (when-let [res (get dict (scoped lvar ?scope resid))] - (reduced (reduced (reduced #_[res resid] res))))) - acc locale-split)) - acc resids)) - nil locale-splits)) - -(def ^:private search-resids*-cached (enc/fmemoize search-resids*)) - -(defn- search-resids [cache? dict locale-splits ?scope resids] - (if cache? - (search-resids*-cached dict locale-splits ?scope resids) - (search-resids* dict locale-splits ?scope resids))) - -#_ -(defmacro vargs "Experimental. Compile-time `impl/vargs`." - [x] - (if (map? x) - (do - (assert (enc/revery? enc/pos-int? (keys x)) - "All arg map keys must be +ive non-zero ints") - (impl/vargs x)) - (have vector? x))) - -#_(comment (macroexpand '(vargs {1 (do "1") 2 (do "2")}))) - -;;;; - #?(:clj (defn load-resource-at-runtime "Experimental, subject to change. @@ -229,122 +179,167 @@ (comment (load-resource-at-compile-time "foo.edn")) -(let [;;; Local aliases to avoid var deref - merge-into-default-opts merge-into-default-opts - scoped scoped - search-resids* search-resids* - search-resids*-cached search-resids*-cached - search-resids search-resids] - - (defn tr - "Next gen Taoensso (tr)anslation API: - - (tr - ;; Opts map to control behaviour: - {:default-locale :en - :dict ; Resource dictionary - {:en {:missing \"Missing translation\" - :example {:greet \"Hello %1\" - :farewell \"Goodbye %1, it was nice to meet you!\"}}}} - - ;; Descending-preference locales to try: - [:fr-FR :en-GB-variation1] - - ;; Descending-preference dictionary resorces to try. May contain a - ;; final non-keyword fallback: - [:example/how-are-you? \"How are you, %1?\"] +(defn- caching [cache? f f*] + (case cache? + (nil false) f + :fn-local (enc/fmemoize f) + f* ; Assume truthy => :global + )) - ;; Optional arbitrary args for insertion into compiled resource: - [\"Steve\"]) +(comment (caching true identity identity)) - => \"How are you, Steve?\" +(let [;;; Global caches + compile-dictionary* (enc/fmemoize impl/compile-dictionary) + expand-locales* (enc/fmemoize impl/expand-locales) + search-resids* (enc/fmemoize impl/search-resids)] + (defn new-tr-fn + "Returns a new translate (\"tr\") function, + (fn tr [locales resource-ids ?resource-args]) -> translation. - Common opts (see `tempura/default-tr-opts` for default vals): + Common opts: - :default-locale ; Optional fallback locale to try when given - ; locales don't have the requested resource/s. + :default-locale ; Optional fallback locale to try when given locales don't + ; have the requested resource/s. + ; Default is `:en`. :dict ; Dictionary map of resources, ; { { ... { }}}. - ; See also `tempura/example-dictionary`. + ; See `tempura/example-dictionary` for more info. - :resource-compiler ; (fn [resource]) -> [(fn [vargs]) -> ]. + :resource-compiler ; (fn [resource]) -> <(fn [vargs]) -> >. ; Useful if you want to customize any part of how ; dictionary resources are compiled. :missing-resource-fn ; (fn [{:keys [opts locales resource-ids resource-args]}]). - ; Called when requested resource/s cannot be - ; found. Useful for logging, etc. May return a - ; non-nil fallback resource value. - - :cache-dict? ; Only reason you'd want this off is if - ; you're using :__load-resource imports and - ; and want dictionary to pick up changes. - - :cache-locales? ; Client will usu. be dealing with a small - ; number of locales, the server often a - ; large number in the general case. `tr` - ; partials may want to enable cached locale - ; expansion (e.g. in the context of a - ; particular user's Ring request, etc.). - - :cache-resources? ; For the very highest possible performance - ; when using a limited domain of locales + - ; resource ids." - - ([opts locales resource-ids] (tr opts locales resource-ids nil)) - ([opts locales resource-ids resource-args] - - (have? vector? resource-ids) - ;; (have? [:or nil? vector? map?] resource-args) - - (when (seq resource-ids) - (let [opts (merge-into-default-opts opts *tr-opts*) - {:keys [default-locale dict scope-fn - cache-dict? #_cache-dict-compilation? - cache-locales? #_cache-locale-expansion? - cache-resources? #_cache-resource-id-searches?]} - opts - - locales (if (nil? locales) [] (have vector? locales)) - dict (impl/compile-dictionary cache-dict? dict) - locale-splits (impl/expand-locales cache-locales? - (enc/conj-some locales default-locale)) - - ?fb-resource (let [last-res (peek resource-ids)] - (when-not (keyword? last-res) last-res)) - resource-ids (if ?fb-resource (pop resource-ids) resource-ids) - - ;; For root scopes, disabling scope, other *vars*, etc. - resid-scope (when-some [f scope-fn] (f)) - - ?matching-resource - (or - (when (seq resource-ids) ; *Any* non-fb resource ids? - (search-resids cache-resources? - dict locale-splits resid-scope resource-ids)) - - ?fb-resource - - ;; No scope from here: - - (when-let [mrf (get opts :missing-resource-fn)] - (mrf ; Nb can return nnil to use result as resource - {:opts opts :locales locales :resource-ids resource-ids - :resource-args resource-args})) - - (search-resids cache-resources? - dict locale-splits nil [:missing]))] - - (when-let [r ?matching-resource] - (let [resource-compiler (get opts :resource-compiler) - vargs (if-some [args resource-args] (impl/vargs args) [])] - - ;; Could also supply matching resid to compiler, but think it'd - ;; be better to keep ids single-purpose. Any meta compiler - ;; options, notes, etc. should be provided with res content. - ((resource-compiler r) vargs)))))))) + ; Called when requested resource/s cannot be found. Useful + ; for logging, etc. May return a non-nil fallback resource + ; value. + + :cache-dict? ; Cache dictionary compilation? Improves performance, + ; usually safe. You probably want this enabled in + ; production, though you might want it disabled in + ; development if you use `:__load-resource` dictionary + ; imports and want resource changes to automatically + ; reflect. + ; + ; Default is `false` for Clj and `:global` for Cljs. + + :cache-locales? ; Cache locales processing? Improves performance, safe iff + ; the returned `tr` fn will see a limited number of unique + ; `locales` arguments (common example: calling + ; `tempura/new-tr-fn` for each Ring request). + ; + ; Default is `false` for Clj and `:global` for Cljs. + + :cache-resources? ; Cache resource lookup? Improves performance but will use + ; memory for every unique combination of `locales` and + ; `resource-ids`. Safe only if these are limited in number. + ; + ; Default is `false`. + + Possible values for `:cach-` options: + + falsey ; Use no cache + `:fn-local` ; Use a cache local to the returned `tr` fn + `:global`/truthy ; Use a cache shared among all `tr` fns with `:global` cache + + Example: + + ;; Define a tr fn + (def my-tr ; (fn [locales resource-ids ?resource-args]) -> translation + (new-tr-fn + {:dict + {:en {:missing \"Missing translation\" + :example {:greet \"Hello %1\" + :farewell \"Goodbye %1, it was nice to meet you!\"}}}})) + + ;; Then call it + (my-tr + [:fr-FR :en-GB-variation1] ; Descending-preference locales to try + + ;; Descending-preference dictionary resorces to try. + ;; May contain a final non-keyword fallback: + [:example/how-are-you? \"How are you, %1?\"] + + ;; Optional arbitrary args for insertion into compiled resource: + [\"Steve\"]) + + => \"How are you, Steve?\" + + See `tempura/default-tr-opts` for detailed default options. + See also `tempura/tr`. + + See GitHub README for more info & examples, Ref. + https://github.com/ptaoussanis/tempura" + + [opts] + (let [opts (merge-into-default-opts opts *tr-opts*) + {:keys [default-locale + dict + scope-fn + cache-dict? + cache-locales? + cache-resources?]} opts + + compile-dictionary (caching cache-dict? impl/compile-dictionary compile-dictionary*) + expand-locales (caching cache-locales? impl/expand-locales expand-locales*) + search-resids (caching cache-resources? impl/search-resids search-resids*)] + + (fn tr + ([locales resource-ids ] (tr locales resource-ids nil)) + ([locales resource-ids resource-args] + + (have? vector? resource-ids) + ;; (have? [:or nil? vector? map?] resource-args) + + (when (seq resource-ids) + (let [dict (compile-dictionary dict) + locales (force locales) + locales (if (nil? locales) [] (have vector? locales)) + locale-splits (expand-locales (enc/conj-some locales default-locale)) + + ?fb-resource (let [last-res (peek resource-ids)] + (when-not (keyword? last-res) last-res)) + resource-ids (if ?fb-resource (pop resource-ids) resource-ids) + + ;; For root scopes, disabling scope, other *vars*, etc. + resid-scope (when-some [f scope-fn] (f)) + + ?matching-resource + (or + (when (seq resource-ids) ; *Any* non-fb resource ids? + (search-resids dict locale-splits resid-scope resource-ids)) + + ?fb-resource + + ;; No scope from here: + + (when-let [mrf (get opts :missing-resource-fn)] + (mrf ; Nb can return nnil to use result as resource + {:opts opts :locales locales :resource-ids resource-ids + :resource-args resource-args})) + + (search-resids dict locale-splits nil [:missing]))] + + (when-let [r ?matching-resource] + (let [resource-compiler (get opts :resource-compiler) + vargs (if-some [args resource-args] (impl/vargs args) [])] + + ;; Could also supply matching resid to compiler, but think it'd + ;; be better to keep ids single-purpose. Any meta compiler + ;; options, notes, etc. should be provided with res content. + ((resource-compiler r) vargs)))))))))) + +(defn tr + "Translate (\"tr\") function, + (fn tr [opts locales resource-ids ?resource-args]) -> translation. + + See `tempura/new-tr-fn` for full documentation, and for fn-local caching." + + ([opts locales resource-ids ] (tr opts locales resource-ids nil)) + ([opts locales resource-ids resource-args] + ((new-tr-fn opts) locales resource-ids resource-args))) (comment (tr {} [:en] [:resid1 "Hello there"]) ; => text @@ -366,7 +361,7 @@ (tr c1 [:en] [:foo :bar]) (with-tr-scope :example (tr c1 [:en] [:foo])) - (qb 1000 + (qb 1e3 (tr c1 [:en] ["Hi %1"] ["Steve"]) (tr c1 [:en] ["Hi %1"] {1 "Steve"}) (tr c1 [:en] [ "Hi **%1**!"] ["Steve"]) @@ -406,9 +401,10 @@ (sort-by m-sort-by (keys m-sort-by)))))))) (comment - (mapv parse-http-accept-header - [nil "en-GB" "da, en-gb;q=0.8, en;q=0.7" "en-GB,en;q=0.8,en-US;q=0.6" - "en-GB , en; q=0.8, en-US; q=0.6" "a," "es-ES, en-US"])) + (enc/qb 1e4 + (mapv parse-http-accept-header + [nil "en-GB" "da, en-gb;q=0.8, en;q=0.7" "en-GB,en;q=0.8,en-US;q=0.6" + "en-GB , en; q=0.8, en-US; q=0.6" "a," "es-ES, en-US"]))) ; 133.9 #?(:clj (defn wrap-ring-request diff --git a/src/taoensso/tempura/impl.cljc b/src/taoensso/tempura/impl.cljc index 19c5cfb..4e90299 100644 --- a/src/taoensso/tempura/impl.cljc +++ b/src/taoensso/tempura/impl.cljc @@ -2,7 +2,7 @@ "Private implementation details." (:require [clojure.string :as str] - #?(:clj [clojure.test :as test :refer [deftest is]]) + [clojure.test :as test :refer [deftest is]] #?(:clj [taoensso.encore :as enc :refer [have have? qb]]) #?(:cljs [taoensso.encore :as enc :refer-macros [have have?]]))) @@ -325,7 +325,25 @@ ;;;; -(def expand-locales +(defn expand-locale + ":en-GB-var1 -> [:en-GB-var1 :en-GB :en], etc." + [locale] + (let [parts (str/split (str/lower-case (name locale)) #"[_-]")] + (if (== (count parts) 1) + [locale] + (loop [[p0 & pn] parts, sb nil, acc ()] + + (let [sb (if sb (enc/sb-append sb "-" p0) (enc/str-builder p0)) + acc (conj acc (keyword (str sb)))] + + (if pn + (recur pn sb acc) + (vec acc))))))) + +(comment (enc/qb 1e5 (expand-locale :en-GB-var1))) ; 91.72 + +(defn expand-locales + [locales] ;; TODO Note that this fallback preference approach might not be ;; sophisticated enough for use with BCP 47, etc. - @@ -335,50 +353,28 @@ ;; strategy later. Indeed might not be necessary if consumers can provide ;; an appropriately prepared input for this fn. - (let [expand-locale - (enc/fmemoize - (fn [locale] - (let [parts (str/split (str/lower-case (name locale)) #"[_-]")] - (mapv #(keyword (str/join "-" %)) - (take-while identity (iterate butlast parts)))))) - - expand-locales* - (fn [locales] - (if (= (count locales) 1) - [(expand-locale (get locales 0))] - (let [[acc _] - (reduce - (fn [[acc seen] in] - (let [lvars (expand-locale in) - lbase (peek lvars)] - (if (seen lbase) - [acc seen] - [(conj acc lvars) (conj seen lbase)]))) - [[] #{}] - locales)] - acc))) - - expand-locales*-cached (enc/fmemoize expand-locales*)] - - ;; Inputs are combinatorial, so can't cache by default: - (fn [cache? locales] - (if cache? - (expand-locales*-cached locales) - (expand-locales* locales))))) + (if (== (count locales) 1) + [(expand-locale (nth locales 0))] + (let [[acc _] + (reduce + (fn [[acc seen] in] + (let [lvars (expand-locale in) + lbase (peek lvars)] -(comment - (qb 1e5 ; [28.12 159.55] - (expand-locales nil [:en-GB-var1]) - (expand-locales nil [:en-US-var1 :fr-FR :fr :en-GD :DE-de]))) + (if (seen lbase) + [acc seen] + [(conj acc lvars) (conj seen lbase)]))) -#?(:clj - (deftest _expand-locales - (is (= [[:en-us-var1 :en-us :en] [:fr-fr :fr] [:de-de :de]] - (expand-locales nil [:en-us-var1 :fr-fr :fr :en-gd :de-de]))) - (is (= [[:en] [:fr-fr :fr] [:de-de :de]] ; Stop :en-* after base :en - (expand-locales nil [:en :en-us-var1 :fr-fr :fr :en-gd :de-de]))) - (is (= [[:en-us :en] [:fr-fr :fr]] ; Never change langs before vars - (expand-locales nil [:en-us :fr-fr :en]))))) + [[] #{}] + locales)] + acc))) + +(comment (enc/qb 1e4 (expand-locales [:en-US-var1 :fr-FR :fr :en-GD :DE-de]))) ; 38.0 + +(deftest _expand-locales + [(is (= (expand-locales [:en-us-var1 :fr-fr :fr :en-gd :de-de]) [[:en-us-var1 :en-us :en] [:fr-fr :fr] [:de-de :de]])) + (is (= (expand-locales [:en :en-us-var1 :fr-fr :fr :en-gd :de-de]) [[:en] [:fr-fr :fr] [:de-de :de]])) ; Stop :en-* after base :en + (is (= (expand-locales [:en-us :fr-fr :en]) [[:en-us :en] [:fr-fr :fr]]))]) ; Never change langs before vars #?(:clj (def ^:private cached-read-edn (enc/fmemoize enc/read-edn))) (defn load-resource [rname] @@ -402,46 +398,35 @@ (comment (load-resource "foo.edn")) -(def compile-dictionary - (let [preprocess ; For pointers and slurps, etc. +(let [preprocess ; For pointers and slurps, etc. + (fn [dict] + (reduce-kv + (fn rf1 [acc k v] + (cond + (keyword? v) ; Pointer + (let [path (enc/explode-keyword v)] + (assoc acc k (get-in dict (mapv keyword path)))) + + (map? v) + (if-let [io-res (:__load-resource v)] + (assoc acc k (load-resource io-res)) + (assoc acc k (reduce-kv rf1 {} v))) + + :else (assoc acc k v))) + {} dict)) + + as-paths ; For locale normalization, lookup speed, etc. + (enc/fmemoize ; Ref. transparent, limited domain (fn [dict] - (reduce-kv - (fn rf1 [acc k v] - (cond - (keyword? v) ; Pointer - (let [path (enc/explode-keyword v)] - (assoc acc k (get-in dict (mapv keyword path)))) - - (map? v) - (if-let [io-res (:__load-resource v)] - (assoc acc k (load-resource io-res)) - (assoc acc k (reduce-kv rf1 {} v))) - - :else (assoc acc k v))) - {} dict)) - - as-paths ; For locale normalization, lookup speed, etc. - (enc/fmemoize ; Ref transparent - (fn [dict] - (reduce - (fn [acc in] - (let [[locale] in - normed-locale (str/lower-case (name locale)) - in (assoc in 0 normed-locale)] - (assoc acc (enc/merge-keywords (pop in)) (peek in)))) - {} (node-paths map? dict)))) - - compile-dictionary* - (enc/memoize 1000 ; Minor caching to help blunt impact on dev benchmarks - (fn [dict] (-> dict preprocess preprocess as-paths))) - - compile-dictionary*-cached (enc/fmemoize compile-dictionary*)] - - ;; We may want resource reloads in dev-mode, so can't cache by default: - (fn [cache? dict] - (if cache? - (compile-dictionary*-cached dict) - (compile-dictionary* dict))))) + (reduce + (fn [acc in] + (let [[locale] in + normed-locale (str/lower-case (name locale)) + in (assoc in 0 normed-locale)] + (assoc acc (enc/merge-keywords (pop in)) (peek in)))) + {} (node-paths map? dict))))] + + (defn compile-dictionary [dict] (-> dict preprocess as-paths))) (comment (qb 1e4 @@ -463,3 +448,45 @@ (have vector? x))) (comment (qb 1e4 (vargs {1 :a 2 :b 3 :c 5 :d}))) + +;;;; + +(def scoped + (enc/fmemoize + (fn [locale ?scope resid] + (enc/merge-keywords [locale ?scope resid])))) + +(comment (scoped :en :scope :resid)) + +(defn search-resids + "loc1 res1 var1 var2 ... base + res2 var1 var2 ... base + ... + loc2 res1 var1 var2 ... base + res2 var1 var2 ... base + ..." + [dict locale-splits ?scope resids] + (reduce + (fn [acc locale-split] + (reduce + (fn [acc resid] + (reduce + (fn [acc lvar] + ;; (debugf "Searching: %s" (scoped lvar ?scope resid)) + (when-let [res (get dict (scoped lvar ?scope resid))] + (reduced (reduced (reduced #_[res resid] res))))) + acc locale-split)) + acc resids)) + nil locale-splits)) + +#_ +(defmacro vargs "Experimental. Compile-time `impl/vargs`." + [x] + (if (map? x) + (do + (assert (enc/revery? enc/pos-int? (keys x)) + "All arg map keys must be +ive non-zero ints") + (impl/vargs x)) + (have vector? x))) + +#_(comment (macroexpand '(vargs {1 (do "1") 2 (do "2")})))