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))) |