From 299b29b66f2dd8cb9ac071a7ce5e058715310d18 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Thu, 18 Feb 2021 16:38:31 +0100 Subject: [PATCH] :tada: Add browser language detection. --- backend/src/app/rpc/mutations/profile.clj | 2 +- frontend/src/app/main.cljs | 4 + frontend/src/app/main/data/auth.cljs | 2 +- frontend/src/app/main/data/users.cljs | 79 +++++++++---------- .../src/app/main/ui/settings/options.cljs | 14 ++-- frontend/src/app/util/i18n.cljs | 60 +++++++++++--- 6 files changed, 102 insertions(+), 59 deletions(-) diff --git a/backend/src/app/rpc/mutations/profile.clj b/backend/src/app/rpc/mutations/profile.clj index 09a21079d6..4ea7d0c8be 100644 --- a/backend/src/app/rpc/mutations/profile.clj +++ b/backend/src/app/rpc/mutations/profile.clj @@ -33,7 +33,7 @@ (s/def ::email ::us/email) (s/def ::fullname ::us/not-empty-string) -(s/def ::lang ::us/not-empty-string) +(s/def ::lang (s/nilable ::us/not-empty-string)) (s/def ::path ::us/string) (s/def ::profile-id ::us/uuid) (s/def ::password ::us/not-empty-string) diff --git a/frontend/src/app/main.cljs b/frontend/src/app/main.cljs index 883e96c93a..23ae18fb4a 100644 --- a/frontend/src/app/main.cljs +++ b/frontend/src/app/main.cljs @@ -99,6 +99,10 @@ (mf/unmount (dom/get-element "modal")) (init-ui)) +(add-watch i18n/locale "locale" (fn [_ _ o v] + (when (not= o v) + (reinit)))) + (defn ^:dev/after-load after-load [] (reinit)) diff --git a/frontend/src/app/main/data/auth.cljs b/frontend/src/app/main/data/auth.cljs index 5ef0ab07ba..8c4871dcac 100644 --- a/frontend/src/app/main/data/auth.cljs +++ b/frontend/src/app/main/data/auth.cljs @@ -119,7 +119,7 @@ ptk/EffectEvent (effect [_ state s] (reset! storage {}) - (i18n/set-default-locale!)))) + (i18n/reset-locale)))) (defn logout [] diff --git a/frontend/src/app/main/data/users.cljs b/frontend/src/app/main/data/users.cljs index 1630aa492a..8490095c21 100644 --- a/frontend/src/app/main/data/users.cljs +++ b/frontend/src/app/main/data/users.cljs @@ -32,7 +32,7 @@ (s/def ::fullname ::us/string) (s/def ::email ::us/email) (s/def ::password ::us/string) -(s/def ::lang ::us/string) +(s/def ::lang (s/nilable ::us/string)) (s/def ::theme ::us/string) (s/def ::created-at ::us/inst) (s/def ::password-1 ::us/string) @@ -50,43 +50,36 @@ ;; --- Profile Fetched (defn profile-fetched - ([data] (profile-fetched nil data)) - ([on-success {:keys [fullname] :as data}] - (us/verify ::profile data) - (ptk/reify ::profile-fetched - ptk/UpdateEvent - (update [_ state] - (assoc state :profile - (cond-> data - (nil? (:lang data)) - (assoc :lang cfg/default-language) + [{:keys [fullname] :as data}] + (us/verify ::profile data) + (ptk/reify ::profile-fetched + ptk/UpdateEvent + (update [_ state] + (assoc state :profile + (cond-> data + (nil? (:theme data)) + (assoc :theme cfg/default-theme)))) - (nil? (:theme data)) - (assoc :theme cfg/default-theme)))) - - ptk/EffectEvent - (effect [_ state stream] - (let [profile (:profile state)] - (swap! storage assoc :profile profile) - (i18n/set-current-locale! (:lang profile)) - (theme/set-current-theme! (:theme profile)) - (when on-success - (on-success))))))) + ptk/EffectEvent + (effect [_ state stream] + (let [profile (:profile state)] + (swap! storage assoc :profile profile) + (i18n/set-locale! (:lang profile)) + (theme/set-current-theme! (:theme profile)))))) ;; --- Fetch Profile (defn fetch-profile - ([] (fetch-profile nil)) - ([on-success] - (reify - ptk/WatchEvent - (watch [_ state s] - (->> (rp/query! :profile) - (rx/map (partial profile-fetched on-success)) - (rx/catch (fn [error] - (if (= (:type error) :not-found) - (rx/of (rt/nav :auth-login)) - (rx/empty))))))))) + [] + (reify + ptk/WatchEvent + (watch [_ state s] + (->> (rp/query! :profile) + (rx/map profile-fetched) + (rx/catch (fn [error] + (if (= (:type error) :not-found) + (rx/of (rt/nav :auth-login)) + (rx/empty)))))))) ;; --- Update Profile @@ -95,15 +88,19 @@ (us/assert ::profile data) (ptk/reify ::update-profile ptk/WatchEvent - (watch [_ state s] - (let [mdata (meta data) + (watch [_ state stream] + (let [mdata (meta data) on-success (:on-success mdata identity) - on-error (:on-error mdata identity) - handle-error #(do (on-error (:payload %)) - (rx/empty))] - (->> (rp/mutation :update-profile data) - (rx/map (constantly (fetch-profile on-success))) - (rx/catch rp/client-error? handle-error)))))) + on-error (:on-error mdata identity)] + (rx/merge + (->> (rp/mutation :update-profile data) + (rx/map fetch-profile) + (rx/catch on-error)) + (->> stream + (rx/filter (ptk/type? ::profile-fetched)) + (rx/take 1) + (rx/tap on-success) + (rx/ignore))))))) ;; --- Request Email Change diff --git a/frontend/src/app/main/ui/settings/options.cljs b/frontend/src/app/main/ui/settings/options.cljs index 0ae1a3d3c1..1ad0d6d2c0 100644 --- a/frontend/src/app/main/ui/settings/options.cljs +++ b/frontend/src/app/main/ui/settings/options.cljs @@ -10,6 +10,7 @@ (ns app.main.ui.settings.options (:require [app.common.spec :as us] + [app.common.data :as d] [app.main.data.messages :as dm] [app.main.data.users :as du] [app.main.refs :as refs] @@ -21,7 +22,7 @@ [cljs.spec.alpha :as s] [rumext.alpha :as mf])) -(s/def ::lang (s/nilable ::us/not-empty-string)) +(s/def ::lang (s/nilable ::us/string)) (s/def ::theme (s/nilable ::us/not-empty-string)) (s/def ::options-form @@ -38,6 +39,9 @@ (defn- on-submit [form event] (let [data (:clean-data @form) + data (cond-> data + (empty? (:lang data)) + (assoc :lang nil)) mdata {:on-success (partial on-success form) :on-error (partial on-error form)}] (st/emit! (du/update-profile (with-meta data mdata))))) @@ -54,12 +58,10 @@ [:h2 (t locale "labels.language")] [:div.fields-row - [:& fm/select {:options [{:label "English" :value "en"} - {:label "Français" :value "fr"} - {:label "Español" :value "es"} - {:label "Русский" :value "ru"}] + [:& fm/select {:options (d/concat [{:label "Auto (browser)" :value ""}] + i18n/supported-locales) :label (t locale "dashboard.select-ui-language") - :default "en" + :default "" :name :lang}]] [:h2 (t locale "dashboard.theme-change")] diff --git a/frontend/src/app/util/i18n.cljs b/frontend/src/app/util/i18n.cljs index 989132af6a..cd8774a82d 100644 --- a/frontend/src/app/util/i18n.cljs +++ b/frontend/src/app/util/i18n.cljs @@ -2,8 +2,10 @@ ;; 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) 2015-2016 Juan de la Cruz -;; Copyright (c) 2015-2019 Andrey Antukh +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; +;; Copyright (c) 2020 UXBOX Labs SL (ns app.util.i18n "A i18n foundation." @@ -17,9 +19,40 @@ [app.util.storage :refer [storage]] [app.util.transit :as t])) -(defonce locale (l/atom (or (get storage ::locale) - cfg/default-language))) +(def supported-locales + [{:label "English" :value "en"} + {:label "Español" :value "es"} + {:label "Français (community)" :value "fr"} + {:label "Русский (community)" :value "ru"} + {:label "简体中文 (community)" :value "zh_cn"}]) + +(defn- parse-locale + [locale] + (let [locale (-> (.-language js/navigator) + (str/lower) + (str/replace "-" "_"))] + (cond-> [locale] + (str/includes? locale "_") + (conj (subs locale 0 2))))) + +(def ^:private browser-locales + (delay + (-> (.-language js/navigator) + (parse-locale)))) + +(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))) + cfg/default-language)))) + (defonce translations #js {}) +(defonce locale (l/atom (or (get storage ::locale) + (autodetect)))) ;; The traslations `data` is a javascript object and should be treated ;; with `goog.object` namespace functions instead of a standart @@ -31,14 +64,21 @@ [data] (set! translations data)) -(defn set-current-locale! - [v] - (swap! storage assoc ::locale v) - (reset! locale v)) +(defn set-locale! + [lang] + (if lang + (do + (swap! storage assoc ::locale lang) + (reset! locale lang)) + (do + (reset! locale (autodetect))))) -(defn set-default-locale! +(defn reset-locale + "Set the current locale to the browser detected one if it is + supported or default locale if not." [] - (set-current-locale! cfg/default-language)) + (swap! storage dissoc ::locale) + (reset! locale (autodetect))) (deftype C [val] IDeref