From 1ca964e43876673ffa7d3b497cade7fcea5a3f9e Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Mon, 19 Oct 2026 21:48:59 +0200 Subject: [PATCH] :sparkles: Add language-specific CJK layout controls Add independent Japanese, Chinese and Korean file switches and all-files preferences under the shared vertical-text-layout feature flag. Keep control availability separate from saved paragraph profiles and rendering. Expose explicit language profiles in separate sidebar sections. Preserve mixed values, report the active paragraph profile at the caret, and keep shared writing-mode controls consistent without radio group collisions. Validation: common JVM/JS and frontend suites, backend profile tests, exporter tests and lint pass. Existing runner formatting and Spanish translation QA issues remain unchanged. AI-assisted-by: GPT-6 --- backend/src/app/rpc/commands/profile.clj | 2 + .../test/backend_tests/rpc_profile_test.clj | 23 ++++++ common/src/app/common/attrs.cljc | 5 ++ common/src/app/common/features.cljc | 3 + common/src/app/common/files/changes.cljc | 13 ++-- .../src/app/common/files/changes_builder.cljc | 14 ++-- common/src/app/common/flags.cljc | 1 + common/src/app/common/types/file.cljc | 37 ++++++--- common/test/common_tests/attrs_test.cljc | 10 ++- .../test/common_tests/files_changes_test.cljc | 52 ++++++++----- frontend/src/app/main/data/workspace.cljs | 12 +-- frontend/src/app/main/refs.cljs | 22 ++++-- .../src/app/main/ui/settings/options.cljs | 52 ++++++++----- .../src/app/main/ui/settings/options.scss | 7 +- .../src/app/main/ui/workspace/main_menu.cljs | 69 ++++++++++------- .../workspace/sidebar/options/menus/text.cljs | 66 ++++++++++------ .../options/menus/text_japanese_layout.cljs | 31 +++++--- .../options/menus/text_language_layout.cljs | 69 +++++++++++++++++ .../options/menus/text_language_layout.scss | 27 +++++++ frontend/src/app/render_wasm/text_editor.cljs | 29 ++++++- frontend/src/app/util/text/layout.cljs | 30 ++++++++ .../render_wasm/texts_test.cljs | 16 ++++ .../frontend_tests/ui/text_options_test.cljs | 75 ++++++++++++++++-- frontend/translations/en.po | 76 +++++++++++++++++++ frontend/translations/es.po | 76 +++++++++++++++++++ 25 files changed, 672 insertions(+), 145 deletions(-) create mode 100644 frontend/src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.cljs create mode 100644 frontend/src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.scss create mode 100644 frontend/src/app/util/text/layout.cljs diff --git a/backend/src/app/rpc/commands/profile.clj b/backend/src/app/rpc/commands/profile.clj index 0ab7d9ae75..53f7c48cbe 100644 --- a/backend/src/app/rpc/commands/profile.clj +++ b/backend/src/app/rpc/commands/profile.clj @@ -63,6 +63,8 @@ [:plugins {:optional true} ctp/schema:plugin-registry] [:renderer {:optional true} [::sm/one-of #{:svg :wasm}]] [:japanese-layout-all-files {:optional true} ::sm/boolean] + [:chinese-layout-all-files {:optional true} ::sm/boolean] + [:korean-layout-all-files {:optional true} ::sm/boolean] [:mcp-enabled {:optional true} ::sm/boolean] [:newsletter-updates {:optional true} ::sm/boolean] [:newsletter-news {:optional true} ::sm/boolean] diff --git a/backend/test/backend_tests/rpc_profile_test.clj b/backend/test/backend_tests/rpc_profile_test.clj index 7475e389e2..836e504277 100644 --- a/backend/test/backend_tests/rpc_profile_test.clj +++ b/backend/test/backend_tests/rpc_profile_test.clj @@ -179,6 +179,29 @@ (t/is (nil? (:error out))) (t/is (true? (get-in out [:result :props :japanese-layout-all-files])))))) +(t/deftest update-profile-language-layout-settings + (let [profile (th/create-profile* 1) + props {:japanese-layout-all-files true + :chinese-layout-all-files true + :korean-layout-all-files false} + params {::th/type :update-profile-props + ::rpc/profile-id (:id profile) + :props props} + out (th/command! params)] + (t/is (nil? (:error out))) + (t/is (= props (select-keys (:result out) (keys props)))) + (let [out (th/command! {::th/type :get-profile + ::rpc/profile-id (:id profile)})] + (t/is (= props (select-keys (get-in out [:result :props]) (keys props))))) + (let [out (th/command! (assoc params :props {:chinese-layout-all-files false}))] + (t/is (nil? (:error out)))) + (let [out (th/command! {::th/type :get-profile + ::rpc/profile-id (:id profile)})] + (t/is (= {:japanese-layout-all-files true + :chinese-layout-all-files false + :korean-layout-all-files false} + (select-keys (get-in out [:result :props]) (keys props))))))) + (t/deftest profile-deletion-1 (let [prof (th/create-profile* 1) file (th/create-file* 1 {:profile-id (:id prof) diff --git a/common/src/app/common/attrs.cljc b/common/src/app/common/attrs.cljc index 205b02ebd4..60e6e39eef 100644 --- a/common/src/app/common/attrs.cljc +++ b/common/src/app/common/attrs.cljc @@ -42,6 +42,11 @@ :multiple) (get obj attr ::unset))) + ;; An absent language is the plain profile, so it must participate + ;; when reporting mixed text selections. + (#{:text-language :korean-wrap} attr) + (get obj attr) + :else (get obj attr ::unset)))) diff --git a/common/src/app/common/features.cljc b/common/src/app/common/features.cljc index 281ed4c9bb..288c28a593 100644 --- a/common/src/app/common/features.cljc +++ b/common/src/app/common/features.cljc @@ -57,6 +57,7 @@ "text-editor/v2" "text-editor-wasm/v1" "text-editor-wasm/v1-html-paste" + "vertical-text-layout" "render-wasm/v1" "variants/v1"}) @@ -84,6 +85,7 @@ "text-editor-wasm/v1" "text-editor-wasm/v1-html-paste" "tokens/numeric-input" + "vertical-text-layout" "render-wasm/v1"}) ;; Features that are mainly backend only or there are a proper @@ -134,6 +136,7 @@ :feature-text-editor-v2-html-paste "text-editor/v2-html-paste" :feature-text-editor-wasm "text-editor-wasm/v1" :feature-text-editor-wasm-html-paste "text-editor-wasm/v1-html-paste" + :vertical-text-layout "vertical-text-layout" :feature-render-wasm "render-wasm/v1" :feature-variants "variants/v1" :feature-token-input "tokens/numeric-input" diff --git a/common/src/app/common/files/changes.cljc b/common/src/app/common/files/changes.cljc index baffa8d91d..3250be7d35 100644 --- a/common/src/app/common/files/changes.cljc +++ b/common/src/app/common/files/changes.cljc @@ -440,9 +440,10 @@ [:file-id ::sm/uuid] [:library-id [:maybe ::sm/uuid]]]] - [:set-japanese-layout - [:map {:title "SetJapaneseLayout"} - [:type [:= :set-japanese-layout]] + [:set-text-layout + [:map {:title "SetTextLayout"} + [:type [:= :set-text-layout]] + [:language [:enum :japanese :chinese :korean]] [:enabled ::sm/boolean]]]]) (def schema:changes @@ -1097,9 +1098,9 @@ [data {:keys [library-id]}] (cfo/set-tokens-source data library-id)) -(defmethod process-change :set-japanese-layout - [data {:keys [enabled]}] - (ctf/set-japanese-layout-enabled data enabled)) +(defmethod process-change :set-text-layout + [data {:keys [language enabled]}] + (ctf/set-text-layout-enabled data language enabled)) ;; === Operations diff --git a/common/src/app/common/files/changes_builder.cljc b/common/src/app/common/files/changes_builder.cljc index d948196886..783b7416d1 100644 --- a/common/src/app/common/files/changes_builder.cljc +++ b/common/src/app/common/files/changes_builder.cljc @@ -1150,15 +1150,17 @@ :library-id prev-val}) (apply-changes-local)))) -(defn set-japanese-layout - [changes enabled?] +(defn set-text-layout + [changes language enabled?] (assert-file-data! changes) - (let [file-data (::file-data (meta changes)) - previous-enabled (ctf/japanese-layout-enabled? file-data)] + (let [file-data (::file-data (meta changes)) + previous-enabled (ctf/text-layout-enabled? file-data language)] (-> changes - (update :redo-changes conj {:type :set-japanese-layout + (update :redo-changes conj {:type :set-text-layout + :language language :enabled enabled?}) - (update :undo-changes conj {:type :set-japanese-layout + (update :undo-changes conj {:type :set-text-layout + :language language :enabled previous-enabled}) (apply-changes-local)))) diff --git a/common/src/app/common/flags.cljc b/common/src/app/common/flags.cljc index 378f1e6d08..9533b9d94e 100644 --- a/common/src/app/common/flags.cljc +++ b/common/src/app/common/flags.cljc @@ -156,6 +156,7 @@ ;; Show WASM renderer info label (hidden by default). :render-wasm-info :render-switch + :vertical-text-layout :hide-release-modal :subscriptions :inspect-styles diff --git a/common/src/app/common/types/file.cljc b/common/src/app/common/types/file.cljc index 6d4914ee06..d0ca2cd39a 100644 --- a/common/src/app/common/types/file.cljc +++ b/common/src/app/common/types/file.cljc @@ -78,7 +78,9 @@ [:map {:title "FileOptions"} [:components-v2 {:optional true} ::sm/boolean] [:base-font-size {:optional true} :string] - [:japanese-layout {:optional true} ::sm/boolean]]) + [:japanese-layout {:optional true} ::sm/boolean] + [:chinese-layout {:optional true} ::sm/boolean] + [:korean-layout {:optional true} ::sm/boolean]]) (def schema:data [:map {:title "FileData"} @@ -1256,17 +1258,32 @@ (assoc-in file-data [:options :base-font-size] base-font-size)) -;; Japanese text layout +;; Text layout controls -(defn japanese-layout-enabled? - [file-data] - (true? (get-in file-data [:options :japanese-layout]))) +(defn text-layout-option + [language] + (case language + :japanese :japanese-layout + :chinese :chinese-layout + :korean :korean-layout)) -(defn set-japanese-layout-enabled - [file-data enabled?] - (if enabled? - (assoc-in file-data [:options :japanese-layout] true) - (d/update-in-when file-data [:options] dissoc :japanese-layout))) +(defn text-layout-preference + [language] + (case language + :japanese :japanese-layout-all-files + :chinese :chinese-layout-all-files + :korean :korean-layout-all-files)) + +(defn text-layout-enabled? + [file-data language] + (true? (get-in file-data [:options (text-layout-option language)]))) + +(defn set-text-layout-enabled + [file-data language enabled?] + (let [option (text-layout-option language)] + (if enabled? + (assoc-in file-data [:options option] true) + (d/update-in-when file-data [:options] dissoc option)))) ;; Ref Chains diff --git a/common/test/common_tests/attrs_test.cljc b/common/test/common_tests/attrs_test.cljc index 340a89e0da..9899e6efb8 100644 --- a/common/test/common_tests/attrs_test.cljc +++ b/common/test/common_tests/attrs_test.cljc @@ -148,4 +148,12 @@ {:x -153}] result (attrs/get-attrs-multi objs [:x])] (t/is (= {:x :multiple} result) - "Different positions should be :multiple")))) \ No newline at end of file + "Different positions should be :multiple")))) +(t/deftest unset-language-profile-counts-as-a-distinct-value + (t/is (= {:text-language :multiple :korean-wrap :multiple} + (attrs/get-attrs-multi [{} {:text-language "ko" :korean-wrap "spaces-only"}] + [:text-language :korean-wrap]))) + (t/is (= {:text-language :multiple} + (attrs/get-attrs-multi [{:text-language "ko"} {}] [:text-language]))) + (t/is (= {:text-language nil :korean-wrap nil} + (attrs/get-attrs-multi [{} {}] [:text-language :korean-wrap])))) diff --git a/common/test/common_tests/files_changes_test.cljc b/common/test/common_tests/files_changes_test.cljc index 0d6dbc86f6..7936eea061 100644 --- a/common/test/common_tests/files_changes_test.cljc +++ b/common/test/common_tests/files_changes_test.cljc @@ -251,25 +251,39 @@ "The parent must use the updated bounds of both children") (assert-idempotent data changes)))))) -(t/deftest set-japanese-layout-file-option - (let [file-id (uuid/custom 2 2) - page-id (uuid/custom 1 1) - data (make-file-data file-id page-id) - enabled (ch/process-changes data [{:type :set-japanese-layout - :enabled true}]) - disabled (ch/process-changes enabled [{:type :set-japanese-layout - :enabled false}]) - changes (-> (pcb/empty-changes) - (pcb/with-file-data data) - (pcb/set-japanese-layout true))] - (t/is (false? (ctf/japanese-layout-enabled? data))) - (t/is (true? (ctf/japanese-layout-enabled? enabled))) - (t/is (false? (ctf/japanese-layout-enabled? disabled))) - (t/is (not (contains? (:options disabled) :japanese-layout))) - (t/is (= [{:type :set-japanese-layout :enabled true}] - (:redo-changes changes))) - (t/is (= '({:type :set-japanese-layout :enabled false}) - (:undo-changes changes))))) +(t/deftest language-layout-changes-are-independent + (let [data (assoc (make-file-data (uuid/custom 2 2) (uuid/custom 1 1)) + :options {:japanese-layout true :base-font-size "16"})] + (doseq [language [:japanese :chinese :korean]] + (let [change {:type :set-text-layout :language language :enabled true} + enabled (ch/process-changes data [change]) + disabled (ch/process-changes enabled [(assoc change :enabled false)]) + option (case language + :japanese :japanese-layout + :chinese :chinese-layout + :korean :korean-layout)] + (t/is (ch/valid-change? change)) + (t/is (true? (get-in enabled [:options option]))) + (t/is (not (contains? (:options disabled) option))) + (t/is (= (dissoc (:options data) option) (:options disabled))) + (t/is (= (dissoc data :options) (dissoc enabled :options)))))) + (t/is (not (ch/valid-change? {:type :set-text-layout :language :english + :enabled true})))) + +(t/deftest language-layout-switches-round-trip-through-undo + (let [data (assoc (make-file-data (uuid/custom 2 2) (uuid/custom 1 1)) + :options {:japanese-layout true :base-font-size "16"})] + (doseq [language [:japanese :chinese :korean] + enabled? [true false]] + (let [changes (-> (pcb/empty-changes) + (pcb/with-file-data data) + (pcb/set-text-layout language enabled?)) + edited (ch/process-changes data (:redo-changes changes)) + restored (ch/process-changes edited (:undo-changes changes))] + (t/is (= enabled? (ctf/text-layout-enabled? edited language))) + (t/is (= data restored)) + (t/is (sm/validate ctf/schema:options (json-roundtrip (:options edited)))) + (t/is (= (:options edited) (json-roundtrip (:options edited)))))))) (t/deftest add-obj (let [file-id (uuid/custom 2 2) diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index ccd7c1ecae..141fb2c025 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -679,15 +679,15 @@ (dm/export layout/toggle-layout-flag) (dm/export layout/remove-layout-flag) -(defn set-japanese-layout - [enabled?] - (ptk/reify ::set-japanese-layout +(defn set-text-layout + [language enabled?] + (ptk/reify ::set-text-layout ptk/WatchEvent (watch [it state _] (let [file-data (dsh/lookup-file-data state) - changes (-> (pcb/empty-changes it) - (pcb/with-file-data file-data) - (pcb/set-japanese-layout enabled?))] + changes (-> (pcb/empty-changes it) + (pcb/with-file-data file-data) + (pcb/set-text-layout language enabled?))] (rx/of (dch/commit-changes changes)))))) ;; --- Profile diff --git a/frontend/src/app/main/refs.cljs b/frontend/src/app/main/refs.cljs index ddf143cae0..c449e7554d 100644 --- a/frontend/src/app/main/refs.cljs +++ b/frontend/src/app/main/refs.cljs @@ -290,13 +290,21 @@ "Currently working file data on workspace" (l/derived dsh/lookup-file-data st/state)) -(def japanese-layout-config - "Japanese layout switches: `:file?` for the workspace file and - `:all-files?` from the profile." - (l/derived (fn [state] - {:file? (ctf/japanese-layout-enabled? (dsh/lookup-file-data state)) - :all-files? (true? (dm/get-in state [:profile :props :japanese-layout-all-files]))}) - st/state =)) +(def text-layout-config + "Control availability by language; it never changes paragraph profiles." + (l/derived + (fn [state] + (let [file-data (dsh/lookup-file-data state) + props (dm/get-in state [:profile :props]) + feature? (contains? (:features state) "vertical-text-layout") + config (fn [language] + {:feature-enabled feature? + :file? (ctf/text-layout-enabled? file-data language) + :all-files? (true? (get props (ctf/text-layout-preference language)))})] + {:japanese (config :japanese) + :chinese (config :chinese) + :korean (config :korean)})) + st/state =)) (def workspace-file-colors (l/derived (fn [{:keys [id] :as data}] diff --git a/frontend/src/app/main/ui/settings/options.cljs b/frontend/src/app/main/ui/settings/options.cljs index dee7ba1d0a..d0c36ada1c 100644 --- a/frontend/src/app/main/ui/settings/options.cljs +++ b/frontend/src/app/main/ui/settings/options.cljs @@ -7,10 +7,12 @@ (ns app.main.ui.settings.options (:require-macros [app.main.style :as stl]) (:require + [app.common.types.file :as ctf] [app.config :as cf] [app.main.data.event :as ev] [app.main.data.notifications :as ntf] [app.main.data.profile :as du] + [app.main.features :as features] [app.main.refs :as refs] [app.main.router :as rt] [app.main.store :as st] @@ -115,35 +117,46 @@ :on-change handle-render-change}]] [:> text* {:typography t/body-medium :class (stl/css :feedback)} [:a {:href "#" :on-click go-settings-feedback :class (stl/css :link)} (tr "dashboard.webgl-switch.feedback") [:> icon* {:icon-id "arrow-up-right" :size "s"}]]]])) -(mf/defc japanese-layout-settings* - [{:keys [enabled]}] +(mf/defc language-layout-setting* + [{:keys [language label enabled]}] (let [handle-change (mf/use-fn + (mf/deps language) (fn [enabled?] - (st/emit! (ev/event {::ev/name (if enabled? - "enable-japanese-layout-all-files" - "disable-japanese-layout-all-files") + (st/emit! (ev/event {::ev/name (str (if enabled? "enable-" "disable-") + (name language) "-layout-all-files") ::ev/origin "settings"}) (du/update-profile-props - {:japanese-layout-all-files enabled?}) + {(ctf/text-layout-preference language) enabled?}) (ntf/success (tr "notifications.profile-saved")))))] - [:section {:class (stl/css :japanese-layout-container)} - [:> heading* {:class (stl/css :title) - :level 2 - :typography t/title-large} - (tr "dashboard.japanese-layout.title")] - [:> text* {:class (stl/css :description) :typography t/body-medium} - (tr "dashboard.japanese-layout.description")] - [:form {:class (stl/css :japanese-layout-form)} - [:> switch* {:label (tr "dashboard.japanese-layout.enable-all-files") - :default-checked enabled - :on-change handle-change}]]])) + [:> switch* {:label label + :default-checked enabled + :on-change handle-change}])) + +(mf/defc language-layout-settings* + [{:keys [config]}] + [:section {:class (stl/css :text-layout-container)} + [:> heading* {:class (stl/css :title) :level 2 :typography t/title-large} + (tr "dashboard.text-layout.title")] + [:> text* {:class (stl/css :description) :typography t/body-medium} + (tr "dashboard.text-layout.description")] + [:div {:class (stl/css :text-layout-form)} + [:> language-layout-setting* {:language :japanese + :label (tr "dashboard.japanese-layout.enable-all-files") + :enabled (get-in config [:japanese :all-files?])}] + [:> language-layout-setting* {:language :chinese + :label (tr "dashboard.chinese-layout.enable-all-files") + :enabled (get-in config [:chinese :all-files?])}] + [:> language-layout-setting* {:language :korean + :label (tr "dashboard.korean-layout.enable-all-files") + :enabled (get-in config [:korean :all-files?])}]]]) (mf/defc options-page* [] (let [profile (mf/deref refs/profile) renderer (or (-> profile :props :renderer) :svg) - japanese-layout-enabled (:all-files? (mf/deref refs/japanese-layout-config))] + layout-config (mf/deref refs/text-layout-config) + layout-feature? (features/use-feature "vertical-text-layout")] (mf/use-effect #(dom/set-html-title (tr "title.settings.options"))) @@ -157,6 +170,7 @@ :id "options-section-title"} (tr "labels.settings")] [:> options-form*]] - [:> japanese-layout-settings* {:enabled japanese-layout-enabled}] + (when layout-feature? + [:> language-layout-settings* {:config layout-config}]) (when (contains? cf/flags :render-switch) [:> webgl-settings* {:renderer renderer}])]])) diff --git a/frontend/src/app/main/ui/settings/options.scss b/frontend/src/app/main/ui/settings/options.scss index af13b89b95..5256ac1938 100644 --- a/frontend/src/app/main/ui/settings/options.scss +++ b/frontend/src/app/main/ui/settings/options.scss @@ -23,7 +23,7 @@ /* Settings sections with standalone controls. */ .webgl-container, -.japanese-layout-container { +.text-layout-container { display: grid; grid-auto-rows: auto; gap: var(--sp-s); @@ -34,7 +34,10 @@ color: var(--color-foreground-primary); } -.japanese-layout-form { +.text-layout-form { + display: flex; + flex-direction: column; + gap: var(--sp-s); padding-block-start: var(--sp-xs); } diff --git a/frontend/src/app/main/ui/workspace/main_menu.cljs b/frontend/src/app/main/ui/workspace/main_menu.cljs index 93c5ffd753..cc5033f6c7 100644 --- a/frontend/src/app/main/ui/workspace/main_menu.cljs +++ b/frontend/src/app/main/ui/workspace/main_menu.cljs @@ -45,7 +45,7 @@ [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] [app.util.keyboard :as kbd] - [app.util.text.writing-mode :as wm] + [app.util.text.layout :as tl] [beicon.v2.core :as rx] [potok.v2.core :as ptk] [rumext.v2 :as mf])) @@ -231,13 +231,28 @@ [:span {:class (stl/css :feedback :item-name)} (tr "labels.give-feedback")]])])) +(mf/defc language-layout-menu-item* + [{:keys [language config can-edit read-only label on-toggle]}] + (let [toggle (mf/use-fn + (mf/deps language on-toggle) + #(on-toggle language %))] + (when (tl/file-action-visible? config can-edit read-only) + [:> dropdown-menu-item* {:on-click toggle + :class (stl/css :base-menu-item :submenu-item) + :on-key-down (fn [event] + (when (kbd/enter? event) (toggle event))) + :data-testid (str (name language) "-layout") + :id (str "file-menu-" (name language) "-layout")} + [:span {:class (stl/css :item-name)} label]]))) + (mf/defc preferences-menu* {::mf/private true ::mf/wrap [mf/memo]} - [{:keys [japanese-layout-enabled japanese-layout-all-files layout profile toggle-flag - on-close toggle-theme toggle-render toggle-japanese-layout]}] + [{:keys [layout-config layout profile toggle-flag + on-close toggle-theme toggle-render toggle-text-layout]}] (let [renderer (or (-> profile :props :renderer) :svg) read-only? (mf/use-ctx ctx/workspace-read-only?) + can-edit (:can-edit (mf/deref refs/permissions)) show-nudge-options (mf/use-fn @@ -320,20 +335,24 @@ :id "file-menu-nudge"} [:span {:class (stl/css :item-name)} (tr "modals.nudge-title")]] - (when (and (not read-only?) - (not japanese-layout-all-files) - (wm/vertical-layout-active?)) - [:> dropdown-menu-item* {:on-click toggle-japanese-layout - :class (stl/css :base-menu-item :submenu-item) - :on-key-down (fn [event] - (when (kbd/enter? event) - (toggle-japanese-layout event))) - :data-testid "japanese-layout" - :id "file-menu-japanese-layout"} - [:span {:class (stl/css :item-name)} - (if japanese-layout-enabled - (tr "workspace.header.menu.disable-japanese-layout") - (tr "workspace.header.menu.enable-japanese-layout"))]]) + [:> language-layout-menu-item* + {:language :japanese :config (:japanese layout-config) + :can-edit can-edit :read-only read-only? :on-toggle toggle-text-layout + :label (if (get-in layout-config [:japanese :file?]) + (tr "workspace.header.menu.disable-japanese-layout") + (tr "workspace.header.menu.enable-japanese-layout"))}] + [:> language-layout-menu-item* + {:language :chinese :config (:chinese layout-config) + :can-edit can-edit :read-only read-only? :on-toggle toggle-text-layout + :label (if (get-in layout-config [:chinese :file?]) + (tr "workspace.header.menu.disable-chinese-layout") + (tr "workspace.header.menu.enable-chinese-layout"))}] + [:> language-layout-menu-item* + {:language :korean :config (:korean layout-config) + :can-edit can-edit :read-only read-only? :on-toggle toggle-text-layout + :label (if (get-in layout-config [:korean :file?]) + (tr "workspace.header.menu.disable-korean-layout") + (tr "workspace.header.menu.enable-korean-layout"))}] [:> dropdown-menu-item* {:on-click toggle-theme :class (stl/css :base-menu-item :submenu-item) @@ -904,8 +923,7 @@ (mf/defc menu* [{:keys [layout file]}] (let [profile (mf/deref refs/profile) - japanese-layout (mf/deref refs/japanese-layout-config) - japanese-layout-enabled (:file? japanese-layout) + layout-config (mf/deref refs/text-layout-config) show-menu* (mf/use-state false) show-menu? (deref show-menu*) @@ -967,12 +985,12 @@ (dom/stop-propagation event) (st/emit! (du/toggle-theme)))) - toggle-japanese-layout + toggle-text-layout (mf/use-fn - (mf/deps japanese-layout-enabled) - (fn [event] + (mf/deps layout-config) + (fn [language event] (dom/stop-propagation event) - (st/emit! (dw/set-japanese-layout (not japanese-layout-enabled))) + (st/emit! (dw/set-text-layout language (not (get-in layout-config [language :file?])))) (reset! show-menu* false) (reset! selected-sub-menu* nil))) @@ -1175,13 +1193,12 @@ :preferences [:> preferences-menu* {:layout layout :profile profile - :japanese-layout-enabled japanese-layout-enabled - :japanese-layout-all-files (:all-files? japanese-layout) + :layout-config layout-config :toggle-flag toggle-flag :toggle-theme toggle-theme :toggle-render toggle-render :show-shortcuts show-shortcuts - :toggle-japanese-layout toggle-japanese-layout + :toggle-text-layout toggle-text-layout :on-close close-sub-menu}] :plugins diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs index ffbf56784d..513efbc884 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs @@ -32,6 +32,7 @@ [app.main.ui.hooks :as hooks] [app.main.ui.workspace.sidebar.options.common :refer [radio-selected]] [app.main.ui.workspace.sidebar.options.menus.text-japanese-layout :as tjl] + [app.main.ui.workspace.sidebar.options.menus.text-language-layout :as tll] [app.main.ui.workspace.sidebar.options.menus.token-typography-row :refer [token-typography-row*]] [app.main.ui.workspace.sidebar.options.menus.typography :refer [text-options* typography-entry*]] [app.main.ui.workspace.tokens.management.forms.controls.utils :as csu] @@ -39,6 +40,7 @@ [app.util.i18n :as i18n :refer [tr]] [app.util.object :as obj] [app.util.text.content :as content] + [app.util.text.layout :as tl] [app.util.text.ui :as txu] [app.util.text.writing-mode :as wm] [app.util.timers :as ts] @@ -314,6 +316,10 @@ (get n-values :text-decoration)) (identical? (get o-values :text-direction) (get n-values :text-direction)) + (identical? (get o-values :text-language) + (get n-values :text-language)) + (identical? (get o-values :korean-wrap) + (get n-values :korean-wrap)) (identical? (get o-values :writing-mode) (get n-values :writing-mode)) (identical? (get o-values :text-orientation) @@ -355,6 +361,26 @@ ;; Main component ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +(mf/defc language-layout-section* + [{:keys [language title config options-props]}] + (let [open* (mf/use-state true) + open? (deref open*) + toggle-open (mf/use-fn #(swap! open* not))] + (when (tl/controls-enabled? config) + [:section {:class (stl/css :element-set) + :data-keep-editing-on-blur true + :aria-label title} + [:div {:class (stl/css :element-title)} + [:> title-bar* {:collapsable true + :collapsed (not open?) + :on-collapsed toggle-open + :title title + :class (stl/css :title-spacing-text)}]] + (when open? + [:div {:class (stl/css :element-content)} + [:> tll/language-layout-options* + (mf/spread-props options-props {:language language})]])]))) + (mf/defc text-menu* {::mf/wrap [#(mf/memo' % check-props)]} [{:keys [ids type values ruby-values text-selection-active @@ -362,15 +388,11 @@ (let [;; --- UI state menu-state* (mf/use-state {:main-menu true - :more-options false - :japanese-layout true}) + :more-options false}) menu-state (deref menu-state*) main-menu-open? (:main-menu menu-state) more-options-open? (:more-options menu-state) - japanese-layout-open? (:japanese-layout menu-state) - - japanese-layout-config-enabled? - (tjl/japanese-layout-config-enabled? (mf/deref refs/japanese-layout-config)) + layout-config (mf/deref refs/text-layout-config) font-id (or (:font-id values) (:font-id txt/default-typography)) @@ -461,10 +483,6 @@ (mf/use-fn #(swap! menu-state* update :more-options not)) - toggle-japanese-layout - (mf/use-fn - #(swap! menu-state* update :japanese-layout not)) - toggle-token-dropdown (mf/use-fn #(swap! token-dropdown-open* not)) @@ -563,7 +581,7 @@ :show-recent true :on-blur on-text-blur}) - japanese-layout-props + language-layout-props (mf/props {:ids ids :values values @@ -683,17 +701,15 @@ [:> text-decoration-options* (mf/spread-props common-props {:token-applied current-token-name})] [:> text-direction-options* common-props]])])] - (when japanese-layout-config-enabled? - [:section {:class (stl/css :element-set) - ;; Focusing these controls must not exit the v3 text editor (see `keep-editing-on-blur?`). - :data-keep-editing-on-blur true - :aria-label (tr "workspace.options.text-options.japanese-layout")} - [:div {:class (stl/css :element-title)} - [:> title-bar* {:collapsable true - :collapsed (not japanese-layout-open?) - :on-collapsed toggle-japanese-layout - :title (tr "workspace.options.text-options.japanese-layout") - :class (stl/css :title-spacing-text)}]] - (when japanese-layout-open? - [:div {:class (stl/css :element-content)} - [:> tjl/japanese-layout-options* japanese-layout-props]])])])) + [:> language-layout-section* {:language :japanese + :title (tr "workspace.options.text-options.japanese-layout") + :config (:japanese layout-config) + :options-props language-layout-props}] + [:> language-layout-section* {:language :chinese + :title (tr "workspace.options.text-options.chinese-layout") + :config (:chinese layout-config) + :options-props language-layout-props}] + [:> language-layout-section* {:language :korean + :title (tr "workspace.options.text-options.korean-layout") + :config (:korean layout-config) + :options-props language-layout-props}]])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text_japanese_layout.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text_japanese_layout.cljs index 441fa3a41e..dbfdb71f41 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text_japanese_layout.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text_japanese_layout.cljs @@ -18,7 +18,6 @@ [app.main.ui.workspace.sidebar.options.common :refer [advanced-options* radio-selected]] [app.util.dom :as dom] [app.util.i18n :refer [tr]] - [app.util.text.writing-mode :as wm] [rumext.v2 :as mf])) (def ^:private mixed-span-values @@ -49,14 +48,6 @@ :disabled true :dimmed true}))) -(defn japanese-layout-config-enabled? - "True under the WASM renderer when Japanese layout is enabled for the file - or in the user's profile. `config` is a `refs/japanese-layout-config` - value." - [{:keys [file? all-files?]}] - (and ^boolean (wm/vertical-layout-active?) - (boolean (or file? all-files?)))) - (defn vertical-japanese-layout? "True when the Japanese layout controls are editing vertical text." [values] @@ -93,6 +84,10 @@ "Radio group bound to one text attribute." [{:keys [values on-change on-blur attr default-value name class options]}] (let [selected (radio-selected (get values attr) default-value) + group-id (mf/use-id) + options (mf/with-memo [options group-id] + (mapv #(update % :id (fn [id] (str group-id "-" id))) options)) + name (str group-id "-" name) handle-change (mf/use-fn (mf/deps attr on-change on-blur) @@ -127,6 +122,24 @@ :label (translate "workspace.options.text-options.text-orientation-upright") :icon i/text-orientation-upright}]) +(mf/defc writing-mode-controls* + [{:keys [values on-change on-blur]}] + (let [props (mf/props {:values values :on-change on-change :on-blur on-blur})] + [:div {:class (stl/css :japanese-icon-options)} + [:> attr-radio-options* (mf/spread-props props + {:attr :writing-mode + :default-value (cjk/enum-default :writing-mode) + :name "writing-mode-options" + :class (stl/css :writing-mode-options) + :options (writing-mode-options tr)})] + (when (vertical-japanese-layout? values) + [:> attr-radio-options* (mf/spread-props props + {:attr :text-orientation + :default-value (cjk/enum-default :text-orientation) + :name "text-orientation-options" + :class (stl/css :text-orientation-options) + :options (text-orientation-options tr)})])])) + ;; Warichu (割注): renders the selection as two half-size lines in one inline ;; position (top/bottom horizontally, right/left vertically). (defn warichu-options diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.cljs new file mode 100644 index 0000000000..2dc175b2b4 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.cljs @@ -0,0 +1,69 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; 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) KALEIDOS SUBSIDIARY SL + +(ns app.main.ui.workspace.sidebar.options.menus.text-language-layout + (:require-macros [app.main.style :as stl]) + (:require + [app.main.ui.ds.controls.select :refer [select*]] + [app.main.ui.workspace.sidebar.options.menus.text-japanese-layout :as tjl] + [app.util.i18n :refer [tr]] + [app.util.text.layout :as tl] + [rumext.v2 :as mf])) + +(defn profile-options + [language value translate] + (let [options (into [{:id "" :label (translate "workspace.options.text-options.profile-plain")}] + (case language + :japanese [{:id "ja" :label (translate "workspace.options.text-options.profile-japanese")}] + :chinese [{:id "zh-Hans" :label (translate "workspace.options.text-options.profile-chinese-simplified")} + {:id "zh-Hant" :label (translate "workspace.options.text-options.profile-chinese-traditional")}] + :korean [{:id "ko" :label (translate "workspace.options.text-options.profile-korean")}])) + mixed? (= :multiple value) + id (if mixed? "multiple" (or value ""))] + (cond-> options + (not (some #(= (:id %) id) options)) + (conj {:id id + :label (if mixed? (translate "labels.mixed-values") value) + :disabled true + :dimmed true})))) + +(defn profile-change + [value] + {:text-language (when (seq value) value)}) + +(mf/defc language-layout-options* + [{:keys [language values ruby-values text-selection-active + on-change on-ruby-presentation-change on-blur]}] + (let [profile (:text-language values) + selected (if (= :multiple profile) "multiple" (or profile "")) + options (mf/with-memo [language profile] (profile-options language profile tr)) + matches? (tl/profile-matches? language profile) + handle-profile-change (mf/use-fn + (mf/deps on-change) + #(on-change (profile-change %))) + props (mf/props {:values values :on-change on-change :on-blur on-blur})] + [:div {:class (stl/css :language-layout-options)} + [:div {:class (stl/css :profile-option)} + [:span {:class (stl/css :option-label)} + (tr "workspace.options.text-options.language-profile")] + [:> select* {:default-selected selected + :aria-label (tr "workspace.options.text-options.language-profile") + :options options + :on-change handle-profile-change}]] + (if (and (= :japanese language) matches?) + [:> tjl/japanese-layout-options* + (mf/spread-props props {:ruby-values ruby-values + :text-selection-active text-selection-active + :on-ruby-presentation-change on-ruby-presentation-change})] + [:> tjl/writing-mode-controls* props]) + (when (and (= :korean language) matches?) + [:> tjl/span-select-option* + (mf/spread-props props + {:attr :korean-wrap + :default-value "syllable" + :label (tr "workspace.options.text-options.korean-wrap") + :options [{:id "syllable" :label (tr "workspace.options.text-options.korean-wrap-syllable")} + {:id "spaces-only" :label (tr "workspace.options.text-options.korean-wrap-spaces-only")}]})])])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.scss new file mode 100644 index 0000000000..3a7b62bfdc --- /dev/null +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.scss @@ -0,0 +1,27 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// 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) KALEIDOS INC Sucursal en España SL + +@use "ds/typography.scss" as t; + +.language-layout-options { + display: flex; + flex-direction: column; + gap: var(--sp-xs); + padding-block: var(--sp-xs); +} + +.profile-option { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 1.35fr); + align-items: center; + gap: var(--sp-s); +} + +.option-label { + @include t.use-typography("body-small"); + + color: var(--color-foreground-secondary); +} diff --git a/frontend/src/app/render_wasm/text_editor.cljs b/frontend/src/app/render_wasm/text_editor.cljs index 6307e5f184..be7813554f 100644 --- a/frontend/src/app/render_wasm/text_editor.cljs +++ b/frontend/src/app/render_wasm/text_editor.cljs @@ -808,13 +808,36 @@ {} spans))))) +(defn selection-paragraph-styles + "Profile attrs of the selected paragraphs, including unset and mixed values." + [content selection] + (when (and content selection) + (let [{:keys [start-para end-para]} (normalize-selection selection) + paragraphs (-> content :children first :children)] + (reduce + (fn [result paragraph] + (reduce + (fn [result attr] + (let [value (get paragraph attr)] + (assoc result attr + (if (and (contains? result attr) + (not= (get result attr) value)) + :multiple + value)))) + result + [:text-language :korean-wrap])) + {} + (subvec paragraphs start-para (inc end-para)))))) + (defn text-editor-get-current-japanese-styles - "Japanese span styles of the active WASM editor selection." + "Japanese span styles and paragraph profiles of the active editor selection." [] (when (wasm/ready?) (let [shape-id (text-editor-get-active-shape-id) - selection (text-editor-get-selection)] - (selection-japanese-styles (get-cached-content shape-id) selection)))) + selection (text-editor-get-selection) + content (get-cached-content shape-id)] + (merge (selection-japanese-styles content selection) + (selection-paragraph-styles content selection))))) (defn apply-attrs-to-paragraph "Apply `styles` (attrs map, or a fn per span) within [sel-start, sel-end), splitting spans." diff --git a/frontend/src/app/util/text/layout.cljs b/frontend/src/app/util/text/layout.cljs new file mode 100644 index 0000000000..c6d1d8282e --- /dev/null +++ b/frontend/src/app/util/text/layout.cljs @@ -0,0 +1,30 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; 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) KALEIDOS SUBSIDIARY SL + +(ns app.util.text.layout + (:require + [app.util.text.writing-mode :as wm] + [clojure.string :as str])) + +(defn controls-enabled? + [{:keys [feature-enabled file? all-files?]}] + (and (true? feature-enabled) + ^boolean (wm/vertical-layout-active?) + (boolean (or file? all-files?)))) + +(defn file-action-visible? + [{:keys [feature-enabled all-files?]} can-edit? read-only?] + (and (true? feature-enabled) + ^boolean (wm/vertical-layout-active?) + (true? can-edit?) + (not read-only?) + (not all-files?))) + +(defn profile-matches? + [language text-language] + (and (string? text-language) + (= (case language :japanese "ja" :chinese "zh" :korean "ko") + (first (str/split (str/lower-case text-language) #"-"))))) diff --git a/frontend/test/frontend_tests/render_wasm/texts_test.cljs b/frontend/test/frontend_tests/render_wasm/texts_test.cljs index fa7fc49224..e688822f0c 100644 --- a/frontend/test/frontend_tests/render_wasm/texts_test.cljs +++ b/frontend/test/frontend_tests/render_wasm/texts_test.cljs @@ -81,6 +81,22 @@ (text-editor/selection-japanese-styles content (selection 0 1 0 2)))))) +(t/deftest paragraph-profiles-follow-the-editor-selection + (let [content {:type "root" + :children [{:type "paragraph-set" + :children [{:type "paragraph" :children [{:text "Plain"}]} + {:type "paragraph" :text-language "ko" + :korean-wrap "spaces-only" + :children [{:text "한국어"}]}]}]}] + (t/is (= {:text-language nil :korean-wrap nil} + (text-editor/selection-paragraph-styles content (selection 0 0 0 0)))) + (t/is (= {:text-language "ko" :korean-wrap "spaces-only"} + (text-editor/selection-paragraph-styles content (selection 1 1 1 1)))) + (t/is (= {:text-language :multiple :korean-wrap :multiple} + (text-editor/selection-paragraph-styles content (selection 0 0 1 1)))) + (t/is (= {:text-language :multiple :korean-wrap :multiple} + (text-editor/selection-paragraph-styles content (selection 1 1 0 0)))))) + (t/deftest japanese-styles-report-mixed-ranges-and-follow-the-caret (let [content (editor-content [{:text "平成" diff --git a/frontend/test/frontend_tests/ui/text_options_test.cljs b/frontend/test/frontend_tests/ui/text_options_test.cljs index e94f233f85..d414491639 100644 --- a/frontend/test/frontend_tests/ui/text_options_test.cljs +++ b/frontend/test/frontend_tests/ui/text_options_test.cljs @@ -16,6 +16,8 @@ [app.main.ui.workspace.sidebar.options.common :refer [radio-selected]] [app.main.ui.workspace.sidebar.options.menus.text :as text-menu] [app.main.ui.workspace.sidebar.options.menus.text-japanese-layout :as tjl] + [app.main.ui.workspace.sidebar.options.menus.text-language-layout :as tll] + [app.util.text.layout :as tl] [app.util.text.writing-mode :as wm] [cljs.test :as t :include-macros true] [cuerdas.core :as str] @@ -35,14 +37,75 @@ :annotation-clearance]) -(t/deftest japanese-layout-controls-follow-file-or-profile-configuration +(t/deftest layout-controls-follow-file-or-profile-configuration (with-redefs [wm/vertical-layout-active? (constantly true)] - (t/is (false? (tjl/japanese-layout-config-enabled? {}))) - (t/is (false? (tjl/japanese-layout-config-enabled? {:file? false :all-files? false}))) - (t/is (true? (tjl/japanese-layout-config-enabled? {:file? true :all-files? false}))) - (t/is (true? (tjl/japanese-layout-config-enabled? {:file? false :all-files? true})))) + (t/is (false? (tl/controls-enabled? {}))) + (t/is (false? (tl/controls-enabled? {:feature-enabled true :file? false :all-files? false}))) + (t/is (true? (tl/controls-enabled? {:feature-enabled true :file? true :all-files? false}))) + (t/is (true? (tl/controls-enabled? {:feature-enabled true :file? false :all-files? true})))) (with-redefs [wm/vertical-layout-active? (constantly false)] - (t/is (false? (tjl/japanese-layout-config-enabled? {:file? true :all-files? true}))))) + (t/is (false? (tl/controls-enabled? {:feature-enabled true :file? true :all-files? true}))))) + +(t/deftest language-controls-and-menu-actions-respect-independent-preferences + (with-redefs [wm/vertical-layout-active? (constantly true)] + (doseq [file? [true false] all-files? [true false]] + (let [config {:feature-enabled true :file? file? :all-files? all-files?}] + (t/is (= (or file? all-files?) (tl/controls-enabled? config))) + (t/is (= (not all-files?) (tl/file-action-visible? config true false))) + (t/is (false? (tl/file-action-visible? config false false))) + (t/is (false? (tl/file-action-visible? config true true))) + (t/is (false? (tl/controls-enabled? (assoc config :feature-enabled false))))))) + (with-redefs [wm/vertical-layout-active? (constantly false)] + (t/is (false? (tl/controls-enabled? {:feature-enabled true :file? true}))) + (t/is (false? (tl/file-action-visible? {:feature-enabled true :file? true} true false))))) + +(t/deftest choosing-a-profile-only-changes-the-paragraph-language + (t/is (= {:text-language "ko"} (tll/profile-change "ko"))) + (t/is (= {:text-language nil} (tll/profile-change ""))) + (t/is (tl/profile-matches? :japanese "ja-JP")) + (t/is (tl/profile-matches? :chinese "zh-Hant-TW")) + (t/is (tl/profile-matches? :korean "KO-kr")) + (t/is (not (tl/profile-matches? :chinese "ja"))) + (t/is (not (tl/profile-matches? :korean :multiple))) + (t/is (not (tl/profile-matches? :japanese nil)))) + +(t/deftest profile-choices-keep-mixed-and-other-language-values-visible + (let [mixed (last (tll/profile-options :korean :multiple identity)) + other (last (tll/profile-options :chinese "ja" identity))] + (t/is (= {:id "multiple" :label "labels.mixed-values" :disabled true :dimmed true} mixed)) + (t/is (= {:id "ja" :label "ja" :disabled true :dimmed true} other)) + (t/is (= ["" "zh-Hans" "zh-Hant"] + (mapv :id (tll/profile-options :chinese nil identity)))))) + +(t/deftest language-specific-controls-require-an-explicit-matching-profile + (with-redefs [hooks/use-portal-container (fn ([] nil) ([_] nil))] + (doseq [[language attr label] [[:japanese "ja" "workspace.options.text-options.line-adjustment"] + [:korean "ko" "workspace.options.text-options.korean-wrap"]] + profile [nil :multiple "zh-Hans" attr]] + (let [changes (atom []) + markup (rds/renderToStaticMarkup + (mf/element tll/language-layout-options* + #js {:language language + :values {:text-language profile} + :on-change #(swap! changes conj %)}))] + (t/is (= (= profile attr) (str/includes? markup label))) + (t/is (str/includes? markup "horizontal-tb-writing-mode")) + (t/is (empty? @changes) "Showing controls must not assign a profile"))))) + +(t/deftest shared-writing-mode-controls-have-independent-radio-groups + (with-redefs [hooks/use-portal-container (fn ([] nil) ([_] nil))] + (let [markup (rds/renderToStaticMarkup + (mf/html + [:div + [:> tll/language-layout-options* {:language :japanese :values {} :on-change identity}] + [:> tll/language-layout-options* {:language :chinese :values {} :on-change identity}] + [:> tll/language-layout-options* {:language :korean :values {} :on-change identity}]])) + inputs (re-seq #"]+>" markup) + ids (map #(second (re-find #"id=\"([^\"]+)\"" %)) inputs) + names (map #(second (re-find #"name=\"([^\"]+)\"" %)) inputs)] + (t/is (= 6 (count (set ids)))) + (t/is (= 3 (count (set names)))) + (t/is (= 3 (count (filter #(str/includes? % "checked=\"\"") inputs))))))) (t/deftest text-emphasis-select-reports-and-restores-canonical-values (let [options (tjl/text-emphasis-options identity) diff --git a/frontend/translations/en.po b/frontend/translations/en.po index e15aa08092..ae86ef2b30 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -9945,3 +9945,79 @@ msgstr "" #: src/app/main/ui/workspace/sidebar/versions.cljs:462 msgid "workspace.versions.warning.text" msgstr "Autosaved versions will be kept for %s days." + +#: src/app/main/ui/settings/options.cljs +msgid "dashboard.text-layout.title" +msgstr "Text layout by language" + +#: src/app/main/ui/settings/options.cljs +msgid "dashboard.text-layout.description" +msgstr "Choose which text layout controls are available in all files." + +#: src/app/main/ui/settings/options.cljs +msgid "dashboard.chinese-layout.enable-all-files" +msgstr "Enable Chinese text layout for all files" + +#: src/app/main/ui/settings/options.cljs +msgid "dashboard.korean-layout.enable-all-files" +msgstr "Enable Korean text layout for all files" + +#: src/app/main/ui/workspace/main_menu.cljs +msgid "workspace.header.menu.enable-chinese-layout" +msgstr "Enable Chinese text layout" + +#: src/app/main/ui/workspace/main_menu.cljs +msgid "workspace.header.menu.disable-chinese-layout" +msgstr "Disable Chinese text layout" + +#: src/app/main/ui/workspace/main_menu.cljs +msgid "workspace.header.menu.enable-korean-layout" +msgstr "Enable Korean text layout" + +#: src/app/main/ui/workspace/main_menu.cljs +msgid "workspace.header.menu.disable-korean-layout" +msgstr "Disable Korean text layout" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.chinese-layout" +msgstr "Chinese text layout" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.korean-layout" +msgstr "Korean text layout" + +#: src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.cljs +msgid "workspace.options.text-options.language-profile" +msgstr "Language profile" + +#: src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.cljs +msgid "workspace.options.text-options.profile-plain" +msgstr "No language profile" + +#: src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.cljs +msgid "workspace.options.text-options.profile-japanese" +msgstr "Japanese" + +#: src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.cljs +msgid "workspace.options.text-options.profile-chinese-simplified" +msgstr "Simplified Chinese" + +#: src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.cljs +msgid "workspace.options.text-options.profile-chinese-traditional" +msgstr "Traditional Chinese" + +#: src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.cljs +msgid "workspace.options.text-options.profile-korean" +msgstr "Korean" + +#: src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.cljs +msgid "workspace.options.text-options.korean-wrap" +msgstr "Line wrapping" + +#: src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.cljs +msgid "workspace.options.text-options.korean-wrap-syllable" +msgstr "By syllable" + +#: src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.cljs +msgid "workspace.options.text-options.korean-wrap-spaces-only" +msgstr "At spaces (experimental)" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 6d20e94fe9..bff81c319d 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -9821,3 +9821,79 @@ msgstr "Si quieres aumentar este límite, contáctanos en [support@penpot.app](% #: src/app/main/ui/workspace/sidebar/versions.cljs:462 msgid "workspace.versions.warning.text" msgstr "Los autoguardados duran %s días." + +#: src/app/main/ui/settings/options.cljs +msgid "dashboard.text-layout.title" +msgstr "Disposición del texto por idioma" + +#: src/app/main/ui/settings/options.cljs +msgid "dashboard.text-layout.description" +msgstr "Elige qué controles de disposición del texto están disponibles en todos los archivos." + +#: src/app/main/ui/settings/options.cljs +msgid "dashboard.chinese-layout.enable-all-files" +msgstr "Activar la disposición del texto chino en todos los archivos" + +#: src/app/main/ui/settings/options.cljs +msgid "dashboard.korean-layout.enable-all-files" +msgstr "Activar la disposición del texto coreano en todos los archivos" + +#: src/app/main/ui/workspace/main_menu.cljs +msgid "workspace.header.menu.enable-chinese-layout" +msgstr "Activar la disposición del texto chino" + +#: src/app/main/ui/workspace/main_menu.cljs +msgid "workspace.header.menu.disable-chinese-layout" +msgstr "Desactivar la disposición del texto chino" + +#: src/app/main/ui/workspace/main_menu.cljs +msgid "workspace.header.menu.enable-korean-layout" +msgstr "Activar la disposición del texto coreano" + +#: src/app/main/ui/workspace/main_menu.cljs +msgid "workspace.header.menu.disable-korean-layout" +msgstr "Desactivar la disposición del texto coreano" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.chinese-layout" +msgstr "Disposición del texto chino" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.korean-layout" +msgstr "Disposición del texto coreano" + +#: src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.cljs +msgid "workspace.options.text-options.language-profile" +msgstr "Perfil de idioma" + +#: src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.cljs +msgid "workspace.options.text-options.profile-plain" +msgstr "Sin perfil de idioma" + +#: src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.cljs +msgid "workspace.options.text-options.profile-japanese" +msgstr "Japonés" + +#: src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.cljs +msgid "workspace.options.text-options.profile-chinese-simplified" +msgstr "Chino simplificado" + +#: src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.cljs +msgid "workspace.options.text-options.profile-chinese-traditional" +msgstr "Chino tradicional" + +#: src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.cljs +msgid "workspace.options.text-options.profile-korean" +msgstr "Coreano" + +#: src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.cljs +msgid "workspace.options.text-options.korean-wrap" +msgstr "Salto de línea" + +#: src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.cljs +msgid "workspace.options.text-options.korean-wrap-syllable" +msgstr "Por sílaba" + +#: src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.cljs +msgid "workspace.options.text-options.korean-wrap-spaces-only" +msgstr "En los espacios (experimental)"