diff --git a/frontend/AGENTS.md b/frontend/AGENTS.md index 0e33a32f30..681528b4f3 100644 --- a/frontend/AGENTS.md +++ b/frontend/AGENTS.md @@ -329,6 +329,31 @@ CSS modules pattern): - [ ] Selectors are flat (no deep nesting). +### Translations (`tr`) and Memoization + +`(tr "some.key")` resolves the translation string from the **currently active +locale at call time**. This has two consequences: + +- **Never call `(tr ...)` at namespace level** (inside a `def` or `defonce`). + Doing so would freeze the label to the locale active at module load time and + break runtime language switching. +- **Always call `(tr ...)` at render time** — either directly in the component + body or inside a `mf/with-memo` / `mf/use-memo` block. + +When a component renders a **static list of options** whose labels come from +`(tr ...)` (e.g. radio button options, select options), wrap the vector in +`mf/with-memo []` with no dependencies. This ensures the vector and its +`(tr ...)` calls are evaluated once per component mount instead of on every +render, while still respecting the render-time requirement: + +```clojure +(let [options (mf/with-memo [] + [{:value "top" :label (tr "some.key.top")} + {:value "center" :label (tr "some.key.center")} + {:value "bottom" :label (tr "some.key.bottom")}])] + ...) +``` + ### Performance Macros (`app.common.data.macros`) Always prefer these macros over their `clojure.core` equivalents — they compile to faster JavaScript: 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 499f065a1d..d13594b525 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 @@ -102,6 +102,21 @@ (mf/defc vertical-align* [{:keys [values on-change on-blur]}] (let [vertical-align (or (:vertical-align values) "top") + options + (mf/with-memo [] + [{:value "top" + :id "vertical-text-align-top" + :label (tr "workspace.options.text-options.align-top") + :icon i/text-top} + {:value "center" + :id "vertical-text-align-center" + :label (tr "workspace.options.text-options.align-middle") + :icon i/text-middle} + {:value "bottom" + :id "vertical-text-align-bottom" + :label (tr "workspace.options.text-options.align-bottom") + :icon i/text-bottom}]) + handle-change (mf/use-fn (mf/deps on-change on-blur) @@ -113,18 +128,7 @@ [:> radio-buttons* {:selected vertical-align :on-change handle-change :name "vertical-align-text-options" - :options [{:value "top" - :id "vertical-text-align-top" - :label (tr "workspace.options.text-options.align-top") - :icon i/text-top} - {:value "center" - :id "vertical-text-align-center" - :label (tr "workspace.options.text-options.align-middle") - :icon i/text-middle} - {:value "bottom" - :id "vertical-text-align-bottom" - :label (tr "workspace.options.text-options.align-bottom") - :icon i/text-bottom}]}]])) + :options options}]])) (mf/defc grow-options* [{:keys [ids values on-blur]}]