♻️ Update import modal UI (#11704)

* 🌐 Translate import dialog strings

Several plain hardcoded strings in the import dialog (the file
rename aria-label, library-resolution messages, table headers, and
the manually-linked summary labels) were never wired to the i18n
system. Wire them up via tr and add the corresponding English and
Spanish entries to the translation catalogs.

* 🐛 Fix library-resolution summary layout in import dialog

The auto-linked/manually-linked summary list had several layout bugs:
name/selection columns stretched unevenly, the auto-linked badge sat
nested inside the name's ellipsis text (unreliable across browsers),
and a long unbreakable library name could grow the whole modal past
its fixed width because a bare 1fr grid track has no minimum-size
cap. Fix the column sizing, split the badge out as a sibling of the
name, and clamp the modal content column with minmax(0, 1fr).

*  Add Skip button to library resolution wizard

Add a "Skip" action to the per-file library resolution step so users
can leave all of a file's pending libraries unconnected and move on
to the next one, instead of being forced to pick a candidate or go
back. Clears any pending selections for the current file before
advancing the wizard, so the summary correctly shows those libraries
as unlinked.

* 🐛 Fix stale translation key on library resolution button

The primary action button in the library resolution wizard referenced
"dashboard.import.review-links", a translation key that no longer
existed, leaving the button with no text. Rename it to
"dashboard.import.connect-selected-libraries" to match the actual
button label.

* 🐛 Align no-selection state with arrow and file name

.summary-no-selection was missing display:flex/align-items:center,
so its icon and text weren't vertically centered against each other
or against the file name column in the import library summary.

* 💄 Use select-accent color for auto-linked badge border

* 💄 Change import summary columns from 50/50 to 40/60 split

Give the "new library" column more room than the "original library"
column in the manually-linked libraries table (header and rows), since
it usually carries the longer "name (project)" text.

* 🐛 Divide back buttons from forward buttons

* 🐛 Fix dropdown typography
This commit is contained in:
Eva Marco 2026-09-21 12:29:14 +02:00 committed by GitHub
parent dc8160c13a
commit 52825f528c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 216 additions and 112 deletions

View File

@ -293,8 +293,7 @@
[:input {:type "text" [:input {:type "text"
:auto-focus true :auto-focus true
:class (stl/css :file-name-input) :class (stl/css :file-name-input)
;;TODO: Add translation for aria-label :aria-label (tr "dashboard.import.file-name-label")
:aria-label "File name"
:default-value (:name entry) :default-value (:name entry)
:on-key-press on-edit-key-press :on-key-press on-edit-key-press
:on-blur on-edit-blur}]] :on-blur on-edit-blur}]]
@ -412,7 +411,7 @@
[:> text* {:class (stl/css :library-resolution-message) [:> text* {:class (stl/css :library-resolution-message)
:as "p" :as "p"
:typography t/body-large} :typography t/body-large}
"Some libraries couldn't be linked automatically. Select the correct library for each:"] (tr "dashboard.import.resolve-libraries")]
[:table {:class (stl/css :library-resolution-table)} [:table {:class (stl/css :library-resolution-table)}
@ -422,13 +421,13 @@
[:> icon* {:icon-id i/library [:> icon* {:icon-id i/library
:class (stl/css :library-resolution-icon) :class (stl/css :library-resolution-icon)
:size "s"}] :size "s"}]
"original library"] (tr "dashboard.import.resolve-libraries.original-library")]
[:th {:class (stl/css :library-resolution-arrow)}] [:th {:class (stl/css :library-resolution-arrow)}]
[:th {:class (stl/css :library-resolution-connection)} [:th {:class (stl/css :library-resolution-connection)}
[:> icon* {:icon-id i/library [:> icon* {:icon-id i/library
:class (stl/css :library-resolution-icon) :class (stl/css :library-resolution-icon)
:size "s"}] :size "s"}]
"connect to"]]] (tr "dashboard.import.resolve-libraries.connect-to")]]]
[:tbody {:class (stl/css :library-resolution-body)} [:tbody {:class (stl/css :library-resolution-body)}
(for [{:keys [id name candidates]} candidates] (for [{:keys [id name candidates]} candidates]
(let [options (mapv (fn [c] (let [options (mapv (fn [c]
@ -477,54 +476,53 @@
:typography t/body-medium} :typography t/body-medium}
(:name resolution-file)]] (:name resolution-file)]]
(when (seq done) [:div {:class (stl/css :summary-body)}
[:div {:class (stl/css :summary-section)} (when (seq done)
[:ul {:class (stl/css :summary-list)} [:div {:class (stl/css :summary-section)}
(for [{:keys [name]} done] [:ul {:class (stl/css :summary-list)}
[:li {:class (stl/css :summary-list-item) (for [{:keys [name]} done]
:key (dm/str name)} [:li {:class (stl/css :summary-list-item)
[:span {:class (stl/css :summary-item-name)} name] :key (dm/str name)}
[:span {:class (stl/css :summary-linked-badge)} [:span {:class (stl/css :summary-item-name)} name]
[:> icon* {:icon-id i/status-tick [:span {:class (stl/css :summary-linked-badge)}
:class (stl/css :summary-badge-icon) (tr "dashboard.import.summary.linked")]])]])
:size "s"}]
(tr "dashboard.import.summary.linked")]])]])
(when (seq pending) (when (seq pending)
[:div {:class (stl/css :summary-section)} [:div {:class (stl/css :summary-section)}
[:div {:class (stl/css :summary-section-header)} [:div {:class (stl/css :summary-section-header)}
;; TODO: Add translation for this string [:> text* {:as "span"
:class (stl/css :summary-section-title)
:typography t/headline-small}
(tr "dashboard.import.summary.manually-linked")]]
[:ul {:class (stl/css :summary-list)}
[:li {:class (stl/css :summary-list-item)
:key "summary-list-header"}
[:span {:class (stl/css :summary-item-name-header)}
(tr "dashboard.import.summary.original")]
[:> text* {:as "span" [:span {:class (stl/css :summary-item-name-header)}
:class (stl/css :summary-section-title) (tr "dashboard.import.summary.new")]]
:typography t/headline-small} (for [{:keys [id name] :as cand} pending]
"linked manually"]] (let [selected-id (get selection id)
[:ul {:class (stl/css :summary-list)} selected-c (when selected-id
[:li {:class (stl/css :summary-list-item) (d/seek #(= (str (:id %)) (str selected-id)) (:candidates cand)))]
:key "summary-list-header"} [:li {:class (stl/css :summary-list-item)
[:span {:class (stl/css :summary-item-name-header)} :key (dm/str id)}
"Original"] [:span {:class (stl/css-case :summary-item-name true
:summary-item-base true)} name]
[:span {:class (stl/css :summary-item-name-header)} (if selected-c
"New"]] [:span {:class (stl/css :summary-linked-info)}
(for [{:keys [id name] :as cand} pending] [:> icon* {:icon-id i/row
(let [selected-id (get selection id) :size "m"
selected-c (when selected-id :class (stl/css :summary-linked-arrow)}]
(d/seek #(= (str (:id %)) (str selected-id)) (:candidates cand)))] [:span {:class (stl/css :summary-linked-name)}
[:li {:class (stl/css :summary-list-item) (dm/str (:name selected-c) " (" (:project-name selected-c) ")")]]
:key (dm/str id)} [:span {:class (stl/css :summary-no-selection)}
[:span {:class (stl/css :summary-item-name)} name] [:> icon* {:icon-id i/row
[:> icon* {:icon-id i/row :size "m"
:size "m" :class (stl/css :summary-linked-arrow)}]
:class (stl/css :summary-linked-arrow)}] (tr "dashboard.import.summary.no-selection")])]))]])]]))
(if selected-c
[:span {:class (stl/css :summary-linked-info)}
[:span {:class (stl/css :summary-linked-name)}
(:name selected-c)]
[:span {:class (stl/css :summary-linked-project)}
(:project-name selected-c)]]
[:span {:class (stl/css :summary-no-selection)}
(tr "dashboard.import.summary.no-selection")])]))]])]))
(mf/defc library-resolution-summary* (mf/defc library-resolution-summary*
{::mf/private true} {::mf/private true}
@ -649,7 +647,7 @@
{::mf/private true} {::mf/private true}
[{:keys [current-unresolved-file selection on-select [{:keys [current-unresolved-file selection on-select
visited all-visited? visited all-visited?
on-wizard-prev on-wizard-next]}] on-wizard-prev on-wizard-next on-wizard-skip]}]
[:* [:*
[:div {:class (stl/css :modal-content)} [:div {:class (stl/css :modal-content)}
[:> library-resolution* [:> library-resolution*
@ -664,12 +662,18 @@
:variant "secondary" :variant "secondary"
:on-click on-wizard-prev} :on-click on-wizard-prev}
(tr "labels.previous")]) (tr "labels.previous")])
[:> button* {:class (stl/css :accept-btn) [:div {:class (stl/css :action-buttons-end)}
:variant "primary" [:> button* {:class (stl/css :secondary-btn)
:on-click on-wizard-next} :variant "secondary"
(if all-visited? :on-click on-wizard-skip}
(tr "labels.next") (tr "labels.skip")]
(tr "dashboard.import.review-links"))]]]])
[:> button* {:class (stl/css :accept-btn)
:variant "primary"
:on-click on-wizard-next}
(if all-visited?
(tr "labels.next")
(tr "dashboard.import.connect-selected-libraries"))]]]]])
(mf/defc import-library-summary-stage* (mf/defc import-library-summary-stage*
{::mf/private true} {::mf/private true}
@ -688,10 +692,11 @@
:variant "secondary" :variant "secondary"
:on-click on-summary-back} :on-click on-summary-back}
(tr "labels.back")]) (tr "labels.back")])
[:> button* {:class (stl/css :accept-btn) [:div {:class (stl/css :action-buttons-end)}
:variant "primary" [:> button* {:class (stl/css :accept-btn)
:on-click on-confirm-library-links} :variant "primary"
(tr "dashboard.import.confirm-library-links")]]]]) :on-click on-confirm-library-links}
(tr "dashboard.import.confirm-library-links")]]]]])
(mf/defc import-dialog (mf/defc import-dialog
{::mf/register modal/components {::mf/register modal/components
@ -855,6 +860,15 @@
(let [file-id (:id current-unresolved-file)] (let [file-id (:id current-unresolved-file)]
(swap! visited* disj file-id)))) (swap! visited* disj file-id))))
on-wizard-skip
(mf/use-fn
(mf/deps current-unresolved-file)
(fn []
(let [file-id (:id current-unresolved-file)
pending-ids (mapv :id (:pending current-unresolved-file))]
(swap! selection* #(apply dissoc % pending-ids))
(swap! visited* conj file-id))))
on-summary-back on-summary-back
(mf/use-fn (mf/use-fn
(mf/deps visited) (mf/deps visited)
@ -971,7 +985,8 @@
:visited visited :visited visited
:all-visited? all-visited? :all-visited? all-visited?
:on-wizard-prev on-wizard-prev :on-wizard-prev on-wizard-prev
:on-wizard-next on-wizard-next}] :on-wizard-next on-wizard-next
:on-wizard-skip on-wizard-skip}]
:library-summary :library-summary
[:> import-library-summary-stage* [:> import-library-summary-stage*

View File

@ -42,9 +42,15 @@
flex: 1; flex: 1;
overflow: hidden auto; overflow: hidden auto;
display: grid; display: grid;
grid-template-columns: 1fr;
// minmax(0, 1fr), not a bare 1fr: a bare 1fr track's minimum size
// defaults to its content's min-content width, so a long unbreakable
// string deep inside (e.g. a library name with no spaces) can grow
// this column - and the fixed-width modal around it - past its
// intended size, since overflow:hidden can't shrink a track that
// already expanded to fit its content.
grid-template-columns: minmax(0, 1fr);
gap: var(--sp-l); gap: var(--sp-l);
margin-block-end: var(--sp-xxl);
min-block-size: px2rem(40); min-block-size: px2rem(40);
} }
@ -71,6 +77,15 @@
gap: var(--sp-l); gap: var(--sp-l);
} }
// Groups the trailing buttons (e.g. skip/accept) and pins them to the end,
// leaving any leading button (e.g. previous/back) at the start - regardless
// of whether that leading button is rendered.
.action-buttons-end {
display: flex;
gap: var(--sp-l);
margin-inline-start: auto;
}
.file-entry { .file-entry {
--file-entry-fg-color: var(--color-foreground-secondary); --file-entry-fg-color: var(--color-foreground-secondary);
@ -246,17 +261,20 @@
} }
.library-resolution-message { .library-resolution-message {
@include use-typography("body-large");
color: var(--color-foreground-secondary); color: var(--color-foreground-secondary);
margin-block-end: var(--sp-s); margin-block-end: var(--sp-s);
} }
.library-resolution-header { .library-resolution-header {
@include use-typography("body-medium"); @include use-typography("body-small");
color: var(--color-foreground-secondary); color: var(--color-foreground-secondary);
display: grid; display: grid;
grid-template-columns: 1fr 32px 1fr; font-weight: 500;
border-block-end: $b-1 solid var(--color-foreground-secondary); grid-template-columns: 1fr $sz-32 1fr;
border-block-end: $b-1 solid var(--color-background-quaternary);
} }
.library-origin-name, .library-origin-name,
@ -270,9 +288,10 @@
.library-resolution-item { .library-resolution-item {
display: grid; display: grid;
grid-template-columns: 1fr 32px 1fr; grid-template-columns: 1fr $sz-32 1fr;
block-size: $sz-32; block-size: $sz-40;
margin-block: var(--sp-s); margin-block-start: var(--sp-s);
border-block-end: $b-1 solid var(--color-background-quaternary);
} }
.library-resolution-icon { .library-resolution-icon {
@ -330,6 +349,12 @@
background: var(--color-background-primary); background: var(--color-background-primary);
} }
.summary-body {
display: flex;
flex-direction: column;
gap: var(--sp-xxl);
}
.summary-list { .summary-list {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -354,6 +379,7 @@
.summary-linked-arrow { .summary-linked-arrow {
color: var(--color-foreground-secondary); color: var(--color-foreground-secondary);
flex-shrink: 0;
} }
.summary-file-icon { .summary-file-icon {
@ -383,36 +409,39 @@
.summary-list-item { .summary-list-item {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: flex-start;
gap: var(--sp-s); gap: var(--sp-s);
padding: var(--sp-xs) var(--sp-s); padding-block: var(--sp-xs);
padding-inline-start: var(--sp-s);
border-radius: $br-4; border-radius: $br-4;
&:hover {
background: var(--color-background-secondary);
}
} }
.summary-item-name { .summary-item-name {
@include use-typography("body-small");
color: var(--color-foreground-primary); color: var(--color-foreground-primary);
flex: 1; flex: 0 1 auto;
min-inline-size: 0; min-inline-size: 0;
display: block; display: block;
max-inline-size: 99%;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
} }
.summary-item-name-header { .summary-item-name-header {
@include use-typography("body-small");
color: var(--color-foreground-secondary); color: var(--color-foreground-secondary);
flex: 1; flex: 2 1 0%;
min-inline-size: 0; min-inline-size: 0;
display: block; display: block;
max-inline-size: 99%;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
&:last-child {
flex: 3 1 0%;
}
} }
// Auto-linked badge // Auto-linked badge
@ -420,10 +449,12 @@
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: var(--sp-xs); gap: var(--sp-xs);
padding: var(--sp-xxs) var(--sp-s); padding-block: var(--sp-xxs);
border-radius: $br-12; padding-inline: var(--sp-s);
border-radius: $br-8;
background: var(--color-accent-success-bg); background: var(--color-accent-success-bg);
color: var(--color-accent-success); color: var(--color-accent-success);
border: $b-1 solid var(--color-accent-background-select);
font-size: px2rem(11); font-size: px2rem(11);
font-weight: 500; font-weight: 500;
flex-shrink: 0; flex-shrink: 0;
@ -437,39 +468,39 @@
.summary-linked-info { .summary-linked-info {
display: flex; display: flex;
align-items: center; align-items: center;
gap: var(--sp-xs); gap: var(--sp-s);
flex-shrink: 0; flex: 3 1 0%;
min-inline-size: 0;
} }
.summary-linked-name { .summary-linked-name {
// TODO: Typography does not match any existing DS typography token. @include use-typography("body-small");
font-family: "worksans", "vazirmatn", sans-serif;
font-size: px2rem(12); color: var(--color-foreground-primary);
line-height: 1.4;
color: var(--color-foreground-secondary);
font-weight: 500; font-weight: 500;
min-inline-size: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
} }
.summary-linked-project { .summary-item-base {
// TODO: Typography does not match any existing DS typography token. flex: 2 1 0%;
font-family: "worksans", "vazirmatn", sans-serif;
font-weight: 400;
line-height: 1.4;
color: var(--color-foreground-secondary); color: var(--color-foreground-secondary);
padding: var(--sp-xxs) var(--sp-s);
border-radius: $br-12;
background: var(--color-background-quaternary);
font-size: px2rem(11);
} }
// No selection state // No selection state
.summary-no-selection { .summary-no-selection {
// TODO: Typography does not match any existing DS typography token. @include use-typography("body-small");
font-family: "worksans", "vazirmatn", sans-serif;
font-size: px2rem(12); display: flex;
font-weight: 400; align-items: center;
line-height: 1.4; gap: var(--sp-s);
color: var(--color-foreground-secondary); color: var(--color-foreground-secondary);
font-style: italic; font-style: italic;
flex-shrink: 0; flex: 3 1 0%;
min-inline-size: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
} }

View File

@ -44,6 +44,8 @@
} }
.option-text { .option-text {
@include use-typography("body-small");
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;

View File

@ -788,6 +788,10 @@ msgstr "Confirm library links"
msgid "dashboard.import.error" msgid "dashboard.import.error"
msgstr "Import failed. Please try again" msgstr "Import failed. Please try again"
#: src/app/main/ui/dashboard/import.cljs:296
msgid "dashboard.import.file-name-label"
msgstr "File name"
#: src/app/main/ui/dashboard/import.cljs:292 #: src/app/main/ui/dashboard/import.cljs:292
#, unused #, unused
msgid "dashboard.import.import-error" msgid "dashboard.import.import-error"
@ -869,9 +873,17 @@ msgstr ""
msgid "dashboard.import.resolve-libraries-summary" msgid "dashboard.import.resolve-libraries-summary"
msgstr "Review the library links before confirming:" msgstr "Review the library links before confirming:"
#: src/app/main/ui/dashboard/import.cljs:672 #: src/app/main/ui/dashboard/import.cljs:430
msgid "dashboard.import.review-links" msgid "dashboard.import.resolve-libraries.connect-to"
msgstr "Review links" msgstr "connect to"
#: src/app/main/ui/dashboard/import.cljs:424
msgid "dashboard.import.resolve-libraries.original-library"
msgstr "original library"
#: src/app/main/ui/dashboard/import.cljs:675
msgid "dashboard.import.connect-selected-libraries"
msgstr "Connect selected libraries"
#, unused #, unused
msgid "dashboard.import.summary.auto-linked" msgid "dashboard.import.summary.auto-linked"
@ -879,12 +891,24 @@ msgstr "Auto-linked"
#: src/app/main/ui/dashboard/import.cljs:491 #: src/app/main/ui/dashboard/import.cljs:491
msgid "dashboard.import.summary.linked" msgid "dashboard.import.summary.linked"
msgstr "Linked" msgstr "auto-linked"
#: src/app/main/ui/dashboard/import.cljs:498
msgid "dashboard.import.summary.manually-linked"
msgstr "linked manually"
#: src/app/main/ui/dashboard/import.cljs:506
msgid "dashboard.import.summary.new"
msgstr "New"
#: src/app/main/ui/dashboard/import.cljs:527 #: src/app/main/ui/dashboard/import.cljs:527
msgid "dashboard.import.summary.no-selection" msgid "dashboard.import.summary.no-selection"
msgstr "No library selected" msgstr "No library selected"
#: src/app/main/ui/dashboard/import.cljs:503
msgid "dashboard.import.summary.original"
msgstr "Original"
#, unused #, unused
msgid "dashboard.import.summary.your-selection" msgid "dashboard.import.summary.your-selection"
msgstr "Your selection" msgstr "Your selection"
@ -3719,6 +3743,10 @@ msgstr "Show only your comments"
msgid "labels.sketch" msgid "labels.sketch"
msgstr "Sketch" msgstr "Sketch"
#: src/app/main/ui/dashboard/import.cljs:668
msgid "labels.skip"
msgstr "Skip"
#: src/app/main/ui/dashboard/sidebar.cljs:1144 #: src/app/main/ui/dashboard/sidebar.cljs:1144
msgid "labels.sources" msgid "labels.sources"
msgstr "Sources" msgstr "Sources"

View File

@ -786,6 +786,10 @@ msgstr "Confirmar vínculos de biblioteca"
msgid "dashboard.import.error" msgid "dashboard.import.error"
msgstr "La importación ha fallado. Intentalo de nuevo, por favor" msgstr "La importación ha fallado. Intentalo de nuevo, por favor"
#: src/app/main/ui/dashboard/import.cljs:296
msgid "dashboard.import.file-name-label"
msgstr "Nombre del archivo"
#: src/app/main/ui/dashboard/import.cljs:292 #: src/app/main/ui/dashboard/import.cljs:292
#, unused #, unused
msgid "dashboard.import.import-error" msgid "dashboard.import.import-error"
@ -867,9 +871,17 @@ msgstr ""
msgid "dashboard.import.resolve-libraries-summary" msgid "dashboard.import.resolve-libraries-summary"
msgstr "Revisa los vínculos de biblioteca antes de confirmar:" msgstr "Revisa los vínculos de biblioteca antes de confirmar:"
#: src/app/main/ui/dashboard/import.cljs:672 #: src/app/main/ui/dashboard/import.cljs:430
msgid "dashboard.import.review-links" msgid "dashboard.import.resolve-libraries.connect-to"
msgstr "Revisar vínculos" msgstr "conectar a"
#: src/app/main/ui/dashboard/import.cljs:424
msgid "dashboard.import.resolve-libraries.original-library"
msgstr "biblioteca original"
#: src/app/main/ui/dashboard/import.cljs:675
msgid "dashboard.import.connect-selected-libraries"
msgstr "Conectar bibliotecas seleccionadas"
#, unused #, unused
msgid "dashboard.import.summary.auto-linked" msgid "dashboard.import.summary.auto-linked"
@ -877,12 +889,24 @@ msgstr "Vinculadas automáticamente"
#: src/app/main/ui/dashboard/import.cljs:491 #: src/app/main/ui/dashboard/import.cljs:491
msgid "dashboard.import.summary.linked" msgid "dashboard.import.summary.linked"
msgstr "Vinculada" msgstr "auto-vinculada"
#: src/app/main/ui/dashboard/import.cljs:498
msgid "dashboard.import.summary.manually-linked"
msgstr "vinculada manualmente"
#: src/app/main/ui/dashboard/import.cljs:506
msgid "dashboard.import.summary.new"
msgstr "Nueva"
#: src/app/main/ui/dashboard/import.cljs:527 #: src/app/main/ui/dashboard/import.cljs:527
msgid "dashboard.import.summary.no-selection" msgid "dashboard.import.summary.no-selection"
msgstr "Ninguna biblioteca seleccionada" msgstr "Ninguna biblioteca seleccionada"
#: src/app/main/ui/dashboard/import.cljs:503
msgid "dashboard.import.summary.original"
msgstr "Original"
#, unused #, unused
msgid "dashboard.import.summary.your-selection" msgid "dashboard.import.summary.your-selection"
msgstr "Tu selección" msgstr "Tu selección"
@ -3620,6 +3644,10 @@ msgstr "Mostrar sólo tus comentarios"
msgid "labels.sketch" msgid "labels.sketch"
msgstr "Sketch" msgstr "Sketch"
#: src/app/main/ui/dashboard/import.cljs:668
msgid "labels.skip"
msgstr "Saltar"
#: src/app/main/ui/dashboard/sidebar.cljs:1144 #: src/app/main/ui/dashboard/sidebar.cljs:1144
msgid "labels.sources" msgid "labels.sources"
msgstr "Recursos" msgstr "Recursos"