Lower-level implementation functions for | (ns metabase.util.i18n.impl (:require [clojure.java.io :as io] [clojure.string :as str] [clojure.tools.reader.edn :as edn] [metabase.plugins.classloader :as classloader] [metabase.util.i18n.plural :as i18n.plural] [metabase.util.log :as log] [potemkin.types :as p.types]) (:import (java.text MessageFormat) (java.util Locale) (org.apache.commons.lang3 LocaleUtils))) |
(set! *warn-on-reflection* true) | |
Protocol for anything that can be coerced to a | (p.types/defprotocol+ CoerceToLocale
(locale ^java.util.Locale [this]
"Coerce `this` to a `java.util.Locale`.")) |
Normalize a locale string to the canonical format. (normalized-locale-string "EN-US") ;-> "en_US" Returns | (defn normalized-locale-string
^String [s]
{:pre [((some-fn nil? string?) s)]}
#_{:clj-kondo/ignore [:discouraged-var]}
(when (string? s)
(when-let [[_ language country] (re-matches #"^(\w{2})(?:[-_](\w{2}))?$" s)]
(let [language (str/lower-case language)]
(if country
(str language \_ (some-> country str/upper-case))
language))))) |
(extend-protocol CoerceToLocale
nil
(locale [_] nil)
Locale
(locale [this] this)
String
(locale [^String s]
(some-> (normalized-locale-string s) LocaleUtils/toLocale))
;; Support namespaced keywords like `:en/US` and `:en/UK` because we can
clojure.lang.Keyword
(locale [this]
(locale (if-let [namespce (namespace this)]
(str namespce \_ (name this))
(name this))))) | |
True if | (defn available-locale?
[locale-or-name]
(boolean
(when-let [locale (locale locale-or-name)]
(LocaleUtils/isAvailableLocale locale)))) |
(defn- available-locale-names* [] (log/info "Reading available locales from locales.clj...") (some-> (io/resource "locales.clj") slurp edn/read-string :locales (->> (apply sorted-set)))) | |
Return sorted set of available locales, as Strings. (available-locale-names) ; -> #{"en" "nl" "pt-BR" "zh"} | (let [locales (delay (available-locale-names*))]
(defn available-locale-names
[]
@locales)) |
(defn- find-fallback-locale*
^Locale [^Locale a-locale]
(some (fn [locale-name]
(let [try-locale (locale locale-name)]
;; The language-only Locale is tried first by virtue of the
;; list being sorted.
(when (and (= (.getLanguage try-locale) (.getLanguage a-locale))
(not (= try-locale a-locale)))
try-locale)))
(available-locale-names))) | |
(def ^:private ^{:arglists '([a-locale])} find-fallback-locale
(memoize find-fallback-locale*)) | |
Find a translated fallback Locale in the following order:
1) If it is a language + country Locale, try the language-only Locale
2) If the language-only Locale isn't translated or the input is a language-only Locale,
find the first language + country Locale we have a translation for.
Return (fallback-locale "en_US") ; -> #locale"en" (fallback-locale "pt") ; -> #locale"pt_BR" (fallback-locale "ptPT") ; -> #locale"ptBR" Note: this logic should be kept in sync with the one in | (defn fallback-locale
^Locale [locale-or-name]
(when-let [a-locale (locale locale-or-name)]
(find-fallback-locale a-locale))) |
The resource URL for the edn file containing translations for (locale-edn-resources "es") ;-> #object[java.net.URL "file:/home/cam/metabase/resources/metabase/es.edn"] | (defn- locale-edn-resource
^java.net.URL [locale-or-name]
(when-let [a-locale (locale locale-or-name)]
(let [locale-name (-> (normalized-locale-string (str a-locale))
(str/replace #"_" "-"))
filename (format "i18n/%s.edn" locale-name)]
(io/resource filename (classloader/the-classloader))))) |
(defn- translations* [a-locale]
(when-let [resource (locale-edn-resource a-locale)]
(edn/read-string (slurp resource)))) | |
Fetch a map of original untranslated message format string -> translated message format string for (translations "es") ;-> {:headers { ... } :messages {"Username" "Nombre Usuario", ...}} | (def ^:private ^{:arglists '([locale-or-name])} translations
(comp (memoize translations*) locale)) |
Find the translated version of
| (defn- translated-format-string*
^String [locale-or-name format-string n]
(when (seq format-string)
(when-let [locale (locale locale-or-name)]
(when-let [translations (translations locale)]
(when-let [string-or-strings (get-in translations [:messages format-string])]
(if (string? string-or-strings)
;; Only a singular form defined; ignore `n`
string-or-strings
(if-let [plural-forms-header (get-in translations [:headers "Plural-Forms"])]
(get string-or-strings (i18n.plural/index plural-forms-header n))
;; Fall-back to singular if no header is present
(first string-or-strings)))))))) |
Find the translated version of | (defn- translated-format-string
^String [locale-or-name format-string {:keys [n format-string-pl]}]
(when-let [a-locale (locale locale-or-name)]
(or (when (= (.getLanguage a-locale) "en")
(if (or (nil? n) (= n 1))
format-string
format-string-pl))
(translated-format-string* a-locale format-string n)
(when-let [fallback-locale (fallback-locale a-locale)]
(log/tracef "No translated string found, trying fallback locale %s" (pr-str fallback-locale))
(translated-format-string* fallback-locale format-string n))
format-string))) |
(defn- message-format ^MessageFormat [locale-or-name ^String format-string pluralization-opts]
(or (when-let [a-locale (locale locale-or-name)]
(when-let [^String translated (translated-format-string a-locale format-string pluralization-opts)]
(MessageFormat. translated a-locale)))
(MessageFormat. format-string))) | |
Find the translated version of
Will attempt to translate (translate "es-MX" "must be {0} characters or less" 140) ; -> "deben tener 140 caracteres o menos" | (defn translate
([locale-or-name ^String format-string]
(translate locale-or-name format-string []))
([locale-or-name ^String format-string args]
(translate locale-or-name format-string args {}))
([locale-or-name ^String format-string args pluralization-opts]
(when (seq format-string)
(try
(.format (message-format locale-or-name format-string pluralization-opts) (to-array args))
(catch Throwable e
;; Not translating this string to prevent an unfortunate stack overflow. If this string happened to be the one
;; that had the typo, we'd just recur endlessly without logging an error.
(log/errorf e "Unable to translate string %s to %s" (pr-str format-string) (str locale-or-name))
(try
(.format (MessageFormat. format-string) (to-array args))
(catch Throwable _
(log/errorf e "Invalid format string %s" (pr-str format-string))
format-string))))))) |
Whether we're currently inside a call to [[site-locale-from-setting]], so we can prevent infinite recursion. | (def ^:private ^:dynamic *in-site-locale-from-setting* false) |
Fetch the value of the | (defn site-locale-from-setting
[]
(when-let [get-value-of-type (resolve 'metabase.models.setting/get-value-of-type)]
(when (bound? get-value-of-type)
;; make sure we don't try to recursively fetch the site locale when we're actively in the process of fetching it,
;; otherwise that will cause infinite loops if we try to log anything... see #32376
(when-not *in-site-locale-from-setting*
(binding [*in-site-locale-from-setting* true]
;; if there is an error fetching the Setting, e.g. if the app DB is in the process of shutting down, then just
;; return nil.
(try
(get-value-of-type :string :site-locale)
(catch Exception _
nil))))))) |
(defmethod print-method Locale [locale ^java.io.Writer writer] ((get-method print-dup Locale) locale writer)) | |
(defmethod print-dup Locale [locale ^java.io.Writer writer] (.write writer (format "#locale %s" (pr-str (str locale))))) | |