Current semantic version:
[com.taoensso/tower "1.2.0"]
The Java platform provides some very capable tools for writing internationalized applications. Unfortunately, these tools can be disappointingly cumbersome when taken into a Clojure context.
Tower is an attempt to present a simple, idiomatic internationalization and localization story for Clojure. It wraps standard Java functionality where possible, but it isn't afraid to step away from Java conventions when there's a good reason to.
- Small, uncomplicated all-Clojure library.
- Consistent, lightweight wrappers for standard Java localization functions.
- Rails-like, all-Clojure translation function.
- 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.
Depend on Tower in your project.clj
:
[com.taoensso/tower "1.2.0"]
and require
the library:
(ns my-app (:use [taoensso.tower :as tower :only (with-locale with-scope t style)]))
Here Tower diverges from the standard Java approach in favour of something simpler and more agile. Let's look at the default config:
@tower/config
=>
{:dev-mode? true
:default-locale :en
:dictionary
{:en {:example {:foo ":en :example/foo text"
:foo_note "Hello translator, please do x"
:bar {:baz ":en :example.bar/baz text"}
:greeting "Hello {0}, how are you?"
:with-markdown "<tag>**strong**</tag>"
:with-exclaim! "<tag>**strong**</tag>"}
:missing "<Translation missing: {0}>"}
:en-US {:example {:foo ":en-US :example/foo text"}}
:en-US-var1 {:example {:foo ":en-US-var1 :example/foo text"}}}
:log-missing-translation-fn! (fn [{:keys [dev-mode? locale k-or-ks]}] ...)}
Note the format of the :dictionary
map since this is the map you'll change to set your own translations. Work with the map in place using set-config!
, or load translations from a ClassLoader resource:
(tower/load-dictionary-from-map-resource! "my-dictionary.clj")
You can put my-dictionary.clj
on your classpath or one of Leiningen's resource paths (e.g. /resources/
).
For now let's play with the default dictionary to see how Tower handles translation:
(with-locale :en-US (t :example/foo)) => ":en-US :example/foo text"
(with-locale :en (t :example/foo)) => ":en :example/foo text"
(with-locale :en (t :example/greeting "Steve")) => "Hello Steve, how are you?"
Translation strings are escaped and parsed as inline Markdown unless suffixed with !
:
(with-locale :en (t :example/with-markdown)) => "<tag><strong>strong</strong></tag>"
(with-locale :en (t :example/with-exclaim!)) => "<tag>**strong**</tag>"
If you're calling the translate fn repeatedly within a specific namespace context, you can specify a translation scope:
(with-locale :en
(with-scope :example
(list (t :foo)
(t :bar/baz)))) => (":en :example/foo text" ":en :example.bar/baz text")
Missing translations are handled gracefully. (with-scope :en-US (t :example/foo))
searches for a translation as follows:
:example/foo
in the:en-US
locale.:example/foo
in the:en
locale.:example/foo
in the default locale,(:default-locale @tower/config)
.:missing
in any of the above locales.
You can also specify fallback keys that'll be tried before other locales. (with-scope :en-US (t [: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 default locale.:example/bar
in the default locale.:missing
in any of the above locales.
In all cases, translation request is logged upon fallback to default locale or :missing key.
If you're not using the provided Ring middleware, you'll need to call localization and translation functions from within a with-locale
body:
(with-locale :en-ZA (tower/format-currency 200)) => "R 200.00"
(with-locale :en-US (tower/format-currency 200)) => "$200.00"
(with-locale :de (tower/format-number 2000.1)) => "2.000,1"
(with-locale :de (tower/parse-number "2.000,1")) => 2000.1
(with-locale :de (tower/format-date (java.util.Date.))) => "12.06.2012"
(with-locale :de (tower/format-date (style :long) (java.util.Date.)))
=> "12. Juni 2012"
(with-locale :it (tower/format-dt (style :long) (style :long) (java.util.Date.)))
=> "12 giugno 2012 16.48.01 ICT"
(with-locale :it (tower/parse-date (style :long) "12 giugno 2012 16.48.01 ICT"))
=> #<Date Tue Jun 12 00:00:00 ICT 2012>
(with-locale :de (tower/format-msg "foobar {0}!" 102.22)) => "foobar 102,22!"
(with-locale :de (tower/format-msg "foobar {0,number,integer}!" 102.22))
=> "foobar 102!"
(with-locale :de
(-> #(tower/format-msg "{0,choice,0#no cats|1#one cat|1<{0,number} cats}" %)
(map (range 5)) doall))
=> ("no cats" "one cat" "2 cats" "3 cats" "4 cats")
(with-locale :pl
(sort tower/l-compare ["Warsaw" "Kraków" "Łódź" "Wrocław" "Poznań"]))
=> ("Kraków" "Łódź" "Poznań" "Warsaw" "Wrocław")
(with-locale :pl (tower/sorted-localized-countries ["GB" "DE" "PL"]))
=> {:sorted-codes ["DE" "PL" "GB"],
:sorted-names ["Niemcy" "Polska" "Wielka Brytania"]}
(with-locale :pl (tower/sorted-localized-languages ["en" "de" "pl"]))
=> {:sorted-codes ["en" "de" "pl"],
:sorted-names ["angielski (English)" "niemiecki (Deutsch)" "polski"]}
(take 5 (:sorted-names (with-locale :en (tower/sorted-localized-countries))))
=> ("Afghanistan" "Åland Islands" "Albania" "Algeria" "American Samoa")
(tower/sorted-timezones)
=> {:sorted-ids ["Pacific/Midway" "Pacific/Niue" ...]
:sorted-names ["(GMT -11:00) Midway" "(GMT -11:00) Niue" ...]
Quickly internationalize your web application by adding (taoensso.tower.ring/make-wrap-i18n-middleware)
to your middleware stack.
For each request, an appropriate locale will be selected from one of the following (descending preference):
- Your own locale selector fn (e.g. for selection by IP address, domain, etc.).
(-> request :session :locale)
.(-> request :params :locale)
, e.g."/my-uri?locale=en-US"
.- A URI selector, e.g.
"/my-uri/locale/en-US/"
. - The request's Accept-Language HTTP header.
ClojureWerkz is a growing collection of open-source, batteries-included Clojure libraries that emphasise modern targets, great documentation, and thorough testing.
CDS (Clojure Documentation Site) is a contributor-friendly community project aimed at producing top-notch Clojure tutorials and documentation.
Reach me (Peter Taoussanis) at taoensso.com for questions/comments/suggestions/whatever. I'm very open to ideas if you have any! I'm also on Twitter: @ptaoussanis.
Copyright © 2012 Peter Taoussanis. Distributed under the Eclipse Public License, the same as Clojure.