taoensso / tower

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

JIT-selecting preferred Locale from accepted locales list and available locales set/map #43

Closed vvvvalvalval closed 10 years ago

vvvvalvalval commented 10 years ago

I find myself in a situation where I do not anticipate what locales are available to me (i.e the config is unanticipated) and I have a list of accepted locales in preference order (typically from an HTTP Accept-languages header). So I'd like to know, for a particular message, which is the best locale to choose from those that are bore available in my config and accepted. I posted on StackOverflow about this.

In other words, it'd be nice to be able to do something like

(t [:fr-FR :fr :en-US :en] my-tconfig :path/to/message)

but you can't. And I cannot add that functionality in a proper way in my own application because I'd need access to the loc-tree function which is private.

So I propose as a first step towards this to implement a preferred-lang function that may look like this :

(defn preferred-lang 
  "
accepted-locs : seq of accepted locales by preference order
available-locs : seq of the available locales in the dictionary
fallback-loc : optional, default locale if no match found

Selects the best locale given the ordered seq of accepted locales and the set of available locales."
  [accepted-locs available-locs & [fallback-loc]]
  ...
  )
(facts "About preferred-lang"
       (preferred-lang [:fr :en :de] #{:fr :en}) => :fr

       ;; here we see the necessity of loc-tree
       (preferred-lang [:fr-FR :en :de] #{:fr :en}) => :fr 

       (preferred-lang [:fr :en :de] #{:en :de}) => :en

       (preferred-lang [:fr] #{:en} :de) => :de
       )

What do you think? Shall I give it a try and make a pull request from it?

ptaoussanis commented 10 years ago

Hi Valentin,

typically from an HTTP Accept-languages header

Sure. Note that taoensso.tower.utils/parse-http-accept-header may be useful, in case you weren't aware that it's there (it's used for the default Ring middleware).

And I cannot add that functionality in a proper way in my own application because I'd need access to the loc-tree function which is private.

The loc-tree fn just splits a locale into constituent parts. If I've understood what you'd like to do correctly, most of the relevant fallback logic is actually in make-t-uncached.

What do you think? Shall I give it a try and make a pull request from it?

What you're trying to do seems quite reasonable, and would be difficult with Tower currently - so in principle I am on board with adding support if we can do it in a way that's sensible...

Let me think about this a little and I'll get back to you!

Cheers :-)

vvvvalvalval commented 10 years ago

Thanks for the walkthrough, I'd been looking for parse-http-request-header.

Anyway, I'd be glad to contribute, let me know if I can help :).

ptaoussanis commented 10 years ago

However, I couldn't find make-t-uncached anywhere... what have I missed?

There's a large update pending on the dev branch. Any work on this will have to target that.

There's some breaking API changes, so I've been reluctant to merge into master until I'm sure I'm satisfied with the changes.

Anyway, I'd be glad to contribute, let me know if I can help.

I appreciate that, thank you. Am actually experimenting with this right now. It might be possible to get this working with only a minor change, but I'd like to confirm. Will definitely come back to you if there's something you could assist with.

ptaoussanis commented 10 years ago

Okay, I've updated v2.1.0-SNAPSHOT so that translation fns can now take a vector of descending-preference locales. https://github.com/ptaoussanis/tower/commit/46f21ce015d015f5ca37b8fe89a71ba48b8f821d

These'll be searched intelligently. From the updated README:

And even fallback locales. `(t [:fr-FR :en-US] :example/foo)` searches:
  1. `:example/foo` in the `:fr-FR` locale.
  2. `:example/foo` in the `:en-US` locale.
  3. `:example/foo` in the `:fr` locale.
  4. `:example/foo` in the `:en` locale.
  5. `:example/foo` in the fallback locale.
  6. `:missing` in any of the above locales.

This works in both Clojure and ClojureScript versions of Tower.

Also modified the Ring middleware to use all Accept-Language header languages in this way: dictionaries will automatically be searched for translation entries intelligently.

Haven't tested this much (esp. the middleware), so feedback and/or bug reports welcome.

Cheers! :-)

vvvvalvalval commented 10 years ago

Thank you, but I think the preference order is not quite right. In your example, I believe 1-3-2-4-5-6 is more relevant (If you value :fr-Fr more than :en-US, then you probably prefer :fr to :en-US too. That's my use case at least).

The examples in my first post can give you an idea of what I have in mind, if you want I can come up with a more complete one.

Cheers,

ptaoussanis commented 10 years ago

Sure, fixed: https://github.com/ptaoussanis/tower/commit/244e082135c5cf0e5dbba1e02f5772ea1381c9f3

ptaoussanis commented 10 years ago

Note that this search strategy does handle some forms of Accept-Language a bit strangely. For example, a ["en-GB" "en-US" "en"] browser preference will become a [:en-GB :en :en-US] locale preference.

I don't personally think that's a big issue though, since the Accept-Language choices are rarely intentionally specific to that extent.

vvvvalvalval commented 10 years ago

That's what I was thinking about when I said "more complete example", and indeed I think it's an issue. But I agree it's not an emergency, I suggest to file it as an improvement and take the time to think about it.

vvvvalvalval commented 10 years ago

Also, if we go down that road, that suggests changing the Ring middleware too, don't you think?

ptaoussanis commented 10 years ago

But I agree it's not an emergency, I suggest to file it as an improvement and take the time to think about it.

Sure.

Also, if we go down that road, that suggests changing the Ring middleware too, don't you think?

I don't follow, sorry?

vvvvalvalval commented 10 years ago

Well, for example we could make the Ring middleware attach to the request a translation (partial) function that uses the Locales list extracted from its accept-language header.

From what I understand, the current Ring middleware selects "blindly" a best locale for the translations that will result from the request, and attaches a partial translation function for that purpose.

But that new implementation of t allows for a more pessimistic behavior, which is using the whole sequence of accepted as a partial argument, i.e something like

(handler (assoc request :locale (tower/locale-key loc)
                                :tconfig tconfig
                                :t (if tconfig
                                     (partial tower/t accepted-locs tconfig)
                                     (partial tower/t accepted-locs))))

in taoensso.tower.ring/wrap-tower-middleware.

ptaoussanis commented 10 years ago

From what I understand, the current Ring middleware selects "blindly" a best locale for the translations that will result from the request, and attaches a partial translation function for that purpose.

Ahh, gotcha. Yes, I actually made the change as part of the earlier 2.1.0 snapshot: https://github.com/ptaoussanis/tower/commit/0265b7d21effdbd1f7cd48625beb9bbc2a6fbce2 :-)

All the commits on the dev branch are here: https://github.com/ptaoussanis/tower/commits/dev

vvvvalvalval commented 10 years ago

My mistake, I wasn't looking at the right place. Fine then:) Thanks very much for reacting so quickly on this. Bests,