🌐 Translate the compare dialog labels and counts (#12566)

The compare dialog named changed properties, booleans and collection
counts from English literals in the UI namespace, so no locale could
replace them; it also passed bare numbers to tr, so a plural catalogue
entry could never reach its singular form.

Property labels now come from workspace.branches.compare.attr.*, the
true and false values from .value-true and .value-false, and every count
travels through i18n/c, which makes the singular msgstr[0] reachable.
Numbers are rendered with the active locale, so Spanish reads 439,5 and
groups thousands. An attr the catalogue does not name still falls back to
its own key, humanised: a technical identifier rather than prose.

The en.po entries for the counts gain their plural forms. es.po does not
carry the compare area yet, so it stays untouched.

AI-assisted-by: claude-opus-5-5

Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro>
This commit is contained in:
Álvaro Tejero-Cantero 2026-10-22 20:40:34 +02:00 committed by GitHub
parent 1c9baa0b06
commit d0414481ef
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 201 additions and 47 deletions

View File

@ -88,21 +88,26 @@
(str (subs s 0 40) "…")
s)))
(def ^:private attr->label
"Friendlier names for attrs whose raw key is unclear; every other attr
is humanized generically (kebab-case -> Capitalized words)."
{:rx "Corner radius" :ry "Corner radius"
:grow-type "Text auto-grow" :blend-mode "Blend mode"
:shape-ref "Component reference" :component-id "Component"
:component-file "Component library" :hidden "Visibility"
:blocked "Locked" :proportion-lock "Lock proportions"
:parent-id "Parent" :frame-id "Container"})
(defn- attr-label
"Human-readable label for a changed property key."
"Human-readable label for a changed property key. The named attrs are
the ones whose raw key means nothing to a user, so each carries a
catalogue entry. Every other attr keeps its own key, humanised
(kebab-case -> Capitalized words): a technical identifier rather than
prose, and the last resort for an attr the catalogue does not name."
[attr]
(or (get attr->label attr)
(-> (name attr) (str/replace #"-" " ") str/capital)))
(case attr
(:rx :ry) (tr "workspace.branches.compare.attr.corner-radius")
:grow-type (tr "workspace.branches.compare.attr.text-auto-grow")
:blend-mode (tr "workspace.branches.compare.attr.blend-mode")
:shape-ref (tr "workspace.branches.compare.attr.component-reference")
:component-id (tr "workspace.branches.compare.attr.component")
:component-file (tr "workspace.branches.compare.attr.component-library")
:hidden (tr "workspace.branches.compare.attr.visibility")
:blocked (tr "workspace.branches.compare.attr.locked")
:proportion-lock (tr "workspace.branches.compare.attr.lock-proportions")
:parent-id (tr "workspace.branches.compare.attr.parent")
:frame-id (tr "workspace.branches.compare.attr.container")
(-> (name attr) (str/replace #"-" " ") str/capital)))
;; --- Compare: grouping by component + per-item type labels
@ -181,10 +186,12 @@
(defn- fmt-number
"At most two decimals, trailing zeros stripped (89.99999 -> \"90\",
439.4999 -> \"439.5\", 386 -> \"386\")."
439.4999 -> \"439.5\", 386 -> \"386\"), grouped and punctuated the way
the active locale writes numbers (\"439,5\" in Spanish)."
[v]
(-> (.toFixed v 2)
(str/replace #"\.?0+$" "")))
(let [locale (-> i18n/*current-locale*
(str/replace "_" "-"))]
(.toLocaleString v locale #js {:maximumFractionDigits 2})))
(defn- display-val
"Compact, human-readable rendering of a property value. Raw uuids and
@ -195,12 +202,14 @@
(nil? v) "—"
(string? v) (if (> (count v) 32) (str (subs v 0 32) "…") v)
(number? v) (fmt-number v)
(boolean? v) (if v "true" "false")
(boolean? v) (if v
(tr "workspace.branches.compare.value-true")
(tr "workspace.branches.compare.value-false"))
(keyword? v) (name v)
(ct/inst? v) (ct/format-inst v :localized-date-time)
(uuid? v) (str (subs (str v) 0 8) "…")
(sequential? v) (tr "workspace.branches.compare.n-items" (dm/str (count v)))
(map? v) (tr "workspace.branches.compare.n-props" (dm/str (count v)))
(sequential? v) (tr "workspace.branches.compare.n-items" (i18n/c (count v)))
(map? v) (tr "workspace.branches.compare.n-props" (i18n/c (count v)))
:else (short-str v)))
(defn- status-matches?
@ -1083,7 +1092,7 @@
(= :added status) (tr "workspace.branches.compare.subtitle-new"
(if type-lbl (tr type-lbl) ""))
(and (= :modified status) (seq attrs))
(tr "workspace.branches.compare.subtitle-mods" (count attrs))
(tr "workspace.branches.compare.subtitle-mods" (i18n/c (count attrs)))
type-lbl (tr type-lbl)
:else nil)]
[:li {:class (stl/css-case :compare-item true
@ -1256,14 +1265,14 @@
(when stats
[:div {:class (stl/css :compare-stats)}
[:span {:class (stl/css :stat-badge :badge-added)}
(dm/str (:added stats) " " (tr "workspace.branches.compare.stats.added"))]
(tr "workspace.branches.compare.stats.added" (i18n/c (:added stats)))]
[:span {:class (stl/css :stat-badge :badge-modified)}
(dm/str (:modified stats) " " (tr "workspace.branches.compare.stats.modified"))]
(tr "workspace.branches.compare.stats.modified" (i18n/c (:modified stats)))]
[:span {:class (stl/css :stat-badge :badge-deleted)}
(dm/str (:deleted stats) " " (tr "workspace.branches.compare.stats.deleted"))]
(tr "workspace.branches.compare.stats.deleted" (i18n/c (:deleted stats)))]
(when (pos? (:conflicts stats))
[:span {:class (stl/css :stat-badge :badge-conflict)}
(dm/str (:conflicts stats) " " (tr "workspace.branches.compare.stats.conflicts"))])])]
(tr "workspace.branches.compare.stats.conflicts" (i18n/c (:conflicts stats)))])])]
[:> modal-content* {:class (stl/css :compare-content)}
(cond
@ -1345,14 +1354,14 @@
(mf/html
[:div {:class (stl/css :compare-footer-info)}
[:> i/icon* {:icon-id i/info :size "s"}]
[:span (tr "workspace.branches.compare.footer-changes" total)]
[:span (tr "workspace.branches.compare.footer-changes" (i18n/c total))]
(when (seq all-keys)
[:span {:class (stl/css :compare-footer-selected)}
(tr "workspace.branches.compare.footer-selected"
(str kept-count) (str (count all-keys)))])
(str kept-count) (i18n/c (count all-keys)))])
(when conflicts?
[:span {:class (stl/css :compare-footer-conflicts)}
(dm/str " · " (tr "workspace.branches.compare.footer-conflicts" conflicts))])])
(dm/str " · " (tr "workspace.branches.compare.footer-conflicts" (i18n/c conflicts)))])])
:end
(mf/html
[:*

View File

@ -110,6 +110,7 @@
[frontend-tests.tokens.style-dictionary-test]
[frontend-tests.tokens.token-errors-test]
[frontend-tests.tokens.workspace-tokens-remap-test]
[frontend-tests.ui.branches-compare-labels-test]
[frontend-tests.ui.check-updates-test]
[frontend-tests.ui.color-token-position-test]
[frontend-tests.ui.colorpicker-token-set-order-test]
@ -267,6 +268,7 @@
'frontend-tests.tokens.token-errors-test
'frontend-tests.tokens.logic.tokens-status-test
'frontend-tests.tokens.workspace-tokens-remap-test
'frontend-tests.ui.branches-compare-labels-test
'frontend-tests.ui.check-updates-test
'frontend-tests.ui.color-token-position-test
'frontend-tests.ui.colorpicker-token-set-order-test

View File

@ -0,0 +1,71 @@
;; 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 frontend-tests.ui.branches-compare-labels-test
"The compare dialog names properties, booleans, numbers and collection
counts through the catalogue, so a translation replaces every one of
them. The catalogue here is a stand-in for en.po: its values are picked
to differ from the English the dialog used to hardcode."
(:require
[app.main.ui.workspace.sidebar.branches :as branches]
[app.util.i18n :as i18n]
[cljs.test :as t :include-macros true]))
;; Access the private formatters for direct testing.
(def ^:private attr-label @#'branches/attr-label)
(def ^:private display-val @#'branches/display-val)
(def ^:private get-translations @#'i18n/get-translations)
(def ^:private catalogue
#js {"workspace.branches.compare.attr.corner-radius" "Radio de esquina"
"workspace.branches.compare.attr.component-reference" "Referencia de componente"
"workspace.branches.compare.value-true" "verdadero"
"workspace.branches.compare.value-false" "falso"
"workspace.branches.compare.n-items" #js ["1 elemento" "%s elementos"]
"workspace.branches.compare.n-props" #js ["1 propiedad" "%s propiedades"]})
(defn- with-catalogue
"Install the stand-in catalogue for one test and put the previous one
back, so other test namespaces keep their own strings."
[f]
(let [translations (get-translations)
previous (unchecked-get translations "en")]
(i18n/set-default-translations catalogue)
(try
(f)
(finally
(i18n/set-translations "en" previous)))))
(t/use-fixtures :each with-catalogue)
(t/deftest property-labels-come-from-the-catalogue
(t/is (= "Radio de esquina" (attr-label :rx)))
(t/is (= "Radio de esquina" (attr-label :ry)))
(t/is (= "Referencia de componente" (attr-label :shape-ref))))
(t/deftest an-attr-with-no-name-of-its-own-falls-back-to-its-key
(t/is (= "Some internal attr" (attr-label :some-internal-attr))
"an attr the catalogue does not name stays a humanised key"))
(t/deftest boolean-values-come-from-the-catalogue
(t/is (= "verdadero" (display-val true)))
(t/is (= "falso" (display-val false))))
(t/deftest collection-counts-choose-the-plural-form
(t/is (= "1 elemento" (display-val [1]))
"a single item reads in the singular")
(t/is (= "3 elementos" (display-val [1 2 3])))
(t/is (= "1 propiedad" (display-val {:x 1})))
(t/is (= "2 propiedades" (display-val {:x 1 :y 2}))))
(t/deftest numbers-are-punctuated-by-the-locale
(binding [i18n/*current-locale* "es"]
(t/is (= "439,5" (display-val 439.5))
"a Spanish locale reads the decimal comma"))
(binding [i18n/*current-locale* "en"]
(t/is (= "439.5" (display-val 439.5)))
(t/is (= "90" (display-val 89.99999)) "at most two decimals")
(t/is (= "386" (display-val 386)))))

View File

@ -6411,6 +6411,50 @@ msgstr "Compare"
msgid "workspace.branches.compare.attr"
msgstr "Property"
#: src/app/main/ui/workspace/sidebar/branches.cljs:99
msgid "workspace.branches.compare.attr.blend-mode"
msgstr "Blend mode"
#: src/app/main/ui/workspace/sidebar/branches.cljs:101
msgid "workspace.branches.compare.attr.component"
msgstr "Component"
#: src/app/main/ui/workspace/sidebar/branches.cljs:102
msgid "workspace.branches.compare.attr.component-library"
msgstr "Component library"
#: src/app/main/ui/workspace/sidebar/branches.cljs:100
msgid "workspace.branches.compare.attr.component-reference"
msgstr "Component reference"
#: src/app/main/ui/workspace/sidebar/branches.cljs:107
msgid "workspace.branches.compare.attr.container"
msgstr "Container"
#: src/app/main/ui/workspace/sidebar/branches.cljs:97
msgid "workspace.branches.compare.attr.corner-radius"
msgstr "Corner radius"
#: src/app/main/ui/workspace/sidebar/branches.cljs:105
msgid "workspace.branches.compare.attr.lock-proportions"
msgstr "Lock proportions"
#: src/app/main/ui/workspace/sidebar/branches.cljs:104
msgid "workspace.branches.compare.attr.locked"
msgstr "Locked"
#: src/app/main/ui/workspace/sidebar/branches.cljs:106
msgid "workspace.branches.compare.attr.parent"
msgstr "Parent"
#: src/app/main/ui/workspace/sidebar/branches.cljs:98
msgid "workspace.branches.compare.attr.text-auto-grow"
msgstr "Text auto-grow"
#: src/app/main/ui/workspace/sidebar/branches.cljs:103
msgid "workspace.branches.compare.attr.visibility"
msgstr "Visibility"
#: src/app/main/ui/workspace/sidebar/branches.cljs:465
#, unused
msgid "workspace.branches.compare.branch"
@ -6428,17 +6472,23 @@ msgstr "Could not load the changes of this branch"
msgid "workspace.branches.compare.export"
msgstr "Export diff"
#: src/app/main/ui/workspace/sidebar/branches.cljs:1118
#: src/app/main/ui/workspace/sidebar/branches.cljs:1351
msgid "workspace.branches.compare.footer-changes"
msgstr "%s changes"
msgid_plural "workspace.branches.compare.footer-changes"
msgstr[0] "1 change"
msgstr[1] "%s changes"
#: src/app/main/ui/workspace/sidebar/branches.cljs:1121
#: src/app/main/ui/workspace/sidebar/branches.cljs:1358
msgid "workspace.branches.compare.footer-conflicts"
msgstr "%s to resolve before merging"
msgid_plural "workspace.branches.compare.footer-conflicts"
msgstr[0] "1 to resolve before merging"
msgstr[1] "%s to resolve before merging"
#: src/app/main/ui/workspace/sidebar/branches.cljs:1391
#: src/app/main/ui/workspace/sidebar/branches.cljs:1354
msgid "workspace.branches.compare.footer-selected"
msgstr "%s of %s selected"
msgid_plural "workspace.branches.compare.footer-selected"
msgstr[0] "%s of 1 selected"
msgstr[1] "%s of %s selected"
#, unused
msgid "workspace.branches.compare.group.colors"
@ -6481,13 +6531,17 @@ msgstr "Computing changes…"
msgid "workspace.branches.compare.main"
msgstr "main"
#: src/app/main/ui/workspace/sidebar/branches.cljs:220
#: src/app/main/ui/workspace/sidebar/branches.cljs:209
msgid "workspace.branches.compare.n-items"
msgstr "%s items"
msgid_plural "workspace.branches.compare.n-items"
msgstr[0] "1 item"
msgstr[1] "%s items"
#: src/app/main/ui/workspace/sidebar/branches.cljs:221
#: src/app/main/ui/workspace/sidebar/branches.cljs:210
msgid "workspace.branches.compare.n-props"
msgstr "%s properties"
msgid_plural "workspace.branches.compare.n-props"
msgstr[0] "1 property"
msgstr[1] "%s properties"
#: src/app/main/ui/workspace/sidebar/branches.cljs:934
msgid "workspace.branches.compare.no-props"
@ -6556,25 +6610,35 @@ msgstr "Text"
msgid "workspace.branches.compare.shape.variant"
msgstr "Variant"
#: src/app/main/ui/workspace/sidebar/branches.cljs:1036
#: src/app/main/ui/workspace/sidebar/branches.cljs:1262
msgid "workspace.branches.compare.stats.added"
msgstr "added"
msgid_plural "workspace.branches.compare.stats.added"
msgstr[0] "1 added"
msgstr[1] "%s added"
#: src/app/main/ui/workspace/sidebar/branches.cljs:1043
#: src/app/main/ui/workspace/sidebar/branches.cljs:1269
msgid "workspace.branches.compare.stats.conflicts"
msgstr "conflicts"
msgid_plural "workspace.branches.compare.stats.conflicts"
msgstr[0] "1 conflict"
msgstr[1] "%s conflicts"
#: src/app/main/ui/workspace/sidebar/branches.cljs:1040
#: src/app/main/ui/workspace/sidebar/branches.cljs:1266
msgid "workspace.branches.compare.stats.deleted"
msgstr "deleted"
msgid_plural "workspace.branches.compare.stats.deleted"
msgstr[0] "1 deleted"
msgstr[1] "%s deleted"
#: src/app/main/ui/workspace/sidebar/branches.cljs:1038
#: src/app/main/ui/workspace/sidebar/branches.cljs:1264
msgid "workspace.branches.compare.stats.modified"
msgstr "modified"
msgid_plural "workspace.branches.compare.stats.modified"
msgstr[0] "1 modified"
msgstr[1] "%s modified"
#: src/app/main/ui/workspace/sidebar/branches.cljs:879
#: src/app/main/ui/workspace/sidebar/branches.cljs:1089
msgid "workspace.branches.compare.subtitle-mods"
msgstr "%s property changes"
msgid_plural "workspace.branches.compare.subtitle-mods"
msgstr[0] "1 property change"
msgstr[1] "%s property changes"
#: src/app/main/ui/workspace/sidebar/branches.cljs:876
msgid "workspace.branches.compare.subtitle-new"
@ -6612,6 +6676,14 @@ msgstr "Token"
msgid "workspace.branches.compare.type.typography"
msgstr "Typography"
#: src/app/main/ui/workspace/sidebar/branches.cljs:205
msgid "workspace.branches.compare.value-false"
msgstr "false"
#: src/app/main/ui/workspace/sidebar/branches.cljs:204
msgid "workspace.branches.compare.value-true"
msgstr "true"
#: src/app/main/ui/workspace/sidebar/branches.cljs:1430
msgid "workspace.branches.conflicts.all-branch"
msgstr "Use branch for all"