This is a tutorial for https://github.com/ptaoussanis/tempura, a Clojurescript library for i18n.
Add the necessary dependency to your project:
[com.taoensso/tempura "1.2.1"]
The following walk-through assumes that you are using a REPL and have required tempura as follows:
(def my-clj-or-cljs-ns
(:require [taoensso.tempura :as tempura :refer [tr]]))
It's best practice to define a Clojure map for localizable resources. At the top-level the keys refer to the language and the values are further maps with translation keys and the corresponding localizations in the given language -- the following example offers a map with localizations in English, German and Chinese:
(def translations
{; English language resources
:en {:missing "**MISSING**" ; Fallback for missing resources
:hello-world "Hello, world!"
:hello-tempura "Hello tempura!"}
; German language resources
:de {:missing "**FEHLT**"
:hello-world "Hallo Welt!"
:hello-tempura "Hallo tempura!"}
; Chinese language resources
:zh {:missing "**失踪**"
:hello-world "世界,你好"}})
Using the translations is straightforward -- using the function call
tr
the correct localization is given via
(tr {:dict translations} [:en] [:hello-world])
which returns the English translation Hello, world!
. The first
parameter of tr
contains the options (where the :dict
is mandatory
to supply the map with translations), the second is a vector of
languages and the third is a vector with the translation key.
Likewise, the Chinese translation is recovered via
(tr {:dict translations} [:zh] [:hello-world])
which correctly returns 世界,你好
.
In the above example the key :hello-tempura
was missing from the
Chinese map. Missing resources (and also misspelled resource keys) are
the reason why both the languages as well as the translation keys are
vectors and not merely simple elements. If a resource is missing then
first the vector of supplied languages is searched (until the resource
is found in a different language) and then the vector of translation
keys is searched. The former allows to display strings in a "fallback"
language like English, the latter allows to mark resources as missing
by using the :missing
key as a marker.
Thus, the call
(tr {:dict translations} [lang :en] [res-key :missing])
will
- search for the key
res-key
in languagelang
in thetranslations
map. - If it fails to find one, it will look up the key in the
:en
English language. - If it still fails to find that one, it displays the value of
:missing
in thelang
language map.
It is a best practice to use a convenience function that encapsulates this default behavior, e.g.
(defn app-tr
"Get a localized resource.
@param resource Resource keyword.
@param params Optional positional parameters.
@return translation of `resource` in active user language or a placeholder."
[resource & params]
(let [lang :zh] ; Retrieve user language from database or other source
(tr {:dict translations} [lang :en] [resource] (vec params))))
Then the function app-tr
returns the localization with the above
fallback behavior in case it could not be translated properly:
(app-tr :hello-world) ; => "世界,你好"
(app-tr :hello-tempura) ; => "Hello tempura!"
(app-tr :haha) ; => "**失踪**"
The above example covered the most important basic functionality. The following are more advanced use cases. In particular, the functionality covered is:
- using Reagent/Hiccup-style vectors,
- using Java-style positional parameters,
- deeper-level nesting of maps,
- escaping special HTML entities,
- custom functions for translations,
- aliasing subtrees,
- loading EDN content from disk or other external sources, and
- using plain text instead of keywords.
The following map illustrates these advanced scenarios:
(def translations
{; British English
:en-GB {:missing "**EN-GB/MISSING**"
; Example of a Hiccup-form (e.g., for Clojurescript/Reagent)
:faq-link [:span "Got lost? See our " [:a {:href "/faq/index.html"} "FAQ"]]
; Example of a Hiccup-form with Markdown
:markdown-text [:span "This is **bold** text"]
; Alternative form of previous example (`[x]` is short-hand for `[:span x]`)
:markdown-text-alt ["This is **bold** text"]
; You can use Java-style positional parameters
:greet-user "Good morning, %1. You're looking %2 today."
; You can nest ids if you like
:mood {:bad {:terrible "terrible"
:horrible "horrible"}
:good {:well "well"}}
; HTML entities can be escaped by prefixing the % with a back-tick
:mail-support "mailto:[email protected]?subject=Help`%20with`%20tempura"
; A translation can also be a custom function
:little-ducks (fn [[count]]
(let [count-word (if (< count 6)
(nth ["No" "One" "Two" "Three" "Four" "Five"] count)
(str count))]
(str count-word " little ducks")))}
; Regular English
:en {:missing "**EN/MISSING**"
; Copy an entire subtree
:mood-copy :en-GB/mood
; Import a resource as EDN content from idisk (it MUST actually exist!)
; :imported {:__load-resource "resources/i18n.clj"}
}})
The following are usage examples of the functionality illustrated above:
(def en-gb-tr (partial tr {:dict translations} [:en-gb]))
(en-gb-tr [:haha]) ; => "**EN-GB/MISSING**"
(en-gb-tr [:faq-link]) ; => [:span "Got lost? See our " [:a {:href "/faq/index.html"} "FAQ"]]
(en-gb-tr [:markdown-text]) ; => [:span "This is " [:strong "bold"] " text"]
(en-gb-tr [:markdown-text-alt]) ; [:span "This is " [:strong "bold"] " text"]
(en-gb-tr [:mood.bad/horrible]) ; => "horrible"
(en-gb-tr [:greet-user] ["Dave" (en-gb-tr [:mood.good/well])]) ; => "Good morning, Dave. You're looking well today."
(en-gb-tr [:mail-support]) ; => "mailto:[email protected]?subject=Help%20with%20tempura"
(en-gb-tr [:little-ducks] [0]) ; => "No little ducks"
(en-gb-tr [:little-ducks] [4]) ; => "Four little ducks"
(en-gb-tr [:little-ducks] [6]) ; => "6 little ducks"
(tr {:dict translations} [:en] [:mood-copy.bad/terrible]) ; => "terrible"
If the translation key is not a keyword but a string it is simply returned verbatim, i.e.,
(en-gb-tr ["Work in progress"]) ; => "Work in progress"
For further options extensive inline documentation is available via
(doc tr)
Note that you CAN use positional parameters in a Hiccup-vector. But you CANNOT use them in a map, i.e.,
(def translations
{:en {:link-tag [:a {:href "%2"} "%1"]}})
will replace the first positional parameter %1
, but NOT the second at %2
. If you
need to use positional parameter at that point you must use a function, e.g.,
(def translations
{:en {:link-faq (fn [[link]]
["Please see the " [:a {:href link} "FAQs"]])}
:de {:link-faq (fn [[link]]
["Schauen Sie sich die " [:a {:href link} "häufigen Fragen"] " an"])}})
This will return the correctly localized Reagent components with the link properly injected:
(tr {:dict translations} [:en] [:link-faq] ["https://tempura.com/faq"])
; => ["Please see the " [:a {:href "https://tempura.com/faq"} "FAQs"]]
(tr {:dict translations} [:de] [:link-faq] ["https://tempura.com/faq"])
; => ["Schauen Sie sich die " [:a {:href "https://tempura.com/faq"} "häufigen Fragen"] " an"]
Tests are supplied inline with the main source code. Test cases in Clojure are run via
lein test