CHANGELOG | API | current Break Version:
[com.taoensso/tempura "1.5.1"] ; See CHANGELOG for details
See here if you're interested in helping support my open-source work, thanks! - Peter Taoussanis
gettext
's ability to handle versioned content through unique content ids.format
performance through compilation + smart caching.(require '[taoensso.tempura :as tempura :refer [tr]]))
(tr ; For "translate"
{:dict ; Dictionary of translations
{:sw {:missing "sw/?" :r1 "sw/r1" :r2 "sw/r2"}
:en {:missing "en/?" :r1 "en/r1" :r2 "en/r2"}}}
[:sw :en <...>] ; Locales (desc priority)
[:r1 :r2 <...> ; Resources (desc priority)
<?fallback-str> ; Optional final fallback string
])
;; =>
(or
sw/r1 sw/r2 <...> ; Descending-priority resources in priority-1 locale
en/r1 en/r2 <...> ; '' in priority-2 locale
<...>
?fallback-str ; Optional fallback string (as last element in resources vec)
sw/? ; Missing (error) resource in priority-1 locale
en/? ; '' priority-2 locale
nil ; If none of the above exist
)
;; etc.
;; Note that ?fallback-str is super handy for development before you
;; have translations ready, e.g.:
(tr {:dict {}} [:en] [:sign-in-btn "Sign in here!"])
;; => "Sign in here!"
;; Tempura also supports Hiccup with Markdown-like styles, e.g.:
(tr {:dict {}} [:en] [:sign-in-btn ["**Sign in** here!"]])
;; => [:span [:strong "Sign in"] " here!"]
See the wiki docs for a more detailed discussion of Tempura's resource search behaviour.
Add the necessary dependency to your project:
Leiningen: [com.taoensso/tempura "1.5.1"] ; or
deps.edn: com.taoensso/tempura {:mvn/version "1.5.1"}
Setup your namespace imports:
(def my-clj-or-cljs-ns
(:require [taoensso.tempura :as tempura :refer [tr]]))
Define a dictionary for translation resources:
(def my-tempura-dictionary
{:en-GB ; Locale
{:missing ":en-GB missing text" ; Fallback for missing resources
:example ; You can nest ids if you like
{:greet "Good day %1!" ; Note Clojure fn-style %1 args
}}
:en ; A second locale
{:missing ":en missing text"
:example
{:greet "Hello %1"
:farewell "Goodbye %1"
:foo "foo"
:bar "bar"
:bar-copy :en.example/bar ; Can alias entries
:baz [:div "This is a **Hiccup** form"]
;; Can use arbitrary fns as resources
:qux (fn [[arg1 arg2]] (str arg1 " and " arg2))}
:example-copy :en/example ; Can alias entire subtrees
:import-example
{:__load-resource ; Inline edn content loaded from disk/resource
"resources/i18n.clj"}}})
And we're ready to go:
(tr ; Just a functional call
{:dict my-tempura-dictionary} ; Opts map, see docstring for details
[:en-GB :fr] ; Vector of descending-preference locales to search
[:example/foo] ; Vector of descending-preference resource-ids to search
) ; => "foo"
(def opts {:dict my-tempura-dictionary})
(def tr (partial tr opts [:en])) ; You'll typically use a partial like this
;; Grab a resource
(tr [:example/foo]) ; => "foo"
;; Missing resource
(tr [:example/invalid]) ; => ":en missing text"
(tr [:example/invalid "inline-fallback"]) ; => "inline-fallback"
(tr [:example/invalid :bar "final-fallback"]) ; => "bar"
;; Let's try some argument interpolation
(tr [:example/greet] ["Steve"]) ; => "Hello Steve"
;; With inline fallback
(tr [:example/invalid "Hi %1"] ["Steve"]) ; => "Hi Steve"
;; Example of a deeply-nested resource id
(tr [:example.buttons/login-button "Login!"]) ; => "Login!"
;; Let's get a Hiccup form for Reactjs, etc.
;; Note how the Markdown gets expanded into appropriate Hiccup forms:
(tr [:example/baz]) ; => [:div "This is a " [:strong "Hiccup"] " form"]
;; With inline fallback
(tr [:example/invalid [:div "My **fallback** div"]]) ; => [:div "My " [:strong "fallback"] " div"]
And that's it, you know the API:
(tr [opts locales resource-ids]) ; Without argument interpolation, or
(tr [opts locales resource-ids resource-args]) ; With argument interpolation
Please see the tr
docstring for more info on available opts, etc.
The support for gettext
-like inline fallback content makes it really easy to write your application in stages, without translations becoming a burden until if/when you need them.
Assuming we have a tr
partial (tr [resource-ids] [resource-ids resource-args])
:
"Please login here" ; Phase 1: no locale support (avoid)
(tr ["Please login here"]) ; Phase 2: works just like a text literal during dev
;; Phase 3: now supports translations when provided under the `:please-login`
;; resource id, otherwise falls back to the (English) text literal:
(tr [:please-login "Please login here"])
This means:
I'll note that since the API is so pleasant, it's actually often much less effort for your developers to use tr
than it would be for them to write the equivalent Hiccup structures by hand, etc.:
;; Compare the following two equivalent values:
(tr [["Hi %1, please enter your **login details** below:"]] [user-name])
[:span "Hi " user-name ", please enter your " [:strong "login details"] " below:"]
Note that
["foo"]
is an optional resource content shorthand for the common-case[:span "foo"]
If it's easy to use, it'll be easy to get your developers in the habit of writing content this way - which means that there's a trivial path to adding multilingual support whenever it makes sense to do so.
See also the wiki docs for more info.
There's two aspects of performance worth measuring: resource lookup, and resource compilation.
Both are highly optimized, and intelligently cached. In fact, caching is quite easy since most applications have a small number of unique multilingual text assets. Assets are compiled each time they're encountered for the first time, and the compilation cached.
As an example:
`(tr [["Hi %1, please enter your **login details** below:"]] [user-name])`
;; Will compile the inner resource to an optimized function like this:
(fn [user-name] [:span "Hi " user-name ", please enter your " [:strong "login details"] " below:"])
So performance is often on par with the best possible hand-optimized monolingual code.
Tempura was specifically designed to work with Reactjs applications, and works great with Reagent out-the-box.
tempura/tr
.tr
with the appropriate dictionary.Couldn't be simpler.
[1] If your dictionaries are small, you could just define them with the rest of your client code. Or you can define them on the server-side and clients can fetch the relevant part/s through an Ajax request, etc. Remember that Tempura dictionaries are just plain Clojure maps, so they're trivially easy to modify/filter.
Please see tempura/wrap-ring-request
.
Shouldn't be hard to do, you'll just need a conversion tool to/from edn. Haven't had a need for this myself, but PRs welcome.
Please use the project's GitHub issues page for all questions, ideas, etc. Pull requests welcome. See the project's GitHub contributors page for a list of contributors.
Otherwise, you can reach me at Taoensso.com. Happy hacking!
Distributed under the EPL v1.0 (same as Clojure).
Copyright © 2016-2022 Peter Taoussanis.
Can you improve this documentation? These fine people already did:
Peter Taoussanis & Ryan FowlerEdit on GitHub
cljdoc is a website building & hosting documentation for Clojure/Script libraries
× close