• Stars
    star
    306
  • Rank 136,456 (Top 3 %)
  • Language
    Clojure
  • License
    Eclipse Public Li...
  • Created over 8 years ago
  • Updated over 2 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Do-it-yourself i18n library for Clojure/Script

tongue build status docs on cljdoc

Tongue is a do-it-yourself i18n library for Clojure and ClojureScript.

Tongue is very simple yet capable:

  • Dictionaries are just Clojure maps.
  • Translations are either strings, template strings or arbitrary functions.
  • No additional build steps, no runtime resource loading.
  • It comes with no built-in knowledge of world locales. It has all the tooling for you to define locales yourself though.
  • Pure Clojure implementation, no dependencies.
  • Can be used from both Clojure and ClojureScript.

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.

Who’s using Tongue?

  • Cognician, coaching platform
  • Logseq - Privacy-first, open source knowledge base

Setup

Add to project.clj:

[tongue "0.4.4"]

In production:

  • Add -Dclojure.spec.compile-asserts=false to JVM options (actual JVM on Clojure, during build on ClojureScript)

In development:

  • Add -Dclojure.spec.check-asserts=true to JVM options.

Usage

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

          ;; aliases, to share common strings but still use specific i18n keys
          :frontpage-greeting :welcome
          
          ;; arbitrary functions
          :count (fn [x]
                   (cond
                     (zero? x) "No items"
                     (= 1 x)   "1 item"
                     :else     "{1} items")) ;; you can return string with substitutions

          ;; optional -- override “Missing key” message
          :tongue/missing-key "Missing key {1}"
        }

    :en-GB { :color "colour" } ;; sublang overrides
    :tongue/fallback :en }     ;; fallback locale key

Then 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|"

Localizing numbers

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"

Localizing dates

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

Interpolation

Tongue supports both positional and named interpolations on strings:

