i18n functionality. | (ns metabase.util.i18n (:require [clojure.string :as str] [clojure.walk :as walk] [metabase.util.i18n.impl :as i18n.impl] [metabase.util.json :as json] [metabase.util.log :as log] [net.cgrand.macrovich :as macros] [potemkin :as p] [potemkin.types :as p.types]) (:import (java.text MessageFormat) (java.util Locale))) |
(set! *warn-on-reflection* true) | |
(p/import-vars [i18n.impl available-locale? fallback-locale locale normalized-locale-string translate]) | |
Bind this to a string, keyword, or | (def ^:dynamic *user-locale* nil) |
Bind this to a string, keyword to override the value returned by | (def ^:dynamic *site-locale-override* nil) |
The default locale string for this Metabase installation. Normally this is the value of the | (defn site-locale-string [] (or *site-locale-override* (i18n.impl/site-locale-from-setting) "en")) |
Locale string we should use for the current User (e.g. | (defn user-locale-string [] (or *user-locale* (site-locale-string))) |
The default locale for this Metabase installation. Normally this is the value of the | (defn site-locale ^Locale [] (locale (site-locale-string))) |
Locale we should use for the current User (e.g. | (defn user-locale ^Locale [] (locale (user-locale-string))) |
Returns all locale abbreviations and their full names | (defn available-locales-with-names [] (for [locale-name (i18n.impl/available-locale-names)] ;; Abbreviation must be normalized or the language picker will show incorrect saved value ;; because the locale is normalized before saving (metabase#15657, metabase#16654) [(normalized-locale-string locale-name) (.getDisplayName (locale locale-name))])) |
Translate a string with the System locale. | (defn- translate-site-locale [format-string args pluralization-opts] (let [translated (translate (site-locale) format-string args pluralization-opts)] (log/tracef "Translated %s for site locale %s -> %s" (pr-str format-string) (pr-str (site-locale-string)) (pr-str translated)) translated)) |
Translate a string with the current User's locale. | (defn- translate-user-locale [format-string args pluralization-opts] (let [translated (translate (user-locale) format-string args pluralization-opts)] (log/tracef "Translating %s for user locale %s (site locale %s) -> %s" (pr-str format-string) (pr-str (user-locale-string)) (pr-str (site-locale-string)) (pr-str translated)) translated)) |
(p.types/defrecord+ UserLocalizedString [format-string args pluralization-opts] Object (toString [_] (translate-user-locale format-string args pluralization-opts))) | |
(p.types/defrecord+ SiteLocalizedString [format-string args pluralization-opts] Object (toString [_] (translate-site-locale format-string args pluralization-opts))) | |
Write a UserLocalizedString or SiteLocalizedString to the | (defn- localized-to-json [localized-string json-generator] (json/write-string json-generator (str localized-string))) |
(json/add-encoder UserLocalizedString localized-to-json) (json/add-encoder SiteLocalizedString localized-to-json) | |
Schema for user and system localized string instances | (def LocalizedString (letfn [(instance-of [^Class klass] [:fn {:error/message (str "instance of " (.getCanonicalName klass))} (partial instance? klass)])] [:or (instance-of UserLocalizedString) (instance-of SiteLocalizedString)])) |
(defn- valid-str-form? [str-form] (and (= (first str-form) 'str) (every? string? (rest str-form)))) | |
Make sure the right number of args were passed to | (defn- validate-number-of-args [format-string-or-str args] (let [format-string (cond (string? format-string-or-str) format-string-or-str (valid-str-form? format-string-or-str) (apply str (rest format-string-or-str)) :else (assert false "The first arg to (deferred-)trs/tru must be a String or a valid `str` form with String arguments!")) message-format (MessageFormat. format-string) ;; number of {n} placeholders in format string including any you may have skipped. e.g. "{0} {2}" -> 3 expected-num-args-by-index (count (.getFormatsByArgumentIndex message-format)) ;; number of {n} placeholders in format string *not* including ones you make have skipped. e.g. "{0} {2}" -> 2 expected-num-args (count (.getFormats message-format)) actual-num-args (count args)] (assert (= expected-num-args expected-num-args-by-index) (format "(deferred-)trs/tru with format string %s is missing some {} placeholders. Expected %s. Did you skip any?" (pr-str (.toPattern message-format)) (str/join ", " (map (partial format "{%d}") (range expected-num-args-by-index))))) (assert (= expected-num-args actual-num-args) (str (format "(deferred-)trs/tru with format string %s expects %d args, got %d." (pr-str (.toPattern message-format)) expected-num-args actual-num-args) " Did you forget to escape a single quote?")))) |
Similar to The first argument can be a format string, or a valid Calling | (defmacro deferred-tru {:style/indent [:form]} [format-string-or-str & args] (validate-number-of-args format-string-or-str args) `(UserLocalizedString. ~format-string-or-str ~(vec args) {})) |
Similar to The first argument can be a format string, or a valid Calling | (defmacro deferred-trs {:style/indent [:form]} [format-string & args] (validate-number-of-args format-string args) `(SiteLocalizedString. ~format-string ~(vec args) {})) |
Ensures that | (def ^String ^{:arglists '([& args])} str* (if *compile-files* (fn [& _] (throw (Exception. "Premature i18n string lookup. Is there a top-level call to `trs` or `tru`?"))) str)) |
Applies The first argument can be a format string, or a valid Prefer this over | (defmacro tru-clj {:style/indent [:form]} [format-string-or-str & args] `(str* (deferred-tru ~format-string-or-str ~@args))) |
Applies The first argument can be a format string, or a valid Prefer this over | (defmacro trs-clj {:style/indent [:form]} [format-string-or-str & args] `(str* (deferred-trs ~format-string-or-str ~@args))) |
Make sure that | (defn- validate-n [format-string format-string-pl] (assert (and (string? format-string) (string? format-string-pl)) "The first and second args to (deferred-)trsn/trun must be Strings!") (let [validate (fn [format-string] (let [message-format (MessageFormat. format-string) ;; number of {n} placeholders in format string including any you may have skipped. e.g. "{0} {2}" -> 3 num-args-by-index (count (.getFormatsByArgumentIndex message-format)) ;; number of {n} placeholders in format string *not* including ones you make have skipped. e.g. "{0} {2}" -> 2 num-args (count (.getFormats message-format))] (assert (and (<= num-args-by-index 1) (<= num-args 1)) (format "(deferred-)trsn/trun only supports a single {0} placeholder for the value `n`"))))] (validate format-string) (validate format-string-pl))) |
Similar to The first argument should be the singular form; the second argument should be the plural form, and the third argument
should be (deferred-trun "{0} table" "{0} tables" n) | (defmacro deferred-trun [format-string format-string-pl n] (validate-n format-string format-string-pl) `(UserLocalizedString. ~format-string ~[n] ~{:n n :format-string-pl format-string-pl})) |
Similar to The first argument should be the singular form; the second argument should be the plural form, and the third argument
should be (trun "{0} table" "{0} tables" n) | (defmacro trun-clj [format-string format-string-pl n] `(str* (deferred-trun ~format-string ~format-string-pl ~n))) |
Similar to The first argument should be the singular form; the second argument should be the plural form, and the third argument
should be (deferred-trsn "{0} table" "{0} tables" n) | (defmacro deferred-trsn [format-string format-string-pl n] (validate-n format-string format-string-pl) `(SiteLocalizedString. ~format-string ~[n] ~{:n n :format-string-pl format-string-pl})) |
Similar to The first argument should be the singular form; the second argument should be the plural form, and the third argument
should be (trsn "{0} table" "{0} tables" n) | (defmacro trsn-clj [format-string format-string-pl n] `(str* (deferred-trsn ~format-string ~format-string-pl ~n))) |
Returns true if | (defn localized-string? [x] (boolean (some #(instance? % x) [UserLocalizedString SiteLocalizedString]))) |
Walks the datastructure | (defn localized-strings->strings [x] (walk/postwalk (fn [node] (cond-> node (localized-string? node) str)) x)) |
Clojure/ClojureScript macros | |
i18n a string with the user's locale. Format string will be translated to the user's locale when the form is eval'ed.
Placeholders should use (tru "Number of cans: {0}" 2) | (defmacro tru {:style/indent [:form]} [format-string & args] (macros/case :clj `(tru-clj ~format-string ~@args) :cljs `(js-i18n ~format-string ~@args))) |
i18n a string with the site's locale, when called from Clojure. Format string will be translated to the site's
locale when the form is eval'ed. Placeholders should use (trs "Number of cans: {0}" 2) NOTE: When called from ClojureScript, this function behaves identically to | (defmacro trs {:style/indent [:form]} [format-string & args] (macros/case :clj `(trs-clj ~format-string ~@args) :cljs `(js-i18n ~format-string ~@args))) |
i18n a string with both singular and plural forms, using the current user's locale. The appropriate plural form will
be returned based on the value of | (defmacro trun {:style/indent [:form]} [format-string format-string-pl n] (macros/case :clj `(trun-clj ~format-string ~format-string-pl ~n) :cljs `(js-i18n-n ~format-string ~format-string-pl ~n))) |
i18n a string with both singular and plural forms, using the site's locale. The appropriate plural form will be
returned based on the value of | (defmacro trsn {:style/indent [:form]} [format-string format-string-pl n] (macros/case :clj `(trsn-clj ~format-string ~format-string-pl ~n) :cljs `(js-i18n-n ~format-string ~format-string-pl ~n))) |