From 480de64083d9e33dc212f52a7820a723d4627f4c Mon Sep 17 00:00:00 2001 From: Pablo Alba Date: Thu, 27 Aug 2026 16:41:42 +0200 Subject: [PATCH] :sparkles: Add check version menu entry --- .../app/main/ui/dashboard/check_updates.cljs | 329 ++++++++++++++++++ .../app/main/ui/dashboard/check_updates.scss | 175 ++++++++++ .../src/app/main/ui/dashboard/sidebar.cljs | 43 ++- .../src/app/main/ui/dashboard/sidebar.scss | 20 ++ frontend/test/frontend_tests/runner.cljs | 1 + .../frontend_tests/ui/check_updates_test.cljs | 69 ++++ frontend/translations/en.po | 45 +++ frontend/translations/es.po | 45 +++ 8 files changed, 723 insertions(+), 4 deletions(-) create mode 100644 frontend/src/app/main/ui/dashboard/check_updates.cljs create mode 100644 frontend/src/app/main/ui/dashboard/check_updates.scss create mode 100644 frontend/test/frontend_tests/ui/check_updates_test.cljs diff --git a/frontend/src/app/main/ui/dashboard/check_updates.cljs b/frontend/src/app/main/ui/dashboard/check_updates.cljs new file mode 100644 index 0000000000..16b9ce79ba --- /dev/null +++ b/frontend/src/app/main/ui/dashboard/check_updates.cljs @@ -0,0 +1,329 @@ +;; 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.dashboard.check-updates + (:require-macros [app.main.style :as stl]) + (:require + [app.config :as cf] + [app.main.data.event :as ev] + [app.main.data.modal :as modal] + [app.main.store :as st] + [app.main.ui.ds.buttons.button :refer [button*]] + [app.main.ui.ds.buttons.icon-button :refer [icon-button*]] + [app.main.ui.ds.foundations.assets.icon :as i :refer [icon*]] + [app.main.ui.ds.foundations.typography :as t] + [app.main.ui.ds.foundations.typography.heading :refer [heading*]] + [app.main.ui.ds.foundations.typography.text :refer [text*]] + [app.util.dom :as dom] + [app.util.http :as http] + [app.util.i18n :refer [tr]] + [beicon.v2.core :as rx] + [clojure.string :as cstr] + [cuerdas.core :as str] + [rumext.v2 :as mf])) + +(def ^:private telemetry-origin + "check-updates-modal") + +(def ^:private highlights-md-url + "https://raw.githubusercontent.com/penpot/penpot/refs/heads/staging/HIGHLIGHTS.md") + +(def ^:private changelog-url + "https://github.com/penpot/penpot/blob/staging/CHANGES.md") + +(def ^:private release-notes-url + "https://penpot.app/release-notes") + +(def ^:private version-heading-re + #"(?m)^##\s+(\d+\.\d+\.\d+)(.*)$") + +(def ^:private bullet-re + #"^- (.+)$") + +(defn- unreleased-suffix? + [suffix] + (str/includes? (str/lower (or suffix "")) "unreleased")) + +(defn- parse-section-items + [section] + (->> (cstr/split-lines section) + (keep (fn [line] + (when-let [[_ item] (re-matches bullet-re (str/trim line))] + item))) + vec)) + +(def ^:private version-triple-re + #"^(\d+)\.(\d+)\.(\d+)$") + +(defn- version-triple + [version] + (when-let [[_ major minor patch] (re-matches version-triple-re version)] + [(js/parseInt major 10) + (js/parseInt minor 10) + (js/parseInt patch 10)])) + +(defn version-compare + "Compare two X.Y.Z versions by major, minor, and patch." + [a b] + (let [[ma mi pa] (or (version-triple a) [0 0 0]) + [mb mj pb] (or (version-triple b) [0 0 0])] + (or (when (not= ma mb) (- ma mb)) + (when (not= mi mj) (- mi mj)) + (- pa pb)))) + +(defn version-newer? + [a b] + (pos? (version-compare a b))) + +(defn parse-highlights + "Parse HIGHLIGHTS.md into released version sections with bullet items. + Skips Unreleased headings. Preserves file order (newest first)." + [markdown] + (if-not (string? markdown) + [] + (->> (str/split markdown #"(?m)(?=^##\s+\d+\.\d+\.\d+)") + (keep (fn [part] + (when-let [[_ version suffix] (re-find version-heading-re part)] + (when-not (unreleased-suffix? suffix) + {:version version + :items (parse-section-items part)})))) + vec))) + +(defn parse-latest-released-version + "Return the first non-unreleased `## X.Y.Z` heading from a highlights body." + [markdown] + (some-> (parse-highlights markdown) first :version)) + +(defn highlights-until-installed + "Keep released sections newer than the installed version (major, minor, + patch). Stops at the installed version or any older section." + [highlights installed] + (into [] + (take-while #(version-newer? (:version %) installed)) + highlights)) + +(defn- show-available-dialog + [{:keys [installed latest highlights]}] + (st/emit! (modal/show {:type :check-updates-available + :installed installed + :latest latest + :highlights highlights}))) + +(defn- show-uptodate-dialog + [version] + (st/emit! (modal/show {:type :check-updates-uptodate + :version version}))) + +(defn- show-unable-dialog + [] + (st/emit! (modal/show {:type :check-updates-unable}))) + +(defn- handle-highlights + [installed body] + (let [sections (parse-highlights body) + latest (some-> sections first :version)] + (cond + (nil? latest) + (show-unable-dialog) + + (not (version-newer? latest installed)) + (show-uptodate-dialog installed) + + :else + (show-available-dialog + {:installed installed + :latest latest + :highlights (highlights-until-installed sections installed)})))) + +(defn check-for-updates! + ([current-version] + (check-for-updates! current-version nil)) + ([current-version {:keys [on-start on-finish]}] + (when on-start (on-start)) + (->> (http/send! {:method :get + :mode :cors + :omit-default-headers true + :uri highlights-md-url + :response-type :text}) + (rx/subs! + (fn [response] + (when on-finish (on-finish)) + (if (http/success? response) + (handle-highlights current-version (:body response)) + (show-unable-dialog))) + (fn [_cause] + (when on-finish (on-finish)) + (show-unable-dialog)))))) + +(mf/defc check-updates-unable-modal* + {::mf/register modal/components + ::mf/register-as :check-updates-unable} + [_] + (let [on-close + (mf/use-fn #(st/emit! (modal/hide))) + + on-try-again + (mf/use-fn + (fn [] + (st/emit! (modal/hide)) + (check-for-updates! (:base cf/version))))] + + [:div {:class (stl/css :modal-overlay)} + [:div {:class (stl/css :modal-container)} + [:div {:class (stl/css :modal-header)} + [:div {:class (stl/css :modal-title-row)} + [:> icon* {:icon-id i/msg-neutral + :class (stl/css :modal-title-icon) + :aria-hidden true}] + [:> heading* {:level 2 + :typography "headline-medium" + :class (stl/css :modal-title)} + (tr "dashboard.check-updates.unable-title")]] + [:> icon-button* {:variant "ghost" + :aria-label (tr "labels.close") + :on-click on-close + :icon i/close + :class (stl/css :modal-close-btn)}]] + + [:div {:class (stl/css :modal-content)} + [:> text* {:as "p" + :typography t/body-large + :class (stl/css :modal-msg)} + (tr "dashboard.check-updates.unable-message")] + [:> text* {:as "p" + :typography t/body-large + :class (stl/css :modal-msg)} + (tr "dashboard.check-updates.unable-hint")]] + + [:div {:class (stl/css :modal-footer)} + [:> button* {:variant "primary" + :on-click on-try-again} + (tr "dashboard.check-updates.try-again")]]]])) + +(mf/defc check-updates-uptodate-modal* + {::mf/register modal/components + ::mf/register-as :check-updates-uptodate} + [{:keys [version]}] + (let [on-close + (mf/use-fn #(st/emit! (modal/hide)))] + + [:div {:class (stl/css :modal-overlay)} + [:div {:class (stl/css :modal-container)} + [:div {:class (stl/css :modal-header)} + [:div {:class (stl/css :modal-title-row)} + [:> icon* {:icon-id i/tick + :class (stl/css :modal-title-icon) + :aria-hidden true}] + [:> heading* {:level 2 + :typography "headline-medium" + :class (stl/css :modal-title)} + (tr "dashboard.check-updates.uptodate-title")]] + [:> icon-button* {:variant "ghost" + :aria-label (tr "labels.close") + :on-click on-close + :icon i/close + :class (stl/css :modal-close-btn)}]] + + [:div {:class (stl/css :modal-content)} + [:> text* {:as "p" + :typography t/body-large + :class (stl/css :modal-msg)} + (tr "dashboard.check-updates.uptodate-message") + " " + [:span {:class (stl/css :version)} version]]] + + [:div {:class (stl/css :modal-footer)} + [:> button* {:variant "secondary" + :on-click on-close} + (tr "labels.close")]]]])) + +(mf/defc check-updates-available-modal* + {::mf/register modal/components + ::mf/register-as :check-updates-available} + [{:keys [installed latest highlights]}] + (let [on-close + (mf/use-fn #(st/emit! (modal/hide))) + + on-changelog + (mf/use-fn + (mf/deps installed) + (fn [] + (st/emit! (ev/event {::ev/name "explore-changelog-click" + ::ev/origin telemetry-origin + :version installed})) + (dom/open-new-window changelog-url))) + + on-release-notes + (mf/use-fn + (mf/deps installed) + (fn [] + (st/emit! (ev/event {::ev/name "explore-product-updates-click" + ::ev/origin telemetry-origin + :version installed})) + (dom/open-new-window release-notes-url)))] + + [:div {:class (stl/css :modal-overlay)} + [:div {:class (stl/css :modal-container :modal-container-available)} + [:div {:class (stl/css :modal-header)} + [:div {:class (stl/css :modal-title-row)} + [:> icon* {:icon-id i/info + :class (stl/css :modal-title-icon) + :aria-hidden true}] + [:> heading* {:level 2 + :typography "headline-medium" + :class (stl/css :modal-title)} + (tr "dashboard.check-updates.available-title")]] + [:> icon-button* {:variant "ghost" + :aria-label (tr "labels.close") + :on-click on-close + :icon i/close + :class (stl/css :modal-close-btn)}]] + + [:div {:class (stl/css :version-bar)} + [:span {:class (stl/css :version-bar-side)} + (tr "dashboard.check-updates.installed-version") + " " + [:span {:class (stl/css :version)} installed]] + [:> icon* {:icon-id i/arrow-right + :class (stl/css :version-bar-arrow) + :size "s" + :aria-hidden true}] + [:span {:class (stl/css :version-bar-side)} + (tr "dashboard.check-updates.latest-version") + " " + [:span {:class (stl/css :version :version-accent)} latest]]] + + [:div {:class (stl/css :modal-content)} + [:> text* {:as "p" + :typography t/body-large + :class (stl/css :modal-msg)} + (tr "dashboard.check-updates.available-message")] + + [:> text* {:as "h3" + :typography t/headline-small + :class (stl/css :highlights-title)} + (tr "dashboard.check-updates.highlights-title")] + + [:div {:class (stl/css :highlights-scroll)} + (for [section highlights] + (let [version (:version section) + items (:items section)] + [:div {:key version + :class (stl/css :highlights-section)} + [:div {:class (stl/css :highlights-version)} version] + [:ul {:class (stl/css :highlights-list)} + (for [item items] + [:li {:key item + :class (stl/css :highlights-item)} + item])]]))]] + + [:div {:class (stl/css :modal-footer :modal-footer-available)} + [:> button* {:variant "secondary" + :on-click on-changelog} + (tr "dashboard.check-updates.view-changelog")] + [:> button* {:variant "primary" + :on-click on-release-notes} + (tr "dashboard.check-updates.view-release-notes")]]]])) diff --git a/frontend/src/app/main/ui/dashboard/check_updates.scss b/frontend/src/app/main/ui/dashboard/check_updates.scss new file mode 100644 index 0000000000..26f5855a09 --- /dev/null +++ b/frontend/src/app/main/ui/dashboard/check_updates.scss @@ -0,0 +1,175 @@ +// 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 + +@use "ds/_utils.scss" as *; +@use "ds/_sizes.scss" as *; +@use "ds/_borders.scss" as *; +@use "ds/typography.scss" as *; + +.modal-overlay { + display: flex; + justify-content: center; + align-items: center; + position: fixed; + inset: 0; + z-index: var(--z-index-set); + background-color: var(--color-overlay-default); +} + +.modal-container { + position: relative; + display: flex; + flex-direction: column; + gap: var(--sp-xxl); + padding: var(--sp-xxxl); + border-radius: $br-8; + background-color: var(--color-background-primary); + border: $b-2 solid var(--color-background-quaternary); + min-inline-size: $sz-364; + max-inline-size: $sz-512; +} + +.modal-container-available { + max-inline-size: $sz-480; + max-block-size: min(90vh, $sz-712); +} + +.modal-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: var(--sp-m); + padding-inline-end: var(--sp-xl); +} + +.modal-title-row { + display: flex; + align-items: center; + gap: var(--sp-s); +} + +.modal-title-icon { + flex-shrink: 0; + color: var(--color-foreground-primary); +} + +.modal-title { + color: var(--color-foreground-primary); +} + +.modal-close-btn { + position: absolute; + inset-block-start: var(--sp-m); + inset-inline-end: var(--sp-m); +} + +.version-bar { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--sp-m); + padding: var(--sp-m) var(--sp-l); + border-radius: $br-8; + background-color: var(--color-background-tertiary); + color: var(--color-foreground-secondary); +} + +.version-bar-side { + @include use-typography("body-large"); +} + +.version-bar-arrow { + flex-shrink: 0; + color: var(--color-foreground-secondary); +} + +.modal-content { + display: flex; + flex-direction: column; + gap: var(--sp-s); + min-block-size: 0; +} + +.modal-msg { + margin: 0; + color: var(--color-foreground-secondary); +} + +.version { + color: var(--color-foreground-primary); + font-weight: 700; +} + +.version-accent { + color: var(--color-accent-primary); +} + +.highlights-title { + margin: var(--sp-m) 0 0; + color: var(--color-foreground-secondary); + text-transform: uppercase; +} + +.highlights-scroll { + display: flex; + flex-direction: column; + gap: var(--sp-l); + margin-block-start: var(--sp-s); + padding-inline-end: var(--sp-s); + max-block-size: $sz-284; + overflow-y: auto; +} + +.highlights-section { + display: flex; + flex-direction: column; + gap: var(--sp-s); +} + +.highlights-version { + @include use-typography("headline-small"); + + color: var(--color-foreground-primary); + font-weight: 700; +} + +.highlights-list { + display: flex; + flex-direction: column; + gap: var(--sp-xs); + margin: 0; + padding: 0; + list-style: none; +} + +.highlights-item { + @include use-typography("body-large"); + + position: relative; + padding-inline-start: var(--sp-l); + color: var(--color-foreground-secondary); + + &::before { + content: ""; + position: absolute; + inset-block-start: 0.55em; + inset-inline-start: 0; + inline-size: $sz-6; + block-size: $sz-6; + border-radius: $br-circle; + background-color: var(--color-accent-primary); + } +} + +.modal-footer { + display: flex; + justify-content: flex-end; + gap: var(--sp-s); +} + +.modal-footer-available { + flex-shrink: 0; +} diff --git a/frontend/src/app/main/ui/dashboard/sidebar.cljs b/frontend/src/app/main/ui/dashboard/sidebar.cljs index 57844a6d9b..7769e3e107 100644 --- a/frontend/src/app/main/ui/dashboard/sidebar.cljs +++ b/frontend/src/app/main/ui/dashboard/sidebar.cljs @@ -26,6 +26,7 @@ dropdown-menu-item*]] [app.main.ui.components.link :refer [link*]] [app.main.ui.components.organization-avatar :refer [organization-avatar*]] + [app.main.ui.dashboard.check-updates :as dcu] [app.main.ui.dashboard.comments :refer [comments-icon* comments-section]] [app.main.ui.dashboard.inline-edition :refer [inline-edition]] [app.main.ui.dashboard.project-menu :refer [project-menu*]] @@ -1256,8 +1257,11 @@ (mf/defc about-penpot-menu* {::mf/private true} - [{:keys [on-close on-pointer-enter on-pointer-leave]}] - (let [version cf/version + [{:keys [on-close on-close-profile on-pointer-enter on-pointer-leave]}] + (let [version cf/version + checking* (mf/use-state false) + checking? (deref checking*) + show-release-notes (mf/use-fn (fn [event] @@ -1275,7 +1279,22 @@ (dom/get-data "eventname"))] (st/emit! (ev/event {::ev/name eventname ::ev/origin "menu:in-app"})) - (dom/open-new-window url))))] + (dom/open-new-window url)))) + + check-for-updates + (mf/use-fn + (mf/deps on-close-profile version) + (fn [event] + (dom/stop-propagation event) + (when-not @checking* + (st/emit! (ev/event {::ev/name "check-for-updates" + ::ev/origin "menu:in-app" + :version (:base version)})) + (dcu/check-for-updates! + (:base version) + {:on-start #(reset! checking* true) + :on-finish #(do (reset! checking* false) + (on-close-profile))}))))] [:> dropdown-menu* {:show true :class (stl/css :sub-menu :about) @@ -1297,7 +1316,22 @@ :data-url "https://penpot.app/terms" :on-click handle-click-url :data-eventname "explore-terms-service-click"} - (tr "auth.terms-of-service")]])) + (tr "auth.terms-of-service")] + (when-not (contains? cf/flags :air-gapped-conf) + [:* + [:hr {:role "separator" :class (stl/css :submenu-separator)}] + [:> dropdown-menu-item* {:class (stl/css-case :submenu-item true + :checking checking?) + :aria-disabled checking? + :can-focus (not checking?) + :on-click check-for-updates} + (if checking? + (tr "labels.checking-for-updates") + (tr "labels.check-for-updates")) + (when checking? + [:> icon* {:icon-id i/reload + :class (stl/css :checking-icon) + :size "s"}])]])])) (mf/defc profile-section* [{:keys [profile team]}] @@ -1512,6 +1546,7 @@ :about-penpot [:> about-penpot-menu* {:on-close close-sub-menu + :on-close-profile on-close :on-pointer-enter on-sub-menu-pointer-enter :on-pointer-leave on-menu-pointer-leave}] nil))])) diff --git a/frontend/src/app/main/ui/dashboard/sidebar.scss b/frontend/src/app/main/ui/dashboard/sidebar.scss index 0bf4b474e3..826d7db7c4 100644 --- a/frontend/src/app/main/ui/dashboard/sidebar.scss +++ b/frontend/src/app/main/ui/dashboard/sidebar.scss @@ -525,6 +525,26 @@ &:hover { color: var(--menu-foreground-color-hover); } + + &.checking { + color: var(--color-foreground-secondary); + pointer-events: none; + + &:hover { + color: var(--color-foreground-secondary); + background-color: transparent; + } + } +} + +.checking-icon { + flex-shrink: 0; + animation: spin-animation 1s infinite linear; +} + +.submenu-separator { + border-top: $b-1 solid var(--color-background-quaternary); + margin: 0; } .about-penpot { diff --git a/frontend/test/frontend_tests/runner.cljs b/frontend/test/frontend_tests/runner.cljs index 629e6c9477..9ee71ec7ec 100644 --- a/frontend/test/frontend_tests/runner.cljs +++ b/frontend/test/frontend_tests/runner.cljs @@ -76,6 +76,7 @@ [frontend-tests.tokens.token-errors-test] [frontend-tests.tokens.workspace-tokens-remap-test] [frontend-tests.ui.colorpicker-token-set-order-test] + [frontend-tests.ui.check-updates-test] [frontend-tests.ui.comments-clustering-test] [frontend-tests.ui.comments-position-modifier-test] [frontend-tests.ui.ds-controls-numeric-input-test] diff --git a/frontend/test/frontend_tests/ui/check_updates_test.cljs b/frontend/test/frontend_tests/ui/check_updates_test.cljs new file mode 100644 index 0000000000..7bef4e3db8 --- /dev/null +++ b/frontend/test/frontend_tests/ui/check_updates_test.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 frontend-tests.ui.check-updates-test + (:require + [app.main.ui.dashboard.check-updates :as dcu] + [cljs.test :as t :include-macros true])) + +(def ^:private sample-highlights + (str "# HIGHLIGHTS\n" + "\n" + "## 2.18.0 (Unreleased)\n" + "\n" + "- To do\n" + "\n" + "## 2.17.2\n" + "\n" + "- Background blur is here\n" + "- WebGL rendering gets stronger\n" + "\n" + "## 2.17.1\n" + "\n" + "- MCP connection status and more\n" + "- Design tokens: more visible, more user-friendly\n")) + +(t/deftest parse-latest-released-version-skips-unreleased + (t/is (= "2.17.2" (dcu/parse-latest-released-version sample-highlights)))) + +(t/deftest parse-latest-released-version-first-released + (t/is (= "2.17.2" + (dcu/parse-latest-released-version + "## 2.17.2\n\n- Fix\n\n## 2.17.1\n\n- Fix\n")))) + +(t/deftest parse-latest-released-version-only-unreleased + (t/is (nil? (dcu/parse-latest-released-version + "## 2.18.0 (Unreleased)\n\n- WIP\n")))) + +(t/deftest parse-latest-released-version-empty + (t/is (nil? (dcu/parse-latest-released-version ""))) + (t/is (nil? (dcu/parse-latest-released-version "# HIGHLIGHTS\n")))) + +(t/deftest parse-highlights-skips-unreleased-and-collects-bullets + (t/is (= [{:version "2.17.2" + :items ["Background blur is here" + "WebGL rendering gets stronger"]} + {:version "2.17.1" + :items ["MCP connection status and more" + "Design tokens: more visible, more user-friendly"]}] + (dcu/parse-highlights sample-highlights)))) + +(t/deftest version-compare + (t/is (zero? (dcu/version-compare "2.17.1" "2.17.1"))) + (t/is (pos? (dcu/version-compare "2.17.2" "2.17.1"))) + (t/is (neg? (dcu/version-compare "2.17.1" "2.17.2"))) + (t/is (pos? (dcu/version-compare "3.0.0" "2.99.99"))) + (t/is (neg? (dcu/version-compare "2.17.2" "2.17.10")))) + +(t/deftest highlights-until-installed + (let [sections (dcu/parse-highlights sample-highlights)] + (t/is (= [{:version "2.17.2" + :items ["Background blur is here" + "WebGL rendering gets stronger"]}] + (dcu/highlights-until-installed sections "2.17.1"))) + (t/is (= [] (dcu/highlights-until-installed sections "2.17.2"))) + (t/is (= sections (dcu/highlights-until-installed sections "2.16.0"))) + (t/is (= [] (dcu/highlights-until-installed sections "2.17.10"))))) diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 701c91e5b7..e886f1dd34 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -3680,6 +3680,51 @@ msgstr "Variant" msgid "labels.version-notes" msgstr "Version %s notes" +msgid "labels.check-for-updates" +msgstr "Check for updates" + +msgid "labels.checking-for-updates" +msgstr "Checking for updates..." + +msgid "dashboard.check-updates.available-title" +msgstr "A NEW PENPOT VERSION IS AVAILABLE" + +msgid "dashboard.check-updates.available-message" +msgstr "Newer releases include performance improvements, new capabilities, and fixes that keep your installation current." + +msgid "dashboard.check-updates.installed-version" +msgstr "Installed version" + +msgid "dashboard.check-updates.latest-version" +msgstr "Latest version" + +msgid "dashboard.check-updates.highlights-title" +msgstr "What's new - Highlights" + +msgid "dashboard.check-updates.view-changelog" +msgstr "View full changelog" + +msgid "dashboard.check-updates.view-release-notes" +msgstr "View release notes" + +msgid "dashboard.check-updates.unable-title" +msgstr "UNABLE TO CHECK FOR UPDATES" + +msgid "dashboard.check-updates.unable-message" +msgstr "This installation could not reach the Penpot releases service." + +msgid "dashboard.check-updates.unable-hint" +msgstr "Check your network or instance configuration and try again." + +msgid "dashboard.check-updates.try-again" +msgstr "Try again" + +msgid "dashboard.check-updates.uptodate-title" +msgstr "PENPOT IS UP TO DATE" + +msgid "dashboard.check-updates.uptodate-message" +msgstr "You're running the latest stable version:" + #: src/app/main/ui/workspace/sidebar/sitemap.cljs:298 msgid "labels.view-only" msgstr "View only" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 945d30a19e..e36e063ce9 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -3570,6 +3570,51 @@ msgstr "Variante" msgid "labels.version-notes" msgstr "Notas versión %s" +msgid "labels.check-for-updates" +msgstr "Comprobar actualizaciones" + +msgid "labels.checking-for-updates" +msgstr "Comprobando actualizaciones..." + +msgid "dashboard.check-updates.available-title" +msgstr "A NEW PENPOT VERSION IS AVAILABLE" + +msgid "dashboard.check-updates.available-message" +msgstr "Las versiones más recientes incluyen mejoras de rendimiento, nuevas capacidades y correcciones que mantienen tu instalación al día." + +msgid "dashboard.check-updates.installed-version" +msgstr "Versión instalada" + +msgid "dashboard.check-updates.latest-version" +msgstr "Última versión" + +msgid "dashboard.check-updates.highlights-title" +msgstr "Novedades - Destacados" + +msgid "dashboard.check-updates.view-changelog" +msgstr "Ver changelog completo" + +msgid "dashboard.check-updates.view-release-notes" +msgstr "Ver notas de la versión" + +msgid "dashboard.check-updates.unable-title" +msgstr "UNABLE TO CHECK FOR UPDATES" + +msgid "dashboard.check-updates.unable-message" +msgstr "Esta instalación no pudo contactar con el servicio de versiones de Penpot." + +msgid "dashboard.check-updates.unable-hint" +msgstr "Comprueba tu red o la configuración de la instancia e inténtalo de nuevo." + +msgid "dashboard.check-updates.try-again" +msgstr "Intentar de nuevo" + +msgid "dashboard.check-updates.uptodate-title" +msgstr "PENPOT IS UP TO DATE" + +msgid "dashboard.check-updates.uptodate-message" +msgstr "Estás usando la última versión estable:" + #: src/app/main/ui/workspace/sidebar/sitemap.cljs:298 msgid "labels.view-only" msgstr "Solo lectura"