;; This Source Code Form is subject to the terms of the Mozilla Public ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; ;; Copyright (c) KALEIDOS INC (ns app.util.i18n "A i18n foundation." (:require [app.common.data :as d] [app.common.i18n] [app.common.logging :as log] [app.common.time :as ct] [app.config :as cf] [app.util.globals :as globals] [app.util.modules :as mod] [app.util.storage :as storage] [cuerdas.core :as str] [goog.object :as gobj] [okulary.core :as l] [promesa.core :as p] [rumext.v2 :as mf])) (log/set-level! :info) (def supported-locales [{:label "English" :value "en"} {:label "Español" :value "es"} {:label "Català" :value "ca"} {:label "Deutsch (community)" :value "de"} {:label "Dutch (community)" :value "nl"} {:label "Euskera (community)" :value "eu"} {:label "Français (community)" :value "fr"} {:label "Français - Canada (community)" :value "fr_CA"} {:label "Gallego (Community)" :value "gl"} {:label "Hausa (Community)" :value "ha"} {:label "Hrvatski (Community)" :value "hr"} {:label "Italiano (community)" :value "it"} {:label "Norsk - Bokmål (community)" :value "nb_no"} {:label "Polski (community)" :value "pl"} {:label "Portuguese - Brazil (community)" :value "pt_br"} {:label "Portuguese - Portugal (community)" :value "pt_pt"} {:label "Bahasa Indonesia (community)" :value "id"} {:label "Rumanian (community)" :value "ro"} {:label "Türkçe (community)" :value "tr"} {:label "Ελληνική γλώσσα (community)" :value "el"} {:label "Русский (community)" :value "ru"} {:label "Украї́нська мо́ва (community)" :value "uk"} {:label "Český jazyk (community)" :value "cs"} {:label "Latviešu valoda (community)" :value "lv"} {:label "Српски (community)" :value "sr"} {:label "Føroyskt mál (community)" :value "fo"} {:label "Korean (community)" :value "ko"} {:label "עִבְרִית (community)" :value "he"} {:label "आधुनिक मानक हिन्दी (community)" :value "hi"} {:label "عربي/عربى (community)" :value "ar"} {:label "فارسی (community)" :value "fa"} {:label "日本語 (Community)" :value "ja_jp"} {:label "简体中文 (community)" :value "zh_cn"} {:label "繁體中文 (community)" :value "zh_hant"}]) (defn- parse-locale [locale] (let [locale (-> (str/lower locale) (str/replace "-" "_"))] (cond-> [locale] (str/includes? locale "_") (conj (subs locale 0 2))))) (def ^:private browser-locales (delay (-> (.-language globals/navigator) (parse-locale)))) ;; Set initial translation loading state as globaly stored variable; ;; this facilitates hot reloading (when-not (exists? (unchecked-get globals/global "penpotTranslations")) (unchecked-set globals/global "penpotTranslations" #js {})) (defn- autodetect [] (let [supported (into #{} (map :value supported-locales))] (loop [locales (seq @browser-locales)] (if-let [locale (first locales)] (if (contains? supported locale) locale (recur (rest locales))) cf/default-language)))) (defn get-current "Get the currently memoized locale or execute the autodetection" [] (or (get storage/global ::locale) (autodetect))) (def ^:dynamic *current-locale* (get-current)) (defonce locale (l/atom *current-locale*)) (defn- get-translations "Get globaly stored mutable object with all loaded translations" [] (unchecked-get globals/global "penpotTranslations")) (defn set-translations "A helper for synchronously set translations data for specified locale" [locale data] (let [translations (get-translations)] (unchecked-set translations locale data) nil)) (defn set-default-translations [data] (set-translations cf/default-language data)) (defn- load [locale] (let [path (str "./translation." locale ".js?version=" cf/version-tag)] (->> (mod/import path) (p/fmap (fn [result] (unchecked-get result "default"))) (p/fnly (fn [data cause] (if cause (js/console.error "unexpected error on fetching locale" cause) (set-translations locale data))))))) (defn init "Initialize the i18n module" [] (load *current-locale*) (when-not (= *current-locale* cf/default-language) (load cf/default-language))) (defn set-locale [lname] (let [lname (if (or (nil? lname) (str/empty? lname)) (autodetect) (let [supported (into #{} (map :value) supported-locales)] (loop [locales (seq (parse-locale lname))] (if-let [locale (first locales)] (if (contains? supported locale) locale (recur (rest locales))) cf/default-language))))] (->> (load lname) (p/fnly (fn [_r _c] (set! *current-locale* lname) (reset! locale lname)))))) (deftype C [val] IDeref (-deref [_] val)) (defn ^boolean c? [r] (instance? C r)) ;; A main public api for translate strings. ;; A marker type that is used just for mark ;; a parameter that represented the counter. (defn c [x] (C. x)) (defn empty-string? [v] (or (nil? v) (empty? v))) (defn t ([locale code] (let [translations (get-translations) code (d/name code) value (gobj/getValueByKeys translations locale code)] (if (empty-string? value) (if (= cf/default-language locale) code (t cf/default-language code)) (if (array? value) (aget value 0) value)))) ([locale code & args] (let [translations (get-translations) code (d/name code) value (gobj/getValueByKeys translations locale code)] (if (empty-string? value) (if (= cf/default-language locale) code (apply t cf/default-language code args)) (let [plural (first (filter c? args)) value (if (array? value) (if (= @plural 1) (aget value 0) (aget value 1)) value)] (apply str/fmt value (map #(if (c? %) @% %) args))))))) (defn tr ([code] (t *current-locale* code)) ([code & args] (apply t *current-locale* code args))) (mf/defc tr-html* {::mf/props :obj} [{:keys [content class tag-name on-click]}] (let [tag-name (d/nilv tag-name "p")] [:> tag-name {:dangerouslySetInnerHTML #js {:__html content} :className class :on-click on-click}])) (add-watch locale "common.time" (fn [_ _ pv cv] (when (not= pv cv) (ct/set-default-locale cv)))) ;; Initialize date-fns locale on startup, the watch above only fires on changes (ct/set-default-locale *current-locale*) ;; We set the real translation function in the common i18n namespace, ;; so that when common code calls (tr ...) it uses this function. (set! app.common.i18n/tr tr) (set! app.common.i18n/c c)