{"id":29404633,"url":"https://github.com/noahtheduke/fluent-clj","last_synced_at":"2025-10-24T13:06:15.997Z","repository":{"id":283754909,"uuid":"952818330","full_name":"NoahTheDuke/fluent-clj","owner":"NoahTheDuke","description":"Project Fluent for Clojure/script","archived":false,"fork":false,"pushed_at":"2025-06-18T17:59:29.000Z","size":152,"stargazers_count":12,"open_issues_count":0,"forks_count":1,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-07-03T23:54:37.892Z","etag":null,"topics":["i18n","internationalization","l10n","localization","project-fluent"],"latest_commit_sha":null,"homepage":"","language":"Clojure","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mpl-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/NoahTheDuke.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2025-03-22T00:05:08.000Z","updated_at":"2025-06-18T17:59:33.000Z","dependencies_parsed_at":"2025-03-22T01:29:44.862Z","dependency_job_id":null,"html_url":"https://github.com/NoahTheDuke/fluent-clj","commit_stats":null,"previous_names":["noahtheduke/fluent-clj"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/NoahTheDuke/fluent-clj","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/NoahTheDuke%2Ffluent-clj","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/NoahTheDuke%2Ffluent-clj/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/NoahTheDuke%2Ffluent-clj/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/NoahTheDuke%2Ffluent-clj/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/NoahTheDuke","download_url":"https://codeload.github.com/NoahTheDuke/fluent-clj/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/NoahTheDuke%2Ffluent-clj/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":264652617,"owners_count":23644289,"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":["i18n","internationalization","l10n","localization","project-fluent"],"created_at":"2025-07-10T20:30:43.814Z","updated_at":"2025-10-24T13:06:15.991Z","avatar_url":"https://github.com/NoahTheDuke.png","language":"Clojure","funding_links":[],"categories":[],"sub_categories":[],"readme":"# fluent-clj\n\n[![Clojars Project](https://img.shields.io/clojars/v/io.github.noahtheduke/fluent-clj.svg)](https://clojars.org/io.github.noahtheduke/fluent-clj)\n[![cljdoc badge](https://cljdoc.org/badge/io.github.noahtheduke/fluent-clj)](https://cljdoc.org/d/io.github.noahtheduke/fluent-clj)\n\n[Project Fluent](https://projectfluent.org/) is very cool. The available [Java](https://github.com/xyzsd/fluent) and [Javascript](https://github.com/projectfluent/fluent.js) packages are relatively easy to use through interop, but without a unified interface, it's hard to write consistent and testable code.\n\nThis library aims to smooth over those differences, making it easy to build your own translation system.\n\n\u003e [!NOTE]\n\u003e Requires Clojure 1.12 because new interop syntax is really nice. I'm not looking to support earlier Clojures at this time.\n\n## Table of Contents\n\n\u003c!-- toc --\u003e\n\n- [Getting Started](#getting-started)\n- [Usage](#usage)\n- [Formatter](#formatter)\n- [Extended example](#extended-example)\n\n\u003c!-- tocstop --\u003e\n\n## Getting Started\n\nAdd it to your deps.edn or project.clj:\n\n```clojure\n{:deps {io.github.noahtheduke/fluent-clj {:mvn/version \"0.1.0\"}}}\n```\n\n## Usage\n\n```clojure\n(require '[noahtheduke.fluent :as i18n])\n\n;; A resource is a string of Fluent messages, terms, etc.\n(def sample-resource\n  \"\nhello = Hello world!\nwelcome = Welcome, {$user}!\nemail-cnt = {$cnt -\u003e\n    [one] {$cnt} email\n    *[other] {$cnt} emails\n}\")\n=\u003e #'sample-resource\n\n;; Bundles are native objects that hold the processed Fluent strings. They can\n;; be interacted with through interop but generally you only need the provided\n;; api functions.\n(def bundle (i18n/build \"en\" sample-resource))\n=\u003e #'bundle\n\n;; Message ids can be specified with strings, keywords, or symbols\n(i18n/format bundle :hello)\n=\u003e \"Hello world!\"\n\n;; Argument maps are just plain clojure maps\n(i18n/format bundle \"welcome\" {:user \"Noah\"})\n=\u003e \"Welcome, Noah!\"\n\n;; And their keys can be strings, keywords, or symbols as well\n(i18n/format bundle :email-cnt {\"cnt\" 1})\n=\u003e \"1 email\"\n\n(i18n/format bundle \"email-cnt\" {:cnt 2})\n=\u003e \"2 emails\"\n```\n\n## Formatter\n\nA no-options formatter for Fluent Files is included, available as a `-X` function. To use it, call `clojure -X noahtheduke.fluent.utils/fmt :file \"corpus/example.ftl\"` to format a single file or `clojure -X noahtheduke.fluent.utils/fmt :dir \"corpus\"` to format all `.ftl` files in the given directory.\n\n## Extended example\n\nI built this library for a website that uses [Reagent](https://reagent-project.github.io), so I'll share how we do it there.\n\nThe translations are stored as both raw text and fluent bundles. During app start, `(load-dictionary! \"resources/public/i18n\")` is called to load all of the Fluent files. Then on app load, the client sets a `GET` request to the server for the desired translation, and stores it locally with `insert-lang!`. The function `tr` (below) is modeled after [Tempura](https://github.com/taoensso/tempura)'s api, where a fallback value can be passed in with the desired translation: `(i18n/tr :hello)` without fallback, `(i18n/tr [:hello \"sup nerd\"])` with fallback.\n\nDone in a `.cljc` like this, translations can be tested in a normal clojure repl.\n\n```clojure\n(ns example.i18n\n  (:require\n   [noahtheduke.fluent :as fluent]\n   #?(:cljs\n     [reagent.core :as r])))\n\n(defonce fluent-dictionary\n  #?(:clj (atom nil)\n     :cljs (r/atom {})))\n\n(defn insert-lang! [lang content]\n  (swap! fluent-dictionary assoc lang {:content content\n                                       :ftl (fluent/build lang content)}))\n\n#?(:clj\n   (defn load-dictionary!\n     [dir]\n     (let [langs (-\u003e\u003e (io/file dir)\n                      (file-seq)\n                      (filter #(.isFile ^java.io.File %))\n                      (filter #(str/ends-with? (str %) \".ftl\"))\n                      (map (fn [^java.io.File f]\n                             (let [n (str/replace (.getName f) \".ftl\" \"\")\n                                   content (slurp f)]\n                               [n content]))))\n           errors (volatile! [])]\n       (doseq [[lang content] langs]\n         (try (insert-lang! lang content)\n              (catch Throwable t\n                (println \"Error inserting i18n data for\" lang)\n                (println (ex-message t))\n                (vswap! errors conj lang))))\n       @errors)))\n\n(defn get-content\n  [lang]\n  (get-in @fluent-dictionary [lang :content]))\n\n(defn get-bundle\n  [lang]\n  (get-in @fluent-dictionary [lang :ftl]))\n\n(defn get-translation\n  [bundle id params]\n  (when bundle\n    (fluent/format bundle id params)))\n\n(defn tr\n  ([lang resource] (tr lang resource nil))\n  ([lang resource params]\n   (let [resource (if (vector? resource) resource [resource])\n         [id fallback] resource]\n     (or (get-translation (get-bundle lang) id params)\n         ;; You can choose to use the fallback directly or use a translation\n         ;; from a different language. Project Fluent's javascript\n         ;; implementation has language negotiation libraries already so those\n         ;; can be used directly as desired.\n         fallback\n         (get-translation (get-bundle \"en\") id params)))))\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnoahtheduke%2Ffluent-clj","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnoahtheduke%2Ffluent-clj","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnoahtheduke%2Ffluent-clj/lists"}