(require '[tongue.core :as tongue])

(def dicts
  { :en { :welcome "Hello, {1}!"
          :mail-title "{user}, {title} - Message received."
        }})

(def tr (tongue/build-translate dicts))

(tr :en :welcome "Nikita") ;; => "Hello, Nikita!"
(tr :en :mail-title {:user "Tom" :title "New message"}) ;; => "Tom, New message - Message received."

The dictionary can contain other kinds of values. In that case, interpolation must be defined for the type by implementing the tongue.core/IInterpolate interface:

(require '[tongue.core :as tongue])

(extend-type clojure.lang.PersistentVector
  tongue/IInterpolate
  (interpolate-named [v dicts locale interpolations]
    (mapv (fn [x]
            (if (and (keyword? x)
                     (= "arg" (namespace x)))
              (get interpolations x)
              x)) v))

  (interpolate-positional [v dicts locale interpolations]
    (mapv (fn [x]
            (if (and (vector? x)
                     (= :arg (first x)))
              (nth interpolations (second x))
              x)) v)))

Now you can put vectors in the dictionary and have values interpolated in them:

(require '[tongue.core :as tongue])

(def dicts
  { :en { :welcome [:div {} "Hello, " [:arg 0]]
          :mail-title [:arg/user ", Message received."]
        }})

(def tr (tongue/build-translate dicts))

(tr :en :welcome "Nikita")
;; => [:div {} "Hello, " "Nikita"]

(tr :en :mail-title {:arg/user "Tom"})
;; => ["Tom" ", Message received."]

Changes

0.4.4 March 23, 2022

  • Fixed warning on Clojure 1.11 #35 #36 thx @stevejmp

0.4.3 December 9, 2021

  • Override “Missing Key” message with :tongue/missing-key #32

0.4.2 October 11, 2021

  • Make format-argument fn public #31 by @duckyuck

0.4.1 October 11, 2021

  • Do not modify String.prototype in CLJS version #29 by @cjohansen

0.4.0 October 5, 2021

  • Added IInterpolate protocol #11 #28 thx @cjohansen

0.3.0 June 16, 2021

  • Values could be aliased to another keys #8 #10 thx @cjohansen
  • Fix ratio formatting #15

0.2.10 September 22, 2020

  • Make build-dicts public #26 thx @hoxu

0.2.9 November 14, 2019

  • Allow qualified keywords in maps (PR #24, thx @just-sultanov)

0.2.8 October 9, 2019

  • Allow dash in map keys (PR #23, thx @mchughs)

0.2.7 July 26, 2019

  • Substitute placeholders from a map (PR #22, thx @katsuyasu-murata)

0.2.6

  • Fix namespaced keys (PR #20, thx @JoelSanchez)

0.2.5

  • Enable deep nesting of dicts (PR #18, thx @valerauko)
  • Bumped clojure-future-spec to 1.9.0

0.2.4

  • Don’t throw on missing argument index (#13)

0.2.3

  • [clojure-future-spec "1.9.0-beta4"]

0.2.2

0.2.1

  • [clojure-future-spec "1.9.0-alpha17"]
  • Tongue now works in both 1.8 and 1.9+ Clojure environments

0.2.0

  • Removed clojure-future-spec, requires Clojure 1.9 or later

0.1.4

  • Use unified {} syntax instead of <...>/%x

0.1.3

  • Date/time formatting can accept arbitrary Inst protocol implementations

0.1.2

  • Date/time formatting
  • ClojureScript now runs tests too
  • clojure.spec 1.9.0-alpha10
  • Disabled spec for ClojureScript

0.1.1

  • Absense of format rules shouldn’t break translate
  • number-format should not use fallback locale
  • updated to clojure.spec 1.9.0-alpha9

0.1.0

Initial release

License

Copyright © 2016 Nikita Prokopov

Distributed under the Eclipse Public License either version 1.0 or (at your option) any later version.

More Repositories

1

FiraCode

Free monospaced font with programming ligatures
Clojure
75,408
star
2

AnyBar

OS X menubar status indicator
Objective-C
5,877
star
3

datascript

Immutable database and Datalog query engine for Clojure, ClojureScript and JS
Clojure
5,446
star
4

rum

Simple, decomplected, isomorphic HTML UI library for Clojure and ClojureScript
HTML
1,791
star
5

vscode-theme-alabaster

A light theme for Visual Studio Code
Clojure
393
star
6

Clojure-Sublimed

Clojure support for Sublime Text 4
Clojure
353
star
7

Universal-Layout

Пакет из английской и русской раскладок, спроектированных для удобного совместного использования
Shell
300
star
8

uberdeps

Uberjar builder for deps.edn
Clojure
300
star
9

font-writer

Monospaced font for long-form writing
253
star
10

sublime-scheme-alabaster

Minimalist color scheme for Sublime Text 3
243
star
11

datascript-chat

Sample SPA using DataScript and core.async
Clojure
160
star
12

clj-reload

Smarter way to reload Clojure code
Clojure
149
star
13

grumpy

Minimalistic blog engine
Clojure
141
star
14

compact-uuids

Compact 26-char URL-safe representation of UUIDs
Clojure
129
star
15

net.async

Network commucations with clojure.core.async interface
Clojure
124
star
16

sublime-scheme-writer

A color scheme for focused long-form writing
122
star
17

clojure-future-spec

A backport of clojure.spec for Clojure 1.8
Clojure
115
star
18

intellij-alabaster

Alabaster color scheme for IntelliJ IDEA
101
star
19

datascript-transit

Transit handlers for DataScript database and datoms
Clojure
100
star
20

sublime-profiles

Profile Switcher for Sublime Text
Python
83
star
21

datascript-todo

DataScript ToDo Sample Application
Clojure
78
star
22

persistent-sorted-set

Fast B-tree based persistent sorted set for Clojure/Script
Clojure
78
star
23

tonsky.github.io

HTML
65
star
24

clojure-warrior

Visual Studio Code extension for Clojure development
TypeScript
57
star
25

cljs-drag-n-drop

Sane wrapper around Drag-n-Drop DOM API
Clojure
55
star
26

clj-simple-router

Simple order-independent Ring router
Clojure
53
star
27

vec

React.js + Immutable.js vector editor
JavaScript
51
star
28

clojure.unicode

Unicode symbols for Clojure
Clojure
49
star
29

41-socks

Simple match game in cljs+om+react
Clojure
37
star
30

remote-require

Require any Clojure snippet from anywhere in the Internet
Clojure
33
star
31

Sublime-Executor

Run any executable from your working dir in Sublime Text
Python
32
star
32

sane-math

Clojure/Script library for infix (normal) math expressions
Clojure
32
star
33

Heroes

A turn-based tactical game in ClojureScript, DataScript and Rum
Clojure
30
star
34

cljs-skeleton

Skeleton CLJS client/server app with WS, Transit, Rum
Clojure
30
star
35

icfpc2019-rust

Re-implementaion of https://github.com/tonsky/icfpc2019 in Rust to compare performance
Rust
29
star
36

alabaster-lighttable-skin

Light skin & theme for LightTable
CSS
27
star
37

openshift-clojure

Clojure/lein openshift cartridge template
Shell
26
star
38

datascript-storage-sql

SQL Storage implementation for DataScript
Clojure
24
star
39

sublime-scheme-commander

Retro color scheme for Sublime Text
23
star
40

Levinson-Layout

Keymap & EN/RU layouts for Levinson 40% split ortholinear keyboard
C
22
star
41

sublime-clojure-repl

Basic Clojure REPL for Sublime Text
Python
21
star
42

extend-clj

Easily extend clojure.core built-in protocols
Clojure
18
star
43

boot-anybar

A boot task reporting build status to AnyBar
Clojure
18
star
44

down-the-rabbit-hole

Entry to Ludum Dare 48
Clojure
16
star
45

bloknote

Fast online notepad
Clojure
16
star
46

sublime-color-schemes

Fun and simple color schemes for Sublime Text
Rust
15
star
47

katybot

Campfire bot written in Clojure
Clojure
14
star
48

toml-clj

Fast TOML parser for Clojure
Java
14
star
49

java-graphics-benchmark

Java Graphics benchmark
Java
13
star
50

Helix-Layout

C
13
star
51

datascript-menu

JavaScript
11
star
52

DarkModeToggle

Statusbar app to quickly toggle between light and dark modes
Swift
11
star
53

tonsky.me

Clojure
11
star
54

icfpc2021

Clojure
11
star
55

humble-ants

Clojure
10
star
56

advent-of-code

https://adventofcode.com/
Clojure
9
star
57

icfpc2019

Clojure
8
star
58

tgadmin

Clojure
7
star
59

jwm

Objective-C++
6
star
60

homm

Clojure
5
star
61

GMTKJam2022

GDScript
5
star
62

advent2018

Solutions to https://adventofcode.com/2018 in Clojure
Clojure
5
star
63

spectre

Fantom
3
star
64

imdbparse

Parser for IMDb text database
Clojure
3
star
65

icfpc2022

Clojure
3
star
66

clojure-bits

Clojure
3
star
67

tonsky

2
star
68

2017-05-RigaDevDays

JavaScript
2
star
69

clojure-bits-server

Clojure
2
star
70

2017-10-Reactive

JavaScript
2
star
71

lein-figwheel-immutant

[tonsky/figwheel-sidecar-immutant "0.5.9"]
Clojure
2
star
72

2018-05-UWDC

http://tonsky.me/2018-05-UWDC/slides/
JavaScript
2
star
73

codingame-fall-2022

Coding Games Fall Challenge 2022
Clojure
2
star
74

datascript-perf

Datasets for DataScript perf testing
Clojure
2
star
75

grumpy_video

1
star
76

icfpc2023

Clojure
1
star
77

roam-calculator

Shell
1
star
78

datascript_compiler_race

Clojure
1
star
79

ldjam53

GDScript
1
star
80

glutin_resize_issue

Rust
1
star