• Stars
    star
    305
  • Rank 131,757 (Top 3 %)
  • Language
    Clojure
  • License
    Eclipse Public Li...
  • Created almost 8 years ago
  • Updated about 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
74,361
star
2

AnyBar

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

datascript

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

rum

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

vscode-theme-alabaster

A light theme for Visual Studio Code
Clojure
386
star
6

Clojure-Sublimed

Clojure support for Sublime Text 4
Clojure
349
star
7

uberdeps

Uberjar builder for deps.edn
Clojure
299
star
8

Universal-Layout

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

font-writer

Monospaced font for long-form writing
242
star
10

sublime-scheme-alabaster

Minimalist color scheme for Sublime Text 3
234
star
11

datascript-chat

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

grumpy

Minimalistic blog engine
Clojure
141
star
13

compact-uuids

Compact 26-char URL-safe representation of UUIDs
Clojure
126
star
14

net.async

Network commucations with clojure.core.async interface
Clojure
123
star
15

sublime-scheme-writer

A color scheme for focused long-form writing
119
star
16

clojure-future-spec

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

intellij-alabaster

Alabaster color scheme for IntelliJ IDEA
102
star
18

datascript-transit

Transit handlers for DataScript database and datoms
Clojure
100
star
19

sublime-profiles

Profile Switcher for Sublime Text
Python
81
star
20

datascript-todo

DataScript ToDo Sample Application
Clojure
78
star
21

persistent-sorted-set

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

tonsky.github.io

HTML
65
star
23

clojure-warrior

Visual Studio Code extension for Clojure development
TypeScript
57
star
24

cljs-drag-n-drop

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

vec

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

clojure.unicode

Unicode symbols for Clojure
Clojure
48
star
27

clj-simple-router

Simple order-independent Ring router
Clojure
48
star
28

41-socks

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

remote-require

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

Sublime-Executor

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

cljs-skeleton

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

Heroes

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

icfpc2019-rust

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

alabaster-lighttable-skin

Light skin & theme for LightTable
CSS
27
star
35

clj-reload

Clojure
27
star
36

openshift-clojure

Clojure/lein openshift cartridge template
Shell
26
star
37

datascript-storage-sql

SQL Storage implementation for DataScript
Clojure
23
star
38

sublime-scheme-commander

Retro color scheme for Sublime Text
23
star
39

sublime-clojure-repl

Basic Clojure REPL for Sublime Text
Python
22
star
40

Levinson-Layout

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

boot-anybar

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

extend-clj

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

down-the-rabbit-hole

Entry to Ludum Dare 48
Clojure
17
star
44

bloknote

Fast online notepad
Clojure
16
star
45

sublime-color-schemes

Fun and simple color schemes for Sublime Text
Rust
16
star
46

katybot

Campfire bot written in Clojure
Clojure
15
star
47

toml-clj

Fast TOML parser for Clojure
Java
14
star
48

java-graphics-benchmark

Java Graphics benchmark
Java
13
star
49

Helix-Layout

C
13
star
50

sane-math

Clojure/Script library for infix (normal) math expressions
Clojure
12
star
51

datascript-menu

JavaScript
11
star
52

DarkModeToggle

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

icfpc2021

Clojure
11
star
54

humble-ants

Clojure
10
star
55

advent-of-code

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

icfpc2019

Clojure
7
star
57

tonsky.me

Clojure
7
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-10-Reactive

JavaScript
2
star
69

2017-05-RigaDevDays

JavaScript
2
star
70

clojure-bits-server

Clojure
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

datascript_compiler_race

Clojure
1
star
77

roam-calculator

Shell
1
star
78

icfpc2023

Clojure
1
star
79

ldjam53

GDScript
1
star
80

glutin_resize_issue

Rust
1
star