https://github.com/tonsky/tongue.git
    git clone 'https://github.com/tonsky/tongue.git'
      
      
(ql:quickload :tonsky.tongue)
    
  
  Tongue is a do-it-yourself i18n library for Clojure and ClojureScript.
Tongue is very simple yet capable:
In contrast with other i18n solutions relying on complex and limiting string-based syntax for defining pluralization, wording, special cases etc, Tongue lets you use arbitrary functions. It gives you convenience, code reuse and endless possibilities.
As a result you have a library that handles exactly your case well with as much detail and precision as you need.
Add to project.clj:
[tongue "0.2.7"]In production:
-Dclojure.spec.compile-asserts=false to JVM options (actual JVM on Clojure, during build on ClojureScript)In development:
-Dclojure.spec.check-asserts=true to JVM options.Define dictionaries:
(require '[tongue.core :as tongue])
(def dicts
  { :en { ;; simple keys
          :color "Color"
          :flower "Flower"
          ;; namespaced keys
          :weather/rain   "Rain"
          :weather/clouds "Clouds"
          ;; nested maps will be unpacked into namespaced keys
          ;; this is purely for ease of dictionary writing
          :animals { :dog "Dog"   ;; => :animals/dog
                     :cat "Cat" } ;; => :animals/cat
          ;; substitutions
          :welcome "Hello, {1}!"
          :between "Value must be between {1} and {2}"
          ;; For using a map
          :mail-title "{user}, {title} - Message received."
          ;; arbitrary functions
          :count (fn [x]
                   (cond
                     (zero? x) "No items"
                     (= 1 x)   "1 item"
                     :else     "{1} items")) ;; you can return string with substitutions
        }
    :en-GB { :color "colour" } ;; sublang overrides
    :tongue/fallback :en }     ;; fallback locale keyThen build translation function:
(def translate ;; [locale key & args] => string
  (tongue/build-translate dicts))And go use it:
(translate :en :color) ;; => "Color"
;; namespaced keys
(translate :en :animals/dog) ;; => "Dog", taken from { :en { :animals { :dog "Dog }}}
;; substitutions
(translate :en :welcome "Nikita") ;; => "Hello, Nikita!"
(translate :en :between 0 100) ;; => "Value must be between 0 and 100"
(translate :en :mail-title {:user "Tom" :title "New message"}) ;; => "Tom, New message - Message received."
;; if key resolves to fn, it will be called with provided arguments
(translate :en :count 0) ;; => "No items"
(translate :en :count 1) ;; => "1 item"
(translate :en :count 2) ;; => "2 items"
;; multi-tag locales will fall back to more generic versions
;; :zh-Hans-CN will look in :zh-Hans-CN first, then :zh-Hans, then :zh, then fallback locale
(translate :en-GB :color) ;; => "Colour", taken from :en-GB
(translate :en-GB :flower) ;; => "Flower", taken from :en
;; if there’s no locale or no key in locale, fallback locale is used
(translate :ru :color) ;; => "Color", taken from :en as a fallback locale
;; if nothing can be found at all
(translate :en :unknown) ;; => "|Missing key :unknown|"Tongue can help you build localized number formatters:
(def format-number-en ;; [number] => string
  (tongue/number-formatter { :group ","
                             :decimal "." }))
(format-number-en 9999.9) ;; => "9,999.9"Use it directly or add :tongue/format-number key to locale’s dictionary. That way format will be applied to all numeric substitutions:
(def dicts
  { :en { :tongue/format-number format-number-en
          :count "{1} items" }
    :ru { :tongue/format-number (tongue/number-formatter { :group " "
                                                           :decimal "," })
          :count "{1} штук" }})
(def translate
  (tongue/build-translate dicts))
;; if locale has :tongue/format-number key, substituted numbers will be formatted
(translate :en :count 9999.9) ;; => "9,999.9 items"
(translate :ru :count 9999.9) ;; => "9 999,9 штук"
;; hint: if you only need a number, use :tongue/format-number key directly
(translate :en :tongue/format-number 9999.9) ;; => "9,999.9"It works almost the same way as with numbers, but requires a little more setup.
First, you’ll need locale strings:
(def inst-strings-en
  { :weekdays-narrow ["S" "M" "T" "W" "T" "F" "S"]
    :weekdays-short  ["Sun" "Mon" "Tue" "Wed" "Thu" "Fri" "Sat"]
    :weekdays-long   ["Sunday" "Monday" "Tuesday" "Wednesday" "Thursday" "Friday" "Saturday"]
    :months-narrow   ["J" "F" "M" "A" "M" "J" "J" "A" "S" "O" "N" "D"]
    :months-short    ["Jan" "Feb" "Mar" "Apr" "May" "Jun" "Jul" "Aug" "Sep" "Oct" "Nov" "Dec"]
    :months-long     ["January" "February" "March" "April" "May" "June" "July" "August" "September" "October" "November" "December"]
    :dayperiods      ["AM" "PM"]
    :eras-short      ["BC" "AD"]
    :eras-long       ["Before Christ" "Anno Domini"] })Feel free to omit keys you’re not going to use. E.g. for ISO 8601 none of these strings are used at all.
Then build a datetime formatter:
(def format-inst ;; [inst] | [inst tz] => string
  (tongue/inst-formatter "{month-short} {day}, {year} at {hour12}:{minutes-padded} {dayperiod}" inst-strings-en))And it’s ready to use:
(format-inst #inst "2016-07-11T22:31:00+06:00") ;; => "Jul 11, 2016 at 4:31 PM"
(format-inst
  #inst "2016-07-11T22:31:00+06:00"
  (java.util.TimeZone/getTimeZone "Asia/Novosibirsk")) ;; => "Jul 11, 2016 at 10:31 PM"tongue.core/inst-formatter builds a function that has two arities: just instant or instant and timezone:
|                  | Clojure              | ClojureScript |
| —————- | ——————– | ——— |
| instant: clojure.core/Inst protocol implementations | java.util.Date, java.time.Instant, …     | js/Date, … |
| timezone         | java.util.Timezone | integer GMT offset in minutes, e.g. 360 for GMT+6 |
| if tz is omitted | assume UTC           | assume browser timezone |
As with numbers, put a :tongue/format-inst key into dictionary to get default formatting for datetime substitutions:
(def dicts
  { :en { :tongue/format-inst (tongue/inst-formatter "{month-short} {day}, {year}" inst-strings-en)
          :published "Published at {1}" } })
(def translate
  (tongue/build-translate dicts))
;; if locale has :tongue/format-inst key, substituted instants will be formatted using it
(translate :en :published #inst "2016-01-01") ;; => "Published at January 1, 2016"Use multiple keys if you need several datetime format options:
(def dicts
  { :en
    { :date-full     (tongue/inst-formatter "{month-long} {day}, {year}" inst-strings-en)
      :date-short    (tongue/inst-formatter "{month-numeric}/{day}/{year-2digit}" inst-strings-en)
      :time-military (tongue/inst-formatter "{hour24-padded}{minutes-padded}")}})
(def translate (tongue/build-translate dicts))
(translate :en :date-full     #inst "2016-01-01T15:00:00") ;; => "January 1, 2016"
(translate :en :date-short    #inst "2016-01-01T15:00:00") ;; => "1/1/16"
(translate :en :time-military #inst "2016-01-01T15:00:00") ;; => "1500"
;; You can use timezones too
(def tz (java.util.TimeZone/getTimeZone "Asia/Novosibirsk"))  ;; GMT+6
(translate :en :time-military #inst "2016-01-01T15:00:00" tz) ;; => "2100"Full list of formatting options:
| Code                 | Example        | Meaning              |
| ——————– | ————– | ——————– |
| {hour24-padded}    | 00, 09, 12, 23 | Hour of day (00-23), 0-padded |
| {hour24}           | 0, 9, 12, 23   | Hour of day (0-23) |
| {hour12-padded}    | 12, 09, 12, 11 | Hour of day (01-12), 0-padded |
| {hour12}           | 12, 9, 12, 11  | Hour of day (1-12) |
| {dayperiod}        | AM, PM         | AM/PM from :dayperiods |
| {minutes-padded}   | 00, 30, 59     | Minutes (00-59), 0-padded |
| {minutes}          | 0, 30, 59      | Minutes (0-59) |
| {seconds-padded}   | 0, 30, 59      | Seconds (00-60), 0-padded |
| {seconds}          | 00, 30, 59     | Seconds (0-60) |
| {milliseconds}     | 000, 123, 999  | Milliseconds (000-999), always 0-padded |
| {weekday-long}     | Wednesday      | Weekday from :weekdays-long |
| {weekday-short}    | Wed, Thu       | Weekday from :weekdays-short |
| {weekday-narrow}   | W, T           | Weekday from :weekdays-narrow |
| {weekday-numeric}  | 1, 4, 5, 7     | Weekday number (1-7, Sunday = 1) |
| {day-padded}       | 01, 15, 29     | Day of month (01-31), 0-padded |
| {day}              | 1, 15, 29      | Day of month (1-31) |
| {month-long}       | January        | Month from :months-long |
| {month-short}      | Jan, Feb       | Month from :months-short |
| {month-narrow}     | J, F           | Month from :months-narrow |
| {month-numeric-padded} | 01, 02, 12 | Month number (01-12, January = 01), 0-padded |
| {month-numeric}    | 1, 2, 12       | Month number (1-12, January = 1) |
| {year}             | 1999, 2016     | Full year (0-9999) |
| {year-2digit}      | 99, 16         | Last two digits of a year (00-99) |
| {era-long}         | Anno Domini    | Era from :eras-long |
| {era-short}        | BC, AD         | Era from :eras-short |
| ...                | …            | anything not in {} is printed as-is |
clojure-future-spec to 1.9.0[clojure-future-spec "1.9.0-beta4"]$ (PR #7, thx Christian Johansen)[clojure-future-spec "1.9.0-alpha17"]{} syntax instead of <...>/%xInst protocol implementationstranslateInitial release
Copyright © 2016 Nikita Prokopov
Distributed under the Eclipse Public License either version 1.0 or (at your option) any later version.