An Architect's View

CFML, Clojure, Software Design, Frameworks and more...

An Architect's View

Real World Clojure - i18n resources

October 31, 2011 ·

With a name like World Singles, you won't be surprised to know that our dating platform supports several different languages so our (non-Clojure) code is littered with calls like i18n.getResource( keyText, user.getPreferredLocale() ) and i18n.formatRBString( translation, [ arg1, arg2, arg3 ]). The former returns the localized translation for the keyText and the latter replaces substitution patterns like {1}, {2}, {3} with the matching argument from the supplied list.

We actually manage the translations in a database table and have a permission-based admin tool that allows translators to see the English text and work on their assigned locale translations. The admin tool also allows us to compare between the data entry system and the translations on production, and select which changes to push live. Other than the persistence layer which has moved to Clojure, the rest of this admin code will likely stay as-is for a while so I'm not going to cover it in this series. The calls shown above that our non-Clojure code makes is the API we needed to reproduce in Clojure.

Since we want to cache the translations for performance but also be able to reload them (after changes have been pushed to production), we use an atom for the in-memory translations so we can (re-)load the translations at will:

(def ^:private i18n-resources (atom nil))

(defn load-resources []
  (reset! i18n-resources
          (crud/execute (comp doall to-locale-map)
                        "select * from i18n order by locale, `key`")))

crud/execute was covered in the previous post in this series. to-locale-map takes the sequence of {:locale l :key k :translation t} rows from the query and reorganize it as a map keyed by locale whose values are maps keyed by the i18n key whose values are translations. That means we can retrieve a translation like this:

(defn get-resource [i18n-key locale]
  (or (get-in @@i18n-map [locale i18n-key])
      (get-in @@i18n-map ["en_US" i18n-key])
      (str "**" i18n-key "**")))

We lookup the key in the requested locale first, then fallback to English, but if the key is unknown we return the **key** string so we can easily spot missing translations in the UI. In our actual code we also support a debugging mode where we annotate results with HTML so we can provide our translators with tooltips on any translated text in our dating platform's UI and direct links to edit those translations.

So what's with the double-@@? As noted above, the translations are cached in an atom so the dereference accounts for one of those @s. Since we want to defer loading the resources until first use but ensure they are actually loaded, we use our delayed execution trick:

(def ^:private i18n-map
  (delay (do (load-resources) i18n-resources)))

By dereferencing the resources thru this variable, we ensure they are loaded once, on first use, but we get back the atom rather than its value so that if we reload the resources, we'll get the updated values. Think of it as (deref (deref i18n-map)) which evaluates to (deref i18n-resources) which evaluates to the current version of the resources in memory.

When we implemented format-string in Clojure, we took advantage of Clojure's flexible argument handling to support both of these calls:

(format-string translation [arg1 arg2 arg3])
(format-string translation arg1 arg2 arg3)

since sometimes we'll have a sequence of substitution values and this saves us writing the following in our own code:

(apply format-string translation [arg1 arg2 arg3])

I'll leave the implementation of format-string as an exercise for the reader. Needless to say, the Clojure version of our i18n code is much more concise than the original non-Clojure version!

Tags: clojure

2 responses

  • 1 Kai Tischler // Nov 3, 2011 at 4:29 AM

    Hello Sean !

    I got the impression that You are a staunch advocate of Clojure these days: Clojure seems to be the programming language "du jour"; or "d'eternite" ?

    Never mind: With a little bit of an Emacs Lisp background, the Clojure Syntax does not look too strange to me ...

    As you can probably remember: I was a long-time fellow of Isaac "Ike" Dealey's onTap/DataFaucet couple ! And g11n/i18n/l10n has played an important role right from the beginning ! But if I'm not in error: Using onTap as well as ColdBox, the code will be littered with those statements You mention at the beginning of Your blog entry. So I am naturally interested in Your new, Clojure-based g11n/i18n/l10n solution !

    Currently I am using CFML as well as Java; but still open for other programming languages like Scala, Groovy, Clojure !

    I guess You recommend to add Clojure into the mix ! And may it only be for the sake of its newly discovered g11n/i18n/l10n goodness !

    Very grateful I would be for pointers how to marry CFML, Java and Clojure !


    Cheers and Tschüss

    Kai
  • 2 Sean Corfield // Nov 4, 2011 at 4:52 PM

    @Kai, my cfmljure project (on github) is how I'm marrying CFML and Clojure right now.