From 632bbf3bc109186328ba758816e6ff4d97da1aa2 Mon Sep 17 00:00:00 2001 From: Roman Volosovskyi Date: Sat, 1 Jun 2019 09:47:37 +0300 Subject: [PATCH] [perf] Move translation to node_modules in release build --- .gitignore | 5 +- Makefile | 8 + build.clj | 2 +- ci/common.groovy | 1 + clj-rn.conf.edn | 6 +- dev/status_im/i18n_resources.cljs | 21 +++ prod/status_im/i18n_resources.cljs | 49 +++++++ project.clj | 20 +-- src/status_im/android/core.cljs | 5 +- src/status_im/i18n.cljs | 227 +---------------------------- src/status_im/ios/core.cljs | 6 +- status-modules/package.json | 5 + test/cljs/status_im/test/i18n.cljs | 208 ++++++++++++++++++++++++-- 13 files changed, 312 insertions(+), 251 deletions(-) create mode 100644 dev/status_im/i18n_resources.cljs create mode 100644 prod/status_im/i18n_resources.cljs create mode 100644 status-modules/package.json diff --git a/.gitignore b/.gitignore index 865ea36ffd..66d6a2c56c 100644 --- a/.gitignore +++ b/.gitignore @@ -162,4 +162,7 @@ conan.cmake # nix /.ran-setup -/.nix-gcroots/ \ No newline at end of file +/.nix-gcroots/ + +#modules +status-modules/translations diff --git a/Makefile b/Makefile index c70332cefe..c5fc814d52 100644 --- a/Makefile +++ b/Makefile @@ -65,6 +65,8 @@ release: release-android release-ios ##@build build release for Android and iOS release-android: export TARGET_OS ?= android release-android: ##@build build release for Android @$(MAKE) prod-build-android && \ + cp -R translations status-modules/translations && \ + cp -R status-modules node_modules/status-modules && \ react-native run-android --variant=release release-ios: export TARGET_OS ?= ios @@ -72,16 +74,22 @@ release-ios: ##@build build release for iOS release # Open XCode inside the Nix context @$(MAKE) prod-build-ios && \ echo "Build in XCode, see https://status.im/build_status/ for instructions" && \ + cp -R translations status-modules/translations && \ + cp -R status-modules node_modules/status-modules && \ open ios/StatusIm.xcworkspace release-desktop: export TARGET_OS ?= $(HOST_OS) release-desktop: ##@build build release for desktop release @$(MAKE) prod-build-desktop && \ + cp -R translations status-modules/translations && \ + cp -R status-modules node_modules/status-modules && \ scripts/build-desktop.sh release-windows-desktop: export TARGET_OS ?= windows release-windows-desktop: ##@build build release for desktop release @$(MAKE) prod-build-desktop && \ + cp -R translations status-modules/translations && \ + cp -R status-modules node_modules/status-modules && \ scripts/build-desktop.sh prod-build: export TARGET_OS ?= all diff --git a/build.clj b/build.clj index dbd01fc04d..43fab5ec89 100644 --- a/build.clj +++ b/build.clj @@ -234,7 +234,7 @@ (defn hawk-handler-translations [ctx e] - (let [path "src/status_im/i18n.cljs" + (let [path "dev/status_im/i18n_resources.cljs" i18n (slurp path)] (spit path (str i18n " ;;")) (spit path i18n)) diff --git a/ci/common.groovy b/ci/common.groovy index c40d1394a0..8c3f3836ad 100644 --- a/ci/common.groovy +++ b/ci/common.groovy @@ -71,6 +71,7 @@ def prep(type = 'nightly') { } /* node deps, pods, and status-go download */ utils.nix.shell("scripts/prepare-for-platform.sh ${prepareTarget}", pure: false) + sh("cp -R translations status-modules/translations && cp -R status-modules node_modules/status-modules") } return this diff --git a/clj-rn.conf.edn b/clj-rn.conf.edn index 7d1ea20642..0313ae80af 100644 --- a/clj-rn.conf.edn +++ b/clj-rn.conf.edn @@ -98,7 +98,7 @@ "cider.piggieback/wrap-cljs-repl"]} :builds [{:id :desktop - :source-paths ["react-native/src/desktop" "src" "env/dev" "components/src"] + :source-paths ["react-native/src/desktop" "src" "env/dev" "components/src" "dev"] :compiler {:output-to "target/desktop/app.js" :main "env.desktop.main" :output-dir "target/desktop" @@ -106,7 +106,7 @@ :optimizations :none} :figwheel true} {:id :ios - :source-paths ["react-native/src/mobile" "src" "env/dev" "components/src"] + :source-paths ["react-native/src/mobile" "src" "env/dev" "components/src" "dev"] :compiler {:output-to "target/ios/app.js" :main "env.ios.main" :output-dir "target/ios" @@ -114,7 +114,7 @@ :optimizations :none} :figwheel true} {:id :android - :source-paths ["react-native/src/mobile" "src" "env/dev" "components/src"] + :source-paths ["react-native/src/mobile" "src" "env/dev" "components/src" "dev"] :compiler {:output-to "target/android/app.js" :main "env.android.main" :output-dir "target/android" diff --git a/dev/status_im/i18n_resources.cljs b/dev/status_im/i18n_resources.cljs new file mode 100644 index 0000000000..83d1baf63e --- /dev/null +++ b/dev/status_im/i18n_resources.cljs @@ -0,0 +1,21 @@ +(ns status-im.i18n-resources + (:require-macros [status-im.i18n :as i18n]) + (:require [status-im.react-native.js-dependencies :as rn-dependencies] + [status-im.utils.types :as types] + [clojure.string :as string])) + +(def default-device-language + (keyword (.-language rn-dependencies/react-native-languages))) + +;; translations +(def translations-by-locale + (->> (i18n/translations [:en :es_419 :fa :ko :ms :pl :ru :zh_Hans_CN]) + (map (fn [[k t]] + (let [k' (-> (name k) + (string/replace "_" "-") + keyword)] + [k' (types/json->clj t)]))) + (into {}))) + +;; API compatibility +(defn load-language [lang]) diff --git a/prod/status_im/i18n_resources.cljs b/prod/status_im/i18n_resources.cljs new file mode 100644 index 0000000000..df43c4fb7e --- /dev/null +++ b/prod/status_im/i18n_resources.cljs @@ -0,0 +1,49 @@ +(ns status-im.i18n-resources + (:require-macros [status-im.utils.js-require :as js-require]) + (:require [status-im.react-native.js-dependencies :as rn-dependencies])) + +(def default-device-language + (keyword (.-language rn-dependencies/react-native-languages))) + +(def languages [:en :es_419 :fa :ko :ms :pl :ru :zh_Hans_CN]) + +(defonce loaded-languages + (atom + (conj #{:en} default-device-language))) + +(def prod-translations + {:en (js-require/js-require "status-modules/translations/en.json") + :es_419 (js-require/js-require "status-modules/translations/es_419.json") + :fa (js-require/js-require "status-modules/translations/fa.json") + :ko (js-require/js-require "status-modules/translations/ko.json") + :ms (js-require/js-require "status-modules/translations/ms.json") + :pl (js-require/js-require "status-modules/translations/pl.json") + :ru (js-require/js-require "status-modules/translations/ru.json") + :zh_Hans_CN (js-require/js-require "status-modules/translations/zh_Hans_CN.json")}) + +(defn valid-language [lang] + (if (contains? prod-translations lang) + lang + (let [short-lang (keyword (subs (name lang) 0 2))] + (when (contains? prod-translations short-lang) + short-lang)))) + +(defn require-translation [lang-key] + (when-let [lang (valid-language lang-key)] + ((get prod-translations lang)))) + +;; translations +(def translations-by-locale + (cond-> + {:en (require-translation :en)} + (not= :en default-device-language) + (assoc default-device-language + (require-translation default-device-language)))) + +(defn load-language [lang] + (let [lang-key (valid-language (keyword lang))] + (when-not (contains? @loaded-languages lang-key) + (aset (.-translations rn-dependencies/i18n) + lang + (require-translation lang-key)) + (swap! loaded-languages conj lang-key)))) diff --git a/project.clj b/project.clj index 83fb7f0831..65e706b7ca 100644 --- a/project.clj +++ b/project.clj @@ -50,20 +50,20 @@ :profiles {:dev {:dependencies [[cider/piggieback "0.4.0"]] :cljsbuild {:builds {:ios - {:source-paths ["components/src" "react-native/src/cljsjs" "react-native/src/mobile" "src"] + {:source-paths ["components/src" "react-native/src/cljsjs" "react-native/src/mobile" "src" "dev"] :compiler {:output-to "target/ios/app.js" :main "env.ios.main" :output-dir "target/ios" :optimizations :none}} :android - {:source-paths ["components/src" "react-native/src/cljsjs" "react-native/src/mobile" "src"] + {:source-paths ["components/src" "react-native/src/cljsjs" "react-native/src/mobile" "src" "dev"] :compiler {:output-to "target/android/app.js" :main "env.android.main" :output-dir "target/android" :optimizations :none} :warning-handlers [status-im.utils.build/warning-handler]} :desktop - {:source-paths ["components/src" "react-native/src/cljsjs" "react-native/src/desktop" "src"] + {:source-paths ["components/src" "react-native/src/cljsjs" "react-native/src/desktop" "src" "dev"] :compiler {:output-to "target/desktop/app.js" :main "env.desktop.main" :output-dir "target/desktop" @@ -76,12 +76,12 @@ [re-frisk-sidecar "0.5.7"] [day8.re-frame/tracing "0.5.0"] [hawk "0.2.11"]] - :source-paths ["src" "env/dev" "react-native/src/cljsjs" "components/src"]}] + :source-paths ["src" "env/dev" "react-native/src/cljsjs" "components/src" "dev"]}] :test {:dependencies [[day8.re-frame/test "0.1.5"]] :plugins [[lein-doo "0.1.9"]] :cljsbuild {:builds [{:id "test" - :source-paths ["components/src" "src" "test/cljs"] + :source-paths ["components/src" "src" "test/cljs" "dev"] :compiler {:main status-im.test.runner :output-to "target/test/test.js" :output-dir "target/test" @@ -89,7 +89,7 @@ :preamble ["js/hook-require.js"] :target :nodejs}} {:id "protocol" - :source-paths ["components/src" "src" "test/cljs"] + :source-paths ["components/src" "src" "test/cljs" "dev"] :compiler {:main status-im.test.protocol.runner :output-to "target/test/test.js" :output-dir "target/test" @@ -97,7 +97,7 @@ :preamble ["js/hook-require.js"] :target :nodejs}} {:id "env-dev-utils" - :source-paths ["env/dev/env/utils.cljs" "test/env/dev"] + :source-paths ["env/dev/env/utils.cljs" "test/env/dev" "dev"] :compiler {:main env.test.runner :output-to "target/test/test.js" :output-dir "target/test" @@ -105,7 +105,7 @@ :target :nodejs}}]}} :prod {:cljsbuild {:builds {:ios - {:source-paths ["components/src" "react-native/src/cljsjs" "react-native/src/mobile" "src" "env/prod"] + {:source-paths ["components/src" "react-native/src/cljsjs" "react-native/src/mobile" "src" "env/prod" "prod"] :compiler {:output-to "index.ios.js" :main "env.ios.main" :output-dir "target/ios-prod" @@ -122,7 +122,7 @@ :language-in :ecmascript5} :warning-handlers [status-im.utils.build/warning-handler]} :android - {:source-paths ["components/src" "react-native/src/cljsjs" "react-native/src/mobile" "src" "env/prod"] + {:source-paths ["components/src" "react-native/src/cljsjs" "react-native/src/mobile" "src" "env/prod" "prod"] :compiler {:output-to "index.android.js" :main "env.android.main" :output-dir "target/android-prod" @@ -139,7 +139,7 @@ :language-in :ecmascript5} :warning-handlers [status-im.utils.build/warning-handler]} :desktop - {:source-paths ["components/src" "react-native/src/cljsjs" "react-native/src/desktop" "src" "env/prod"] + {:source-paths ["components/src" "react-native/src/cljsjs" "react-native/src/desktop" "src" "env/prod" "prod"] :compiler {:output-to "index.desktop.js" :main "env.desktop.main" :output-dir "target/desktop-prod" diff --git a/src/status_im/android/core.cljs b/src/status_im/android/core.cljs index 38e7694c38..25b700d505 100644 --- a/src/status_im/android/core.cljs +++ b/src/status_im/android/core.cljs @@ -9,17 +9,16 @@ [status-im.ui.screens.views :as views] [status-im.ui.components.react :as react] [status-im.native-module.core :as status] - [status-im.notifications.core :as notifications] [status-im.core :as core] [status-im.react-native.js-dependencies :as rn-dependencies] [status-im.utils.snoopy :as snoopy] - [taoensso.timbre :as log])) + [status-im.i18n :as i18n])) (defn app-state-change-handler [state] (dispatch [:app-state-change state])) (defn on-languages-change [event] - (set! (.-locale rn-dependencies/i18n) (.-language event))) + (i18n/set-language (.-language event))) (defn on-shake [] (dispatch [:shake-event])) diff --git a/src/status_im/i18n.cljs b/src/status_im/i18n.cljs index 44f1517b1c..d01e548369 100644 --- a/src/status_im/i18n.cljs +++ b/src/status_im/i18n.cljs @@ -1,232 +1,19 @@ (ns status-im.i18n - (:require-macros [status-im.i18n :as i18n]) (:require - [cljs.spec.alpha :as spec] [status-im.react-native.js-dependencies :as rn-dependencies] [clojure.string :as string] - [clojure.set :as set] - [status-im.utils.types :as types])) + [status-im.i18n-resources :as i18n-resources])) -(set! (.-locale rn-dependencies/i18n) (.-language rn-dependencies/react-native-languages)) +(set! (.-locale rn-dependencies/i18n) (name i18n-resources/default-device-language)) (set! (.-fallbacks rn-dependencies/i18n) true) (set! (.-defaultSeparator rn-dependencies/i18n) "/") -;; translations -(def translations-by-locale - (->> (i18n/translations [:en :es_419 :fa :ko :ms :pl :ru :zh_Hans_CN]) - (map (fn [[k t]] - (let [k' (-> (name k) - (string/replace "_" "-") - keyword)] - [k' (types/json->clj t)]))) - (into {}))) - -;; english as source of truth -(def labels (set (keys (:en translations-by-locale)))) - -(spec/def ::label labels) -(spec/def ::labels (spec/coll-of ::label :kind set? :into #{})) - -(defn labels-for-all-locales [] - (->> translations-by-locale - (mapcat #(-> % val keys)) - set)) - -;; checkpoints - -;; Checkpoints specify milestones for locales. -;; -;; With milestones we can ensure that expected supported languages -;; are actually supported, and visualize the translation state for -;; the rest of locales according to these milestones. -;; -;; Checkpoints are defined by indicating the labels that need to be present -;; in a locale to achieve that checkpoint. -;; -;; We need to define the checkpoint that needs to be achieved for -;; a locale to be considered supported. This is why as we develop -;; we add translations, so we need to be defining a new target -;; for supported languages to achieve. -;; -;; Checkpoints are only used in dev and test. In dev when we want to -;; manually check the state of checkpoints for locales, and in test -;; to automatically check supported locales against the target checkpoint. - -(spec/def ::checkpoint.id keyword?) -(spec/def ::checkpoint-defs (spec/map-of ::checkpoint.id ::labels)) - -;; We define here the labels for the first specified checkpoint. -(def checkpoint-0-9-12-labels - #{:validation-amount-invalid-number :transaction-details :confirm :description - :phone-national :amount :open :close-app-title :members-active :chat-name - :phew-here-is-your-passphrase :public-group-topic :debug-enabled - :chat-settings :offline :update-status :invited :chat-send-eth :address - :new-public-group-chat :datetime-hour :wallet-settings - :datetime-ago-format :close-app-button :block :camera-access-error - :wallet-invalid-address :wallet-invalid-address-checksum :address-explication :remove - :transactions-delete-content :transactions-unsigned-empty - :transaction-moved-text :add-members :sign-later-title - :yes :dapps :popular-tags :network-settings :twelve-words-in-correct-order - :transaction-moved-title :photos-access-error :hash - :removed-from-chat :done :remove-from-contacts :delete-chat :new-group-chat - :edit-chats :wallet :wallet-exchange :wallet-request :sign-in - :datetime-yesterday :create-new-account :sign-in-to-status :save-password :save-password-unavailable :dapp-profile - :sign-later-text :datetime-ago :no-hashtags-discovered-body :contacts - :search-chat :got-it :delete-group-confirmation :public-chats - :not-applicable :move-to-internal-failure-message :active-online - :password :status-seen-by-everyone :edit-group :not-specified - :delete-group :send-request :paste-json :browsing-title - :wallet-add-asset :reorder-groups :transactions-history-empty :discover - :browsing-cancel :faucet-success :intro-status :name :gas-price - :view-transaction-details :wallet-error - :validation-amount-is-too-precise :copy-transaction-hash :unknown-address - :received-invitation :show-qr :edit-network-config :connect - :choose-from-contacts :edit :wallet-address-from-clipboard - :account-generation-message :remove-network :no-messages :passphrase - :recipient :members-title :new-group :suggestions-requests - :connected :rpc-url :settings :remove-from-group :specify-rpc-url - :transactions-sign-all :gas-limit :wallet-browse-photos - :add-new-contact :no-statuses-discovered-body :add-json-file :delete - :search-contacts :chats :transaction-sent :transaction :public-group-status - :leave-chat :transactions-delete :mainnet-text :image-source-make-photo - :chat :start-conversation :topic-format :add-new-network :save - :enter-valid-public-key :faucet-error :all - :confirmations-helper-text :search-for :sharing-copy-to-clipboard - :your-wallets :sync-in-progress :enter-password - :enter-address :switch-users :send-transaction :confirmations - :recover-access :image-source-gallery :sync-synced - :currency :status-pending :delete-contact :connecting-requires-login - :no-hashtags-discovered-title :datetime-day :request-transaction - :wallet-send :mute-notifications :scan-qr :contact-s - :unsigned-transaction-expired :status-sending :gas-used - :transactions-filter-type :next :recent - :open-on-etherscan :share :status :from - :wrong-password :search-chats :transactions-sign-later :in-contacts - :transactions-sign :sharing-share :type-a-message - :usd-currency :existing-networks :node-unavailable :url :shake-your-phone - :add-network :unknown-status-go-error :contacts-group-new-chat :and-you - :wallets :clear-history :wallet-choose-from-contacts - :signing-phrase-description :no-contacts :here-is-your-signing-phrase - :soon :close-app-content :status-sent :status-prompt - :delete-contact-confirmation :datetime-today :add-a-status - :web-view-error :notifications-title :error :transactions-sign-transaction - :edit-contacts :more :cancel :no-statuses-found :can-not-add-yourself - :transaction-description :add-to-contacts :available - :paste-json-as-text :You :main-wallet :process-json :testnet-text - :transactions :transactions-unsigned :members :intro-message1 - :public-chat-user-count :eth :transactions-history :not-implemented - :new-contact :datetime-second :status-failed :is-typing :recover - :suggestions-commands :nonce :new-network :contact-already-added :datetime-minute - :browsing-open-in-ios-web-browser :browsing-open-in-android-web-browser - :delete-group-prompt :wallet-total-value - :wallet-insufficient-funds :edit-profile :active-unknown - :search-tags :transaction-failed :public-key :error-processing-json - :status-seen :transactions-filter-tokens :status-delivered :profile - :wallet-choose-recipient :no-statuses-discovered :none :removed :empty-topic - :no :transactions-filter-select-all :transactions-filter-title :message - :here-is-your-passphrase :wallet-assets :image-source-title :current-network - :left :edit-network-warning :to :data :cost-fee}) - -;; NOTE: the rest checkpoints are based on the previous one, defined -;; like this: -;; (def checkpoint-2-labels (set/union checkpoint-1-labels #{:foo :bar}) -;; (def checkpoint-3-labels (set/union checkpoint-2-labels #{:baz}) - -;; NOTE: This defines the scope of each checkpoint. To support a checkpoint, -;; change the var `checkpoint-to-consider-locale-supported` a few lines -;; below. -(def checkpoints-def (spec/assert ::checkpoint-defs - {::checkpoint-0-9-12 checkpoint-0-9-12-labels})) -(def checkpoints (set (keys checkpoints-def))) - -(spec/def ::checkpoint checkpoints) - -(def checkpoint-to-consider-locale-supported ::checkpoint-0-9-12) - -(defn checkpoint->labels [checkpoint] - (get checkpoints-def checkpoint)) - -(defn checkpoint-val-to-compare [c] - (-> c name (string/replace #"^.*\|" "") int)) - -(defn >checkpoints [& cs] - (apply > (map checkpoint-val-to-compare cs))) - -(defn labels-that-are-not-in-current-checkpoint [] - (set/difference labels (checkpoint->labels checkpoint-to-consider-locale-supported))) - -;; locales - -(def locales (set (keys translations-by-locale))) - -(spec/def ::locale locales) -(spec/def ::locales (spec/coll-of ::locale :kind set? :into #{})) - -;; NOTE: Add new locale keywords here to indicate support for them. -#_(def supported-locales (spec/assert ::locales #{:fr - :zh - :zh-hans - :zh-hans-cn - :zh-hans-mo - :zh-hant - :zh-hant-sg - :zh-hant-hk - :zh-hant-tw - :zh-hant-mo - :zh-hant-cn - :sr-RS_#Cyrl - :el - :en - :de - :lt - :sr-RS_#Latn - :sr - :sv - :ja - :uk})) -(def supported-locales (spec/assert ::locales #{:en})) - -(spec/def ::supported-locale supported-locales) -(spec/def ::supported-locales (spec/coll-of ::supported-locale :kind set? :into #{})) - -(defn locale->labels [locale] - (-> translations-by-locale (get locale) keys set)) - -(defn locale->checkpoint [locale] - (let [locale-labels (locale->labels locale) - checkpoint (->> checkpoints-def - (filter (fn [[checkpoint checkpoint-labels]] - (set/subset? checkpoint-labels locale-labels))) - ffirst)] - checkpoint)) - -(defn locales-with-checkpoint [] - (->> locales - (map (fn [locale] - [locale (locale->checkpoint locale)])) - (into {}))) - -(defn locale-is-supported-based-on-translations? [locale] - (let [c (locale->checkpoint locale)] - (and c (or (= c checkpoint-to-consider-locale-supported) - (>checkpoints checkpoint-to-consider-locale-supported c))))) - -(defn actual-supported-locales [] - (->> locales - (filter locale-is-supported-based-on-translations?) - set)) - -(defn locales-with-full-support [] - (->> locales - (filter (fn [locale] - (set/subset? labels (locale->labels locale)))) - set)) - -(defn supported-locales-that-are-not-considered-supported [] - (set/difference (actual-supported-locales) supported-locales)) - (set! (.-translations rn-dependencies/i18n) - (clj->js translations-by-locale)) + (clj->js i18n-resources/translations-by-locale)) + +(defn set-language [lang] + (i18n-resources/load-language lang) + (set! (.-locale rn-dependencies/i18n) lang)) ;;:zh, :zh-hans-xx, :zh-hant-xx have been added until this bug will be fixed https://github.com/fnando/i18n-js/issues/460 diff --git a/src/status_im/ios/core.cljs b/src/status_im/ios/core.cljs index f2b12b7792..9726486ba5 100644 --- a/src/status_im/ios/core.cljs +++ b/src/status_im/ios/core.cljs @@ -9,15 +9,15 @@ [status-im.react-native.js-dependencies :as rn-dependencies] [status-im.ui.screens.views :as views] [status-im.ui.components.react :as react] - [status-im.notifications.core :as notifications] [status-im.core :as core] - [status-im.utils.snoopy :as snoopy])) + [status-im.utils.snoopy :as snoopy] + [status-im.i18n :as i18n])) (defn app-state-change-handler [state] (dispatch [:app-state-change state])) (defn on-languages-change [event] - (set! (.-locale rn-dependencies/i18n) (.-language event))) + (i18n/set-language (.-language event))) (defn on-shake [] (dispatch [:shake-event])) diff --git a/status-modules/package.json b/status-modules/package.json new file mode 100644 index 0000000000..78f9b03977 --- /dev/null +++ b/status-modules/package.json @@ -0,0 +1,5 @@ +{ + "name": "status-modules", + "version": "1.0.0", + "files": [] +} diff --git a/test/cljs/status_im/test/i18n.cljs b/test/cljs/status_im/test/i18n.cljs index 02334357a8..de2e2283db 100644 --- a/test/cljs/status_im/test/i18n.cljs +++ b/test/cljs/status_im/test/i18n.cljs @@ -1,29 +1,217 @@ (ns status-im.test.i18n (:require [cljs.test :refer-macros [deftest is]] [status-im.i18n :as i18n] + [status-im.i18n-resources :as i18n-resources] [clojure.set :as set] - [cljs.spec.alpha :as spec])) + [cljs.spec.alpha :as spec] + [clojure.string :as string])) + +;; english as source of truth +(def labels (set (keys (:en i18n-resources/translations-by-locale)))) + +(spec/def ::label labels) +(spec/def ::labels (spec/coll-of ::label :kind set? :into #{})) + +(defn labels-for-all-locales [] + (->> i18n-resources/translations-by-locale + (mapcat #(-> % val keys)) + set)) + +;; checkpoints + +;; Checkpoints specify milestones for locales. +;; +;; With milestones we can ensure that expected supported languages +;; are actually supported, and visualize the translation state for +;; the rest of locales according to these milestones. +;; +;; Checkpoints are defined by indicating the labels that need to be present +;; in a locale to achieve that checkpoint. +;; +;; We need to define the checkpoint that needs to be achieved for +;; a locale to be considered supported. This is why as we develop +;; we add translations, so we need to be defining a new target +;; for supported languages to achieve. +;; +;; Checkpoints are only used in dev and test. In dev when we want to +;; manually check the state of checkpoints for locales, and in test +;; to automatically check supported locales against the target checkpoint. + +(spec/def ::checkpoint.id keyword?) +(spec/def ::checkpoint-defs (spec/map-of ::checkpoint.id ::labels)) + +;; We define here the labels for the first specified checkpoint. +(def checkpoint-0-9-12-labels + #{:validation-amount-invalid-number :transaction-details :confirm :description + :phone-national :amount :open :close-app-title :members-active :chat-name + :phew-here-is-your-passphrase :public-group-topic :debug-enabled + :chat-settings :offline :update-status :invited :chat-send-eth :address + :new-public-group-chat :datetime-hour :wallet-settings + :datetime-ago-format :close-app-button :block :camera-access-error + :wallet-invalid-address :wallet-invalid-address-checksum :address-explication :remove + :transactions-delete-content :transactions-unsigned-empty + :transaction-moved-text :add-members :sign-later-title + :yes :dapps :popular-tags :network-settings :twelve-words-in-correct-order + :transaction-moved-title :photos-access-error :hash + :removed-from-chat :done :remove-from-contacts :delete-chat :new-group-chat + :edit-chats :wallet :wallet-exchange :wallet-request :sign-in + :datetime-yesterday :create-new-account :sign-in-to-status :save-password :save-password-unavailable :dapp-profile + :sign-later-text :datetime-ago :no-hashtags-discovered-body :contacts + :search-chat :got-it :delete-group-confirmation :public-chats + :not-applicable :move-to-internal-failure-message :active-online + :password :status-seen-by-everyone :edit-group :not-specified + :delete-group :send-request :paste-json :browsing-title + :wallet-add-asset :reorder-groups :transactions-history-empty :discover + :browsing-cancel :faucet-success :intro-status :name :gas-price + :view-transaction-details :wallet-error + :validation-amount-is-too-precise :copy-transaction-hash :unknown-address + :received-invitation :show-qr :edit-network-config :connect + :choose-from-contacts :edit :wallet-address-from-clipboard + :account-generation-message :remove-network :no-messages :passphrase + :recipient :members-title :new-group :suggestions-requests + :connected :rpc-url :settings :remove-from-group :specify-rpc-url + :transactions-sign-all :gas-limit :wallet-browse-photos + :add-new-contact :no-statuses-discovered-body :add-json-file :delete + :search-contacts :chats :transaction-sent :transaction :public-group-status + :leave-chat :transactions-delete :mainnet-text :image-source-make-photo + :chat :start-conversation :topic-format :add-new-network :save + :enter-valid-public-key :faucet-error :all + :confirmations-helper-text :search-for :sharing-copy-to-clipboard + :your-wallets :sync-in-progress :enter-password + :enter-address :switch-users :send-transaction :confirmations + :recover-access :image-source-gallery :sync-synced + :currency :status-pending :delete-contact :connecting-requires-login + :no-hashtags-discovered-title :datetime-day :request-transaction + :wallet-send :mute-notifications :scan-qr :contact-s + :unsigned-transaction-expired :status-sending :gas-used + :transactions-filter-type :next :recent + :open-on-etherscan :share :status :from + :wrong-password :search-chats :transactions-sign-later :in-contacts + :transactions-sign :sharing-share :type-a-message + :usd-currency :existing-networks :node-unavailable :url :shake-your-phone + :add-network :unknown-status-go-error :contacts-group-new-chat :and-you + :wallets :clear-history :wallet-choose-from-contacts + :signing-phrase-description :no-contacts :here-is-your-signing-phrase + :soon :close-app-content :status-sent :status-prompt + :delete-contact-confirmation :datetime-today :add-a-status + :web-view-error :notifications-title :error :transactions-sign-transaction + :edit-contacts :more :cancel :no-statuses-found :can-not-add-yourself + :transaction-description :add-to-contacts :available + :paste-json-as-text :You :main-wallet :process-json :testnet-text + :transactions :transactions-unsigned :members :intro-message1 + :public-chat-user-count :eth :transactions-history :not-implemented + :new-contact :datetime-second :status-failed :is-typing :recover + :suggestions-commands :nonce :new-network :contact-already-added :datetime-minute + :browsing-open-in-ios-web-browser :browsing-open-in-android-web-browser + :delete-group-prompt :wallet-total-value + :wallet-insufficient-funds :edit-profile :active-unknown + :search-tags :transaction-failed :public-key :error-processing-json + :status-seen :transactions-filter-tokens :status-delivered :profile + :wallet-choose-recipient :no-statuses-discovered :none :removed :empty-topic + :no :transactions-filter-select-all :transactions-filter-title :message + :here-is-your-passphrase :wallet-assets :image-source-title :current-network + :left :edit-network-warning :to :data :cost-fee}) + +;; NOTE: the rest checkpoints are based on the previous one, defined +;; like this: +;; (def checkpoint-2-labels (set/union checkpoint-1-labels #{:foo :bar}) +;; (def checkpoint-3-labels (set/union checkpoint-2-labels #{:baz}) + +;; NOTE: This defines the scope of each checkpoint. To support a checkpoint, +;; change the var `checkpoint-to-consider-locale-supported` a few lines +;; below. +(def checkpoints-def (spec/assert ::checkpoint-defs + {::checkpoint-0-9-12 checkpoint-0-9-12-labels})) +(def checkpoints (set (keys checkpoints-def))) + +(spec/def ::checkpoint checkpoints) + +(def checkpoint-to-consider-locale-supported ::checkpoint-0-9-12) + +(defn checkpoint->labels [checkpoint] + (get checkpoints-def checkpoint)) + +(defn checkpoint-val-to-compare [c] + (-> c name (string/replace #"^.*\|" "") int)) + +(defn >checkpoints [& cs] + (apply > (map checkpoint-val-to-compare cs))) + +;; locales + +(def locales (set (keys i18n-resources/translations-by-locale))) + +(spec/def ::locale locales) +(spec/def ::locales (spec/coll-of ::locale :kind set? :into #{})) + +(defn locale->labels [locale] + (-> i18n-resources/translations-by-locale (get locale) keys set)) + +(defn locale->checkpoint [locale] + (let [locale-labels (locale->labels locale) + checkpoint (->> checkpoints-def + (filter (fn [[checkpoint checkpoint-labels]] + (set/subset? checkpoint-labels locale-labels))) + ffirst)] + checkpoint)) + +(defn locale-is-supported-based-on-translations? [locale] + (let [c (locale->checkpoint locale)] + (and c (or (= c checkpoint-to-consider-locale-supported) + (>checkpoints checkpoint-to-consider-locale-supported c))))) + +(defn actual-supported-locales [] + (->> locales + (filter locale-is-supported-based-on-translations?) + set)) + +;; NOTE: Add new locale keywords here to indicate support for them. +#_(def supported-locales (spec/assert ::locales #{:fr + :zh + :zh-hans + :zh-hans-cn + :zh-hans-mo + :zh-hant + :zh-hant-sg + :zh-hant-hk + :zh-hant-tw + :zh-hant-mo + :zh-hant-cn + :sr-RS_#Cyrl + :el + :en + :de + :lt + :sr-RS_#Latn + :sr + :sv + :ja + :uk})) +(def supported-locales (spec/assert ::locales #{:en})) + +(spec/def ::supported-locale supported-locales) +(spec/def ::supported-locales (spec/coll-of ::supported-locale :kind set? :into #{})) (deftest label-options (is (not (nil? (:key (i18n/label-options {:key nil})))))) (deftest locales-only-have-existing-tran-ids - (is (spec/valid? ::i18n/labels (i18n/labels-for-all-locales)) - (->> i18n/locales - (remove #(spec/valid? ::i18n/labels (i18n/locale->labels %))) + (is (spec/valid? ::labels (labels-for-all-locales)) + (->> locales + (remove #(spec/valid? ::labels (locale->labels %))) (map (fn [l] (str "Extra translations in locale " l "\n" - (set/difference (i18n/locale->labels l) i18n/labels) + (set/difference (locale->labels l) labels) "\n\n"))) (apply str)))) (deftest supported-locales-are-actually-supported - (is (set/subset? i18n/supported-locales (i18n/actual-supported-locales)) - (->> i18n/supported-locales - (remove i18n/locale-is-supported-based-on-translations?) + (is (set/subset? supported-locales (actual-supported-locales)) + (->> supported-locales + (remove locale-is-supported-based-on-translations?) (map (fn [l] (str "Missing translations in supported locale " l "\n" - (set/difference (i18n/checkpoint->labels i18n/checkpoint-to-consider-locale-supported) - (i18n/locale->labels l)) + (set/difference (checkpoint->labels checkpoint-to-consider-locale-supported) + (locale->labels l)) "\n\n"))) (apply str))))