tonsky / tongue

Do-it-yourself i18n library for Clojure/Script
Eclipse Public License 1.0
307 stars 19 forks source link

i18n for components? #11

Closed cjohansen closed 2 years ago

cjohansen commented 6 years ago

This is a question more than an issue, please let me know if there's a better place to do this.

Working with react-like components it would sometimes be useful if translation keys could contain "markup":

{:en {:email/footer [:p "See our mega cool site " [:a {:href "{1}"} "over here!"]}}

Poor example, but links are the typical use case: you sometimes want to place them in sentences, but breaking the sentence into three parts for i18n is awkward and inflexible across languages.

I was thinking maybe one could device a solution with clojure.walk, but quickly realized I would need to make recursive calls to the translating function, and inside the custom functions you no longer have access to the locale...

(defn component [component]
  (fn [& args]
    (postwalk #(if (string? %)
                 (apply tr LOCALE?? args)
                 %) component)))

;;...

{:en {:email/footer (component [:p "See our mega cool site " [:a {:href "{1}"} "over here!"])}}

Another problem with this approach is that Tongue's specs aren't thrilled with it.

Any thoughts on this? Feels like it would need some help from tongue to pull this through, but maybe there's another way I haven't thought of.

cjohansen commented 6 years ago

Having thought some more on this, I guess what I'm asking for is that instead of tongue's translate only ever returning strings, it would allow dictionaries to hold arbitrary data structures, and walk them to translate any strings within.

In the current implementation, all the internals are private, thus compiling tongue's building blocks on the inside to achieve this is not possible (as far as I can tell). I can see at least two ways to solve this, and would gladly PR either, maybe you have other suggestions as well:

  1. Replace the calls to str/replace in tongue.core/translate with something that accepts any data structure, walks it and performs the string replace on strings within. For string values this will not incur much more overhead than an extra if. This would make arbitrary data as dictionary values a tongue feature.
  2. Replace the str/replace calls in tongue.core/translate with something of an extension point, and making translate public - allowing users to bolt on this functionality on their own.
tonsky commented 6 years ago

I’m not big fan of a walking arbitrary data structures. It might go into places you don’t want it to go.

There’s another issue similar to yours: https://github.com/tonsky/tongue/issues/2

I’ll think more about what could be done. Seems like a useful feature, I even had to work around it once already too.

cjohansen commented 6 years ago

I think supporting this would be genuinely useful, but providing an extension point would make it easy to offer as an opt-in feature.

I imagine something like this would work (not really tested, just sketched real fast):

(defn interpolate-string [s dicts locale args]
  (str/replace s #"\{(\d+)\}" (fn [[_ n]]
                                (let [idx (dec (parse-long n))
                                      arg (nth args idx)]
                                  (format-argument dicts locale arg)))))

(defmulti interpolate (fn [v dicts locale args] (type v)))
(defmethod interpolate :default [s dicts locale args] (interpolate-string s dicts locale args))

(defn translate [dicts locale key & args]
  (let [t (lookup-template dicts locale key)
        v (if (fn? t) (apply t args) t)]
    (interpolate v dicts locale args)))

This would provide an extension point for consumers to allow the data types they care about. The specs would have to be adjusted for this though.

pavel-klavik commented 3 years ago

Hi, how about solving this by passing function as a translation which can return arbitrary Clojure data. The example above would be:

:email/footer (fn [url] [:p "See our mega cool site " [:a {:href url} "over here!"]])

Only when a string is returned, parameters in {} are replaced. It currently works fine but it breaks the spec which we don't use. If one want to use parameter replacement inside a string, one could always call the replacement function from tongue directly.

cjohansen commented 2 years ago

Hi there 👋 I came back to discuss this only to find my own issue from 4 years ago 😄

Tongue currently enforces translation maps to only contain strings. I really want it to contain hiccup. Suggested solution: introduce an interpolation protocol. Make the current interpolation implementation the protocol implementation for String, and do nothing else. This way Tongue can be extended to translate non-string values, without Tongue offering anything other than string translations out of the box.

@tonsky If you think this is an acceptable solution, I will gladly code it up in a PR.

tonsky commented 2 years ago

So this is what we’ll end up with for dictionary definition:

Looks good, let’s do this!

tonsky commented 2 years ago

Just 4 years after the original issue 🙈