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