Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Fetching contributors…

Cannot retrieve contributors at this time

212 lines (154 sloc) 10.063 kB

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

Tower, a Clojure/Script i18n & L10n library

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.

What's in the box™?

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

Getting started

Dependencies

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

Translation

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) => "&lt;tag&gt;<strong>strong</strong>&lt;/tag&gt;"
(t :en :example/block-markdown)  => "<p>&lt;tag&gt;<strong>strong</strong>&lt;/tag&gt;</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:

  1. :example/foo in the :en-US locale.
  2. :example/foo in the :en locale.
  3. :example/foo in the dictionary's fallback locale.
  4. :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:

  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 fallback locale.
  6. :example/bar in the fallback locale.
  7. :missing in any of the above locales.

And even fallback locales. (t [:fr-FR :en-US] :example/foo) searches:

  1. :example/foo in the :fr-FR locale.
  2. :example/foo in the :fr locale.
  3. :example/foo in the :en-US locale.
  4. :example/foo in the :en locale.
  5. :example/foo in the fallback locale.
  6. :missing in any of the above locales.

In all cases, translation requests are logged upon fallback to fallback locale or :missing key.

ClojureScript translations (early support)

;; This example requires v3.1.0+
(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:

  1. The dictionary is provided in a pre-compiled form so that it can be inlined directly into your Cljs.
  2. 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.

Use with React (Reagent/Om/etc.)

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:

  1. Content intended to allow translator-controlled inline styles needs to provided to React with the dangerouslySetInnerHTML property.
  2. All other content should get a :<key>!-style translation to prevent double escaping (Tower already escapes translations not marked with an exlamation point).

Localization

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.

Country and languages names, timezones, etc.

Check out countries, languages, and timezones.

Ring middleware

Quickly internationalize your Ring web apps by adding taoensso.tower.ring/wrap-tower to your middleware stack.

See the docstring for details.

This project supports the CDS and ClojureWerkz goals

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

Contact & contributing

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!

License

Copyright © 2012-2014 Peter Taoussanis. Distributed under the Eclipse Public License, the same as Clojure.

Jump to Line
Something went wrong with that request. Please try again.