Coder Social home page Coder Social logo

taoensso / tower Goto Github PK

View Code? Open in Web Editor NEW
277.0 20.0 24.0 651 KB

i18n & L10n library for Clojure/Script

Home Page: https://www.taoensso.com/tower

License: Eclipse Public License 1.0

Clojure 100.00%
clojure clojurescript epl taoensso translation i18n localization l10n

tower's People

Contributors

cgmartin avatar ifesdjeen avatar martinklepsch avatar mopemope avatar mylesmegyesi avatar philipa avatar ptaoussanis avatar robert-stuttaford avatar yogthos avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

tower's Issues

Alternative to using a huge map for translations

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?

Clarification

Really the README introduction could use a little more work. I would love to do it and already started adding some semi-colons to the examples for better cargo-cult copy/paste but.... for example:

(ns my-app (:require [taoensso.tower :as tower
                      :refer (with-locale with-tscope t *locale*)])) ; ns

and

(def my-tconfig
  {:dev-mode? true
   :fallback-locale :en
   :scope-var #'*tscope*
  ...

will fail. What is earmuffed tscope? How does this relate to the lib? The term is never properly introduced, nor is the map. The Var my-tconfig is declared and assigned a value but then never used again in any of the examples? Also the (Date.) doesn't work out of the box without java.util namespaces and I would love to pull request some of those changes but it would be a bit helpful as to explain why using e.g. (t :en-US :example/foo) doesn't give the result that is claimed in the README.

Something left in old documentation from the 1.7 version?? Either way, this write-up really needs to get a bit better introduction of those concepts, e.g. begin by showing how to load an inline map, and only after that more to how to load a file. Now, only the latter form is briefly clear to me, the other (memory) I'm left in the dark.

Translations with fallback

Hi,

While using Tower on a hobby project, I noticed that there doesn't seem to be any easy way to fall back on defaults, because taoensso.tower/t returns a string even when the translation is missing. Such a feature would come in handy e.g. when I want to customize the form submit button text on a per form basis but want a default to fall back on unless I've specified one.

Based on a quick look at the source, I think we could do so fairly easily without breaking backwards compatibility. What would you say if t would optionally accept a seq of possible keys to try instead of just scalar values? Something like:

(t [:profile.edit/submit :app.actions/submit]) ; => "Save profile" or "Save changes"
(t :profile.new/submit) ; => "**:this-is-invalid**"

The implementation would be fairly straightforward:

  1. If given a seq, take first value in seq and run the function normally
  2. In the last or, just before the :missing-translation-fn, check if we have a seq with other remaining values
  3. If yes, call the function again with rest of the seq as an argument
  4. If the seq is empty or we have a scalar value, proceed with the :missing-translation-fn

If you think this makes sense, I can go ahead and write a patch. Or let me know if I missed something.

Janne

Dependency problem

With 3.1.0-beta4 I get this exception:

#error {
 :cause Insufficient com.taoensso/encore version (< 1.21). You may have a Leiningen dependency conflict (see http://goo.gl/qBbLvC for solution).
 :data {:min-version 1.21}
 :via
 [{:type clojure.lang.ExceptionInfo
   :message failed compiling ...

I don't have encore as a direct dependency in my project. The dependency I import is:
[com.taoensso/tower "3.1.0-beta4" :exclusions [com.taoensso/encore]].

I believe it is the same problem as this: taoensso/timbre#143

Replace config atom

Currently configuration is done via the config atom. If we were to move away from a global, behind-the-scenes configuration and bindings to a closure based approach (see #27), then we'll need an alternative way of handling configuration.

Currently configuration handles:

  • Dev mode (toggles dictionary reloading)
  • Default locale
  • Missing translation treatment

Two options that come to mind are passing the configuration to each call to t and to do away with configuration entirely.

Option 1: Configuration as an argument to t

I personally don't like this option since it makes calls to t quite verbose, although that can be partly remedied by the use of partial. There's also something odd about passing the same config to the same function all across the app (the likely usage scenario).

If we go this route, the likely result will be something like this:

(ns my-project.core)

(def dict (load-dict))
(def config {:dev-mode? (are-we-in-dev?)
           :default-locale :en
           :log-missing-translation!-fn my-log-missing-translation!)

(def t (partial tower/t dict config))

Option 2: Doing away with configuration

Perhaps configuration is not even necessary? If we look at the configuration options above:

  • Dictionary reloading seems quite workflow dependent and could anyway be handled outside the library with add-watch
  • Default locale is something I usually want to handle within my application logic rather than in the I18n library config
  • We could offer a few different translation functions (e.g. t, t-or-die) that would handle the missing translations. Or just return nil and leave it to the library user to decide what to do

Please document decorators

Unless I'm missing something - there's no documentation about decorators?

I'd would find this useful - every time I forget what they mean I end up reading the source.

Named interpolations?

Hi,

What do you think about named interpolations?

For example:

Given that example.greeting == "Hello %{name}, how are you?"

(t :example/greeting {:name "Steve"})
;; => "Hello Steve, how are you?"

Parent scopes are no longer searched for missing translations in 2.0.0

I just upgraded my project to tower 2.0.0 from 1.2.0, and it seems that parent scopes are no longer searched for missing translations. If I have the dictionary:

{:en
 {:missing "<Translation missing: %s>"
  :error
  {:not-found "The requested resource %s was not found."}}}

and I call

(with-tscope :error
  (t :en my-tconfig :conflict conflicting-id))

then I get a NullPointerException because the key :error/missing is not found. I have to add another :missing translation in the :error subdictionary for this to work.

Is this behavior intentional? The comment at https://github.com/ptaoussanis/tower/blob/master/src/taoensso/tower.clj#L438 seems to indicate that parent scopes should be searched for missing translation messages.

Here are traces of the behavior with and without the extra missing translation key:

user> (tower/with-tscope :error
       (tower/t :en
                {:fallback-locale :en
                 :dictionary {:en
                              {:missing "<Translation missing: %s>"
                               :error
                               {:not-found "The requested resource %s was not found."}}}}
                :conflict
                1))
TRACE t8492: (taoensso.tower/t :en {:fallback-locale :en, :dictionary {:en {:error {:not-found "The requested resource %s was not found."}, :missing "<Translation missing: %s>"}}} :conflict 1)
TRACE t8493: | (taoensso.tower/translate :en {:fallback-locale :en, :dictionary {:en {:error {:not-found "The requested resource %s was not found."}, :missing "<Translation missing: %s>"}}} :taoensso.tower/scope-var :conflict 1)
TRACE t8494: | | (taoensso.tower/fmt-str :en nil 1)
NullPointerException   java.util.regex.Matcher.getTextLength (Matcher.java:1234)
user> (tower/with-tscope :error
       (tower/t :en
                {:fallback-locale :en
                 :dictionary {:en
                              {:missing "<Translation missing: %s>"
                               :error
                               {:not-found "The requested resource %s was not found."
                                :missing "<Translation missing: %s>"}}}}
                :conflict
                1))
TRACE t8499: (taoensso.tower/t :en {:fallback-locale :en, :dictionary {:en {:error {:not-found "The requested resource %s was not found.", :missing "<Translation missing: %s>"}, :missing "<Translation missing: %s>"}}} :conflict 1)
TRACE t8500: | (taoensso.tower/translate :en {:fallback-locale :en, :dictionary {:en {:error {:not-found "The requested resource %s was not found.", :missing "<Translation missing: %s>"}, :missing "<Translation missing: %s>"}}} :taoensso.tower/scope-var :conflict 1)
TRACE t8501: | | (taoensso.tower/fmt-str :en "&lt;Translation missing: %s&gt;" ":en" ":error" "[:conflict]")
TRACE t8501: | | => "&lt;Translation missing: :en&gt;"
TRACE t8502: | | (taoensso.tower/fmt-str :en "&lt;Translation missing: :en&gt;" 1)
TRACE t8502: | | => "&lt;Translation missing: :en&gt;"
TRACE t8500: | => "&lt;Translation missing: :en&gt;"
TRACE t8499: => "&lt;Translation missing: :en&gt;"
"&lt;Translation missing: :en&gt;"

AssertionError from make-t when map with :missing is loaded from external resource

Hi again, Peter!
3.1.0-beta1 fixed the other issue I reported, but unfortunately there appears to be regression from 3.0.2 when locale maps are loaded from external resources. Specifically, if a :missing key happens to be present in the external resource map, an AssertionError is thrown from make-t:

java.lang.AssertionError: Condition failed in taoensso.tower:516 [pred-form, val]: [(map? (load1 dict)), #[ExceptionInfo clojure.lang.ExceptionInfo: Failed to load dictionary from resource: |Missing translation: [%1$s %2$s %3$s]| {:dict "|Missing translation: [%1$s %2$s %3$s]|"}]]
at taoensso.encore$assertion_error.invoke(encore.clj:314)
at taoensso.encore$hthrow.doInvoke(encore.clj:327)
at clojure.lang.RestFn.invoke(RestFn.java:533)
at taoensso.tower$fn__15838$fn__15853.invoke(tower.clj:516)
at taoensso.tower$fn__15838$fn__15853$iter__15860__15864$fn__15865.invoke(tower.clj:526)
at clojure.lang.LazySeq.sval(LazySeq.java:40)
at clojure.lang.LazySeq.seq(LazySeq.java:49)
at clojure.lang.Cons.next(Cons.java:39)
at clojure.lang.RT.next(RT.java:598)
at clojure.core$next.invoke(core.clj:64)
at clojure.core.protocols$fn__6086.invoke(protocols.clj:146)
at clojure.core.protocols$fn__6057$G__6052__6066.invoke(protocols.clj:19)
at clojure.core.protocols$seq_reduce.invoke(protocols.clj:31)
at clojure.core.protocols$fn__6078.invoke(protocols.clj:54)
at clojure.core.protocols$fn__6031$G__6026__6044.invoke(protocols.clj:13)
at clojure.core$reduce.invoke(core.clj:6289)
at clojure.core$into.invoke(core.clj:6341)
at taoensso.tower$fn__15838$fn__15853.invoke(tower.clj:523)
at taoensso.tower$fn__15838$fn__15853$iter__15860__15864$fn__15865.invoke(tower.clj:526)
at clojure.lang.LazySeq.sval(LazySeq.java:40)
at clojure.lang.LazySeq.seq(LazySeq.java:49)
at clojure.lang.RT.seq(RT.java:484)
at clojure.core$seq.invoke(core.clj:133)
at clojure.core.protocols$seq_reduce.invoke(protocols.clj:30)
at clojure.core.protocols$fn__6078.invoke(protocols.clj:54)
at clojure.core.protocols$fn__6031$G__6026__6044.invoke(protocols.clj:13)
at clojure.core$reduce.invoke(core.clj:6289)
at clojure.core$into.invoke(core.clj:6341)
at taoensso.tower$fn__15838$fn__15853.invoke(tower.clj:523)
at taoensso.tower$fn__15937$fn__15945.doInvoke(tower.clj:632)
at clojure.lang.RestFn.invoke(RestFn.java:423)
at taoensso.tower$make_t_uncached$compile1__16006.invoke(tower.clj:695)
at clojure.lang.AFn.applyToHelper(AFn.java:152)
at clojure.lang.AFn.applyTo(AFn.java:144)
at clojure.core$apply.invoke(core.clj:624)
at taoensso.encore$memoize_STAR_$fn__14359$fn__14368$fn__14370.invoke(encore.clj:1373)
at clojure.lang.Delay.deref(Delay.java:37)
at clojure.core$deref.invoke(core.clj:2200)
at taoensso.encore$memoize_STAR_$fn__14359.doInvoke(encore.clj:1363)
at clojure.lang.RestFn.invoke(RestFn.java:397)
at taoensso.tower$make_t_uncached$fn__16010.invoke(tower.clj:699)
at taoensso.tower$make_t_uncached$new_t__16012.doInvoke(tower.clj:702)

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

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?

Consider clj18n-style `t` closure

As an alternative to the compiled dictionary atom, consider a (ƒ create-t [config|dict]) that calls a (memoized) expand-dict fn and returns a t closing over the expanded dict (reload logic can be added to the t closure as necessary). A (ƒ with-t [config|dict]) can setup bindings for dev/non-Ring applications, and the Ring wrapper can add a :t key to requests. Ref. https://github.com/asmala/clj18n.

PROS

  • Dictionary becomes an explicit arg which is cleaner (particularly for multi-dictionary apps).
  • Anything else?

CONS

  • More cumbersome API for non-Ring / non-binding applications, and for REPL dev.
  • May need config atom anyway for non-translation features. Could drop the watch though.
  • Anything else?

My current inclination is to say this probably doesn't buy us enough to make the refactoring worthwhile. I've never been a fan of the config atoms though. Will definitely consider this in the context of a larger refactoring (to facilitate #25 for example).

Thoughts / experimental PRs welcome.

Semantics of fallback in case of invalid locale

Hi @ptaoussanis

I was bitten recently by the InvalidLocale exception when one of the consumers of my code sent in a locale like "foobar" which doesn't exist.

My naive understanding was that this would be handled by translate or t by falling back to the fallback locale specified in my config. I got around the problem by wrapping locale-key catching the exception and falling back to my prefered locale.

I wanted to start this discussion with a view to clarify the semantics of fallback.

Wouldn't it be better for translate to fallback when given an invalid locale?

Internationalized site not crawled by search engines?

I noticed that while I can easily rank up in the default language (en), my website doesn't appear at all in other languages.

I read the Google best practices and the mention how one should use a different URL for a different language.

Should I change my site structure to have multiple urls, or is there a way to get tower powered sites to be crawled in all languages?

using figwheel in translation

We are used to the rapid feedback cycle that figwheel gives us in development. So we tried to make translation fit in nicely, but can't seem to get it to work. Any idea what could cause this?

We are trying to achieve this by adding figwheel-always to the metadata (see below). What we expect is that the dictionary reloads for every cljs file that we save. But it doesn't. When I eval the complete namespace (emacs cider-eval-buffer) the dictionary does reload. Especially that last fact puzzles me.

(ns ^:figwheel-always relations-for-jira.localization
  (:require [taoensso.tower :as tower :include-macros true]))

(def ^:private tconfig
  {:fallback-locale :en-US
   :dev-mode? true
   :compiled-dictionary (tower/dict-compile* "i18n/dictionary.clj")})

(def t (tower/make-t tconfig))

How to use with clojurescript

Hello Peter, I was wondering if you could please point to some examples/resources to better understand how to use tower in cljs. The part I don't get in the README.md example is:

;; Inlined (macro) dict => this ns needs rebuild for dict changes to reflect:
   :compiled-dictionary (tower-macros/dict-compile "my-dict.clj")

And i'm using it like this:

(ns tower-example.core
  (:require [om.core :as om :include-macros true]
            [om.dom :as dom :include-macros true]
            [taoensso.tower :as tower :refer-macros (with-tscope)]
            [tower-example.brepl]))

(enable-console-print!)

(def ^:private tconfig
  {:fallback-locale :en
   ;; Inlined (macro) dict => this ns needs rebuild for dict changes to reflect:
   :compiled-dictionary (tower/dict-compile "i18n/dictionary.clj")})

(def t (tower/make-t tconfig)) ; Create translation fn

(def app-state (atom {:text "Hello world!"
                      :text2 (t :es tconfig :default/greeting)}))

(om/root
  (fn [app owner]
    (dom/h1 nil (:text app))
    (dom/h3 nil (:text2 app)))
  app-state
  {:target (. js/document (getElementById "app"))})

When I load the page in the browser, I get in the console:

TypeError: taoensso.tower.dict_compile is undefined

I'm sorry if I'm ignoring too much, but I've been googling for it but I don't find any answers.

Thank you for your time.

Wrong Swedish currency output

(fmt :sv-SE 200 :currency)
=> "200,00 kr"

All is correct

(fmt :sv-SE 2000 :currency)
=> "2á000,00 kr"
;; Should be => "2000,00 kr"

This happens for every thousand:

(fmt :sv-SE 2000000 :currency)
=> "2á000á000,00 kr"
;; Should be => "2000000,00 kr"

Implement `dictionary->xliff`, `xliff->dictionary` (or equivalents)

Haven't done any research yet on this myself, but it'd be nice (and hopefully pretty straight-forward) to provide some basic tools for translating Tower dictionaries to & from an industry-standard format.

This way we get things like translation-change-tracking (and a bunch of other useful stuff) for free, and it'll be easier to interface with professional translators.

Issue with ring middleware

Right I have an Android device set to Spanish (the entire device). The Accept-Language header appears like this: "es-ES, en-US". The issue is that it still prefers the en-US locale when it should use the first one. The issue is in the parse-http-accept-header function.

(defn parse-http-accept-header
  "Parses HTTP Accept header and returns sequence of [choice weight] pairs
  sorted by weight."
  [header]
  (->> (for [choice (->> (str/split (str header) #",")
                         (filter (complement str/blank?)))]
         (let [[lang q] (str/split choice #";")]
           [(-> lang str/trim)
            (or (when q (Float/parseFloat (second (str/split q #"="))))
                1)]))
       (sort-by second) reverse))

Simply change

 (sort-by second) reverse

To

(sort-by second >)

As this accomplishes the same thing and preserves the order.

user=> (parse-http-accept-header "es-ES, en-US")
(["es-ES" 1] ["en-US" 1])
user=> (parse-http-accept-header "en-GB  ,  en; q=0.8, en-US;  q=0.6")
(["en-GB" 1] ["en" 0.8] ["en-US" 0.6])
user=> (parse-http-accept-header "en-GB")
(["en-GB" 1])
user=> (parse-http-accept-header "en-GB,en;q=0.8,en-US;q=0.6")
(["en-GB" 1] ["en" 0.8] ["en-US" 0.6])
user=> (parse-http-accept-header "en-GB;q=0.1,en;q=0.8,en-US;q=0.6")
(["en" 0.8] ["en-US" 0.6] ["en-GB" 0.1])

`dict-compile` macro doesn't evaluate symbols

I'm sure the title is explanation enough for seasoned macro-writers like yourselves, but for completeness:

(def dict {:en {:greetings {:hello "world"}}})
(def compiled-dict (tower-macros/dict-compile dict))

fails because the library functions that actually compile the dictionary are given the symbol dict instead of the map.

I'm new enough to Clojure that anything but simple macros make my head spin: I've tried and failed to fix this. I know it's easily worked around by just putting the dictionary in another file and passing in a string but I'm filing this anyway as I'm guessing this should be fixed/documented at some point.

translate not working for locales fallback

I wanted to use your library, but it's severly broken for tower/t(ranslate). As you describe in the README, (translate :en-US :example/foo) searches in :en-US, in :en and then in the fallback-locale (by default :en). But in reality that is not the case. It is really only searched in :en-US and in the fallback locale. When testing with another locale (:de-DE) that error hit me:

(tower/t :de-DE {:fallback-locale :en :dictionary {:en {:example {:today "today"} }, :de {:example {:today "heute"} } } :example/today)

gives "today" instead of "heute". Giving a :log-missing-translation-fn prints that :example/today was not found (missing).

Examining the code I was confirmed. tower.clj:426 (in 2.0.1) searches only for exact locale matches, because 'loc' is not handled. The comment (Try loc & parents) is unclear or misleading, because this line only tries the different given keys from k-or-ks, but not the 'parent' locale (:de for :de-DE, or :de-DE and :de for :de-DE-somevariant). The code afterwards (the second form of the 'or') handles only the missing case (fallback value or fallback locale), but again wrong (line 436) with the same type of error but the same comment (above).

Using .toLanguageTag() instead of .toString() on Locale

Hi Peter,

I am not entirely sure if all the web frameworks actually follow that standard, but there's an IETF BCP 47 standard for Language Tags, which is usually used for creating language files.

just a bit more info for consideration: http://docs.oracle.com/javase/7/docs/api/java/util/Locale.html#toLanguageTag()

Do you think it's possible to use Locale#toLanguageTag instead of Locale#toString

p.s. also, on a completely unrelated note, would you mind if I write a couple of unit tests? :) I'm not that fast in writing them, as we're currently prototyping something new, but I happen to have a minute here and there during evening.

ClojureScript support

This is a completely unbaked idea; have given it zero consideration. Would love to see something happen long-term though, so open to ideas/comments/use-cases to start warming this up...

Script support for languages

I'm writing this on my mobile phone so sorry if I'm describing this too shortly.

On JVM languages the language tags are considered to be in format language-region-variant. This is valid, but makes it impossible to select translation based on script, which is kind of a deal breaker for me.

To give an example, we could use Serbian language. There are versions sr-Latn-RS (Serbian with latin script in Serbia), sr-Cyrl-RS (Serbian with cyrillic script in Serbia), sr-Latn-MN (Serbian with latin script in Montenegro), sr-Cyrl-MN (Serbian with cyrillic script in Montenegro). There is currently no way of providing script information to the library.

Another example is Chinese, where zh-Hans (simplified Chinese) is used in mainland and Singapore, whereas zh-Hant is used in Taiwan and Hong Kong. It would be really useful to have generic localizations for simplified and traditional Chinese and some country specific localizations for example for Taiwan and Singapore.

Now there are simple fixes and more complicated fixes, the most simple would be to require the language tags to be in form language-script-region-variant, but that would break backwards compatibility. The more complicated version would require regexps and/or parsing etc. Which version you would like to have a pull request of?

make-t fails for a dictionary containing language tag :id

The following tests demonstrate the issue. The exception is a bit different when there's just the language subtag (:id) or language and region subtags (:id-ID):

(use 'taoensso.tower)
=> nil
(def tower-conf-id {:dictionary {:id {:example {:foo "bar"}}}})
=> #'user/tower-conf-id
(make-t tower-conf-id)
ArityException Wrong number of args (1) passed to: encore/fn--8871  clojure.lang.AFn.throwArity (AFn.java:429)

(def tower-conf-id-ID {:dictionary {:id-ID {:example {:foo "bar"}}}})
=> #'user/tower-conf-id-ID
(make-t tower-conf-id-ID)
AssertionError Assert failed: (>= (count path) 3)  taoensso.tower/dict-compile-path (tower.clj:445)

I found out that the root cause is a peculiarity of java.util.Locale:

Locale's constructor has always converted three language codes to their earlier,
obsoleted forms: he maps to iw, yi maps to ji, and id maps to in. This continues
to be the case, in order to not break backwards compatibility.

http://docs.oracle.com/javase/8/docs/api/java/util/Locale.html

Demonstrated by this snippet:

(str (java.util.Locale. "id"))
=> "in"

Which in turn affects tower's kw-locale and further loc-tree:

(kw-locale :id)
=> :in
(loc-tree :id)
=> [:in]

Which eventually causes dictionary compilation to fail when a dictionary contains the :id language.

An easy way to fix this would be in kw-locale (tower.clj line 74) to call

(.toLanguageTag jvm-loc)

instead of

(str jvm-loc)

(As a nice side-effect, this would also allow removing the (str/replace "_" "-") call in kw-locale.)

The downside is that this would be a breaking change for someone using tower in a way that relies on the :id -> :in mapping, i.e. having the obsolete language tag in their dictionary. So the exact same failure would apply for a dictionary with the obsolete :in tag as now applies for one with :id.

A better fix would be something that worked for both the current and obsolete forms of the locale. I'm not familiar enough with tower's code to suggest a concrete modification for this purpose.

Any ideas?

What is correct way to specify deeply nested translation path?

Actually, using either (t :en :a/b/c) or (t :en :a.b/c) works fine, but formally neither is correct. Keywords must be correctly namespaced in Clojure (having at most only one namespace and thus one /) but http://clojure.org/reader also bans using .:

Keywords are like symbols, except:
They can and must begin with a colon, e.g. :fred.
They cannot contain '.' or name classes.
A keyword that begins with two colons is resolved in the current namespace:
In the user namespace, ::rect is read as :user/rect

I'm confused a little bit.

`nil` when using undescores in keys?

From the REPL:

user> (def display-strings {:dev-mode? false 
                            :dictionary {:it {:a-key "x" 
                                              :a_key "y" 
                                              :a-mixed_key "w"
                                              :another_mixed-key "z"}}})
#'user/display-strings
user> (taoensso.tower/t :it display-strings :a-key)
"x"
user> (taoensso.tower/t :it display-strings :a_key)
nil
user> (taoensso.tower/t :it display-strings :a-mixed_key)
nil
user> (taoensso.tower/t :it display-strings :another_mixed-key)
nil

Is this correct? Am I missing something obvious?

Mysterious issues with Timbre dependency (?)

I'm reluctant to file this issue since I have been unable to trace down the exact issue but perhaps Peter or someone will be able to help me on the right track.

I'm building a library where Tower is a dependency and under certain circumstances I get the following error during compilation:

java.lang.IllegalArgumentException: Unable to resolve classname: :private (timbre.clj:79)

That line is for taoensso.timbre/set-level!. Other potentially relevant messages are:

        at taoensso.tower$eval152$loading__4410__auto____153.invoke(tower.clj:1)
        at taoensso.tower$eval152.invoke(tower.clj:1)
        ...
        at clj_simple_form.i18n$eval146$loading__4410__auto____147.invoke(i18n.clj:1)
        at clj_simple_form.i18n$eval146.invoke(i18n.clj:1)

The poetry that is Clojure stacktraces leads me to suspect that the error occurs already when Timbre is being required in Tower and, by extension, clj-simple-form. Unfortunately I have not been able to pin down what exact conditions produce this error.

Am I missing something dead obvious?

First call to load-dictionary-from-map-resource! should wipe the old dictionary

When the first call to (load-dictionary-from-map-resource! "blah.clj") is made, the old dictionary should be wiped. Is there another way to accomplish this from an API level? For example, after the call is made for the first time, this is my compiled-dictionary:

#strong</tag>", :example/with-exclaim "**strong**", :missing "<Missing translation: {0}>", :login/applicationName "IC Mobile", :login/message "Please log in with a provider"}, :fr {:login/applicationName "IC Mobiles", :login/message "S'il vous plaît vous connecter avec un fournisseur:"}, :es {:login/applicationName "IC Mobile", :login/message "Por favor, inicie sesión con un proveedor:"}}>

Adding locales which aren't in getAvailableLocales()

Hey Peter,

Thanks for the great library. I just wanted to ask if there is a simple way to add language/locale that doesn't exist in the java getAvailableLocales method you are using to check for validity?

I'm trying to add afrikaans which isn't included by default. I don't really need all the formatting stuff, just translations, but obviously trying to use :af in the dictionary blows up.

I searched for ways of adding locales to LocaleServiceProvider via java but it seems like quite a mission for what I want to actually do.

Any advice?

Thanks!

Oliver

Add 'with-locales' macro

If would be nice to be able to pass multiple locales to a 'with-locales' macro in descending order of preference. For example:

(with-locales '(:es :fr :de) (t :scope/message))

Where it's behavior would be check :es first, then check :fr, then check :de, and then resort to default. Right now from what I can see there is no way to do this and this is a common use case.

If you don't like this idea, how about a simple 'exists?' function such as:

(exists? :es :scope/message)

Maybe both?

Thanks,
Vince

Formatting time and timezone

Hey,
How do you format a date time according to a certain timezone?
Timezones are always printed as the JVM default timezone:

Example:
(tower/fmt :en (java.util.Date.) :dt-long) => "April 18, 2016 10:48:45 AM PDT"

What if I wanted to print the date according to the user's timezone?
I've been digging around the codebase and couldn't find the answer.

Scoping keys

This ticket is related to #27, which would see us moving to a closure based implementation rather than bindings. According to that proposal we would create a translation closure t based on a dictionary, locale, and eventual config arguments. Since we would no longer use bindings, with-scope would no longer work and we'd need another solution.

Some options are:

  • Don't use scoping
  • Encourage use of Clojure namespaced keywords
  • Another closure to capture the scope
  • Separating string interpolation from translation lookup

Option 1: Don't use scoping

Not much to be said about this one. If (t :a.namespaced/key) is tolerable, then we're good.

Option 2: Clojure namespaced keywords

Since (t ::key) is equivalent to (t :my.namespace/key), this would lead to concise calls to t, but with the obvious minus that translation structure would be tied to structure of source code files. On the other hand, it would align the use of keywords with idiomatic Clojure, since namespacing of keywords is really meant for Clojure namespaces, not translation "namespaces".

Option 3: Closure for scopes

This seems a bit hacky but the syntax is concise enough:

; The following are equivalent
(t [:my.namespace/key])
(let [t (scope-t :my.namespace)]
  (t :key))

Option 4: Separate string interpolation from translation

If the dictionary is a simple map and t doesn't need to handle string interpolation or anything else except for lookups, then we can treat t as simple submap. This leads to some nice, idiomatic Clojure:

(def dict (load-dict))

(let [t (-> dict :en :front-page)]
  [:div.content
   [:h1 (t :title)]
   (let [t (t :content)]
    [:p.welcome (-> t :welcome :sub-heading)])])

The obvious issue is that string interpolation gets verbose, e.g. (loc-fmt locale (t :hello) "Peter") instead of just (t :hello "Peter").

Add stuff to compiled dictionary

Hi,

We're trying to load our CSV translation files directly to compiled dictionary (as they already have a correct interface for that case), but because (name) function is used twice, we can't really get stuff into the dictionary, as second (name) call removes the path:

(name (keyword "str/str"))
=> "str"

Could you give a hint, what would be the best way to do it without breaking Tower? :)

timbre/logp not found

Using tower 3.0.2 I get the error that timbre/logp is not found because some other library is using timbre 4.1.1 and this version of tower is using timbre 3.4.0. Even when I specify timbre 3.4.0, it says No such var: encore/cond!, compiling:(taoensso/tower.clj:371:4) which I did not investigate further, but that effectively prevents me from using tower.

Use defonce instead of def for compiled-dictionary

Hi,

Just faced that issue in production mode ;)

Because of def, not defonce, my compiled dictionary is empty after first request.

I'm in a middle of backporting it to 0.8.0 right now, since can't upgrade just yet. But just FYI.

Unifying t and t*

Sorry to beat a dead horse, but I had a thought about how t and t* could be fairly elegantly unified with the default value as last key mechanism I proposed. As it's largely a matter of taste and the current implementation works for me at least, feel free to close this issue if you disagree. But if you think unification of t and t* makes sense, then something like this might work nicely:

(defn t
  "Works like current `t` but allows a default value to be passes as last
  value in `k-or-ks`."
  [k-or-ks & args]
  (if (and (sequential? k-or-ks)
           (not (keyword? (last k-or-ks))))
    (or (apply translate true (drop-last k-or-ks) args)
        (last k-or-ks))
    (apply translate false k-or-ks args)))

(t [:missing1 :missing2]) ; => "Translation missing for [:missing1 :missing2]"
(t [:missing1 :missing2 "Submit"]) ; => "Submit"
(t [:missing1 :missing2 nil]) ; => nil

The check for a default value could, of course, be inlined into translate, which would mean that we could do away with having the functionality in 2-3 different functions (t, t*, translate).

I'll trust your final judgement and shut up on this topic now. :-)

Underscores as decorator separators?

It appears that tower is also using underscores as a decorator separator in the translation keys. I noticed this while using field names from a third-party app as translation keys. Solving my particular issue with a tiny filter is fairly trivial but perhaps it would be worth either to remove the underscore as a decorator separator or to mention it in the README? I would personally vote for the former to keep things simple. I can submit a patch for either if you want.

By the way, thanks for putting this library out there! I was rather missing the Rails I18n helpers when working on multilingual projects.

Tower 3.1.0-beta3 with ClojureScript: IllegalArgumentException with tower/dict-compile*

I see the stack trace at the bottom of this issue when I try to compile the translations dictionary in ClojureScript.

Sorry for the hyper-long outputs, but I hope they can give some clues.

Environment, setup, etc

I have created a dict.clj under resources with this content (yes, copy&paste just to get started):

 {:dictionary ; Map or named resource containing map
   {:en   {:example {:foo         ":en :example/foo text"
                     :foo_comment "Hello translator, please do x"
                     :bar {:baz ":en :example.bar/baz text"}
                     :greeting "Hello %s, how are you?"
                     :inline-markdown "<tag>**strong**</tag>"
                     :block-markdown* "<tag>**strong**</tag>"
                     :with-exclaim!   "<tag>**strong**</tag>"
                     :with-arguments  "Num %d = %s"
                     :greeting-alias :example/greeting
                     :baz-alias      :example.bar/baz}
           :missing  "|Missing translation: [%1$s %2$s %3$s]|"}
    :en-US {:example {:foo ":en-US :example/foo text"}}
    :de    {:example {:foo ":de :example/foo text"}}
    :ja "test_ja.clj" ; Import locale's map from external resource
    }
   :dev-mode? true ; Set to true for auto dictionary reloading
   :fallback-locale :de}

The relevant parts in the .cljs:

;; ...
[taoensso.tower                 :as tower
             :refer-macros (with-tscope)]
;; ...
(def tconfig
  {:fallback-locale :en
   :compiled-dictionary (tower/dict-compile* "dict.clj")})

Here is the dependencies tree from boot:

��[adzerk/boot-cljs-repl "0.1.9" :scope "test"]
[adzerk/boot-cljs "0.0-3308-0" :scope "test"]
[adzerk/boot-reload "0.3.1" :scope "test"]
[adzerk/boot-test "1.0.4" :scope "test"]
[buddy "0.6.1" :exclusions [[org.clojure/clojure] [com.taoensso/encore] [org.clojure/tools.reader]]]
├── [buddy/buddy-auth "0.6.1"]
│   └── [funcool/cuerdas "0.6.0"]
├── [buddy/buddy-core "0.6.0"]
│   ├── [commons-codec "1.10"]
│   ├── [org.bouncycastle/bcpkix-jdk15on "1.52"]
│   ├── [org.bouncycastle/bcprov-jdk15on "1.52"]
│   └── [slingshot "0.12.2"]
├── [buddy/buddy-hashers "0.6.0"]
│   └── [clojurewerkz/scrypt "1.2.0"]
│       └── [com.lambdaworks/scrypt "1.4.0"]
└── [buddy/buddy-sign "0.6.1"]
    ├── [cheshire "5.5.0"]
    │   ├── [com.fasterxml.jackson.core/jackson-core "2.5.3"]
    │   ├── [com.fasterxml.jackson.dataformat/jackson-dataformat-cbor "2.5.3"]
    │   ├── [com.fasterxml.jackson.dataformat/jackson-dataformat-smile "2.5.3"]
    │   └── [tigris "0.1.1"]
    ├── [clj-time "0.10.0"]
    │   └── [joda-time "2.7"]
    ├── [com.taoensso/nippy "2.9.0"]
    │   ├── [net.jpountz.lz4/lz4 "1.3"]
    │   ├── [org.iq80.snappy/snappy "0.3"]
    │   └── [org.tukaani/xz "1.5"]
    └── [funcool/cats "0.6.1"]
[com.cognitect/transit-clj "0.8.281" :exclusions [[com.fasterxml.jackson.core/jackson-core] [commons-codec]]]
└── [com.cognitect/transit-java "0.8.304"]
    ├── [com.fasterxml.jackson.datatype/jackson-datatype-json-org "2.3.2"]
    │   ├── [com.fasterxml.jackson.core/jackson-databind "2.3.2"]
    │   │   └── [com.fasterxml.jackson.core/jackson-annotations "2.3.0"]
    │   └── [org.json/json "20090211"]
    ├── [org.apache.directory.studio/org.apache.commons.codec "1.8"]
    └── [org.msgpack/msgpack "0.6.10"]
        ├── [com.googlecode.json-simple/json-simple "1.1.1" :exclusions [[junit]]]
        └── [org.javassist/javassist "3.18.1-GA"]
[com.cognitect/transit-cljs "0.8.220" :exclusions [[org.clojure/clojure]]]
└── [com.cognitect/transit-js "0.8.755"]
[com.taoensso/sente "1.6.0" :exclusions [[org.clojure/clojure]]]
├── [com.taoensso/encore "2.4.2"]
└── [org.clojure/tools.reader "0.9.2"]
[com.taoensso/timbre "4.1.0" :exclusions [[org.clojure/clojure] [com.taoensso/encore]]]
└── [io.aviso/pretty "0.1.18"]
[com.taoensso/tower "3.1.0-beta3" :exclusions [[org.clojure/clojure] [com.taoensso/encore] [io.aviso/pretty] [com.taoensso/timbre]]]
└── [markdown-clj "0.9.65"]
[compojure "1.4.0" :exclusions [[org.clojure/clojure] [clj-time] [commons-codec] [org.clojure/tools.reader] [joda-time]]]
├── [clout "2.1.2"]
│   └── [instaparse "1.4.0" :exclusions [[org.clojure/clojure]]]
├── [medley "0.6.0"]
└── [org.clojure/tools.macro "0.1.5"]
[danielsz/boot-environ "0.0.5"]
[environ "1.0.0" :exclusions [[org.clojure/clojure]]]
[hiccup "1.0.5" :exclusions [[org.clojure/clojure]]]
[http-kit "2.1.19" :exclusions [[org.clojure/clojure]]]
[org.clojure/clojure "1.7.0"]
[org.clojure/clojurescript "1.7.58" :exclusions [[org.clojure/clojure] [org.clojure/tools.reader]]]
├── [com.google.javascript/closure-compiler "v20150729"]
│   ├── [args4j "2.0.26"]
│   ├── [com.google.code.findbugs/jsr305 "1.3.9"]
│   ├── [com.google.code.gson/gson "2.2.4"]
│   ├── [com.google.guava/guava "18.0"]
│   ├── [com.google.javascript/closure-compiler-externs "v20150729"]
│   └── [com.google.protobuf/protobuf-java "2.5.0"]
├── [org.clojure/data.json "0.2.6"]
├── [org.clojure/google-closure-library "0.0-20150805-acd8b553"]
│   └── [org.clojure/google-closure-library-third-party "0.0-20150805-acd8b553"]
└── [org.mozilla/rhino "1.7R5"]
[org.clojure/core.async "0.1.346.0-17112a-alpha" :exclusions [[org.clojure/clojure]]]
└── [org.clojure/tools.analyzer.jvm "0.1.0-beta12"]
    ├── [org.clojure/core.memoize "0.5.6"]
    │   └── [org.clojure/core.cache "0.6.3"]
    │       └── [org.clojure/data.priority-map "0.0.2"]
    ├── [org.clojure/tools.analyzer "0.1.0-beta12"]
    └── [org.ow2.asm/asm-all "4.1"]
[org.clojure/test.check "0.7.0" :scope "test" :exclusions [[org.clojure/clojure]]]
[org.danielsz/system "0.1.8" :exclusions [[org.clojure/clojure] [ns-tracker]]]
├── [com.stuartsierra/component "0.2.2"]
│   └── [com.stuartsierra/dependency "0.1.1"]
└── [reloaded.repl "0.1.0"]
    └── [org.clojure/tools.namespace "0.2.4"]
[quiescent "0.2.0-RC2" :exclusions [[org.clojure/data.json] [org.clojure/clojure] [org.clojure/google-closure-library] [org.clojure/clojurescript] [org.mozilla/rhino] [org.clojure/google-closure-library-third-party] [com.google.javascript/closure-compiler-externs] [com.google.guava/guava] [com.google.javascript/closure-compiler] [org.clojure/tools.reader]]]
└── [cljsjs/react-with-addons "0.13.3-0"]
[ring/ring-core "1.4.0" :exclusions [[org.clojure/clojure] [clj-time] [commons-codec] [org.clojure/tools.reader]]]
├── [commons-fileupload "1.3.1"]
├── [commons-io "2.4"]
├── [crypto-equality "1.0.0"]
├── [crypto-random "1.2.0"]
└── [ring/ring-codec "1.0.0"]
[ring/ring-defaults "0.1.5" :exclusions [[org.clojure/clojure] [clj-time] [commons-fileupload] [commons-codec] [org.clojure/tools.reader] [ring/ring-core]]]
├── [javax.servlet/servlet-api "2.5"]
├── [ring/ring-anti-forgery "1.0.0"]
├── [ring/ring-headers "0.1.3"]
└── [ring/ring-ssl "0.2.1"]
[ring/ring-devel "1.4.0" :scope "test" :exclusions [[org.clojure/clojure] [clj-time] [org.clojure/tools.namespace] [commons-codec] [org.clojure/tools.reader] [joda-time]]]
├── [clj-stacktrace "0.2.8" :scope "test"]
└── [ns-tracker "0.3.0" :scope "test"]
    └── [org.clojure/java.classpath "0.2.2" :scope "test"]

Stack trace

java.lang.IllegalArgumentException: Don't know how to create ISeq from: java.lang.Boolean
                                               ...                          
                                  clojure.core/seq            core.clj:  137
          taoensso.encore/nested-merge-with/merge2          encore.clj: 1225
                                               ...                          
                               clojure.core/reduce            core.clj: 6514
                 taoensso.encore/nested-merge-with          encore.clj: 1226
                                               ...                          
                           clojure.core/partial/fn            core.clj: 2494
                                               ...                          
                                clojure.core/apply            core.clj:  630
                      taoensso.tower/fn/fn/iter/fn           tower.clj:  543
                                               ...                          
                                 clojure.core/next            core.clj:   64
                         clojure.core.protocols/fn       protocols.clj:  170
                       clojure.core.protocols/fn/G       protocols.clj:   19
                 clojure.core.protocols/seq-reduce       protocols.clj:   31
                         clojure.core.protocols/fn       protocols.clj:  101
                       clojure.core.protocols/fn/G       protocols.clj:   13
                               clojure.core/reduce            core.clj: 6519
                                 clojure.core/into            core.clj: 6600
                              taoensso.tower/fn/fn           tower.clj:  538
                              taoensso.tower/fn/fn           tower.clj:  648
                                               ...                          
                      taoensso.tower/dict-compile*           tower.clj:  656
                                               ...                          
                                clojure.core/apply            core.clj:  634
          cljs.analyzer$macroexpand_1_STAR_.invoke       analyzer.cljc: 2322
                cljs.analyzer$macroexpand_1.invoke       analyzer.cljc: 2362
                  cljs.analyzer$analyze_seq.invoke       analyzer.cljc: 2392
                 cljs.analyzer$analyze_form.invoke       analyzer.cljc: 2503
                cljs.analyzer$analyze_STAR_.invoke       analyzer.cljc: 2550
                      cljs.analyzer$analyze.invoke       analyzer.cljc: 2566
                      cljs.analyzer$analyze.invoke       analyzer.cljc: 2561
                      cljs.analyzer$analyze.invoke       analyzer.cljc: 2560
cljs.analyzer$analyze_map$fn__2045$fn__2046.invoke       analyzer.cljc: 2401
                               clojure.core/map/fn            core.clj: 2624
                                               ...                          
                                  clojure.core/vec            core.clj:  361
         cljs.analyzer$analyze_map$fn__2045.invoke       analyzer.cljc: 2401
                  cljs.analyzer$analyze_map.invoke       analyzer.cljc: 2401
                 cljs.analyzer$analyze_form.invoke       analyzer.cljc: 2504
                cljs.analyzer$analyze_STAR_.invoke       analyzer.cljc: 2550
                      cljs.analyzer$analyze.invoke       analyzer.cljc: 2566
                      cljs.analyzer$analyze.invoke       analyzer.cljc: 2561
   cljs.analyzer$eval1579$fn__1580$fn__1583.invoke       analyzer.cljc: 1101
            cljs.analyzer$eval1579$fn__1580.invoke       analyzer.cljc: 1100
                                               ...                          
            cljs.analyzer$analyze_seq_STAR_.invoke       analyzer.cljc: 2368
       cljs.analyzer$analyze_seq_STAR__wrap.invoke       analyzer.cljc: 2373
                  cljs.analyzer$analyze_seq.invoke       analyzer.cljc: 2394
                 cljs.analyzer$analyze_form.invoke       analyzer.cljc: 2503
                cljs.analyzer$analyze_STAR_.invoke       analyzer.cljc: 2550
                      cljs.analyzer$analyze.invoke       analyzer.cljc: 2566
  cljs.compiler$compile_file_STAR_$fn__3289.invoke       compiler.cljc: 1125
               cljs.compiler$with_core_cljs.invoke       compiler.cljc: 1053
           cljs.compiler$compile_file_STAR_.invoke       compiler.cljc: 1076
        cljs.compiler$compile_file$fn__3330.invoke       compiler.cljc: 1237
                 cljs.compiler$compile_file.invoke       compiler.cljc: 1216
                         cljs.closure/compile-file         closure.clj:  426
                          cljs.closure/eval3713/fn         closure.clj:  479
                        cljs.closure/eval3665/fn/G         closure.clj:  383
                          cljs.closure/eval3717/fn         closure.clj:  484
                        cljs.closure/eval3665/fn/G         closure.clj:  383
                    cljs.closure/get-compiled-cljs         closure.clj:  548
                    cljs.closure/cljs-dependencies         closure.clj:  619
                     cljs.closure/add-dependencies         closure.clj:  642
                                               ...                          
                                clojure.core/apply            core.clj:  632
                                cljs.closure/build         closure.clj: 1676
                                cljs.closure/build         closure.clj: 1627
                adzerk.boot-cljs.impl/compile-cljs            impl.clj:   55
                                               ...                          
                                clojure.core/apply            core.clj:  630
                             boot.pod/eval-fn-call             pod.clj:  183
                                 boot.pod/call-in*             pod.clj:  190
                                               ...                          
                                 boot.pod/call-in*             pod.clj:  193
                          adzerk.boot-cljs/compile       boot_cljs.clj:   93
              adzerk.boot-cljs/eval485/fn/fn/fn/fn       boot_cljs.clj:  167
                 adzerk.boot-cljs/eval485/fn/fn/fn       boot_cljs.clj:  162
                 adzerk.boot-cljs/eval455/fn/fn/fn       boot_cljs.clj:  109
            adzerk.boot-cljs-repl/eval568/fn/fn/fn  boot_cljs_repl.clj:  137
                    boot.task.built-in/fn/fn/fn/fn        built_in.clj:  277
                    boot.task.built-in/fn/fn/fn/fn        built_in.clj:  274
               adzerk.boot-reload/eval631/fn/fn/fn     boot_reload.clj:   88
               adzerk.boot-reload/eval631/fn/fn/fn     boot_reload.clj:   81
                     system.boot/eval1709/fn/fn/fn            boot.clj:   25
                 adzerk.boot-test/eval265/fn/fn/fn       boot_test.clj:   50
              boot.task.built-in/fn/fn/fn/fn/fn/fn        built_in.clj:  226
                 boot.task.built-in/fn/fn/fn/fn/fn        built_in.clj:  226
                    boot.task.built-in/fn/fn/fn/fn        built_in.clj:  223
        danielsz.boot-environ/eval1510/fn/fn/fn/fn    boot_environ.clj:   15
                       clojure.core/with-redefs-fn            core.clj: 7209
           danielsz.boot-environ/eval1510/fn/fn/fn    boot_environ.clj:   14
                               boot.core/run-tasks            core.clj:  688
                                 boot.core/boot/fn            core.clj:  698
               clojure.core/binding-conveyor-fn/fn            core.clj: 1916
                                               ...                          

Tower 1.2.0 - Problem with Clojure 1.4.0

user=> (require '[taoensso.tower :as [tower]])
ClassCastException clojure.lang.PersistentVector cannot be cast to clojure.lang.Symbol clojure.core/alias (core.clj:3799)

Any ideas? Thank you!

Compilation of tower.cljs fails with ClojureScript 0.0-2913

> NS form of taoensso/tower.cljs can't be parsed: unrecognized ns part

clojure.lang.ExceptionInfo: unrecognized ns part 
{:form (ns taoensso.tower "Tower ClojureScript stuff - still pretty limited." 
{:author "Peter Taoussanis"} (:require-macros [taoensso.tower :as tower-macros]) 
(:require [clojure.string :as str] [taoensso.encore :as encore])), 
:part {:author "Peter Taoussanis"}}

Warn about `dev-mode?` in readme and changelog?

I know the library is EOL, but could you add a warning to the README about dev-mode? so that people will turn it off in production?

I just discovered that one of our servers was experiencing periodic hangs due to threads blocked on re-reading the jar file every time a translation was needed. This might no longer be relevant in v3 -- I couldn't quite tell from the changelog -- but the server uses v2.0.1 and this took some time to debug since dev-mode isn't documented.

The README currently implies that dev-mode is off by default ("Enable the :dev-mode? option and you're good to go!"; various places where it is shown as explicitly enabled) -- a "hey, turn this off in prod" would be great.

I could send a PR if that would be helpful.

Best way to make dynamic paths

Hello,

Excuse me, but it's not clear to me how can i quickly make dynamic paths, could you please show some example?

For example i have resource lie {:a {:b {:c "Hello"} :d {:c "world"}}.
I'd like extract path for either b or d giving this key as an argument.
It would be nice to do something like (t :en :a variable :c) instead of (keyword (str "a/" (name variable) "/c")).

Thanks in advance.

Maximum text size (hint for translator) and entity property texts

Hi,

Is it possible possible to achieve the following with current functionality:

  1. Assuming there is some UI tool for translation:
    developer adds a new text that is used as a button caption. Due to UI layout restrictions he wants to enforce that text is not longer than 20 symbols in any language. So UI tool should be able to read this restriction from data and limit number of symbols in text field
  2. Managing different types of texts for entity properties. E.g. entity Person has 2 properties: first-name, last-name. These properties sometimes displayed on the form, sometimes in the table/grid. Also when user selects the field in UI the hint/description is shown in the status bar. So we have 3 types of texts for the property.
    In the ideal solution it should be possible to associate all 3 texts with 1 key and let the caller decide which text to use depending on context. Already mentioned imaginary UI tool would also enforce some global length restriction on these 3 types of texts. E.g. grid - 20, form - 40, description - 150. So translator would have to come up with some reasonable abbreviation for grid text if necessary.
    I think it can be achieved now by introducing some reserved suffixed for keywords, like :last-name-grid, :last-name-form. But maybe there is a better solution since the one I suggested has some disadvantages.

Thanks

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.