API docs | CHANGELOG | other Clojure libs | Twitter | contact/contrib | current Break Version:
[com.taoensso/tower "3.0.2"] ; Stable
[com.taoensso/tower "3.1.0-beta3"] ; Dev, please see CHANGELOG for details
The Java platform provides some very capable tools for writing internationalized applications. Unfortunately, they can be... cumbersome. We can do much better in Clojure.
Tower's an attempt to present a simple, idiomatic internationalization and localization story for Clojure. It wraps standard Java functionality where possible - with warm, fuzzy, functional love. It does go in its own direction for translation, but I suspect you'll like the direction it goes.
- Small, uncomplicated all-Clojure library.
- Ridiculously simple, high-performance wrappers for standard Java localization features.
- Rails-like, all-Clojure translation function (incl. ClojureScript support).
- Simple, map-based translation dictionary format. No XML or resource files!
- Automatic dev-mode dictionary reloading for rapid REPL development.
- Seamless markdown support for translators.
- Ring middleware.
- TODO: export/import to allow use with industry-standard tools for translators.
Add the necessary dependency to your Leiningen project.clj
and require
the library in your ns:
[com.taoensso/tower "3.0.2"] ; project.clj
(ns my-app (:require [taoensso.tower :as tower :refer (with-tscope)])) ; ns
The make-t
fn handles translations. You give it a config map which includes your dictionary, and get back a (fn [locale-or-locales k-or-ks & fmt-args])
:
(def my-tconfig
{: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})
(def t (tower/make-t my-tconfig)) ; Create translation fn
(t :en-US :example/foo) => ":en-US :example/foo text"
(t :en :example/foo) => ":en :example/foo text"
(t :en :example/greeting "Steve") => "Hello Steve, how are you?"
;;; Translation strings are escaped and parsed as inline or block Markdown:
(t :en :example/inline-markdown) => "<tag><strong>strong</strong></tag>"
(t :en :example/block-markdown) => "<p><tag><strong>strong</strong></tag></p>" ; Notice no "*" suffix here, only in dictionary map
(t :en :example/with-exclaim) => "<tag>**strong**</tag>" ; Notice no "!" suffix here, only in dictionary map
(t :en :example/with-arguments 42 "forty two") => "Num 42 = forty two"
It's simple to get started, but there's a number of advanced features for if/when you need them:
Loading dictionaries from disk/resources: Just use a string for the :dictionary
and/or any locale value(s) in your config map. Be sure to check that the appropriate files are available on your classpath or one of Leiningen's resource paths (e.g. resources/
).
Reloading dictionaries on modification: Enable the :dev-mode?
option and you're good to go!
Scoping translations: Use with-tscope
if you're calling t
repeatedly within a specific translation-namespace context:
(with-tscope :example
[(t :en :foo)
(t :en :bar/baz)]) => [":en :example/foo text" ":en :example.bar/baz text"]
Missing translations: These are handled gracefully. (t :en-US :example/foo)
will search for a translation as follows:
:example/foo
in the:en-US
locale.:example/foo
in the:en
locale.:example/foo
in the dictionary's fallback locale.:missing
in any of the above locales.
You can also specify fallback keys that'll be tried before other locales. (t :en-US [:example/foo :example/bar]))
searches:
:example/foo
in the:en-US
locale.:example/bar
in the:en-US
locale.:example/foo
in the:en
locale.:example/bar
in the:en
locale.:example/foo
in the fallback locale.:example/bar
in the fallback locale.:missing
in any of the above locales.
And even fallback locales. (t [:fr-FR :en-US] :example/foo)
searches:
:example/foo
in the:fr-FR
locale.:example/foo
in the:fr
locale.:example/foo
in the:en-US
locale.:example/foo
in the:en
locale.:example/foo
in the fallback locale.:missing
in any of the above locales.
In all cases, translation requests are logged upon fallback to fallback locale or :missing key.
(ns my-clojurescript-ns
(:require [taoensso.tower :as tower :refer-macros (with-tscope)]))
(def ^:private tconfig
{:fallback-locale :en
;; Inlined (macro) dict => this ns needs rebuild for dict changes to reflect.
;; (dictionary .clj file can be placed in project's `/resources` dir):
:compiled-dictionary (tower-macros/dict-compile* "my-dict.clj")})
(def t (tower/make-t tconfig)) ; Create translation fn
(t :en-US :example/foo) => ":en-US :example/foo text"
There's two notable differences from JVM translations:
- The dictionary is provided in a pre-compiled form so that it can be inlined directly into your Cljs.
- Since we lack a locale-aware Cljs
format
fn, your translations cannot use JVM locale formatting patterns.
The API is otherwise exactly the same, including support for all decorators.
React presents a bit of a challenge to translations since it automatically escapes all text content as a security measure.
This has two important implications for use with Tower's translations:
- Content intended to allow translator-controlled inline styles needs to provided to React with the
dangerouslySetInnerHTML
property. - All other content should get a
:<key>!
-style translation to prevent double escaping (Tower already escapes translations not marked with an exlamation point).
Check out fmt
, parse
, lsort
, fmt-str
, fmt-msg
:
(tower/fmt :en-ZA 200 :currency) => "R 200.00"
(tower/fmt :en-US 200 :currency) => "$200.00"
(tower/parse :en-US "$200.00" :currency) => 200
(tower/fmt :de-DE 2000.1 :number) => "2.000,1"
(tower/fmt :de-DE (java.util.Date.)) => "12.06.2012"
(tower/fmt :de-DE (java.util.Date.) :date-long) => "12. Juni 2012"
(tower/fmt :de-DE (java.util.Date.) :dt-long) => "12 giugno 2012 16.48.01 ICT"
(tower/lsort :pl ["Warsaw" "Kraków" "Łódź" "Wrocław" "Poznań"])
=> ("Kraków" "Łódź" "Poznań" "Warsaw" "Wrocław")
(mapv #(tower/fmt-msg :de "{0,choice,0#no cats|1#one cat|1<{0,number} cats}" %)
(range 5))
=> ["no cats" "one cat" "2 cats" "3 cats" "4 cats"]
Yes, seriously- it's that simple. See the appropriate docstrings for details.
Check out countries
, languages
, and timezones
.
Quickly internationalize your Ring web apps by adding taoensso.tower.ring/wrap-tower
to your middleware stack.
See the docstring for details.
-
CDS, the Clojure Documentation Site, is a contributer-friendly community project aimed at producing top-notch, beginner-friendly Clojure tutorials and documentation. Awesome resource.
-
ClojureWerkz is a growing collection of open-source, batteries-included Clojure libraries that emphasise modern targets, great documentation, and thorough testing. They've got a ton of great stuff, check 'em out!
lein start-dev
to get a (headless) development repl that you can connect to with Cider (emacs) or your IDE.
Please use the project's GitHub issues page for project questions/comments/suggestions/whatever (pull requests welcome!). Am very open to ideas if you have any!
Otherwise reach me (Peter Taoussanis) at taoensso.com or on Twitter. Cheers!
Copyright © 2012-2014 Peter Taoussanis. Distributed under the Eclipse Public License, the same as Clojure.