taoensso / tower

i18n & L10n library for Clojure/Script
https://www.taoensso.com/tower
Eclipse Public License 1.0
278 stars 24 forks source link

Alternative to using a huge map for translations #41

Closed Frozenlock closed 10 years ago

Frozenlock commented 10 years ago

This is not much an issue, but rather a suggestion (and a question).

Rather than having a huge map for each languages, I prefer to divide my translations locally, in different namespaces. This has the advantage of being:

  1. Prettier. Huge map are ugly;
  2. Easier to maintain. If I remove a namespace, I don't have to think about removing its translation in the language map.

Here's a little example:


(def translation-config (atom ...)) ;; like tconfig
;; Notice the atom

(defn recursive-merge
  "Recursively merge hash maps."
  ([a b]
     (if (and (map? a) (map? b))
       (merge-with recursive-merge a b) b))
  ([a b & more]
     (reduce recursive-merge (recursive-merge a b) more)))

(defn add-translation 
  "Add a given translation to the dictionary. `domain-ks' can be a
  keyword or a collection of keyword." [domain-ks lang-k k-or-ks content]
  (let [ks (if (coll? k-or-ks) k-or-ks [k-or-ks])
        trans-map (assoc-in {} ks content)
        domain-ks (if-not (coll? domain-ks) (vector domain-ks) domain-ks)
        dict @translation-config
        new-dict (update-in dict
                            (concat [:dictionary lang-k] domain-ks)
                            recursive-merge trans-map)]
    (reset! translation-config new-dict)))

The translation map (tconfig) will be updated by each call to add-translation.

Here's what it looks like in another namespace:

(def vali-dict (partial dict/add-translation [:user :registration]))

(vali-dict :en :email "A valid email is required.")
(vali-dict :fr :email "Un courriel valide est nécessaire.")

IMO this is much cleaner than using a map (or a separate file per language).

A little warning however... Because we update the tconfig, we also must be ready to update any t we've generated. I use this:

(def t (tower/make-t @dict/translation-config)) ; Create translation fn

(add-watch dict/translation-config
           ::translation (fn [_ _ _  new]
                           (alter-var-root (var t)
                                           (constantly (tower/make-t @dict/translation-config)))))

What do you think? Am I screwing up big time with an unseen effect?

ptaoussanis commented 10 years ago

Hi there! Sure, new ideas always welcome :-)

Okay, so let me check first that I've understood your motivation correctly (please correct any misunderstandings):

On your implementation:

(:require [taoensso.encore :as encore])
(encore/merge-deep m1 m2 ...)

On the general idea:

I don't think there's anything fundamentally wrong with the approach if you're comfortable with it. I may suggest avoiding the per-entry add-translation fn though. Regular maps are portable, translator friendly, and amenable to inspection with the standard seq API when debugging, etc.

I might suggest a middle ground by defining, say, a ns-level map of however-many translations, and then doing a swap-in! merge-deep for the whole lot at once. That way you've still got the benefit of working with plain data, but you can keep it & merge it wherever you find most convenient.

One sharp edge to be aware of: when you're in production, you'll need to make sure that all your dictionary modifiers have executed before the first call to any t fn. The first call will cache the dictionary's value forever. Does that make sense?

Frozenlock commented 10 years ago

It does, thank vou very much!

Oh, and good catch on the merge-deep function!

On Thu, Apr 3, 2014 at 7:09 AM, Peter Taoussanis notifications@github.comwrote:

Hi there! Sure, new ideas always welcome :-)

Okay, so let me check first that I've understood your motivation correctly (please correct any misunderstandings):

  • You want to define a single dictionary by merging small parts so that each small part is relatively easier to manage.
  • You want to be able to define parts in different namespaces.
  • You don't want to import dictionary parts from external resources (as per the :ja in the `example-tconfig).
  • You want to define individual translation entries with a function.

On your implementation:

  • There's a recursive merge fn already available in one of Tower's dependencies which you may find convenient:

(:require [taoensso.encore :as encore])(encore/merge-deep m1 m2 ...)

  • You might also find encore/swap-in! useful as a substitute for add-translation!.
  • The watch shouldn't be necessary in dev-mode, at least with Tower 2.1. All t fns will automatically recompile any dictionary changes. This may not be true of earlier versions, I don't recall off-hand.

On the general idea:

I don't think there's anything fundamentally wrong with the approach if you're comfortable with it. I may suggest avoiding the per-entry add-translation fn though. Regular maps are portable, translator friendly, and amenable to inspection with the standard seq API when debugging, etc.

I might suggest a middle ground by defining, say, a ns-level map of however-many translations, and then doing a swap-in! merge-deep for the whole lot at once. That way you've still got the benefit of working with plain data, but you can keep it & merge it wherever you find most convenient.

One sharp edge to be aware of: when you're in production, you'll need to make sure that all your dictionary modifiers have executed before the first call to any t fn. The first call will cache the dictionary's value forever. Does that make sense?

— Reply to this email directly or view it on GitHubhttps://github.com/ptaoussanis/tower/issues/41#issuecomment-39438949 .

Frozenlock commented 10 years ago

Why are the given dictionaries paths loaded with io/resource?

With a simple slurp I could load the EDN file wherever it is. But by using io/resource, I need to place my files in the resources directory.

ptaoussanis commented 10 years ago

Resources are more flexible: they're packaged up with your application and can be accessed via the application jar without a concrete file being present and w/o needing to manually distribute your slurps.