{"id":13646146,"url":"https://github.com/tonsky/tongue","last_synced_at":"2025-12-29T23:55:57.209Z","repository":{"id":9575286,"uuid":"62632664","full_name":"tonsky/tongue","owner":"tonsky","description":"Do-it-yourself i18n library for Clojure/Script","archived":false,"fork":false,"pushed_at":"2024-05-23T15:37:45.000Z","size":114,"stargazers_count":314,"open_issues_count":6,"forks_count":20,"subscribers_count":9,"default_branch":"master","last_synced_at":"2025-04-16T16:13:28.962Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Clojure","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"epl-1.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/tonsky.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null},"funding":{"github":["tonsky"],"patreon":"tonsky"}},"created_at":"2016-07-05T11:49:07.000Z","updated_at":"2025-03-27T17:58:59.000Z","dependencies_parsed_at":"2022-08-07T05:01:07.744Z","dependency_job_id":null,"html_url":"https://github.com/tonsky/tongue","commit_stats":null,"previous_names":[],"tags_count":22,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tonsky%2Ftongue","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tonsky%2Ftongue/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tonsky%2Ftongue/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tonsky%2Ftongue/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/tonsky","download_url":"https://codeload.github.com/tonsky/tongue/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":250100616,"owners_count":21374974,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-08-02T01:02:49.394Z","updated_at":"2025-12-29T23:55:57.182Z","avatar_url":"https://github.com/tonsky.png","language":"Clojure","funding_links":["https://github.com/sponsors/tonsky","https://patreon.com/tonsky"],"categories":["Clojure","config"],"sub_categories":[],"readme":"\u003cimg src=\"http://s.tonsky.me/imgs/tongue_logo.svg\"\u003e\n\n[![tongue](https://img.shields.io/clojars/v/tongue.svg)](http://clojars.org/tongue) [![build status](https://img.shields.io/circleci/project/tonsky/tongue.svg)](https://circleci.com/gh/tonsky/tongue) [![docs on cljdoc](https://cljdoc.xyz/badge/tongue)](https://cljdoc.xyz/d/tongue/tongue/CURRENT)\n\nTongue is a do-it-yourself i18n library for Clojure and ClojureScript.\n\nTongue is very simple yet capable:\n\n- Dictionaries are just Clojure maps.\n- Translations are either strings, template strings or arbitrary functions.\n- No additional build steps, no runtime resource loading.\n- It comes with no built-in knowledge of world locales. It has all the tooling for you to define locales yourself though.\n- Pure Clojure implementation, no dependencies.\n- Can be used from both Clojure and ClojureScript.\n\nIn 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.\n\nAs a result you have a library that handles exactly your case well with as much detail and precision as you need.\n\n### Who’s using Tongue?\n\n- [Cognician](https://www.cognician.com), coaching platform\n- [Logseq](https://logseq.com/) - Privacy-first, open source knowledge base\n\n## Setup\n\nAdd to `project.clj`:\n\n```clj\n[tongue \"0.4.4\"]\n```\n\nIn production:\n\n-  Add `-Dclojure.spec.compile-asserts=false` to JVM options (actual JVM on Clojure, during build on ClojureScript)\n\nIn development:\n\n- Add `-Dclojure.spec.check-asserts=true` to JVM options.\n\n## Usage\n\nDefine dictionaries:\n\n```clj\n(require '[tongue.core :as tongue])\n\n(def dicts\n  { :en { ;; simple keys\n          :color \"Color\"\n          :flower \"Flower\"\n\n          ;; namespaced keys\n          :weather/rain   \"Rain\"\n          :weather/clouds \"Clouds\"\n\n          ;; nested maps will be unpacked into namespaced keys\n          ;; this is purely for ease of dictionary writing\n          :animals { :dog \"Dog\"   ;; =\u003e :animals/dog\n                     :cat \"Cat\" } ;; =\u003e :animals/cat\n\n          ;; substitutions\n          :welcome \"Hello, {1}!\"\n          :between \"Value must be between {1} and {2}\"\n\n          ;; For using a map\n          :mail-title \"{user}, {title} - Message received.\"\n\n          ;; aliases, to share common strings but still use specific i18n keys\n          :frontpage-greeting :welcome\n          \n          ;; arbitrary functions\n          :count (fn [x]\n                   (cond\n                     (zero? x) \"No items\"\n                     (= 1 x)   \"1 item\"\n                     :else     \"{1} items\")) ;; you can return string with substitutions\n\n          ;; optional -- override “Missing key” message\n          :tongue/missing-key \"Missing key {1}\"\n        }\n\n    :en-GB { :color \"colour\" } ;; sublang overrides\n    :tongue/fallback :en }     ;; fallback locale key\n```\n\nThen build translation function:\n\n```clj\n(def translate ;; [locale key \u0026 args] =\u003e string\n  (tongue/build-translate dicts))\n```\n\nAnd go use it:\n\n```clj\n(translate :en :color) ;; =\u003e \"Color\"\n\n;; namespaced keys\n(translate :en :animals/dog) ;; =\u003e \"Dog\", taken from { :en { :animals { :dog \"Dog }}}\n\n;; substitutions\n(translate :en :welcome \"Nikita\") ;; =\u003e \"Hello, Nikita!\"\n(translate :en :between 0 100) ;; =\u003e \"Value must be between 0 and 100\"\n(translate :en :mail-title {:user \"Tom\" :title \"New message\"}) ;; =\u003e \"Tom, New message - Message received.\"\n\n;; if key resolves to fn, it will be called with provided arguments\n(translate :en :count 0) ;; =\u003e \"No items\"\n(translate :en :count 1) ;; =\u003e \"1 item\"\n(translate :en :count 2) ;; =\u003e \"2 items\"\n\n;; multi-tag locales will fall back to more generic versions\n;; :zh-Hans-CN will look in :zh-Hans-CN first, then :zh-Hans, then :zh, then fallback locale\n(translate :en-GB :color) ;; =\u003e \"Colour\", taken from :en-GB\n(translate :en-GB :flower) ;; =\u003e \"Flower\", taken from :en\n\n;; if there’s no locale or no key in locale, fallback locale is used\n(translate :ru :color) ;; =\u003e \"Color\", taken from :en as a fallback locale\n\n;; if nothing can be found at all\n(translate :en :unknown) ;; =\u003e \"|Missing key :unknown|\"\n```\n\n### Localizing numbers\n\nTongue can help you build localized number formatters:\n\n```clj\n(def format-number-en ;; [number] =\u003e string\n  (tongue/number-formatter { :group \",\"\n                             :decimal \".\" }))\n\n(format-number-en 9999.9) ;; =\u003e \"9,999.9\"\n```\n\nUse it directly or add `:tongue/format-number` key to locale’s dictionary. That way format will be applied to all numeric substitutions:\n\n```clj\n(def dicts\n  { :en { :tongue/format-number format-number-en\n          :count \"{1} items\" }\n    :ru { :tongue/format-number (tongue/number-formatter { :group \" \"\n                                                           :decimal \",\" })\n          :count \"{1} штук\" }})\n\n(def translate\n  (tongue/build-translate dicts))\n\n;; if locale has :tongue/format-number key, substituted numbers will be formatted\n(translate :en :count 9999.9) ;; =\u003e \"9,999.9 items\"\n(translate :ru :count 9999.9) ;; =\u003e \"9 999,9 штук\"\n\n;; hint: if you only need a number, use :tongue/format-number key directly\n(translate :en :tongue/format-number 9999.9) ;; =\u003e \"9,999.9\"\n```\n\n### Localizing dates\n\nIt works almost the same way as with numbers, but requires a little more setup.\n\nFirst, you’ll need locale strings:\n\n```clj\n(def inst-strings-en\n  { :weekdays-narrow [\"S\" \"M\" \"T\" \"W\" \"T\" \"F\" \"S\"]\n    :weekdays-short  [\"Sun\" \"Mon\" \"Tue\" \"Wed\" \"Thu\" \"Fri\" \"Sat\"]\n    :weekdays-long   [\"Sunday\" \"Monday\" \"Tuesday\" \"Wednesday\" \"Thursday\" \"Friday\" \"Saturday\"]\n    :months-narrow   [\"J\" \"F\" \"M\" \"A\" \"M\" \"J\" \"J\" \"A\" \"S\" \"O\" \"N\" \"D\"]\n    :months-short    [\"Jan\" \"Feb\" \"Mar\" \"Apr\" \"May\" \"Jun\" \"Jul\" \"Aug\" \"Sep\" \"Oct\" \"Nov\" \"Dec\"]\n    :months-long     [\"January\" \"February\" \"March\" \"April\" \"May\" \"June\" \"July\" \"August\" \"September\" \"October\" \"November\" \"December\"]\n    :dayperiods      [\"AM\" \"PM\"]\n    :eras-short      [\"BC\" \"AD\"]\n    :eras-long       [\"Before Christ\" \"Anno Domini\"] })\n```\n\nFeel free to omit keys you’re not going to use. E.g. for ISO 8601 none of these strings are used at all.\n\nThen build a datetime formatter:\n\n```clj\n(def format-inst ;; [inst] | [inst tz] =\u003e string\n  (tongue/inst-formatter \"{month-short} {day}, {year} at {hour12}:{minutes-padded} {dayperiod}\" inst-strings-en))\n```\n\nAnd it’s ready to use:\n\n```clj\n(format-inst #inst \"2016-07-11T22:31:00+06:00\") ;; =\u003e \"Jul 11, 2016 at 4:31 PM\"\n\n(format-inst\n  #inst \"2016-07-11T22:31:00+06:00\"\n  (java.util.TimeZone/getTimeZone \"Asia/Novosibirsk\")) ;; =\u003e \"Jul 11, 2016 at 10:31 PM\"\n```\n\n`tongue.core/inst-formatter` builds a function that has two arities: just instant or instant and timezone:\n\n|                  | Clojure              | ClojureScript |\n| ---------------- | -------------------- | --------- |\n| instant: `clojure.core/Inst` protocol implementations | `java.util.Date`, `java.time.Instant`, ...     | `js/Date`, ... |\n| timezone         | `java.util.Timezone` | integer GMT offset in minutes, e.g. `360` for GMT+6 |\n| if tz is omitted | assume UTC           | assume browser timezone |\n\nAs with numbers, put a `:tongue/format-inst` key into dictionary to get default formatting for datetime substitutions:\n\n```clj\n(def dicts\n  { :en { :tongue/format-inst (tongue/inst-formatter \"{month-short} {day}, {year}\" inst-strings-en)\n          :published \"Published at {1}\" } })\n\n(def translate\n  (tongue/build-translate dicts))\n\n;; if locale has :tongue/format-inst key, substituted instants will be formatted using it\n(translate :en :published #inst \"2016-01-01\") ;; =\u003e \"Published at January 1, 2016\"\n```\n\nUse multiple keys if you need several datetime format options:\n\n```clj\n(def dicts\n  { :en\n    { :date-full     (tongue/inst-formatter \"{month-long} {day}, {year}\" inst-strings-en)\n      :date-short    (tongue/inst-formatter \"{month-numeric}/{day}/{year-2digit}\" inst-strings-en)\n      :time-military (tongue/inst-formatter \"{hour24-padded}{minutes-padded}\")}})\n\n(def translate (tongue/build-translate dicts))\n\n(translate :en :date-full     #inst \"2016-01-01T15:00:00\") ;; =\u003e \"January 1, 2016\"\n(translate :en :date-short    #inst \"2016-01-01T15:00:00\") ;; =\u003e \"1/1/16\"\n(translate :en :time-military #inst \"2016-01-01T15:00:00\") ;; =\u003e \"1500\"\n\n;; You can use timezones too\n(def tz (java.util.TimeZone/getTimeZone \"Asia/Novosibirsk\"))  ;; GMT+6\n(translate :en :time-military #inst \"2016-01-01T15:00:00\" tz) ;; =\u003e \"2100\"\n```\n\n\nFull list of formatting options:\n\n| Code                 | Example        | Meaning              |\n| -------------------- | -------------- | -------------------- |\n| `{hour24-padded}`    | 00, 09, 12, 23 | Hour of day (00-23), 0-padded |\n| `{hour24}`           | 0, 9, 12, 23   | Hour of day (0-23) |\n| `{hour12-padded}`    | 12, 09, 12, 11 | Hour of day (01-12), 0-padded |\n| `{hour12}`           | 12, 9, 12, 11  | Hour of day (1-12) |\n| `{dayperiod}`        | AM, PM         | AM/PM from `:dayperiods` |\n| `{minutes-padded}`   | 00, 30, 59     | Minutes (00-59), 0-padded |\n| `{minutes}`          | 0, 30, 59      | Minutes (0-59) |\n| `{seconds-padded}`   | 0, 30, 59      | Seconds (00-60), 0-padded |\n| `{seconds}`          | 00, 30, 59     | Seconds (0-60) |\n| `{milliseconds}`     | 000, 123, 999  | Milliseconds (000-999), always 0-padded |\n| `{weekday-long}`     | Wednesday      | Weekday from `:weekdays-long` |\n| `{weekday-short}`    | Wed, Thu       | Weekday from `:weekdays-short` |\n| `{weekday-narrow}`   | W, T           | Weekday from `:weekdays-narrow` |\n| `{weekday-numeric}`  | 1, 4, 5, 7     | Weekday number (1-7, Sunday = 1) |\n| `{day-padded}`       | 01, 15, 29     | Day of month (01-31), 0-padded |\n| `{day}`              | 1, 15, 29      | Day of month (1-31) |\n| `{month-long}`       | January        | Month from `:months-long` |\n| `{month-short}`      | Jan, Feb       | Month from `:months-short` |\n| `{month-narrow}`     | J, F           | Month from `:months-narrow` |\n| `{month-numeric-padded}` | 01, 02, 12 | Month number (01-12, January = 01), 0-padded |\n| `{month-numeric}`    | 1, 2, 12       | Month number (1-12, January = 1) |\n| `{year}`             | 1999, 2016     | Full year (0-9999) |\n| `{year-2digit}`      | 99, 16         | Last two digits of a year (00-99) |\n| `{era-long}`         | Anno Domini    | Era from `:eras-long` |\n| `{era-short}`        | BC, AD         | Era from `:eras-short` |\n| `...`                | ...            | anything not in `{}` is printed as-is |\n\n\n## Interpolation\n\nTongue supports both positional and named interpolations on strings:\n\n```clj\n(require '[tongue.core :as tongue])\n\n(def dicts\n  { :en { :welcome \"Hello, {1}!\"\n          :mail-title \"{user}, {title} - Message received.\"\n        }})\n\n(def tr (tongue/build-translate dicts))\n\n(tr :en :welcome \"Nikita\") ;; =\u003e \"Hello, Nikita!\"\n(tr :en :mail-title {:user \"Tom\" :title \"New message\"}) ;; =\u003e \"Tom, New message - Message received.\"\n```\n\nThe dictionary can contain other kinds of values. In that case, interpolation\nmust be defined for the type by implementing the `tongue.core/IInterpolate`\ninterface:\n\n```clj\n(require '[tongue.core :as tongue])\n\n(extend-type clojure.lang.PersistentVector\n  tongue/IInterpolate\n  (interpolate-named [v dicts locale interpolations]\n    (mapv (fn [x]\n            (if (and (keyword? x)\n                     (= \"arg\" (namespace x)))\n              (get interpolations x)\n              x)) v))\n\n  (interpolate-positional [v dicts locale interpolations]\n    (mapv (fn [x]\n            (if (and (vector? x)\n                     (= :arg (first x)))\n              (nth interpolations (second x))\n              x)) v)))\n```\n\nNow you can put vectors in the dictionary and have values interpolated in them:\n\n```clj\n(require '[tongue.core :as tongue])\n\n(def dicts\n  { :en { :welcome [:div {} \"Hello, \" [:arg 0]]\n          :mail-title [:arg/user \", Message received.\"]\n        }})\n\n(def tr (tongue/build-translate dicts))\n\n(tr :en :welcome \"Nikita\")\n;; =\u003e [:div {} \"Hello, \" \"Nikita\"]\n\n(tr :en :mail-title {:arg/user \"Tom\"})\n;; =\u003e [\"Tom\" \", Message received.\"]\n```\n\n## Changes\n\n### 0.4.4 March 23, 2022\n\n- Fixed warning on Clojure 1.11 #35 #36 thx @stevejmp\n\n### 0.4.3 December 9, 2021\n\n- Override “Missing Key” message with `:tongue/missing-key` #32\n\n### 0.4.2 October 11, 2021\n\n- Make format-argument fn public #31 by @duckyuck\n\n### 0.4.1 October 11, 2021\n\n- Do not modify String.prototype in CLJS version #29 by @cjohansen\n\n### 0.4.0 October 5, 2021\n\n- Added IInterpolate protocol #11 #28 thx @cjohansen\n\n### 0.3.0 June 16, 2021\n\n- Values could be aliased to another keys #8 #10 thx @cjohansen\n- Fix ratio formatting #15\n\n### 0.2.10 September 22, 2020\n\n- Make build-dicts public #26 thx @hoxu\n\n### 0.2.9 November 14, 2019\n\n- Allow qualified keywords in maps (PR #24, thx @just-sultanov)\n\n### 0.2.8 October 9, 2019\n\n- Allow dash in map keys (PR #23, thx @mchughs)\n\n### 0.2.7 July 26, 2019\n\n- Substitute placeholders from a map (PR #22, thx @katsuyasu-murata)\n\n### 0.2.6\n\n- Fix namespaced keys (PR #20, thx @JoelSanchez)\n\n### 0.2.5\n\n- Enable deep nesting of dicts (PR #18, thx @valerauko)\n- Bumped `clojure-future-spec` to 1.9.0\n\n### 0.2.4\n\n- Don’t throw on missing argument index (#13)\n\n### 0.2.3\n\n- `[clojure-future-spec \"1.9.0-beta4\"]`\n\n### 0.2.2\n\n- Fixed incorrect substitution if replacement contained `$` (PR #7, thx [Christian Johansen](https://github.com/cjohansen))\n\n### 0.2.1\n\n- `[clojure-future-spec \"1.9.0-alpha17\"]`\n- Tongue now works in both 1.8 and 1.9+ Clojure environments\n\n### 0.2.0\n\n- Removed clojure-future-spec, requires Clojure 1.9 or later\n\n### 0.1.4\n\n- Use unified `{}` syntax instead of `\u003c...\u003e`/`%x`\n\n### 0.1.3\n\n- Date/time formatting can accept arbitrary `Inst` protocol implementations\n\n### 0.1.2\n\n- Date/time formatting\n- ClojureScript now runs tests too\n- clojure.spec 1.9.0-alpha10\n- Disabled spec for ClojureScript\n\n### 0.1.1\n\n- Absense of format rules shouldn’t break `translate`\n- number-format should not use fallback locale\n- updated to clojure.spec 1.9.0-alpha9\n\n### 0.1.0\n\nInitial release\n\n## License\n\nCopyright © 2016 Nikita Prokopov\n\nDistributed under the Eclipse Public License either version 1.0 or (at your option) any later version.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftonsky%2Ftongue","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftonsky%2Ftongue","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftonsky%2Ftongue/lists"}