Open nathell opened 3 months ago
Sorry it took me so long to get back to it. I now have loaded into my head all the context needed to talk about this :)
I guess it might be potentially beneficial to enable this recursive behaviour in build-translate itself, without having to write a wrapper, but this might break backwards compatibility.
How would that look like? If I understand correctly, you want to allow:
(def translate
(tongue/build-translate
{:en-GB {:country "the UK"}
:en-US {:country "the US"}
:en {:welcome "Welcome to {country}!"}
:ru {:welcome "Добро пожаловать в Россию!"}}))
(translate :en-GB :welcome {:country "Canada"}) ; => "Welcome to Canada"
(translate :en-GB :welcome) ; => "Welcome to the UK"
Basically adding a behaviour that if key is needed but not provided it is looked up in the dictionary again?
Would that completely solve your use-case? I think we can add it despite API breakage (it will only break for missing keys, hope nobody is relying on those). Or would you prefer to have fn interpolation too?
Sorry for the delay, I wanted to think about it some more :)
It turned out that it’d be sometimes handy to apply some transformations to the looked-up key, e.g. capitalize it. I was considering full-blown fn interpolation à la clojure.core.strint; but it’s macro-based, so I think it would be hard to get working with dynamically-constructed translation maps.
Instead, I ended up using a static mapping of transformations. E.g. I have a transformation named capitalize
that lets me say:
(def strings
{:en-GB {:country "the UK"}
:en-US {:country "the US"}
:en {:welcome "{country->capitalize} welcomes you!"}}) ; -> marks transformation; I’d have slightly preferred | but tongue’s interpolation regex doesn’t include it
;; later:
(message :en-US :welcome) ;=> "The US welcomes you!"
Right now, this is the code I have (works with current Tongue):
;; ugly code to make tongue think a fn is a map
#?(:clj
(potemkin/def-map-type FunctionWrappingMap [f]
(get [_ k _] (f k))
(assoc [this k v] this)
(dissoc [this k] this)
(keys [_] [])
(meta [_] nil)
(with-meta [this _] this)))
(defn function-wrapping-map
"Returns a map-like object that responds true to map? and
delegates all lookups to a function f. Don't try to do
anything other than lookup with it."
[f]
#?(:clj (FunctionWrappingMap. f)
:cljs
(reify
IMap
(-dissoc [coll k] coll)
ILookup
(-lookup [coll k] (f k)))))
;; these are the transformations that a keyword might include
(def transformations
{"capitalize" str/capitalize})
;; messages map
(def strings
{:en-GB {:country "the UK"}
:en-US {:country "the US"}
:en {:welcome "{country->capitalize} welcomes you!"}})
;; in my code, this has a more elaborate logic
(defn current-locale []
"en")
(def translate (tongue/build-translate strings))
(declare message)
(defn transform [k]
(let [[s transformation-name] (str/split (name k) #"->")
transformation (get transformations transformation-name)]
(cond-> (message (keyword s))
transformation transformation)))
(defn message
([k] (translate (current-locale) k (function-wrapping-map transform)))
([k m] (translate (current-locale) k (function-wrapping-map #(or (m %) (transform %)))))
([k arg & args] (apply translate (current-locale) k arg args)))
This is arguably hacky, but seems to satisfy my needs right now. WDYT?
I am thinking if there are other use-cases besides UK/US English. For most other languages, you’d have to change the entire string, so this is not as useful?
What do you plan to use this feature for? Some concrete examples? I just don’t want to bring in complexity to such a simple tool. I want it to stay simple, as long as it lets you do what you need.
What
This PR allows to pass a function instead of a map to
translate
for interpolation. E.g. assumingtranslate
is a result of callingbuild-translate
, where previously you could doyou can now also do:
Why
To make Tongue more dynamic. For example, the third arg could be a generic resolver that looks up keys in a DB or some third-party API.
My specific use case is to be able to create a wrapper around
translate
that passes itself as an interpolate fn, recursively. This allows you to have specific substrings that can be used within larger, more generic translations. I have added a test that illustrates this.I guess it might be potentially beneficial to enable this recursive behaviour in
build-translate
itself, without having to write a wrapper, but this might break backwards compatibility. As it stands, the PR is conservative, allowing recursive lookup as an opt-in.