Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Clojure/Script i18n & L10n library
Clojure

Fetching latest commit…

Cannot retrieve the latest commit at this time

Failed to load latest commit information.
src/taoensso
test/test_tower
.gitignore
.travis.yml
README.md
epl-v10.html
project.clj

README.md

Current semantic version:

[com.taoensso/tower "0.12.0"]

Breaking changes since 0.10.x:

  • Affecting translations: all translations now escaped and parsed as Markdown by default.
  • :missing-translation-fn config option has been removed. See commit for more info.

Tower, a simple internationalization (i18n) library for Clojure.

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.

What's In The Box?

  • 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.

Status Build Status

Tower is still currently experimental. It has not yet been thoroughly tested in production and its API is subject to change. To run tests against all supported Clojure versions, use:

lein2 all test

Getting Started

Leiningen

Depend on Tower in your project.clj:

[com.taoensso/tower "0.12.0"]

and require the library:

(ns my-app (:use [taoensso.tower :as tower :only (with-locale with-scope t style)])

Translation

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)) => "&lt;tag&gt;<strong>strong</strong>&lt;/tag&gt;"
(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:

  1. :example/foo in the :en-US locale.
  2. :example/foo in the :en locale.
  3. :example/foo in the default locale, (:default-locale @tower/config).
  4. :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:

  1. :example/foo in the :en-US locale.
  2. :example/bar in the :en-US locale.
  3. :example/foo in the :en locale.
  4. :example/bar in the :en locale.
  5. :example/foo in the default locale.
  6. :example/bar in the default locale.
  7. :missing in any of the above locales.

In all cases, translation request is logged upon fallback to default locale or :missing key.

Localization

If you're not using the provided Ring middleware, you'll need to call localization and translation functions from within a with-locale body:

Numbers

(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

Dates and Times

(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>

Text

(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")

Collation/Sorting

(with-locale :pl
  (sort tower/l-compare ["Warsaw" "Kraków" "Łódź" "Wrocław" "Poznań"]))
=> ("Kraków" "Łódź" "Poznań" "Warsaw" "Wrocław")

Country and Language Names

(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")

Timezones

(tower/sorted-timezones)
=> {:sorted-ids   ["Pacific/Midway" "Pacific/Niue" ...]
    :sorted-names ["(GMT -11:00) Midway" "(GMT -11:00) Niue" ...]

Ring Middlware

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.

Tower supports the ClojureWerkz Project Goals

ClojureWerkz is a growing collection of open-source, batteries-included Clojure libraries that emphasise modern targets, great documentation, and thorough testing.

Contact & Contribution

Reach me (Peter Taoussanis) at ptaoussanis at gmail.com for questions/comments/suggestions/whatever. I'm very open to ideas if you have any!

I'm also on Twitter: @ptaoussanis.

License

Copyright © 2012 Peter Taoussanis

Distributed under the Eclipse Public License, the same as Clojure.

Something went wrong with that request. Please try again.