taoensso / tower Goto Github PK
View Code? Open in Web Editor NEWi18n & L10n library for Clojure/Script
Home Page: https://www.taoensso.com/tower
License: Eclipse Public License 1.0
i18n & L10n library for Clojure/Script
Home Page: https://www.taoensso.com/tower
License: Eclipse Public License 1.0
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:
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?
This only seems to be affecting :sw
locale and nothing else. This bug doesn't occur for missing locales, which I found interesting, but it still occurs for the :sw
locale even when it's missing.
I created a repo to demonstrate the bug https://github.com/okal/tower-invalid-locale
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.
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:
or
, just before the :missing-translation-fn
, check if we have a seq with other remaining values: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
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
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:
Two options that come to mind are passing the configuration to each call to t
and to do away with configuration entirely.
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))
Perhaps configuration is not even necessary? If we look at the configuration options above:
add-watch
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 doUnless 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.
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?"
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 "<Translation missing: %s>" ":en" ":error" "[:conflict]")
TRACE t8501: | | => "<Translation missing: :en>"
TRACE t8502: | | (taoensso.tower/fmt-str :en "<Translation missing: :en>" 1)
TRACE t8502: | | => "<Translation missing: :en>"
TRACE t8500: | => "<Translation missing: :en>"
TRACE t8499: => "<Translation missing: :en>"
"<Translation missing: :en>"
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)
I see that you parse the Accept-Language header value for locale determination.
Where is the code that does that?
I need to parse it to get the locale for my own application and it would be helpful to see the code for how it is done.
Are you using http://jetty.codehaus.org/jetty/jetty-6/apidocs/org/mortbay/jetty/Request.html#getLocale()?
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?
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
CONS
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.
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?
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?
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))
Based on our discussion of multiple libraries using tower, in the same application, but each expecting their own behavior without interference from other consumers of tower.
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.
(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"
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.
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])
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.
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).
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.
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...
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?
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?
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.
ptaoussanis/#30
@ptaoussanis
[@ptaoussanis]
[#30]
[@ptaoussanis - #30]
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?
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?
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:"}}>
One should usually let the browser know what is the language of the document.
(see http://nimbupani.com/declaring-languages-in-html-5.html)
How is that usually done with Tower?
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
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
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.
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:
Not much to be said about this one. If (t :a.namespaced/key)
is tolerable, then we're good.
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".
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))
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")
.
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? :)
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.
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.
I would like to separate out the dictionary for each locale into a separate file either as a EDN (clojure map) or a standard resource bundle.
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. :-)
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.
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.
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"]
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
...
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!
The ClojureScript code currently only provides translation facilities. Input, suggestions, and PRs (especially) welcome on this.
Cheers! :-)
> 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"}}
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.
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.
Hi,
Is it possible possible to achieve the following with current functionality:
Thanks
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.