From f095e1b29fd5dd4487d05890a5205039196c6f14 Mon Sep 17 00:00:00 2001
From: Andrey Antukh <niwi@niwi.nz>
Date: Thu, 20 Jun 2024 08:03:18 +0200
Subject: [PATCH 1/3] :sparkles: Replace custom all-spaces? fn with generic
 str/blank?

---
 frontend/src/app/main/ui/comments.cljs | 7 +++----
 1 file changed, 3 insertions(+), 4 deletions(-)

diff --git a/frontend/src/app/main/ui/comments.cljs b/frontend/src/app/main/ui/comments.cljs
index fd156e6a7..5427b29f1 100644
--- a/frontend/src/app/main/ui/comments.cljs
+++ b/frontend/src/app/main/ui/comments.cljs
@@ -17,7 +17,6 @@
    [app.main.refs :as refs]
    [app.main.store :as st]
    [app.main.ui.components.dropdown :refer [dropdown]]
-   [app.main.ui.components.forms :as fm]
    [app.main.ui.icons :as i]
    [app.util.dom :as dom]
    [app.util.i18n :as i18n :refer [tr]]
@@ -96,7 +95,7 @@
   (let [show-buttons? (mf/use-state false)
         content       (mf/use-state "")
 
-        disabled? (or (fm/all-spaces? @content)
+        disabled? (or (str/blank? @content)
                       (str/empty-or-nil? @content))
 
         on-focus
@@ -155,7 +154,7 @@
         pos-x    (* (:x position) zoom)
         pos-y    (* (:y position) zoom)
 
-        disabled? (or (fm/all-spaces? content)
+        disabled? (or (str/blank? content)
                       (str/empty-or-nil? content))
 
         on-esc
@@ -225,7 +224,7 @@
          (mf/deps @content)
          (fn [] (on-submit @content)))
 
-        disabled? (or (fm/all-spaces? @content)
+        disabled? (or (str/blank? @content)
                       (str/empty-or-nil? @content))]
 
     [:div {:class (stl/css :edit-form)}

From 7be79c10fd4cbdef5499d4be583daf3d69826a38 Mon Sep 17 00:00:00 2001
From: Andrey Antukh <niwi@niwi.nz>
Date: Thu, 13 Jun 2024 11:43:21 +0200
Subject: [PATCH 2/3] :recycle: Refactor forms

Mainly replace spec with schema with better
and more reusable validations
---
 common/src/app/common/data.cljc               |   9 +-
 common/src/app/common/schema.cljc             | 185 ++++++++++++++++--
 common/src/app/common/schema/generators.cljc  |  21 +-
 frontend/src/app/main/ui/auth/login.cljs      |  21 +-
 frontend/src/app/main/ui/auth/recovery.cljs   |  41 ++--
 .../app/main/ui/auth/recovery_request.cljs    |  29 ++-
 frontend/src/app/main/ui/auth/register.cljs   | 153 ++++++---------
 .../src/app/main/ui/components/forms.cljs     |  60 ++----
 .../app/main/ui/dashboard/change_owner.cljs   |  11 +-
 frontend/src/app/main/ui/dashboard/team.cljs  |  40 ++--
 .../src/app/main/ui/dashboard/team_form.cljs  |  24 ++-
 .../src/app/main/ui/onboarding/questions.cljs | 176 ++++++++---------
 .../app/main/ui/onboarding/team_choice.cljs   |  34 ++--
 .../app/main/ui/settings/access_tokens.cljs   |  25 +--
 .../app/main/ui/settings/change_email.cljs    |  56 ++----
 .../src/app/main/ui/settings/feedback.cljs    |  27 ++-
 .../src/app/main/ui/settings/options.cljs     |  13 +-
 .../src/app/main/ui/settings/password.cljs    |  52 ++---
 .../src/app/main/ui/settings/profile.cljs     |  57 +++---
 .../ui/workspace/sidebar/assets/groups.cljs   |  22 +--
 frontend/src/app/util/forms.cljs              | 183 +++++++++--------
 frontend/translations/af.po                   |  20 +-
 frontend/translations/ar.po                   |  20 +-
 frontend/translations/bn.po                   |   2 +-
 frontend/translations/ca.po                   |   4 +-
 frontend/translations/cs.po                   |  28 +--
 frontend/translations/da.po                   |   2 +-
 frontend/translations/de.po                   |  40 +---
 frontend/translations/el.po                   |   4 +-
 frontend/translations/en.po                   |  33 ++--
 frontend/translations/es.po                   |  33 ++--
 frontend/translations/es_419.po               |  19 +-
 frontend/translations/eu.po                   |   6 +-
 frontend/translations/fa.po                   |   4 +-
 frontend/translations/fin_FI.po               |   2 +-
 frontend/translations/fr.po                   |  28 +--
 frontend/translations/gl.po                   |   2 +-
 frontend/translations/ha.po                   |  24 +--
 frontend/translations/he.po                   |  40 +---
 frontend/translations/hr.po                   |   4 +-
 frontend/translations/id.po                   |  40 +---
 frontend/translations/ig.po                   |  24 +--
 frontend/translations/it.po                   |   4 +-
 frontend/translations/jpn_JP.po               |   4 +-
 frontend/translations/ko.po                   |  22 +--
 frontend/translations/lt.po                   |   2 +-
 frontend/translations/lv.po                   |  28 +--
 frontend/translations/ml.po                   |   2 +-
 frontend/translations/ms.po                   |  24 +--
 frontend/translations/nb_NO.po                |   2 +-
 frontend/translations/nl.po                   |  40 +---
 frontend/translations/pl.po                   |   6 +-
 frontend/translations/pt_BR.po                |   6 +-
 frontend/translations/pt_PT.po                |  41 +---
 frontend/translations/ro.po                   |  28 +--
 frontend/translations/ru.po                   |   4 +-
 frontend/translations/ta.po                   |   2 +-
 frontend/translations/tr.po                   |  28 +--
 frontend/translations/ukr_UA.po               |   2 +-
 frontend/translations/yo.po                   |  24 +--
 frontend/translations/zh_CN.po                |  40 +---
 frontend/translations/zh_Hant.po              |  24 +--
 62 files changed, 786 insertions(+), 1165 deletions(-)

diff --git a/common/src/app/common/data.cljc b/common/src/app/common/data.cljc
index 645091fd0..ecf55b115 100644
--- a/common/src/app/common/data.cljc
+++ b/common/src/app/common/data.cljc
@@ -9,7 +9,7 @@
   data resources."
   (:refer-clojure :exclude [read-string hash-map merge name update-vals
                             parse-double group-by iteration concat mapcat
-                            parse-uuid max min])
+                            parse-uuid max min regexp?])
   #?(:cljs
      (:require-macros [app.common.data]))
 
@@ -641,6 +641,13 @@
 ;; Utilities
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 
+(defn regexp?
+  "Return `true` if `x` is a regexp pattern
+  instance."
+  [x]
+  #?(:cljs (cljs.core/regexp? x)
+     :clj (instance? java.util.regex.Pattern x)))
+
 (defn nilf
   "Returns a new function that if you pass nil as any argument will
   return nil"
diff --git a/common/src/app/common/schema.cljc b/common/src/app/common/schema.cljc
index a74b88f1f..37636d5b2 100644
--- a/common/src/app/common/schema.cljc
+++ b/common/src/app/common/schema.cljc
@@ -44,6 +44,14 @@
   [o]
   (m/schema? o))
 
+(defn properties
+  [s]
+  (m/properties s))
+
+(defn type-properties
+  [s]
+  (m/type-properties s))
+
 (defn lazy-schema?
   [s]
   (satisfies? ILazySchema s))
@@ -144,6 +152,8 @@
    (m/encode s val options transformer)))
 
 (defn decode
+  ([s val]
+   (m/decode s val default-options default-transformer))
   ([s val transformer]
    (m/decode s val default-options transformer))
   ([s val options transformer]
@@ -428,23 +438,33 @@
   [s]
   (if (string? s)
     (re-matches email-re s)
-    s))
+    nil))
+
+(defn email-string?
+  [s]
+  (and (string? s)
+       (re-seq email-re s)))
 
-;; FIXME: add proper email generator
 (define! ::email
-  {:type ::email
-   :pred (fn [s]
-           (and (string? s)
-                (< (count s) 250)
-                (re-seq email-re s)))
+  {:type :string
+   :pred email-string?
+   :property-pred
+   (fn [{:keys [max] :as props}]
+     (if (some? max)
+       (fn [value]
+         (<= (count value) max))
+       (constantly true)))
+
    :type-properties
    {:title "email"
     :description "string with valid email address"
-    :error/message "expected valid email"
-    :gen/gen (-> :string sg/generator)
+    :error/code "errors.invalid-email"
+    :gen/gen (sg/email)
     ::oapi/type "string"
     ::oapi/format "email"
-    ::oapi/decode parse-email}})
+    ::oapi/decode
+    (fn [v]
+      (or (parse-email v) v))}})
 
 (def non-empty-strings-xf
   (comp
@@ -452,6 +472,59 @@
    (remove str/empty?)
    (remove str/blank?)))
 
+;; NOTE: this is general purpose set spec and should be used over the other
+
+(define! ::set
+  {:type :set
+   :compile
+   (fn [{:keys [coerce kind max min] :as props} _ _]
+     (let [xform (if coerce
+                   (comp non-empty-strings-xf (map coerce))
+                   non-empty-strings-xf)
+           pred  (cond
+                   (fn? kind)  kind
+                   (nil? kind) any?
+                   :else       (validator kind))
+
+           pred (cond
+                  (and max min)
+                  (fn [value]
+                    (let [size (count value)]
+                      (and (set? value)
+                           (<= min size max)
+                           (every? pred value))))
+
+                  min
+                  (fn [value]
+                    (let [size (count value)]
+                      (and (set? value)
+                           (<= min size)
+                           (every? pred value))))
+
+                  max
+                  (fn [value]
+                    (let [size (count value)]
+                      (and (set? value)
+                           (<= size max)
+                           (every? pred value))))
+
+                  :else
+                  pred)]
+
+       {:pred pred
+        :type-properties
+        {:title "set"
+         :description "Set of Strings"
+         :error/message "should be a set of strings"
+         :gen/gen (-> kind sg/generator sg/set)
+         ::oapi/type "array"
+         ::oapi/format "set"
+         ::oapi/items {:type "string"}
+         ::oapi/unique-items true
+         ::oapi/decode (fn [v]
+                         (let [v (if (string? v) (str/split v #"[\s,]+") v)]
+                           (into #{} xform v)))}}))})
+
 (define! ::set-of-strings
   {:type ::set-of-strings
    :pred #(and (set? %) (every? string? %))
@@ -634,6 +707,8 @@
 (define! ::fn
   [:schema fn?])
 
+;; FIXME: deprecated, replace with ::text
+
 (define! ::word-string
   {:type ::word-string
    :pred #(and (string? %) (not (str/blank? %)))
@@ -649,16 +724,102 @@
 (define! ::uri
   {:type ::uri
    :pred u/uri?
+   :property-pred
+   (fn [{:keys [min max prefix] :as props}]
+     (if (seq props)
+       (fn [value]
+         (let [value  (str value)
+               size   (count value)]
+
+           (and
+            (cond
+              (and min max)
+              (<= min size max)
+
+              min
+              (<= min size)
+
+              max
+              (<= size max))
+
+            (cond
+              (d/regexp? prefix)
+              (some? (re-seq prefix value))
+
+              :else
+              true))))
+
+       (constantly true)))
+
    :type-properties
    {:title "uri"
     :description "URI formatted string"
-    :error/message "expected URI instance"
+    :error/code "errors.invalid-uri"
     :gen/gen (sg/uri)
     ::oapi/type "string"
     ::oapi/format "uri"
     ::oapi/decode (comp u/uri str/trim)}})
 
-(def! ::plugin-data
+(define! ::text
+  {:type :string
+   :pred #(and (string? %) (not (str/blank? %)))
+   :property-pred
+   (fn [{:keys [min max] :as props}]
+     (if (seq props)
+       (fn [value]
+         (let [size (count value)]
+           (cond
+             (and min max)
+             (<= min size max)
+
+             min
+             (<= min size)
+
+             max
+             (<= size max))))
+       (constantly true)))
+
+   :type-properties
+   {:title "string"
+    :description "not whitespace string"
+    :gen/gen (sg/word-string)
+    :error/code "errors.invalid-text"
+    :error/fn
+    (fn [{:keys [value schema]}]
+      (let [{:keys [max min] :as props} (properties schema)]
+        (cond
+          (and (string? value)
+               (number? max)
+               (> (count value) max))
+          ["errors.field-max-length" max]
+
+          (and (string? value)
+               (number? min)
+               (< (count value) min))
+          ["errors.field-min-length" min]
+
+          (and (string? value)
+               (str/blank? value))
+          "errors.field-not-all-whitespace")))}})
+
+(define! ::password
+  {:type :string
+   :pred
+   (fn [value]
+     (and (string? value)
+          (>= (count value) 8)
+          (not (str/blank? value))))
+   :type-properties
+   {:title "password"
+    :gen/gen (->> (sg/word-string)
+                  (sg/filter #(>= (count %) 8)))
+    :error/code "errors.password-too-short"
+    ::oapi/type "string"
+    ::oapi/format "password"}})
+
+
+;; FIXME: this should not be here
+(define! ::plugin-data
   [:map-of {:gen/max 5} :string :string])
 
 ;; ---- PREDICATES
diff --git a/common/src/app/common/schema/generators.cljc b/common/src/app/common/schema/generators.cljc
index 83e00bfd8..081e1d5ca 100644
--- a/common/src/app/common/schema/generators.cljc
+++ b/common/src/app/common/schema/generators.cljc
@@ -77,10 +77,23 @@
 
 (defn word-string
   []
-  (->> (tg/such-that #(re-matches #"\w+" %)
-                     tg/string-alphanumeric
-                     50)
-       (tg/such-that (complement str/blank?))))
+  (as-> tg/string-alphanumeric $$
+    (tg/such-that (fn [v] (re-matches #"\w+" v)) $$ 50)
+    (tg/such-that (fn [v]
+                    (and (not (str/blank? v))
+                         (not (re-matches #"^\d+.*" v))))
+                  $$
+                  50)))
+
+
+(defn email
+  []
+  (->> (word-string)
+       (tg/such-that (fn [v] (>= (count v) 4)))
+       (tg/fmap str/lower)
+       (tg/fmap (fn [v]
+                  (str v "@example.net")))))
+
 
 (defn uri
   []
diff --git a/frontend/src/app/main/ui/auth/login.cljs b/frontend/src/app/main/ui/auth/login.cljs
index 389a901f5..7324c7ec1 100644
--- a/frontend/src/app/main/ui/auth/login.cljs
+++ b/frontend/src/app/main/ui/auth/login.cljs
@@ -7,8 +7,8 @@
 (ns app.main.ui.auth.login
   (:require-macros [app.main.style :as stl])
   (:require
-   [app.common.data :as d]
    [app.common.logging :as log]
+   [app.common.schema :as sm]
    [app.common.spec :as us]
    [app.config :as cf]
    [app.main.data.messages :as msg]
@@ -72,20 +72,19 @@
   (s/keys :req-un [::email ::password]
           :opt-un [::invitation-token]))
 
-(defn handle-error-messages
-  [errors _data]
-  (d/update-when errors :email
-                 (fn [{:keys [code] :as error}]
-                   (cond-> error
-                     (= code ::us/email)
-                     (assoc :message (tr "errors.email-invalid"))))))
+(def ^:private schema:login-form
+  [:map {:title "LoginForm"}
+   [:email [::sm/email {:error/code "errors.invalid-email"}]]
+   [:password [:string {:min 1}]]
+   [:invitation-token {:optional true}
+    [:string {:min 1}]]])
 
 (mf/defc login-form
   [{:keys [params on-success-callback origin] :as props}]
-  (let [initial (mf/use-memo (mf/deps params) (constantly params))
+  (let [initial (mf/with-memo [params] params)
         error   (mf/use-state false)
-        form    (fm/use-form :spec ::login-form
-                             :validators [handle-error-messages]
+        form    (fm/use-form :schema schema:login-form
+                             ;; :validators [handle-error-messages]
                              :initial initial)
 
         on-error
diff --git a/frontend/src/app/main/ui/auth/recovery.cljs b/frontend/src/app/main/ui/auth/recovery.cljs
index 85657eef6..6ec730c5b 100644
--- a/frontend/src/app/main/ui/auth/recovery.cljs
+++ b/frontend/src/app/main/ui/auth/recovery.cljs
@@ -7,39 +7,29 @@
 (ns app.main.ui.auth.recovery
   (:require-macros [app.main.style :as stl])
   (:require
-   [app.common.spec :as us]
+   [app.common.schema :as sm]
    [app.main.data.messages :as msg]
    [app.main.data.users :as du]
    [app.main.store :as st]
    [app.main.ui.components.forms :as fm]
    [app.util.i18n :as i18n :refer [tr]]
    [app.util.router :as rt]
-   [cljs.spec.alpha :as s]
    [rumext.v2 :as mf]))
 
-(s/def ::password-1 ::us/not-empty-string)
-(s/def ::password-2 ::us/not-empty-string)
-(s/def ::token ::us/not-empty-string)
-
-(s/def ::recovery-form
-  (s/keys :req-un [::password-1
-                   ::password-2]))
-
-(defn- password-equality
-  [errors data]
-  (let [password-1 (:password-1 data)
-        password-2 (:password-2 data)]
-    (cond-> errors
-      (and password-1 password-2
-           (not= password-1 password-2))
-      (assoc :password-2 {:message "errors.password-invalid-confirmation"})
-
-      (and password-1 (> 8 (count password-1)))
-      (assoc :password-1 {:message "errors.password-too-short"}))))
+(def ^:private schema:recovery-form
+  [:and
+   [:map {:title "RecoveryForm"}
+    [:token ::sm/text]
+    [:password-1 ::sm/password]
+    [:password-2 ::sm/password]]
+   [:fn {:error/code "errors.password-invalid-confirmation"
+         :error/field :password-2}
+    (fn [{:keys [password-1 password-2]}]
+      (= password-1 password-2))]])
 
 (defn- on-error
   [_form _error]
-  (st/emit! (msg/error (tr "auth.notifications.invalid-token-error"))))
+  (st/emit! (msg/error (tr "errors.invalid-recovery-token"))))
 
 (defn- on-success
   [_]
@@ -56,14 +46,13 @@
 
 (mf/defc recovery-form
   [{:keys [params] :as props}]
-  (let [form (fm/use-form :spec ::recovery-form
-                          :validators [password-equality
-                                       (fm/validate-not-empty :password-1 (tr "auth.password-not-empty"))
-                                       (fm/validate-not-empty :password-2 (tr "auth.password-not-empty"))]
+  (let [form (fm/use-form :schema schema:recovery-form
                           :initial params)]
+
     [:& fm/form {:on-submit on-submit
                  :class (stl/css :recovery-form)
                  :form form}
+
      [:div {:class (stl/css :fields-row)}
       [:& fm/input {:type "password"
                     :name :password-1
diff --git a/frontend/src/app/main/ui/auth/recovery_request.cljs b/frontend/src/app/main/ui/auth/recovery_request.cljs
index 43988fb3c..d3ce49eaa 100644
--- a/frontend/src/app/main/ui/auth/recovery_request.cljs
+++ b/frontend/src/app/main/ui/auth/recovery_request.cljs
@@ -7,8 +7,7 @@
 (ns app.main.ui.auth.recovery-request
   (:require-macros [app.main.style :as stl])
   (:require
-   [app.common.data :as d]
-   [app.common.spec :as us]
+   [app.common.schema :as sm]
    [app.main.data.messages :as msg]
    [app.main.data.users :as du]
    [app.main.store :as st]
@@ -17,30 +16,24 @@
    [app.util.i18n :as i18n :refer [tr]]
    [app.util.router :as rt]
    [beicon.v2.core :as rx]
-   [cljs.spec.alpha :as s]
    [rumext.v2 :as mf]))
 
-(s/def ::email ::us/email)
-(s/def ::recovery-request-form (s/keys :req-un [::email]))
-(defn handle-error-messages
-  [errors _data]
-  (d/update-when errors :email
-                 (fn [{:keys [code] :as error}]
-                   (cond-> error
-                     (= code :missing)
-                     (assoc :message (tr "errors.email-invalid"))))))
+(def ^:private schema:recovery-request-form
+  [:map {:title "RecoverRequestForm"}
+   [:email ::sm/email]])
 
 (mf/defc recovery-form
   [{:keys [on-success-callback] :as props}]
-  (let [form      (fm/use-form :spec ::recovery-request-form
-                               :validators [handle-error-messages]
+  (let [form      (fm/use-form :schema schema:recovery-request-form
                                :initial {})
         submitted (mf/use-state false)
 
-        default-success-finish #(st/emit! (msg/info (tr "auth.notifications.recovery-token-sent")))
+        default-success-finish
+        (mf/use-fn
+         #(st/emit! (msg/info (tr "auth.notifications.recovery-token-sent"))))
 
         on-success
-        (mf/use-callback
+        (mf/use-fn
          (fn [cdata _]
            (reset! submitted false)
            (if (nil? on-success-callback)
@@ -48,7 +41,7 @@
              (on-success-callback (:email cdata)))))
 
         on-error
-        (mf/use-callback
+        (mf/use-fn
          (fn [data cause]
            (reset! submitted false)
            (let [code (-> cause ex-data :code)]
@@ -65,7 +58,7 @@
                (rx/throw cause)))))
 
         on-submit
-        (mf/use-callback
+        (mf/use-fn
          (fn []
            (reset! submitted true)
            (let [cdata  (:clean-data @form)
diff --git a/frontend/src/app/main/ui/auth/register.cljs b/frontend/src/app/main/ui/auth/register.cljs
index 90daacf1e..ae8e47d74 100644
--- a/frontend/src/app/main/ui/auth/register.cljs
+++ b/frontend/src/app/main/ui/auth/register.cljs
@@ -7,8 +7,7 @@
 (ns app.main.ui.auth.register
   (:require-macros [app.main.style :as stl])
   (:require
-   [app.common.data :as d]
-   [app.common.spec :as us]
+   [app.common.schema :as sm]
    [app.config :as cf]
    [app.main.data.messages :as msg]
    [app.main.data.users :as du]
@@ -22,67 +21,42 @@
    [app.util.router :as rt]
    [app.util.storage :as sto]
    [beicon.v2.core :as rx]
-   [cljs.spec.alpha :as s]
    [rumext.v2 :as mf]))
 
 ;; --- PAGE: Register
 
-(defn- validate-password-length
-  [errors data]
-  (let [password (:password data)]
-    (cond-> errors
-      (> 8 (count password))
-      (assoc :password {:message "errors.password-too-short"}))))
-
-(defn- validate-email
-  [errors _]
-  (d/update-when errors :email
-                 (fn [{:keys [code] :as error}]
-                   (cond-> error
-                     (= code ::us/email)
-                     (assoc :message (tr "errors.email-invalid"))))))
-
-(s/def ::fullname ::us/not-empty-string)
-(s/def ::password ::us/not-empty-string)
-(s/def ::email ::us/email)
-(s/def ::invitation-token ::us/not-empty-string)
-(s/def ::terms-privacy ::us/boolean)
-
-(s/def ::register-form
-  (s/keys :req-un [::password ::email]
-          :opt-un [::invitation-token]))
-
-(defn- on-prepare-register-error
-  [form cause]
-  (let [{:keys [type code]} (ex-data cause)]
-    (condp = [type code]
-      [:restriction :registration-disabled]
-      (st/emit! (msg/error (tr "errors.registration-disabled")))
-
-      [:restriction :email-domain-is-not-allowed]
-      (st/emit! (msg/error (tr "errors.email-domain-not-allowed")))
-
-      [:validation :email-as-password]
-      (swap! form assoc-in [:errors :password]
-             {:message "errors.email-as-password"})
-
-      (st/emit! (msg/error (tr "errors.generic"))))))
-
-(defn- on-prepare-register-success
-  [params]
-  (st/emit! (rt/nav :auth-register-validate {} params)))
+(def ^:private schema:register-form
+  [:map {:title "RegisterForm"}
+   [:password ::sm/password]
+   [:email ::sm/email]
+   [:invitation-token {:optional true} ::sm/text]])
 
 (mf/defc register-form
+  {::mf/props :obj}
   [{:keys [params on-success-callback]}]
   (let [initial (mf/use-memo (mf/deps params) (constantly params))
-        form    (fm/use-form :spec ::register-form
-                             :validators [validate-password-length
-                                          validate-email
-                                          (fm/validate-not-empty :password (tr "auth.password-not-empty"))]
+        form    (fm/use-form :schema schema:register-form
                              :initial initial)
 
         submitted? (mf/use-state false)
 
+        on-error
+        (mf/use-fn
+         (fn [form cause]
+           (let [{:keys [type code]} (ex-data cause)]
+             (condp = [type code]
+               [:restriction :registration-disabled]
+               (st/emit! (msg/error (tr "errors.registration-disabled")))
+
+               [:restriction :email-domain-is-not-allowed]
+               (st/emit! (msg/error (tr "errors.email-domain-not-allowed")))
+
+               [:validation :email-as-password]
+               (swap! form assoc-in [:errors :password]
+                      {:code "errors.email-as-password"})
+
+               (st/emit! (msg/error (tr "errors.generic")))))))
+
         on-submit
         (mf/use-fn
          (mf/deps on-success-callback)
@@ -90,16 +64,14 @@
            (reset! submitted? true)
            (let [cdata      (:clean-data @form)
                  on-success (fn [data]
-                              (if (nil? on-success-callback)
-                                (on-prepare-register-success data)
-                                (on-success-callback data)))
-                 on-error   (fn [data]
-                              (on-prepare-register-error form data))]
+                              (if (fn? on-success-callback)
+                                (on-success-callback data)
+                                (st/emit! (rt/nav :auth-register-validate {} data))))]
 
              (->> (rp/cmd! :prepare-register-profile cdata)
                   (rx/map #(merge % params))
                   (rx/finalize #(reset! submitted? false))
-                  (rx/subs! on-success on-error)))))]
+                  (rx/subs! on-success (partial on-error form))))))]
 
     [:& fm/form {:on-submit on-submit :form form}
      [:div {:class (stl/css :fields-row)}
@@ -164,33 +136,6 @@
 
 ;; --- PAGE: register validation
 
-(defn- on-register-success
-  [data]
-  (cond
-    (some? (:invitation-token data))
-    (let [token (:invitation-token data)]
-      (st/emit! (rt/nav :auth-verify-token {} {:token token})))
-
-    (:is-active data)
-    (st/emit! (du/login-from-register))
-
-    :else
-    (do
-      (swap! sto/storage assoc ::email (:email data))
-      (st/emit! (rt/nav :auth-register-success)))))
-
-(s/def ::accept-terms-and-privacy (s/and ::us/boolean true?))
-(s/def ::accept-newsletter-subscription ::us/boolean)
-
-(if (contains? cf/flags :terms-and-privacy-checkbox)
-  (s/def ::register-validate-form
-    (s/keys :req-un [::token ::fullname ::accept-terms-and-privacy]
-            :opt-un [::accept-newsletter-subscription]))
-  (s/def ::register-validate-form
-    (s/keys :req-un [::token ::fullname]
-            :opt-un [::accept-terms-and-privacy
-                     ::accept-newsletter-subscription])))
-
 (mf/defc terms-and-privacy
   {::mf/props :obj
    ::mf/private true}
@@ -210,34 +155,48 @@
                    :default-checked false
                    :label terms-label}]]))
 
+(def ^:private schema:register-validate-form
+  [:map {:title "RegisterValidateForm"}
+   [:token ::sm/text]
+   [:fullname [::sm/text {:max 250}]]
+   [:accept-terms-and-privacy {:optional (not (contains? cf/flags :terms-and-privacy-checkbox))}
+    [:and :boolean [:= true]]]])
+
 (mf/defc register-validate-form
-  {::mf/props :obj}
+  {::mf/props :obj
+   ::mf/private true}
   [{:keys [params on-success-callback]}]
-  (let [validators (mf/with-memo []
-                     [(fm/validate-not-empty :fullname (tr "auth.name.not-all-space"))
-                      (fm/validate-length :fullname fm/max-length-allowed (tr "auth.name.too-long"))])
-
-        form       (fm/use-form :spec ::register-validate-form
-                                :validators validators
-                                :initial params)
-
+  (let [form       (fm/use-form :schema schema:register-validate-form :initial params)
         submitted? (mf/use-state false)
 
         on-success
         (mf/use-fn
          (mf/deps on-success-callback)
          (fn [params]
-           (if (nil? on-success-callback)
-             (on-register-success params)
-             (on-success-callback (:email params)))))
+           (if (fn? on-success-callback)
+             (on-success-callback (:email params))
+
+             (cond
+               (some? (:invitation-token params))
+               (let [token (:invitation-token params)]
+                 (st/emit! (rt/nav :auth-verify-token {} {:token token})))
+
+               (:is-active params)
+               (st/emit! (du/login-from-register))
+
+               :else
+               (do
+                 (swap! sto/storage assoc ::email (:email params))
+                 (st/emit! (rt/nav :auth-register-success)))))))
 
         on-error
         (mf/use-fn
-         (fn [_cause]
+         (fn [_]
            (st/emit! (msg/error (tr "errors.generic")))))
 
         on-submit
         (mf/use-fn
+         (mf/deps on-success on-error)
          (fn [form _]
            (reset! submitted? true)
            (let [params (:clean-data @form)]
diff --git a/frontend/src/app/main/ui/components/forms.cljs b/frontend/src/app/main/ui/components/forms.cljs
index e454be3a3..eef34a8cf 100644
--- a/frontend/src/app/main/ui/components/forms.cljs
+++ b/frontend/src/app/main/ui/components/forms.cljs
@@ -18,7 +18,6 @@
    [app.util.keyboard :as kbd]
    [app.util.object :as obj]
    [cljs.core :as c]
-   [clojure.string]
    [cuerdas.core :as str]
    [rumext.v2 :as mf]))
 
@@ -26,7 +25,9 @@
 (def use-form fm/use-form)
 
 (mf/defc input
-  [{:keys [label help-icon disabled form hint trim children data-testid on-change-value placeholder show-success?] :as props}]
+  [{:keys [label help-icon disabled form hint trim children data-testid on-change-value placeholder show-success? show-error]
+    :or {show-error true}
+    :as props}]
   (let [input-type   (get props :type "text")
         input-name   (get props :name)
         more-classes (get props :class)
@@ -152,11 +153,14 @@
          children])
 
       (cond
-        (and touched? (:message error))
-        [:div {:id (dm/str "error-" input-name)
-               :class (stl/css :error)
-               :data-testid (clojure.string/join [data-testid "-error"])}
-         (tr (:message error))]
+        (and touched? (:code error) show-error)
+        (let [code (:code error)]
+          [:div {:id (dm/str "error-" input-name)
+                 :class (stl/css :error)
+                 :data-testid (dm/str data-testid "-error")}
+           (if (vector? code)
+             (tr (nth code 0) (i18n/c (nth code 1)))
+             (tr code))])
 
         (string? hint)
         [:div {:class (stl/css :hint)} hint])]]))
@@ -207,8 +211,8 @@
      [:label {:class (stl/css :textarea-label)} label]
      [:> :textarea props]
      (cond
-       (and touched? (:message error))
-       [:span {:class (stl/css :error)} (tr (:message error))]
+       (and touched? (:code error))
+       [:span {:class (stl/css :error)} (tr (:code error))]
 
        (string? hint)
        [:span {:class (stl/css :hint)} hint])]))
@@ -550,41 +554,3 @@
             [:span {:class (stl/css :text)} (:text item)]
             [:button {:class (stl/css :icon)
                       :on-click #(remove-item! item)} i/close]]])])]))
-
-;; --- Validators
-
-(defn all-spaces?
-  [value]
-  (let [trimmed (str/trim value)]
-    (str/empty? trimmed)))
-
-(def max-length-allowed 250)
-(def max-uri-length-allowed 2048)
-
-(defn max-length?
-  [value length]
-  (> (count value) length))
-
-(defn validate-length
-  [field length errors-msg]
-  (fn [errors data]
-    (cond-> errors
-      (max-length? (get data field) length)
-      (assoc field {:message errors-msg}))))
-
-(defn validate-not-empty
-  [field error-msg]
-  (fn [errors data]
-    (cond-> errors
-      (all-spaces? (get data field))
-      (assoc field {:message error-msg}))))
-
-(defn validate-not-all-spaces
-  [field error-msg]
-  (fn [errors data]
-    (let [value (get data field)]
-      (cond-> errors
-        (and
-         (all-spaces? value)
-         (> (count value) 0))
-        (assoc field {:message error-msg})))))
diff --git a/frontend/src/app/main/ui/dashboard/change_owner.cljs b/frontend/src/app/main/ui/dashboard/change_owner.cljs
index b3a8e04d2..d87056e00 100644
--- a/frontend/src/app/main/ui/dashboard/change_owner.cljs
+++ b/frontend/src/app/main/ui/dashboard/change_owner.cljs
@@ -7,25 +7,24 @@
 (ns app.main.ui.dashboard.change-owner
   (:require-macros [app.main.style :as stl])
   (:require
-   [app.common.spec :as us]
+   [app.common.schema :as sm]
    [app.main.data.modal :as modal]
    [app.main.refs :as refs]
    [app.main.store :as st]
    [app.main.ui.components.forms :as fm]
    [app.main.ui.icons :as i]
    [app.util.i18n :as i18n :refer [tr]]
-   [cljs.spec.alpha :as s]
    [rumext.v2 :as mf]))
 
-(s/def ::member-id ::us/uuid)
-(s/def ::leave-modal-form
-  (s/keys :req-un [::member-id]))
+(def ^:private schema:leave-modal-form
+  [:map {:title "LeaveModalForm"}
+   [:member-id ::sm/uuid]])
 
 (mf/defc leave-and-reassign-modal
   {::mf/register modal/components
    ::mf/register-as :leave-and-reassign}
   [{:keys [profile team accept]}]
-  (let [form        (fm/use-form :spec ::leave-modal-form :initial {})
+  (let [form        (fm/use-form :schema schema:leave-modal-form :initial {})
         members-map (mf/deref refs/dashboard-team-members)
         members     (vals members-map)
 
diff --git a/frontend/src/app/main/ui/dashboard/team.cljs b/frontend/src/app/main/ui/dashboard/team.cljs
index 0d78ee520..3a0d3421f 100644
--- a/frontend/src/app/main/ui/dashboard/team.cljs
+++ b/frontend/src/app/main/ui/dashboard/team.cljs
@@ -9,6 +9,7 @@
   (:require
    [app.common.data :as d]
    [app.common.data.macros :as dm]
+   [app.common.schema :as sm]
    [app.common.spec :as us]
    [app.config :as cfg]
    [app.main.data.dashboard :as dd]
@@ -33,7 +34,6 @@
    [cuerdas.core :as str]
    [rumext.v2 :as mf]))
 
-
 (def ^:private arrow-icon
   (i/icon-xref :arrow (stl/css :arrow-icon)))
 
@@ -131,6 +131,12 @@
 (s/def ::invite-member-form
   (s/keys :req-un [::role ::emails ::team-id]))
 
+(def ^:private schema:invite-member-form
+  [:map {:title "InviteMemberForm"}
+   [:role :keyword]
+   [:emails [::sm/set {:kind ::sm/email :min 1}]]
+   [:team-id ::sm/uuid]])
+
 (mf/defc invite-members-modal
   {::mf/register modal/components
    ::mf/register-as :invite-members
@@ -139,9 +145,14 @@
   (let [members-map (mf/deref refs/dashboard-team-members)
         perms       (:permissions team)
 
-        roles       (mf/use-memo (mf/deps perms) #(get-available-roles perms))
-        initial     (mf/use-memo (constantly {:role "editor" :team-id (:id team)}))
-        form        (fm/use-form :spec ::invite-member-form
+        roles       (mf/with-memo [perms]
+                      (get-available-roles perms))
+        team-id     (:id team)
+
+        initial     (mf/with-memo [team-id]
+                      {:role "editor" :team-id team-id})
+
+        form        (fm/use-form :schema schema:invite-member-form
                                  :initial initial)
         error-text  (mf/use-state  "")
 
@@ -746,10 +757,11 @@
 ;; WEBHOOKS SECTION
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 
-(s/def ::uri ::us/uri)
-(s/def ::mtype ::us/not-empty-string)
-(s/def ::webhook-form
-  (s/keys :req-un [::uri ::mtype]))
+(def ^:private schema:webhook-form
+  [:map {:title "WebhookForm"}
+   [:uri [::sm/uri {:max 4069 :prefix #"^http[s]?://"
+                    :error/code "errors.webhooks.invalid-uri"}]]
+   [:mtype ::sm/text]])
 
 (def valid-webhook-mtypes
   [{:label "application/json" :value "application/json"}
@@ -763,12 +775,12 @@
   {::mf/register modal/components
    ::mf/register-as :webhook}
   [{:keys [webhook] :as props}]
-  ;; FIXME: this is a workaround because input fields do not support rendering hooks
-  (let [initial (mf/use-memo (fn [] (or (some-> webhook (update :uri str))
-                                        {:is-active false :mtype "application/json"})))
-        form    (fm/use-form :spec ::webhook-form
-                             :initial initial
-                             :validators [(fm/validate-length :uri fm/max-uri-length-allowed (tr "team.webhooks.max-length"))])
+
+  (let [initial (mf/with-memo []
+                  (or (some-> webhook (update :uri str))
+                      {:is-active false :mtype "application/json"}))
+        form    (fm/use-form :schema schema:webhook-form
+                             :initial initial)
         on-success
         (mf/use-fn
          (fn [_]
diff --git a/frontend/src/app/main/ui/dashboard/team_form.cljs b/frontend/src/app/main/ui/dashboard/team_form.cljs
index 7a37ec9c6..3d7e790f2 100644
--- a/frontend/src/app/main/ui/dashboard/team_form.cljs
+++ b/frontend/src/app/main/ui/dashboard/team_form.cljs
@@ -7,7 +7,7 @@
 (ns app.main.ui.dashboard.team-form
   (:require-macros [app.main.style :as stl])
   (:require
-   [app.common.spec :as us]
+   [app.common.schema :as sm]
    [app.main.data.dashboard :as dd]
    [app.main.data.messages :as msg]
    [app.main.data.modal :as modal]
@@ -19,12 +19,11 @@
    [app.util.keyboard :as kbd]
    [app.util.router :as rt]
    [beicon.v2.core :as rx]
-   [cljs.spec.alpha :as s]
    [rumext.v2 :as mf]))
 
-(s/def ::name ::us/not-empty-string)
-(s/def ::team-form
-  (s/keys :req-un [::name]))
+(def ^:private schema:team-form
+  [:map {:title "TeamForm"}
+   [:name [::sm/text {:max 250}]]])
 
 (defn- on-create-success
   [_form response]
@@ -68,24 +67,23 @@
       (on-update-submit form)
       (on-create-submit form))))
 
-(mf/defc team-form-modal {::mf/register modal/components
-                          ::mf/register-as :team-form}
+(mf/defc team-form-modal
+  {::mf/register modal/components
+   ::mf/register-as :team-form}
   [{:keys [team] :as props}]
   (let [initial (mf/use-memo (fn [] (or team {})))
-        form    (fm/use-form :spec ::team-form
-                             :validators [(fm/validate-not-empty :name (tr "auth.name.not-all-space"))
-                                          (fm/validate-length :name fm/max-length-allowed (tr "auth.name.too-long"))]
+        form    (fm/use-form :schema schema:team-form
                              :initial initial)
         handle-keydown
-        (mf/use-callback
-         (mf/deps)
+        (mf/use-fn
          (fn [e]
            (when (kbd/enter? e)
              (dom/prevent-default e)
              (dom/stop-propagation e)
              (on-submit form e))))
 
-        on-close #(st/emit! (modal/hide))]
+        on-close
+        (mf/use-fn #(st/emit! (modal/hide)))]
 
     [:div {:class (stl/css :modal-overlay)}
      [:div {:class (stl/css :modal-container)}
diff --git a/frontend/src/app/main/ui/onboarding/questions.cljs b/frontend/src/app/main/ui/onboarding/questions.cljs
index 3d715a185..d8d939685 100644
--- a/frontend/src/app/main/ui/onboarding/questions.cljs
+++ b/frontend/src/app/main/ui/onboarding/questions.cljs
@@ -10,13 +10,13 @@
   (:require
    [app.common.data :as d]
    [app.common.data.macros :as dm]
+   [app.common.schema :as sm]
    [app.main.data.events :as-alias ev]
    [app.main.data.users :as du]
    [app.main.store :as st]
    [app.main.ui.components.forms :as fm]
    [app.main.ui.icons :as i]
    [app.util.i18n :as i18n :refer [tr]]
-   [cljs.spec.alpha :as s]
    [cuerdas.core :as str]
    [potok.v2.core :as ptk]
    [rumext.v2 :as mf]))
@@ -56,25 +56,20 @@
                  (tr "labels.start"))
         :class (stl/css :next-button)}]]]))
 
-(s/def ::questions-form-step-1
-  (s/keys :req-un [::planning
-                   ::expected-use]
-          :opt-un [::planning-other]))
+(def ^:private schema:questions-form-1
+  [:and
 
-(defn- step-1-form-validator
-  [errors data]
-  (let [planning       (:planning data)
-        planning-other (:planning-other data)]
-    (cond-> errors
-      (and (= planning "other")
-           (str/blank? planning-other))
-      (assoc :planning-other {:code "missing"})
+   [:map {:title "QuestionsFormStep1"}
+    [:planning ::sm/text]
+    [:expected-use [:enum "work" "education" "personal"]]
+    [:planning-other {:optional true}
+     [::sm/text {:max 512}]]]
 
-      (not= planning "other")
-      (assoc :planning-other nil)
-
-      (str/blank? planning)
-      (assoc :planning {:code "missing"}))))
+   [:fn {:error/field :planning-other}
+    (fn [{:keys [planning planning-other]}]
+      (or (not= planning "other")
+          (and (= planning "other")
+               (not (str/blank? planning-other)))))]])
 
 (mf/defc step-1
   {::mf/props :obj}
@@ -143,24 +138,24 @@
         [:& fm/input {:name :planning-other
                       :class (stl/css :input-spacing)
                       :placeholder (tr "labels.other")
+                      :show-error false
                       :label ""}])]]))
 
-(s/def ::questions-form-step-2
-  (s/keys :req-un [::experience-design-tool]
-          :opt-un [::experience-design-tool-other]))
+(def ^:private schema:questions-form-2
+  [:and
+   [:map {:title "QuestionsFormStep2"}
+    [:experience-design-tool
+     [:enum "figma" "sketch" "adobe-xd" "canva" "invision" "other"]]
+    [:experience-design-tool-other {:optional true}
+     [::sm/text {:max 512}]]]
 
-(defn- step-2-form-validator
-  [errors data]
-  (let [experience       (:experience-design-tool data)
-        experience-other (:experience-design-tool-other data)]
-
-    (cond-> errors
-      (and (= experience "other")
-           (str/blank? experience-other))
-      (assoc :experience-design-tool-other {:code "missing"})
-
-      (not= experience "other")
-      (assoc :experience-design-tool-other nil))))
+   [:fn {:error/field :experience-design-tool-other}
+    (fn [data]
+      (let [experience       (:experience-design-tool data)
+            experience-other (:experience-design-tool-other data)]
+        (or (not= experience "other")
+            (and (= experience "other")
+                 (not (str/blank? experience-other))))))]])
 
 (mf/defc step-2
   {::mf/props :obj}
@@ -180,7 +175,7 @@
               (conj {:label (tr "labels.other-short")  :value "other" :icon i/curve})))
 
         current-experience
-        (dm/get-in @form [:clean-data :experience-design-tool])
+        (dm/get-in @form [:data :experience-design-tool])
 
         on-design-tool-change
         (mf/use-fn
@@ -212,33 +207,34 @@
         [:& fm/input {:name :experience-design-tool-other
                       :class (stl/css :input-spacing)
                       :placeholder (tr "labels.other")
+                      :show-error false
                       :label ""}])]]))
 
-(s/def ::questions-form-step-3
-  (s/keys :req-un [::team-size ::role ::responsability]
-          :opt-un [::role-other ::responsability-other]))
 
-(defn- step-3-form-validator
-  [errors data]
-  (let [role                 (:role data)
-        role-other           (:role-other data)
-        responsability       (:responsability data)
-        responsability-other (:responsability-other data)]
+(def ^:private schema:questions-form-3
+  [:and
+   [:map {:title "QuestionsFormStep3"}
+    [:team-size
+     [:enum "more-than-50" "31-50" "11-30" "2-10" "freelancer" "personal-project"]]
+    [:role
+     [:enum "designer" "developer" "student-teacher" "graphic-design" "marketing" "manager" "other"]]
+    [:responsability
+     [:enum "team-leader" "team-member" "freelancer" "ceo-founder" "director" "student-teacher" "other"]]
 
-    (cond-> errors
-      (and (= role "other")
-           (str/blank? role-other))
-      (assoc :role-other {:code "missing"})
+    [:role-other {:optional true} [::sm/text {:max 512}]]
+    [:responsability-other {:optional true} [::sm/text {:max 512}]]]
 
-      (not= role "other")
-      (assoc :role-other nil)
+   [:fn {:error/field :role-other}
+    (fn [{:keys [role role-other]}]
+      (or (not= role "other")
+          (and (= role "other")
+               (not (str/blank? role-other)))))]
 
-      (and (= responsability "other")
-           (str/blank? responsability-other))
-      (assoc :responsability-other {:code "missing"})
-
-      (not= responsability "other")
-      (assoc :responsability-other nil))))
+   [:fn {:error/field :responsability-other}
+    (fn [{:keys [responsability responsability-other]}]
+      (or (not= responsability "other")
+          (and (= responsability "other")
+               (not (str/blank? responsability-other)))))]])
 
 (mf/defc step-3
   {::mf/props :obj}
@@ -264,7 +260,6 @@
                         {:label (tr "labels.director") :value "director"}])
               (conj {:label (tr "labels.other-short") :value "other"})))
 
-
         team-size-options
         (mf/with-memo []
           [{:label (tr "labels.select-option") :value "" :key "team-size" :disabled true}
@@ -301,6 +296,7 @@
         [:& fm/input {:name :role-other
                       :class (stl/css :input-spacing)
                       :placeholder (tr "labels.other")
+                      :show-error false
                       :label ""}])]
 
      [:div {:class (stl/css :modal-question)}
@@ -314,6 +310,7 @@
         [:& fm/input {:name :responsability-other
                       :class (stl/css :input-spacing)
                       :placeholder (tr "labels.other")
+                      :show-error false
                       :label ""}])]
 
      [:div {:class (stl/css :modal-question)}
@@ -323,21 +320,18 @@
                      :select-class (stl/css :select-class)
                      :name :team-size}]]]))
 
-(s/def ::questions-form-step-4
-  (s/keys :req-un [::start-with]
-          :opt-un [::start-with-other]))
+(def ^:private schema:questions-form-4
+  [:and
+   [:map {:title "QuestionsFormStep4"}
+    [:start-with
+     [:enum "ui" "wireframing" "prototyping" "ds" "code" "other"]]
+    [:start-with-other {:optional true} [::sm/text {:max 512}]]]
 
-(defn- step-4-form-validator
-  [errors data]
-  (let [start       (:start-with data)
-        start-other (:start-with-other data)]
-    (cond-> errors
-      (and (= start "other")
-           (str/blank? start-other))
-      (assoc :start-with-other {:code "missing"})
-
-      (not= start "other")
-      (assoc :start-with-other nil))))
+   [:fn {:error/field :start-with-other}
+    (fn [{:keys [start-with start-with-other]}]
+      (or (not= start-with "other")
+          (and (= start-with "other")
+               (not (str/blank? start-with-other)))))]])
 
 (mf/defc step-4
   {::mf/props :obj}
@@ -386,23 +380,21 @@
         [:& fm/input {:name :start-with-other
                       :class (stl/css :input-spacing)
                       :label ""
+                      :show-error false
                       :placeholder (tr "labels.other")}])]]))
 
-(s/def ::questions-form-step-5
-  (s/keys :req-un [::referer]
-          :opt-un [::referer-other]))
+(def ^:private schema:questions-form-5
+  [:and
+   [:map {:title "QuestionsFormStep5"}
+    [:referer
+     [:enum "youtube" "event" "search" "social" "article" "other"]]
+    [:referer-other {:optional true} [::sm/text {:max 512}]]]
 
-(defn- step-5-form-validator
-  [errors data]
-  (let [referer       (:referer data)
-        referer-other (:referer-other data)]
-    (cond-> errors
-      (and (= referer "other")
-           (str/blank? referer-other))
-      (assoc :referer-other {:code "missing"})
-
-      (not= referer "other")
-      (assoc :referer-other nil))))
+   [:fn {:error/field :referer-other}
+    (fn [{:keys [referer referer-other]}]
+      (or (not= referer "other")
+          (and (= referer "other")
+               (not (str/blank? referer-other)))))]])
 
 (mf/defc step-5
   {::mf/props :obj}
@@ -444,6 +436,7 @@
         [:& fm/input {:name :referer-other
                       :class (stl/css :input-spacing)
                       :label ""
+                      :show-error false
                       :placeholder (tr "labels.other")}])]]))
 
 (mf/defc questions-modal
@@ -456,28 +449,23 @@
         ;; and we want to keep the filled info
         step-1-form (fm/use-form
                      :initial {}
-                     :validators [step-1-form-validator]
-                     :spec ::questions-form-step-1)
+                     :schema schema:questions-form-1)
 
         step-2-form (fm/use-form
                      :initial {}
-                     :validators [step-2-form-validator]
-                     :spec ::questions-form-step-2)
+                     :schema schema:questions-form-2)
 
         step-3-form (fm/use-form
                      :initial {}
-                     :validators [step-3-form-validator]
-                     :spec ::questions-form-step-3)
+                     :schema schema:questions-form-3)
 
         step-4-form (fm/use-form
                      :initial {}
-                     :validators [step-4-form-validator]
-                     :spec ::questions-form-step-4)
+                     :schema schema:questions-form-4)
 
         step-5-form (fm/use-form
                      :initial {}
-                     :validators [step-5-form-validator]
-                     :spec ::questions-form-step-5)
+                     :schema schema:questions-form-5)
 
         on-next
         (mf/use-fn
diff --git a/frontend/src/app/main/ui/onboarding/team_choice.cljs b/frontend/src/app/main/ui/onboarding/team_choice.cljs
index 4525d7b71..b97079518 100644
--- a/frontend/src/app/main/ui/onboarding/team_choice.cljs
+++ b/frontend/src/app/main/ui/onboarding/team_choice.cljs
@@ -8,7 +8,7 @@
   (:require-macros [app.main.style :as stl])
   (:require
    [app.common.data.macros :as dm]
-   [app.common.spec :as us]
+   [app.common.schema :as sm]
    [app.main.data.dashboard :as dd]
    [app.main.data.events :as ev]
    [app.main.data.messages :as msg]
@@ -18,7 +18,6 @@
    [app.main.ui.icons :as i]
    [app.util.i18n :as i18n :refer [tr]]
    [app.util.router :as rt]
-   [cljs.spec.alpha :as s]
    [potok.v2.core :as ptk]
    [rumext.v2 :as mf]))
 
@@ -55,11 +54,10 @@
      [:p {:class (stl/css :modal-desc)}
       (tr "onboarding.team-modal.create-team-feature-5")]]]])
 
-
-(s/def ::emails (s/and ::us/set-of-valid-emails))
-(s/def ::role  ::us/keyword)
-(s/def ::invite-form
-  (s/keys :req-un [::role ::emails]))
+(def ^:private schema:invite-form
+  [:map {:title "InviteForm"}
+   [:role :keyword]
+   [:emails [::sm/set {:kind ::sm/email}]]])
 
 (defn- get-available-roles
   []
@@ -73,7 +71,7 @@
                  #(do {:role "editor"
                        :name name}))
 
-        form    (fm/use-form :spec ::invite-form
+        form    (fm/use-form :schema schema:invite-form
                              :initial initial)
 
         params  (:clean-data @form)
@@ -151,7 +149,7 @@
                             :name :emails
                             :auto-focus? true
                             :trim true
-                            :valid-item-fn us/parse-email
+                            :valid-item-fn sm/parse-email
                             :caution-item-fn #{}
                             :label (tr "modals.invite-member.emails")
                             :on-submit on-submit}]]
@@ -172,18 +170,16 @@
 
      [:div {:class (stl/css :paginator)} "2/2"]]))
 
+(def ^:private schema:team-form
+  [:map {:title "TeamForm"}
+   [:name [::sm/text {:max 250}]]])
+
 (mf/defc team-form-step-1
   {::mf/props :obj
    ::mf/private true}
   [{:keys [on-submit]}]
-  (let [validators (mf/with-memo []
-                     [(fm/validate-not-empty :name (tr "auth.name.not-all-space"))
-                      (fm/validate-length :name fm/max-length-allowed (tr "auth.name.too-long"))])
-
-        form       (fm/use-form
-                    :spec ::team-form
-                    :initial {}
-                    :validators validators)
+  (let [form (fm/use-form :schema schema:team-form
+                          :initial {})
 
         on-submit*
         (mf/use-fn
@@ -240,10 +236,6 @@
 
      [:div {:class (stl/css :paginator)} "1/2"]]))
 
-(s/def ::name ::us/not-empty-string)
-(s/def ::team-form
-  (s/keys :req-un [::name]))
-
 (mf/defc onboarding-team-modal
   {::mf/props :obj}
   []
diff --git a/frontend/src/app/main/ui/settings/access_tokens.cljs b/frontend/src/app/main/ui/settings/access_tokens.cljs
index 663c9a09d..c98ed00e3 100644
--- a/frontend/src/app/main/ui/settings/access_tokens.cljs
+++ b/frontend/src/app/main/ui/settings/access_tokens.cljs
@@ -7,7 +7,7 @@
 (ns app.main.ui.settings.access-tokens
   (:require-macros [app.main.style :as stl])
   (:require
-   [app.common.spec :as us]
+   [app.common.schema :as sm]
    [app.main.data.messages :as msg]
    [app.main.data.modal :as modal]
    [app.main.data.users :as du]
@@ -20,8 +20,6 @@
    [app.util.keyboard :as kbd]
    [app.util.time :as dt]
    [app.util.webapi :as wapi]
-   [cljs.spec.alpha :as s]
-   [cuerdas.core :as str]
    [okulary.core :as l]
    [rumext.v2 :as mf]))
 
@@ -40,17 +38,10 @@
 (def token-created-ref
   (l/derived :access-token-created st/state))
 
-(s/def ::name ::us/not-empty-string)
-(s/def ::expiration-date ::us/not-empty-string)
-(s/def ::access-token-form
-  (s/keys :req-un [::name ::expiration-date]))
-
-(defn- name-validator
-  [errors data]
-  (let [name (:name data)]
-    (cond-> errors
-      (str/blank? name)
-      (assoc :name {:message (tr "dashboard.access-tokens.errors-required-name")}))))
+(def ^:private schema:form
+  [:map {:title "AccessTokenForm"}
+   [:name [::sm/text {:max 250}]]
+   [:expiration-date [::sm/text {:max 250}]]])
 
 (def initial-data
   {:name "" :expiration-date "never"})
@@ -61,10 +52,8 @@
   []
   (let [form    (fm/use-form
                  :initial initial-data
-                 :spec ::access-token-form
-                 :validators [name-validator
-                              (fm/validate-not-empty :name (tr "auth.name.not-all-space"))
-                              (fm/validate-length :name fm/max-length-allowed (tr "auth.name.too-long"))])
+                 :schema schema:form)
+
         created  (mf/deref token-created-ref)
         created? (mf/use-state false)
         locale   (mf/deref i18n/locale)
diff --git a/frontend/src/app/main/ui/settings/change_email.cljs b/frontend/src/app/main/ui/settings/change_email.cljs
index b90a55ee6..91f866e1a 100644
--- a/frontend/src/app/main/ui/settings/change_email.cljs
+++ b/frontend/src/app/main/ui/settings/change_email.cljs
@@ -7,9 +7,7 @@
 (ns app.main.ui.settings.change-email
   (:require-macros [app.main.style :as stl])
   (:require
-   [app.common.data :as d]
-   [app.common.data.macros :as dma]
-   [app.common.spec :as us]
+   [app.common.schema :as sm]
    [app.main.data.messages :as msg]
    [app.main.data.modal :as modal]
    [app.main.data.users :as du]
@@ -20,24 +18,8 @@
    [app.main.ui.notifications.context-notification :refer [context-notification]]
    [app.util.i18n :as i18n :refer [tr]]
    [beicon.v2.core :as rx]
-   [cljs.spec.alpha :as s]
    [rumext.v2 :as mf]))
 
-(s/def ::email-1 ::us/email)
-(s/def ::email-2 ::us/email)
-
-(defn- email-equality
-  [errors data]
-  (let [email-1 (:email-1 data)
-        email-2 (:email-2 data)]
-    (cond-> errors
-      (and email-1 email-2 (not= email-1 email-2))
-      (assoc :email-2 {:message (tr "errors.email-invalid-confirmation")
-                       :code :different-emails}))))
-
-(s/def ::email-change-form
-  (s/keys :req-un [::email-1 ::email-2]))
-
 (defn- on-error
   [form error]
   (case (:code (ex-data error))
@@ -71,30 +53,32 @@
                 :on-success (partial on-success profile)}]
     (st/emit! (du/request-email-change (with-meta params mdata)))))
 
+(def ^:private schema:email-change-form
+  [:and
+   [:map {:title "EmailChangeForm"}
+    [:email-1 ::sm/email]
+    [:email-2 ::sm/email]]
+   [:fn {:error/code "errors.invalid-email-confirmation"
+         :error/field :email-2}
+    (fn [data]
+      (let [email-1 (:email-1 data)
+            email-2 (:email-2 data)]
+        (= email-1 email-2)))]])
+
 (mf/defc change-email-modal
   {::mf/register modal/components
    ::mf/register-as :change-email}
   []
   (let [profile (mf/deref refs/profile)
-        form    (fm/use-form :spec ::email-change-form
-                             :validators [email-equality]
+        form    (fm/use-form :schema schema:email-change-form
                              :initial profile)
         on-close
-        (mf/use-callback #(st/emit! (modal/hide)))
+        (mf/use-fn #(st/emit! (modal/hide)))
 
         on-submit
-        (mf/use-callback
+        (mf/use-fn
          (mf/deps profile)
-         (partial on-submit profile))
-
-        on-email-change
-        (mf/use-callback
-         (fn [_ _]
-           (let [different-emails-error? (= (dma/get-in @form [:errors :email-2 :code]) :different-emails)
-                 email-1                 (dma/get-in @form [:clean-data :email-1])
-                 email-2                 (dma/get-in @form [:clean-data :email-2])]
-             (when (and different-emails-error? (= email-1 email-2))
-               (swap! form d/dissoc-in [:errors :email-2])))))]
+         (partial on-submit profile))]
 
     [:div {:class (stl/css :modal-overlay)}
      [:div {:class (stl/css :modal-container)}
@@ -118,16 +102,14 @@
                        :name :email-1
                        :label (tr "modals.change-email.new-email")
                        :trim true
-                       :show-success? true
-                       :on-change-value on-email-change}]]
+                       :show-success? true}]]
 
         [:div {:class (stl/css :fields-row)}
          [:& fm/input {:type "email"
                        :name :email-2
                        :label (tr "modals.change-email.confirm-email")
                        :trim true
-                       :show-success? true
-                       :on-change-value on-email-change}]]]
+                       :show-success? true}]]]
 
        [:div {:class (stl/css :modal-footer)}
         [:div {:class (stl/css :action-buttons)
diff --git a/frontend/src/app/main/ui/settings/feedback.cljs b/frontend/src/app/main/ui/settings/feedback.cljs
index 4fcc7f790..d8c5c1e3a 100644
--- a/frontend/src/app/main/ui/settings/feedback.cljs
+++ b/frontend/src/app/main/ui/settings/feedback.cljs
@@ -8,7 +8,7 @@
   "Feedback form."
   (:require-macros [app.main.style :as stl])
   (:require
-   [app.common.spec :as us]
+   [app.common.schema :as sm]
    [app.main.data.messages :as msg]
    [app.main.refs :as refs]
    [app.main.repo :as rp]
@@ -17,25 +17,22 @@
    [app.util.dom :as dom]
    [app.util.i18n :as i18n :refer [tr]]
    [beicon.v2.core :as rx]
-   [cljs.spec.alpha :as s]
    [rumext.v2 :as mf]))
 
-(s/def ::content ::us/not-empty-string)
-(s/def ::subject ::us/not-empty-string)
-
-(s/def ::feedback-form
-  (s/keys :req-un [::subject ::content]))
+(def ^:private schema:feedback-form
+  [:map {:title "FeedbackForm"}
+   [:subject [::sm/text {:max 250}]]
+   [:content [::sm/text {:max 5000}]]])
 
 (mf/defc feedback-form
+  {::mf/private true}
   []
   (let [profile (mf/deref refs/profile)
-        form    (fm/use-form :spec ::feedback-form
-                             :validators [(fm/validate-length :subject fm/max-length-allowed (tr "auth.name.too-long"))
-                                          (fm/validate-not-empty :subject (tr "auth.name.not-all-space"))])
+        form    (fm/use-form :schema schema:feedback-form)
         loading (mf/use-state false)
 
         on-succes
-        (mf/use-callback
+        (mf/use-fn
          (mf/deps profile)
          (fn [_]
            (reset! loading false)
@@ -43,7 +40,7 @@
            (swap! form assoc :data {} :touched {} :errors {})))
 
         on-error
-        (mf/use-callback
+        (mf/use-fn
          (mf/deps profile)
          (fn [{:keys [code] :as error}]
            (reset! loading false)
@@ -52,7 +49,7 @@
              (st/emit! (msg/error (tr "errors.generic"))))))
 
         on-submit
-        (mf/use-callback
+        (mf/use-fn
          (mf/deps profile)
          (fn [form _]
            (reset! loading true)
@@ -106,8 +103,8 @@
 
 (mf/defc feedback-page
   []
-  (mf/use-effect
-   #(dom/set-html-title (tr "title.settings.feedback")))
+  (mf/with-effect []
+    (dom/set-html-title (tr "title.settings.feedback")))
 
   [:div {:class (stl/css :dashboard-settings)}
    [:div {:class (stl/css :form-container)}
diff --git a/frontend/src/app/main/ui/settings/options.cljs b/frontend/src/app/main/ui/settings/options.cljs
index ef83e220e..36f0fe778 100644
--- a/frontend/src/app/main/ui/settings/options.cljs
+++ b/frontend/src/app/main/ui/settings/options.cljs
@@ -7,7 +7,6 @@
 (ns app.main.ui.settings.options
   (:require-macros [app.main.style :as stl])
   (:require
-   [app.common.spec :as us]
    [app.main.data.messages :as msg]
    [app.main.data.users :as du]
    [app.main.refs :as refs]
@@ -15,14 +14,12 @@
    [app.main.ui.components.forms :as fm]
    [app.util.dom :as dom]
    [app.util.i18n :as i18n :refer [tr]]
-   [cljs.spec.alpha :as s]
    [rumext.v2 :as mf]))
 
-(s/def ::lang (s/nilable ::us/string))
-(s/def ::theme (s/nilable ::us/not-empty-string))
-
-(s/def ::options-form
-  (s/keys :opt-un [::lang ::theme]))
+(def ^:private schema:options-form
+  [:map {:title "OptionsForm"}
+   [:lang {:optional true} [:string {:max 20}]]
+   [:theme {:optional true} [:string {:max 250}]]])
 
 (defn- on-success
   [profile]
@@ -41,7 +38,7 @@
   (let [profile (mf/deref refs/profile)
         initial (mf/with-memo [profile]
                   (update profile :lang #(or % "")))
-        form    (fm/use-form :spec ::options-form
+        form    (fm/use-form :schema schema:options-form
                              :initial initial)]
 
     [:& fm/form {:class (stl/css :options-form)
diff --git a/frontend/src/app/main/ui/settings/password.cljs b/frontend/src/app/main/ui/settings/password.cljs
index 3f3a4ca72..ac3373c45 100644
--- a/frontend/src/app/main/ui/settings/password.cljs
+++ b/frontend/src/app/main/ui/settings/password.cljs
@@ -7,14 +7,13 @@
 (ns app.main.ui.settings.password
   (:require-macros [app.main.style :as stl])
   (:require
-   [app.common.spec :as us]
+   [app.common.schema :as sm]
    [app.main.data.messages :as msg]
    [app.main.data.users :as udu]
    [app.main.store :as st]
    [app.main.ui.components.forms :as fm]
    [app.util.dom :as dom]
    [app.util.i18n :as i18n :refer [tr]]
-   [cljs.spec.alpha :as s]
    [rumext.v2 :as mf]))
 
 (defn- on-error
@@ -22,10 +21,10 @@
   (case (:code (ex-data error))
     :old-password-not-match
     (swap! form assoc-in [:errors :password-old]
-           {:message (tr "errors.wrong-old-password")})
+           {:code "errors.wrong-old-password"})
     :email-as-password
     (swap! form assoc-in [:errors :password-1]
-           {:message (tr "errors.email-as-password")})
+           {:code "errors.email-as-password"})
 
     (let [msg (tr "generic.error")]
       (st/emit! (msg/error msg)))))
@@ -47,40 +46,29 @@
                   :on-error (partial on-error form)})]
     (st/emit! (udu/update-password params))))
 
-(s/def ::password-1 ::us/not-empty-string)
-(s/def ::password-2 ::us/not-empty-string)
-(s/def ::password-old (s/nilable ::us/string))
-
-(defn- password-equality
-  [errors data]
-  (let [password-1 (:password-1 data)
-        password-2 (:password-2 data)]
-
-    (cond-> errors
-      (and password-1 password-2 (not= password-1 password-2))
-      (assoc :password-2 {:message (tr "errors.password-invalid-confirmation")})
-
-      (and password-1 (> 8 (count password-1)))
-      (assoc :password-1 {:message (tr "errors.password-too-short")}))))
-
-(s/def ::password-form
-  (s/keys :req-un [::password-1
-                   ::password-2
-                   ::password-old]))
+(def ^:private schema:password-form
+  [:and
+   [:map {:title "PasswordForm"}
+    [:password-1 ::sm/password]
+    [:password-2 ::sm/password]
+    [:password-old ::sm/password]]
+   [:fn {:error/code "errors.password-invalid-confirmation"
+         :error/field :password-2}
+    (fn [{:keys [password-1 password-2]}]
+      (= password-1 password-2))]])
 
 (mf/defc password-form
   []
-  (let [initial (mf/use-memo (constantly {:password-old nil}))
-        form (fm/use-form :spec ::password-form
-                          :validators [(fm/validate-not-all-spaces :password-old (tr "auth.password-not-empty"))
-                                       (fm/validate-not-empty :password-1 (tr "auth.password-not-empty"))
-                                       (fm/validate-not-empty :password-2 (tr "auth.password-not-empty"))
-                                       password-equality]
-                          :initial initial)]
+  (let [initial (mf/with-memo []
+                  {:password-old ""
+                   :password-1 ""
+                   :password-2 ""})
+        form    (fm/use-form :schema schema:password-form
+                             :initial initial)]
+
     [:& fm/form {:class (stl/css :password-form)
                  :on-submit on-submit
                  :form form}
-
      [:div {:class (stl/css :fields-row)}
       [:& fm/input
        {:type "password"
diff --git a/frontend/src/app/main/ui/settings/profile.cljs b/frontend/src/app/main/ui/settings/profile.cljs
index abc96cf37..370938539 100644
--- a/frontend/src/app/main/ui/settings/profile.cljs
+++ b/frontend/src/app/main/ui/settings/profile.cljs
@@ -7,7 +7,7 @@
 (ns app.main.ui.settings.profile
   (:require-macros [app.main.style :as stl])
   (:require
-   [app.common.spec :as us]
+   [app.common.schema :as sm]
    [app.config :as cf]
    [app.main.data.messages :as msg]
    [app.main.data.modal :as modal]
@@ -18,14 +18,12 @@
    [app.main.ui.components.forms :as fm]
    [app.util.dom :as dom]
    [app.util.i18n :as i18n :refer [tr]]
-   [cljs.spec.alpha :as s]
    [rumext.v2 :as mf]))
 
-(s/def ::fullname ::us/not-empty-string)
-(s/def ::email ::us/email)
-
-(s/def ::profile-form
-  (s/keys :req-un [::fullname ::email]))
+(def ^:private schema:profile-form
+  [:map {:title "ProfileForm"}
+   [:fullname [::sm/text {:max 250}]]
+   [:email ::sm/email]])
 
 (defn- on-submit
   [form _event]
@@ -37,19 +35,18 @@
 ;; --- Profile Form
 
 (mf/defc profile-form
+  {::mf/private true}
   []
   (let [profile (mf/deref refs/profile)
-        form    (fm/use-form :spec ::profile-form
-                             :initial profile
-                             :validators [(fm/validate-length :fullname fm/max-length-allowed (tr "auth.name.too-long"))
-                                          (fm/validate-not-empty :fullname (tr "auth.name.not-all-space"))])
+        form    (fm/use-form :schema schema:profile-form
+                             :initial profile)
 
-        handle-show-change-email
-        (mf/use-callback
+        on-show-change-email
+        (mf/use-fn
          #(modal/show! :change-email {}))
 
-        handle-show-delete-account
-        (mf/use-callback
+        on-show-delete-account
+        (mf/use-fn
          #(modal/show! :delete-account {}))]
 
     [:& fm/form {:on-submit on-submit
@@ -62,7 +59,7 @@
         :label (tr "dashboard.your-name")}]]
 
      [:div {:class (stl/css :fields-row)
-            :on-click handle-show-change-email}
+            :on-click on-show-change-email}
       [:& fm/input
        {:type "email"
         :name :email
@@ -71,7 +68,7 @@
 
       [:div {:class (stl/css :options)}
        [:div.change-email
-        [:a {:on-click handle-show-change-email}
+        [:a {:on-click on-show-change-email}
          (tr "dashboard.change-email")]]]]
 
      [:> fm/submit-button*
@@ -81,17 +78,25 @@
 
      [:div {:class (stl/css :links)}
       [:div {:class (stl/css :link-item)}
-       [:a {:on-click handle-show-delete-account
+       [:a {:on-click on-show-delete-account
             :data-testid "remove-acount-btn"}
         (tr "dashboard.remove-account")]]]]))
 
 ;; --- Profile Photo Form
 
-(mf/defc profile-photo-form []
-  (let [file-input     (mf/use-ref nil)
-        profile        (mf/deref refs/profile)
-        photo          (cf/resolve-profile-photo-url profile)
-        on-image-click #(dom/click (mf/ref-val file-input))
+(mf/defc profile-photo-form
+  {::mf/private true}
+  []
+  (let [input-ref  (mf/use-ref nil)
+        profile    (mf/deref refs/profile)
+
+        photo
+        (mf/with-memo [profile]
+          (cf/resolve-profile-photo-url profile))
+
+        on-image-click
+        (mf/use-fn
+         #(dom/click (mf/ref-val input-ref)))
 
         on-file-selected
         (fn [file]
@@ -104,15 +109,17 @@
       [:img {:src photo}]
       [:& file-uploader {:accept "image/jpeg,image/png"
                          :multi false
-                         :ref file-input
+                         :ref input-ref
                          :on-selected on-file-selected
                          :data-testid "profile-image-input"}]]]))
 
 ;; --- Profile Page
 
-(mf/defc profile-page []
+(mf/defc profile-page
+  []
   (mf/with-effect []
     (dom/set-html-title (tr "title.settings.profile")))
+
   [:div {:class (stl/css :dashboard-settings)}
    [:div {:class (stl/css :form-container)}
     [:h2 (tr "labels.profile")]
diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets/groups.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets/groups.cljs
index dc882483e..82d97180d 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/assets/groups.cljs
+++ b/frontend/src/app/main/ui/workspace/sidebar/assets/groups.cljs
@@ -8,7 +8,7 @@
   (:require-macros [app.main.style :as stl])
   (:require
    [app.common.files.helpers :as cfh]
-   [app.common.spec :as us]
+   [app.common.schema :as sm]
    [app.main.data.modal :as modal]
    [app.main.data.workspace :as dw]
    [app.main.store :as st]
@@ -18,7 +18,6 @@
    [app.main.ui.workspace.sidebar.assets.common :as cmm]
    [app.util.dom :as dom]
    [app.util.i18n :as i18n :refer [tr]]
-   [cljs.spec.alpha :as s]
    [rumext.v2 :as mf]))
 
 (mf/defc asset-group-title
@@ -92,21 +91,18 @@
                                (compare key1 key2))))
             assets)))
 
-(s/def ::asset-name ::us/not-empty-string)
-(s/def ::name-group-form
-  (s/keys :req-un [::asset-name]))
+(def ^:private schema:group-form
+  [:map {:title "GroupForm"}
+   [:name [::sm/text {:max 250}]]])
 
 (mf/defc name-group-dialog
   {::mf/register modal/components
    ::mf/register-as :name-group-dialog}
   [{:keys [path last-path accept] :as ctx
     :or {path "" last-path ""}}]
-  (let [initial  (mf/use-memo
-                  (mf/deps last-path)
-                  (constantly {:asset-name last-path}))
-        form     (fm/use-form :spec ::name-group-form
-                              :validators [(fm/validate-not-empty :asset-name (tr "auth.name.not-all-space"))
-                                           (fm/validate-length :asset-name fm/max-length-allowed (tr "auth.name.too-long"))]
+  (let [initial  (mf/with-memo [last-path]
+                   {:asset-name last-path})
+        form     (fm/use-form :schema schema:group-form
                               :initial initial)
 
         create?  (empty? path)
@@ -117,7 +113,7 @@
         (mf/use-fn
          (mf/deps form)
          (fn [_]
-           (let [asset-name (get-in @form [:clean-data :asset-name])]
+           (let [asset-name (get-in @form [:clean-data :name])]
              (if create?
                (accept asset-name)
                (accept path asset-name))
@@ -135,7 +131,7 @@
 
       [:div {:class (stl/css :modal-content)}
        [:& fm/form {:form form :on-submit on-accept}
-        [:& fm/input {:name :asset-name
+        [:& fm/input {:name :name
                       :class (stl/css :input-wrapper)
                       :auto-focus? true
                       :label (tr "workspace.assets.group-name")
diff --git a/frontend/src/app/util/forms.cljs b/frontend/src/app/util/forms.cljs
index 8cbb792b2..d1e1c9b48 100644
--- a/frontend/src/app/util/forms.cljs
+++ b/frontend/src/app/util/forms.cljs
@@ -8,119 +8,146 @@
   (:refer-clojure :exclude [uuid])
   (:require
    [app.common.data :as d]
-   [app.common.spec :as us]
+   [app.common.data.macros :as dm]
+   [app.common.schema :as sm]
    [app.util.i18n :refer [tr]]
-   [cljs.spec.alpha :as s]
    [cuerdas.core :as str]
+   [malli.core :as m]
    [rumext.v2 :as mf]))
 
 ;; --- Handlers Helpers
 
-(defn- interpret-problem
-  [acc {:keys [path pred via] :as problem}]
-  (cond
-    (and (empty? path)
-         (list? pred)
-         (= (first (last pred)) 'cljs.core/contains?))
-    (let [field (last (last pred))
-          path  (conj path field)
-          root  (first via)]
-      (assoc-in acc path {:code :missing :type :builtin :root root :field field}))
+(defn- interpret-schema-problem
+  [acc {:keys [schema in value] :as problem}]
+  (let [props (merge (m/type-properties schema)
+                     (m/properties schema))
+        field (or (first in) (:error/field props))]
 
-    (and (seq path) (seq via))
-    (let [field (first path)
-          code  (last via)
-          root  (first via)]
-      (assoc-in acc path {:code code :type :builtin :root root :field field}))
+    (if (contains? acc field)
+      acc
+      (cond
+        (nil? value)
+        (assoc acc field {:code "errors.field-missing"})
 
-    :else acc))
+        (contains? props :error/code)
+        (assoc acc field {:code (:error/code props)})
 
-(declare create-form-mutator)
+        (contains? props :error/message)
+        (assoc acc field {:code (:error/message props)})
 
-(defn use-form
-  [& {:keys [initial] :as opts}]
-  (let [state      (mf/useState 0)
-        render     (aget state 1)
+        (contains? props :error/fn)
+        (let [v-fn (:error/fn props)
+              code (v-fn problem)]
+          (assoc acc field {:code code}))
 
-        get-state  (mf/use-callback
-                    (mf/deps initial)
-                    (fn []
-                      {:data (if (fn? initial) (initial) initial)
-                       :errors {}
-                       :touched {}}))
+        (contains? props :error/validators)
+        (let [validators (:error/validators props)
+              props      (reduce #(%2 %1 value) props validators)]
+          (assoc acc field {:code (d/nilv (:error/code props) "errors.invalid-data")}))
 
-        state-ref  (mf/use-ref (get-state))
-        form       (mf/use-memo (mf/deps initial) #(create-form-mutator state-ref render get-state opts))]
+        :else
+        (assoc acc field {:code "errors.invalid-data"})))))
 
-    (mf/use-effect
-     (mf/deps initial)
+(defn- use-rerender-fn
+  []
+  (let [state     (mf/useState 0)
+        render-fn (aget state 1)]
+    (mf/use-fn
+     (mf/deps render-fn)
      (fn []
-       (if (fn? initial)
-         (swap! form update :data merge (initial))
-         (swap! form update :data merge initial))))
+       (render-fn inc)))))
 
-    form))
+(defn- apply-validators
+  [validators state errors]
+  (reduce (fn [errors validator-fn]
+            (merge errors (validator-fn errors (:data state))))
+          errors
+          validators))
 
-(defn- wrap-update-fn
-  [f {:keys [spec validators]}]
+(defn- collect-schema-errors
+  [schema validators state]
+  (let [explain (sm/explain schema (:data state))
+        errors  (->> (reduce interpret-schema-problem {} (:errors explain))
+                     (apply-validators validators state))]
+
+    (-> (:errors state)
+        (merge errors)
+        (d/without-nils)
+        (not-empty))))
+
+(defn- wrap-update-schema-fn
+  [f {:keys [schema validators]}]
   (fn [& args]
-    (let [state    (apply f args)
-          cleaned  (s/conform spec (:data state))
-          problems (when (= ::s/invalid cleaned)
-                     (::s/problems (s/explain-data spec (:data state))))
-
-          errors   (reduce interpret-problem {} problems)
-
-
-          errors   (reduce (fn [errors vf]
-                             (merge errors (vf errors (:data state))))
-                           errors
-                           validators)
-          errors   (merge (:errors state) errors)
-          errors   (d/without-nils errors)]
-
+    (let [state   (apply f args)
+          cleaned (sm/decode schema (:data state))
+          valid?  (sm/validate schema cleaned)
+          errors  (when-not valid?
+                    (collect-schema-errors schema validators state))]
 
       (assoc state
              :errors errors
-             :clean-data (when (not= cleaned ::s/invalid) cleaned)
-             :valid (and (empty? errors)
-                         (not= cleaned ::s/invalid))))))
+             :clean-data (when valid? cleaned)
+             :valid (and (not errors) valid?)))))
 
 (defn- create-form-mutator
-  [state-ref render get-state opts]
+  [internal-state rerender-fn wrap-update-fn initial opts]
   (reify
     IDeref
     (-deref [_]
-      (mf/ref-val state-ref))
+      (mf/ref-val internal-state))
 
     IReset
     (-reset! [_ new-value]
       (if (nil? new-value)
-        (mf/set-ref-val! state-ref (get-state))
-        (mf/set-ref-val! state-ref new-value))
-      (render inc))
+        (mf/set-ref-val! internal-state (if (fn? initial) (initial) initial))
+        (mf/set-ref-val! internal-state new-value))
+      (rerender-fn))
 
     ISwap
     (-swap! [_ f]
       (let [f (wrap-update-fn f opts)]
-        (mf/set-ref-val! state-ref (f (mf/ref-val state-ref)))
-        (render inc)))
-
+        (mf/set-ref-val! internal-state (f (mf/ref-val internal-state)))
+        (rerender-fn)))
 
     (-swap! [_ f x]
       (let [f (wrap-update-fn f opts)]
-        (mf/set-ref-val! state-ref (f (mf/ref-val state-ref) x))
-        (render inc)))
+        (mf/set-ref-val! internal-state (f (mf/ref-val internal-state) x))
+        (rerender-fn)))
 
     (-swap! [_ f x y]
       (let [f (wrap-update-fn f opts)]
-        (mf/set-ref-val! state-ref (f (mf/ref-val state-ref) x y))
-        (render inc)))
+        (mf/set-ref-val! internal-state (f (mf/ref-val internal-state) x y))
+        (rerender-fn)))
 
     (-swap! [_ f x y more]
       (let [f (wrap-update-fn f opts)]
-        (mf/set-ref-val! state-ref (apply f (mf/ref-val state-ref) x y more))
-        (render inc)))))
+        (mf/set-ref-val! internal-state (apply f (mf/ref-val internal-state) x y more))
+        (rerender-fn)))))
+
+(defn use-form
+  [& {:keys [initial] :as opts}]
+  (let [rerender-fn (use-rerender-fn)
+
+        internal-state
+        (mf/use-ref nil)
+
+        form-mutator
+        (mf/with-memo [initial]
+          (create-form-mutator internal-state rerender-fn wrap-update-schema-fn initial opts))]
+
+    ;; Initialize internal state once
+    (mf/with-effect []
+      (mf/set-ref-val! internal-state
+                       {:data {}
+                        :errors {}
+                        :touched {}}))
+
+    (mf/with-effect [initial]
+      (if (fn? initial)
+        (swap! form-mutator update :data merge (initial))
+        (swap! form-mutator update :data merge initial)))
+
+    form-mutator))
 
 (defn on-input-change
   ([form field value]
@@ -150,8 +177,8 @@
 (mf/defc field-error
   [{:keys [form field type]
     :as props}]
-  (let [{:keys [message] :as error} (get-in form [:errors field])
-        touched? (get-in form [:touched field])
+  (let [{:keys [message] :as error} (dm/get-in form [:errors field])
+        touched? (dm/get-in form [:touched field])
         show? (and touched? error message
                    (cond
                      (nil? type) true
@@ -164,12 +191,6 @@
 
 (defn error-class
   [form field]
-  (when (and (get-in form [:errors field])
-             (get-in form [:touched field]))
+  (when (and (dm/get-in form [:errors field])
+             (dm/get-in form [:touched field]))
     "invalid"))
-
-;; --- Form Specs and Conformers
-
-(s/def ::email ::us/email)
-(s/def ::not-empty-string ::us/not-empty-string)
-(s/def ::color ::us/rgb-color-str)
diff --git a/frontend/translations/af.po b/frontend/translations/af.po
index 5f940756e..2ade2f3ed 100644
--- a/frontend/translations/af.po
+++ b/frontend/translations/af.po
@@ -76,20 +76,12 @@ msgstr "LDAP"
 msgid "auth.login-with-oidc-submit"
 msgstr "OpenID"
 
-#: src/app/main/ui/settings/team-form.cljs, src/app/main/ui/auth/register.cljs, src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/onboarding/team_choice.cljs, src/app/main/ui/settings/access_tokens.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/settings/profile.cljs, src/app/main/ui/workspace/sidebar/assets.cljs
-msgid "auth.name.not-all-space"
-msgstr "Die naam moet 'n ander karakter as spasie bevat."
-
-#: src/app/main/ui/auth/register.cljs, src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/onboarding/team_choice.cljs, src/app/main/ui/settings/access_tokens.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/settings/profile.cljs, src/app/main/ui/workspace/sidebar/assets.cljs
-msgid "auth.name.too-long"
-msgstr "Die naam moet hoogstens 250 karakters bevat."
-
 #: src/app/main/ui/auth/recovery.cljs
 msgid "auth.new-password"
 msgstr "Tik 'n nuwe wagwoord in"
 
 #: src/app/main/ui/auth/recovery.cljs
-msgid "auth.notifications.invalid-token-error"
+msgid "errors.invalid-recovery-token"
 msgstr "Die hersteltoken is ongeldig."
 
 #: src/app/main/ui/auth/recovery.cljs
@@ -118,10 +110,6 @@ msgstr "Wagwoord"
 msgid "auth.password-length-hint"
 msgstr "Ten minste 8 karakters"
 
-#: src/app/main/ui/auth/register.cljs
-msgid "auth.password-not-empty"
-msgstr "Wagwoord moet 'n ander karakter as spasie bevat."
-
 msgid "auth.privacy-policy"
 msgstr "Privaatheidsbeleid"
 
@@ -275,7 +263,7 @@ msgid "dashboard.access-tokens.create"
 msgstr "Genereer nuwe token"
 
 #: src/app/main/ui/settings/access-tokens.cljs
-msgid "dashboard.access-tokens.create.success"
+msgid "dashboard.access-tokens.create-success"
 msgstr "Toegangstoken is suksesvol geskep."
 
 #: src/app/main/ui/settings/access-tokens.cljs
@@ -286,10 +274,6 @@ msgstr "Druk die knoppie \"Genereer nuwe token\" om een te genereer."
 msgid "dashboard.access-tokens.empty.no-access-tokens"
 msgstr "Jy het tot dusver geen tokens nie."
 
-#: src/app/main/ui/settings/access-tokens.cljs
-msgid "dashboard.access-tokens.errors-required-name"
-msgstr "Die naam word vereis"
-
 #: src/app/main/ui/settings/access-tokens.cljs
 msgid "dashboard.access-tokens.expiration-180-days"
 msgstr "180 dae"
diff --git a/frontend/translations/ar.po b/frontend/translations/ar.po
index 8c1d9e966..a47dac159 100644
--- a/frontend/translations/ar.po
+++ b/frontend/translations/ar.po
@@ -78,7 +78,7 @@ msgid "auth.new-password"
 msgstr "اكتب كلمة مرور جديدة"
 
 #: src/app/main/ui/auth/recovery.cljs
-msgid "auth.notifications.invalid-token-error"
+msgid "errors.invalid-recovery-token"
 msgstr "رمز الاسترداد غير صالح."
 
 #: src/app/main/ui/auth/recovery.cljs
@@ -743,11 +743,11 @@ msgstr "يحتوي البريد الإلكتروني «%s» على العديد
 
 #: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs,
 #: src/app/main/ui/auth/recovery_request.cljs
-msgid "errors.email-invalid"
+msgid "errors.invalid-email"
 msgstr "أدخل بريدًا إلكترونيًا صالحًا من فضلك"
 
 #: src/app/main/ui/settings/change_email.cljs
-msgid "errors.email-invalid-confirmation"
+msgid "errors.invalid-email-confirmation"
 msgstr "يجب أن يتطابق البريد الإلكتروني للتأكيد"
 
 msgid "errors.email-spam-or-permanent-bounces"
@@ -2292,10 +2292,6 @@ msgstr "زيادة عدسة التكبير"
 msgid "shortcuts.zoom-selected"
 msgstr "كبر المحدد"
 
-#: src/app/main/ui/dashboard/team.cljs
-msgid "team.webhooks.max-length"
-msgstr "يجب الا يزيد اسم الويبهوك على 2048 حرفا"
-
 #: src/app/main/ui/dashboard/files.cljs
 msgid "title.dashboard.files"
 msgstr "%s - Penpotعنوان ملفات لوحة القيادة"
@@ -4307,14 +4303,6 @@ msgstr "الرمز منسوخ"
 msgid "auth.login-tagline"
 msgstr "Penpot هو أداة تصميم مجانية ومفتوحة المصدر للتعاون بين التصميم والبرمجة"
 
-#: src/app/main/ui/settings/team-form.cljs, src/app/main/ui/auth/register.cljs, src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/onboarding/team_choice.cljs, src/app/main/ui/settings/access_tokens.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/settings/profile.cljs, src/app/main/ui/workspace/sidebar/assets.cljs
-msgid "auth.name.not-all-space"
-msgstr "يجب أن يحتوي الاسم على بعض الأحرف غير الفراغات."
-
-#: src/app/main/ui/auth/register.cljs, src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/onboarding/team_choice.cljs, src/app/main/ui/settings/access_tokens.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/settings/profile.cljs, src/app/main/ui/workspace/sidebar/assets.cljs
-msgid "auth.name.too-long"
-msgstr "يجب أن يحتوي الاسم على 250 حرفًا كحد أقصى."
-
 #: src/app/main/ui/settings/access-tokens.cljs
 msgid "dashboard.access-tokens.empty.add-one"
 msgstr "اضغط على الزر \"إنشاء رمز جديد\" لإنشاء واحد."
@@ -4332,5 +4320,5 @@ msgid "dashboard.access-tokens.create"
 msgstr "قم بإنشاء رمز جديد"
 
 #: src/app/main/ui/settings/access-tokens.cljs
-msgid "dashboard.access-tokens.create.success"
+msgid "dashboard.access-tokens.create-success"
 msgstr "تم إنشاء رمز الوصول بنجاح."
diff --git a/frontend/translations/bn.po b/frontend/translations/bn.po
index e6303cdcc..478d20c96 100644
--- a/frontend/translations/bn.po
+++ b/frontend/translations/bn.po
@@ -77,5 +77,5 @@ msgid "auth.new-password"
 msgstr "নতুন পাসওয়ার্ড টাইপ করুন"
 
 #: src/app/main/ui/auth/recovery.cljs
-msgid "auth.notifications.invalid-token-error"
+msgid "errors.invalid-recovery-token"
 msgstr "রিকভারি টোকেন সঠিক নয়।"
diff --git a/frontend/translations/ca.po b/frontend/translations/ca.po
index b803c7206..62017b4ca 100644
--- a/frontend/translations/ca.po
+++ b/frontend/translations/ca.po
@@ -81,7 +81,7 @@ msgid "auth.new-password"
 msgstr "Escriviu una contrasenya nova"
 
 #: src/app/main/ui/auth/recovery.cljs
-msgid "auth.notifications.invalid-token-error"
+msgid "errors.invalid-recovery-token"
 msgstr "El codi de recuperació no és vàlid."
 
 #: src/app/main/ui/auth/recovery.cljs
@@ -722,7 +722,7 @@ msgid "errors.email-has-permanent-bounces"
 msgstr "El correu «%s» té molts informes de retorn permanents."
 
 #: src/app/main/ui/settings/change_email.cljs
-msgid "errors.email-invalid-confirmation"
+msgid "errors.invalid-email-confirmation"
 msgstr "El correu de confirmació ha de coincidir"
 
 msgid "errors.email-spam-or-permanent-bounces"
diff --git a/frontend/translations/cs.po b/frontend/translations/cs.po
index 862545f4c..1140f40b0 100644
--- a/frontend/translations/cs.po
+++ b/frontend/translations/cs.po
@@ -76,20 +76,12 @@ msgstr "LDAP"
 msgid "auth.login-with-oidc-submit"
 msgstr "OpenID"
 
-#: src/app/main/ui/settings/team-form.cljs, src/app/main/ui/auth/register.cljs, src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/onboarding/team_choice.cljs, src/app/main/ui/settings/access_tokens.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/settings/profile.cljs, src/app/main/ui/workspace/sidebar/assets.cljs
-msgid "auth.name.not-all-space"
-msgstr "Název musí obsahovat jiný znak než mezeru."
-
-#: src/app/main/ui/auth/register.cljs, src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/onboarding/team_choice.cljs, src/app/main/ui/settings/access_tokens.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/settings/profile.cljs, src/app/main/ui/workspace/sidebar/assets.cljs
-msgid "auth.name.too-long"
-msgstr "Název musí obsahovat maximálně 250 znaků."
-
 #: src/app/main/ui/auth/recovery.cljs
 msgid "auth.new-password"
 msgstr "Zadejte nové heslo"
 
 #: src/app/main/ui/auth/recovery.cljs
-msgid "auth.notifications.invalid-token-error"
+msgid "errors.invalid-recovery-token"
 msgstr "Token pro obnovení je neplatný."
 
 #: src/app/main/ui/auth/recovery.cljs
@@ -116,10 +108,6 @@ msgstr "Heslo"
 msgid "auth.password-length-hint"
 msgstr "Minimálně 8 znaků"
 
-#: src/app/main/ui/auth/register.cljs
-msgid "auth.password-not-empty"
-msgstr "Heslo musí obsahovat jiný znak než mezeru."
-
 msgid "auth.privacy-policy"
 msgstr "Zásady ochrany osobních údajů"
 
@@ -278,7 +266,7 @@ msgid "dashboard.access-tokens.create"
 msgstr "Generovat nový token"
 
 #: src/app/main/ui/settings/access-tokens.cljs
-msgid "dashboard.access-tokens.create.success"
+msgid "dashboard.access-tokens.create-success"
 msgstr "Přístupový token byl úspěšně vytvořen."
 
 #: src/app/main/ui/settings/access-tokens.cljs
@@ -291,10 +279,6 @@ msgstr ""
 msgid "dashboard.access-tokens.empty.no-access-tokens"
 msgstr "Zatím nemáte žádné tokeny."
 
-#: src/app/main/ui/settings/access-tokens.cljs
-msgid "dashboard.access-tokens.errors-required-name"
-msgstr "Jméno je povinné"
-
 #: src/app/main/ui/settings/access-tokens.cljs
 msgid "dashboard.access-tokens.expiration-180-days"
 msgstr "180 dní"
@@ -865,11 +849,11 @@ msgstr "E-mail «%s» má mnoho trvalých zpráv o nedoručitelnosti."
 
 #: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs,
 #: src/app/main/ui/auth/recovery_request.cljs
-msgid "errors.email-invalid"
+msgid "errors.invalid-email"
 msgstr "Zadejte prosím platný email"
 
 #: src/app/main/ui/settings/change_email.cljs
-msgid "errors.email-invalid-confirmation"
+msgid "errors.invalid-email-confirmation"
 msgstr "Potvrzovací e-mail se musí shodovat"
 
 msgid "errors.email-spam-or-permanent-bounces"
@@ -2974,10 +2958,6 @@ msgstr "Zvětšení zoomu"
 msgid "shortcuts.zoom-selected"
 msgstr "Přiblížit na vybrané"
 
-#: src/app/main/ui/dashboard/team.cljs
-msgid "team.webhooks.max-length"
-msgstr "Název webhooku musí obsahovat maximálně 2048 znaků."
-
 #: src/app/main/ui/dashboard/files.cljs
 msgid "title.dashboard.files"
 msgstr "%s - Penpot"
diff --git a/frontend/translations/da.po b/frontend/translations/da.po
index d90f002d5..7c176ff70 100644
--- a/frontend/translations/da.po
+++ b/frontend/translations/da.po
@@ -81,7 +81,7 @@ msgid "auth.new-password"
 msgstr "Indtast et nyt kodeord"
 
 #: src/app/main/ui/auth/recovery.cljs
-msgid "auth.notifications.invalid-token-error"
+msgid "errors.invalid-recovery-token"
 msgstr "Genopretningspoletten er ugyldig."
 
 #: src/app/main/ui/auth/recovery.cljs
diff --git a/frontend/translations/de.po b/frontend/translations/de.po
index 48214d1fa..207abcb76 100644
--- a/frontend/translations/de.po
+++ b/frontend/translations/de.po
@@ -86,32 +86,12 @@ msgstr "LDAP"
 msgid "auth.login-with-oidc-submit"
 msgstr "OpenID"
 
-#: src/app/main/ui/settings/team-form.cljs, src/app/main/ui/auth/register.cljs,
-#: src/app/main/ui/dashboard/team_form.cljs,
-#: src/app/main/ui/onboarding/team_choice.cljs,
-#: src/app/main/ui/settings/access_tokens.cljs,
-#: src/app/main/ui/settings/feedback.cljs,
-#: src/app/main/ui/settings/profile.cljs,
-#: src/app/main/ui/workspace/sidebar/assets.cljs
-msgid "auth.name.not-all-space"
-msgstr "Der Name darf keine Leerzeichen enthalten."
-
-#: src/app/main/ui/auth/register.cljs,
-#: src/app/main/ui/dashboard/team_form.cljs,
-#: src/app/main/ui/onboarding/team_choice.cljs,
-#: src/app/main/ui/settings/access_tokens.cljs,
-#: src/app/main/ui/settings/feedback.cljs,
-#: src/app/main/ui/settings/profile.cljs,
-#: src/app/main/ui/workspace/sidebar/assets.cljs
-msgid "auth.name.too-long"
-msgstr "Der Name darf höchstens 250 Zeichen lang sein."
-
 #: src/app/main/ui/auth/recovery.cljs
 msgid "auth.new-password"
 msgstr "Geben Sie ein neues Passwort ein"
 
 #: src/app/main/ui/auth/recovery.cljs
-msgid "auth.notifications.invalid-token-error"
+msgid "errors.invalid-recovery-token"
 msgstr "Der Wiederherstellungscode ist ungültig."
 
 #: src/app/main/ui/auth/recovery.cljs
@@ -141,10 +121,6 @@ msgstr "Passwort"
 msgid "auth.password-length-hint"
 msgstr "Mindestens 8 Zeichen"
 
-#: src/app/main/ui/auth/register.cljs
-msgid "auth.password-not-empty"
-msgstr "Das Passwort darf keine Leerzeichen enthalten."
-
 msgid "auth.privacy-policy"
 msgstr "Datenschutzerklärung"
 
@@ -305,7 +281,7 @@ msgid "dashboard.access-tokens.create"
 msgstr "Neues Token generieren"
 
 #: src/app/main/ui/settings/access-tokens.cljs
-msgid "dashboard.access-tokens.create.success"
+msgid "dashboard.access-tokens.create-success"
 msgstr "Der Zugangstoken wurde erfolgreich erstellt."
 
 #: src/app/main/ui/settings/access-tokens.cljs
@@ -318,10 +294,6 @@ msgstr ""
 msgid "dashboard.access-tokens.empty.no-access-tokens"
 msgstr "Du hast bisher keine Token."
 
-#: src/app/main/ui/settings/access-tokens.cljs
-msgid "dashboard.access-tokens.errors-required-name"
-msgstr "Der Name ist erforderlich"
-
 #: src/app/main/ui/settings/access-tokens.cljs
 msgid "dashboard.access-tokens.expiration-180-days"
 msgstr "180 Tage"
@@ -921,11 +893,11 @@ msgstr ""
 
 #: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs,
 #: src/app/main/ui/auth/recovery_request.cljs
-msgid "errors.email-invalid"
+msgid "errors.invalid-email"
 msgstr "Geben Sie bitte eine gültige E-Mail-Adresse ein"
 
 #: src/app/main/ui/settings/change_email.cljs
-msgid "errors.email-invalid-confirmation"
+msgid "errors.invalid-email-confirmation"
 msgstr "Bestätigungs-E-Mail muss übereinstimmen"
 
 msgid "errors.email-spam-or-permanent-bounces"
@@ -3099,10 +3071,6 @@ msgstr "Ansicht mit Zoomwerkzeug vergrößern"
 msgid "shortcuts.zoom-selected"
 msgstr "Zur Auswahl zoomen"
 
-#: src/app/main/ui/dashboard/team.cljs
-msgid "team.webhooks.max-length"
-msgstr "Der Name des Webhooks darf höchstens 2048 Zeichen lang sein."
-
 #: src/app/main/ui/dashboard/files.cljs
 msgid "title.dashboard.files"
 msgstr "%s - Penpot"
diff --git a/frontend/translations/el.po b/frontend/translations/el.po
index 4f4c677c1..27ea17f25 100644
--- a/frontend/translations/el.po
+++ b/frontend/translations/el.po
@@ -77,7 +77,7 @@ msgid "auth.new-password"
 msgstr "Πληκτρολογήστε έναν νέο κωδικό πρόσβασης."
 
 #: src/app/main/ui/auth/recovery.cljs
-msgid "auth.notifications.invalid-token-error"
+msgid "errors.invalid-recovery-token"
 msgstr "Ο κωδικός ανάκτησης δεν είναι έγκυρος."
 
 #: src/app/main/ui/auth/recovery.cljs
@@ -384,7 +384,7 @@ msgid "errors.email-has-permanent-bounces"
 msgstr "Το email «%s» έχει πολλές μόνιμες αναφορές αναπήδησης."
 
 #: src/app/main/ui/settings/change_email.cljs
-msgid "errors.email-invalid-confirmation"
+msgid "errors.invalid-email-confirmation"
 msgstr "Το email επιβεβαίωσης πρέπει να ταιριάζει"
 
 #: src/app/main/ui/auth/verify_token.cljs,
diff --git a/frontend/translations/en.po b/frontend/translations/en.po
index 8458241ba..b05464a4c 100644
--- a/frontend/translations/en.po
+++ b/frontend/translations/en.po
@@ -98,19 +98,24 @@ msgid "auth.login-with-oidc-submit"
 msgstr "OpenID"
 
 #: src/app/main/ui/settings/team-form.cljs, src/app/main/ui/auth/register.cljs, src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/onboarding/team_choice.cljs, src/app/main/ui/settings/access_tokens.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/settings/profile.cljs, src/app/main/ui/workspace/sidebar/assets.cljs
-msgid "auth.name.not-all-space"
+msgid "errors.field-not-all-whitespace"
 msgstr "The name must contain some character other than space."
 
-#: src/app/main/ui/auth/register.cljs, src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/onboarding/team_choice.cljs, src/app/main/ui/settings/access_tokens.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/settings/profile.cljs, src/app/main/ui/workspace/sidebar/assets.cljs
-msgid "auth.name.too-long"
-msgstr "The name must contain at most 250 characters."
+#: src/app/main/ui/auth/register.cljs, src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/onboarding/team_choice.cljs, src/app/main/ui/setti ngs/access_tokens.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/settings/profile.cljs, src/app/main/ui/workspace/sidebar/assets.cljs
+msgid "errors.field-max-length"
+msgstr[0] "Must contain at most 1 characters."
+msgstr[1] "Must contain at most %s characters."
+
+msgid "errors.field-min-length"
+msgstr[0] "Must contain at least 1 character."
+msgstr[1] "Must contain at least %s characters."
 
 #: src/app/main/ui/auth/recovery.cljs
 msgid "auth.new-password"
 msgstr "Type a new password"
 
 #: src/app/main/ui/auth/recovery.cljs
-msgid "auth.notifications.invalid-token-error"
+msgid "errors.invalid-recovery-token"
 msgstr "The recovery token is invalid."
 
 #: src/app/main/ui/auth/recovery.cljs
@@ -137,10 +142,6 @@ msgstr "Password"
 msgid "auth.password-length-hint"
 msgstr "At least 8 characters"
 
-#: src/app/main/ui/auth/register.cljs
-msgid "auth.password-not-empty"
-msgstr "Password must contain some character other than space."
-
 msgid "auth.privacy-policy"
 msgstr "Privacy policy"
 
@@ -309,7 +310,7 @@ msgid "dashboard.access-tokens.create"
 msgstr "Generate new token"
 
 #: src/app/main/ui/settings/access-tokens.cljs
-msgid "dashboard.access-tokens.create.success"
+msgid "dashboard.access-tokens.create-success"
 msgstr "Access token created successfully."
 
 #: src/app/main/ui/settings/access-tokens.cljs
@@ -320,10 +321,6 @@ msgstr "Press the button \"Generate new token\" to generate one."
 msgid "dashboard.access-tokens.empty.no-access-tokens"
 msgstr "You have no tokens so far."
 
-#: src/app/main/ui/settings/access-tokens.cljs
-msgid "dashboard.access-tokens.errors-required-name"
-msgstr "The name is required"
-
 #: src/app/main/ui/settings/access-tokens.cljs
 msgid "dashboard.access-tokens.expiration-180-days"
 msgstr "180 days"
@@ -898,11 +895,11 @@ msgid "errors.email-has-permanent-bounces"
 msgstr "The email «%s» has many permanent bounce reports."
 
 #: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs, src/app/main/ui/auth/recovery_request.cljs
-msgid "errors.email-invalid"
+msgid "errors.invalid-email"
 msgstr "Enter a valid email please"
 
 #: src/app/main/ui/settings/change_email.cljs
-msgid "errors.email-invalid-confirmation"
+msgid "errors.invalid-email-confirmation"
 msgstr "Confirmation email must match"
 
 msgid "errors.email-spam-or-permanent-bounces"
@@ -3131,10 +3128,6 @@ msgstr "Zoom lense increase"
 msgid "shortcuts.zoom-selected"
 msgstr "Zoom to selected"
 
-#: src/app/main/ui/dashboard/team.cljs
-msgid "team.webhooks.max-length"
-msgstr "The webhook name must contain at most 2048 characters."
-
 #: src/app/main/ui/dashboard/files.cljs
 msgid "title.dashboard.files"
 msgstr "%s - Penpot"
diff --git a/frontend/translations/es.po b/frontend/translations/es.po
index 187c38f82..c226af892 100644
--- a/frontend/translations/es.po
+++ b/frontend/translations/es.po
@@ -98,19 +98,24 @@ msgid "auth.login-with-oidc-submit"
 msgstr "OpenID"
 
 #: src/app/main/ui/settings/team-form.cljs, src/app/main/ui/auth/register.cljs, src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/onboarding/team_choice.cljs, src/app/main/ui/settings/access_tokens.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/settings/profile.cljs, src/app/main/ui/workspace/sidebar/assets.cljs
-msgid "auth.name.not-all-space"
-msgstr "El nombre debe contener algún carácter diferente de espacio"
+msgid "errors.field-not-all-whitespace"
+msgstr "Debe contener algún carácter diferente de espacio."
 
 #: src/app/main/ui/auth/register.cljs, src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/onboarding/team_choice.cljs, src/app/main/ui/settings/access_tokens.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/settings/profile.cljs, src/app/main/ui/workspace/sidebar/assets.cljs
-msgid "auth.name.too-long"
-msgstr "El nombre debe contener como máximo 250 caracteres."
+msgid "errors.field-max-length"
+msgstr[0] "Debe contener como máximo 1 caracter."
+msgstr[1] "Debe contener como máximo %s caracteres."
+
+msgid "errors.field-min-length"
+msgstr[0] "Debe contener como mínimo 1 caracter."
+msgstr[1] "Debe contener como mínimo %s caracteres."
 
 #: src/app/main/ui/auth/recovery.cljs
 msgid "auth.new-password"
 msgstr "Introduce la nueva contraseña"
 
 #: src/app/main/ui/auth/recovery.cljs
-msgid "auth.notifications.invalid-token-error"
+msgid "errors.invalid-recovery-token"
 msgstr "El código de recuperación no es válido."
 
 #: src/app/main/ui/auth/recovery.cljs
@@ -139,10 +144,6 @@ msgstr "Contraseña"
 msgid "auth.password-length-hint"
 msgstr "8 caracteres como mínimo"
 
-#: src/app/main/ui/auth/register.cljs
-msgid "auth.password-not-empty"
-msgstr "La contraseña debe contener algún caracter diferente de espacio"
-
 msgid "auth.privacy-policy"
 msgstr "Política de privacidad"
 
@@ -315,7 +316,7 @@ msgid "dashboard.access-tokens.create"
 msgstr "Generar nuevo token"
 
 #: src/app/main/ui/settings/access-tokens.cljs
-msgid "dashboard.access-tokens.create.success"
+msgid "dashboard.access-tokens.create-success"
 msgstr "Access token creado con éxito."
 
 #: src/app/main/ui/settings/access-tokens.cljs
@@ -326,10 +327,6 @@ msgstr "Pulsa el botón \"Generar nuevo token\" para generar uno."
 msgid "dashboard.access-tokens.empty.no-access-tokens"
 msgstr "Todavía no tienes ningún token."
 
-#: src/app/main/ui/settings/access-tokens.cljs
-msgid "dashboard.access-tokens.errors-required-name"
-msgstr "El nombre es obligatorio"
-
 #: src/app/main/ui/settings/access-tokens.cljs
 msgid "dashboard.access-tokens.expiration-180-days"
 msgstr "180 días"
@@ -924,11 +921,11 @@ msgid "errors.email-has-permanent-bounces"
 msgstr "El correo electrónico «%s» tiene varios reportes de rebote permanente."
 
 #: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs, src/app/main/ui/auth/recovery_request.cljs
-msgid "errors.email-invalid"
+msgid "errors.invalid-email"
 msgstr "Por favor, escribe un email válido"
 
 #: src/app/main/ui/settings/change_email.cljs
-msgid "errors.email-invalid-confirmation"
+msgid "errors.invalid-email-confirmation"
 msgstr "El correo de confirmación debe coincidir"
 
 msgid "errors.email-spam-or-permanent-bounces"
@@ -3188,10 +3185,6 @@ msgstr "Incrementar zoom a objetivo"
 msgid "shortcuts.zoom-selected"
 msgstr "Zoom a selección"
 
-#: src/app/main/ui/dashboard/team.cljs
-msgid "team.webhooks.max-length"
-msgstr "El nombre del webhook debe contener como máximo 2048 caracteres."
-
 #: src/app/main/ui/dashboard/files.cljs
 msgid "title.dashboard.files"
 msgstr "%s - Penpot"
diff --git a/frontend/translations/es_419.po b/frontend/translations/es_419.po
index 97dfd6988..a89a3a263 100644
--- a/frontend/translations/es_419.po
+++ b/frontend/translations/es_419.po
@@ -77,19 +77,20 @@ msgid "auth.login-with-oidc-submit"
 msgstr "Open ID"
 
 #: src/app/main/ui/settings/team-form.cljs, src/app/main/ui/auth/register.cljs, src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/onboarding/team_choice.cljs, src/app/main/ui/settings/access_tokens.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/settings/profile.cljs, src/app/main/ui/workspace/sidebar/assets.cljs
-msgid "auth.name.not-all-space"
+msgid "errors.field-not-all-whitespace"
 msgstr "El nombre debe contener algún carácter distinto al del espacio."
 
 #: src/app/main/ui/auth/register.cljs, src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/onboarding/team_choice.cljs, src/app/main/ui/settings/access_tokens.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/settings/profile.cljs, src/app/main/ui/workspace/sidebar/assets.cljs
-msgid "auth.name.too-long"
-msgstr "El nombre debe contener como máximo 250 caracteres."
+msgid "errors.field-max-length"
+msgstr[0] "El nombre debe contener como máximo 1 caracter."
+msgstr[1] "El nombre debe contener como máximo %s caracteres."
 
 #: src/app/main/ui/auth/recovery.cljs
 msgid "auth.new-password"
 msgstr "Escribe una nueva contraseña"
 
 #: src/app/main/ui/auth/recovery.cljs
-msgid "auth.notifications.invalid-token-error"
+msgid "errors.invalid-recovery-token"
 msgstr "El token de recuperación no es válido."
 
 #: src/app/main/ui/auth/recovery.cljs
@@ -118,10 +119,6 @@ msgstr "Contraseña"
 msgid "auth.password-length-hint"
 msgstr "Al menos 8 carácteres"
 
-#: src/app/main/ui/auth/register.cljs
-msgid "auth.password-not-empty"
-msgstr "La contraseña debe contener algún carácter que no sea espacio."
-
 msgid "auth.privacy-policy"
 msgstr "Política de privacidad"
 
@@ -280,7 +277,7 @@ msgid "dashboard.access-tokens.create"
 msgstr "Generar nuevo token"
 
 #: src/app/main/ui/settings/access-tokens.cljs
-msgid "dashboard.access-tokens.create.success"
+msgid "dashboard.access-tokens.create-success"
 msgstr "Token de acceso creado correctamente."
 
 #: src/app/main/ui/settings/access-tokens.cljs
@@ -291,10 +288,6 @@ msgstr "Presione el botón \"Generar nuevo token\" para generar uno."
 msgid "dashboard.access-tokens.empty.no-access-tokens"
 msgstr "No tienes tokens hasta el momento."
 
-#: src/app/main/ui/settings/access-tokens.cljs
-msgid "dashboard.access-tokens.errors-required-name"
-msgstr "El nombre es requerido"
-
 #: src/app/main/ui/settings/access-tokens.cljs
 msgid "dashboard.access-tokens.expiration-180-days"
 msgstr "180 días"
diff --git a/frontend/translations/eu.po b/frontend/translations/eu.po
index 485001f16..c47aad698 100644
--- a/frontend/translations/eu.po
+++ b/frontend/translations/eu.po
@@ -81,7 +81,7 @@ msgid "auth.new-password"
 msgstr "Sartu Pasahitz berria"
 
 #: src/app/main/ui/auth/recovery.cljs
-msgid "auth.notifications.invalid-token-error"
+msgid "errors.invalid-recovery-token"
 msgstr "Berreskuratzeko kodea ez da zuzena."
 
 #: src/app/main/ui/auth/recovery.cljs
@@ -758,11 +758,11 @@ msgstr "«%s» helbideak ez ditu mezuak ondo jasotzen, itzuli egiten ditu."
 
 #: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs,
 #: src/app/main/ui/auth/recovery_request.cljs
-msgid "errors.email-invalid"
+msgid "errors.invalid-email"
 msgstr "Mesedez, idatzi eposta helbide zuzen bat"
 
 #: src/app/main/ui/settings/change_email.cljs
-msgid "errors.email-invalid-confirmation"
+msgid "errors.invalid-email-confirmation"
 msgstr "Egiaztapenereko epostak bat etorri behar du aurrekoarekin"
 
 msgid "errors.email-spam-or-permanent-bounces"
diff --git a/frontend/translations/fa.po b/frontend/translations/fa.po
index af9c817b2..08838d22a 100644
--- a/frontend/translations/fa.po
+++ b/frontend/translations/fa.po
@@ -81,7 +81,7 @@ msgid "auth.new-password"
 msgstr "یک رمزعبور جدید تایپ کنید"
 
 #: src/app/main/ui/auth/recovery.cljs
-msgid "auth.notifications.invalid-token-error"
+msgid "errors.invalid-recovery-token"
 msgstr "توکن بازیابی نامعتبر است."
 
 #: src/app/main/ui/auth/recovery.cljs
@@ -728,7 +728,7 @@ msgid "errors.email-as-password"
 msgstr "شما نمی‌توانید از ایمیل خود به عنوان رمزعبور استفاده کنید"
 
 #: src/app/main/ui/settings/change_email.cljs
-msgid "errors.email-invalid-confirmation"
+msgid "errors.invalid-email-confirmation"
 msgstr "ایمیل تأیید باید مطابقت داشته باشد"
 
 #: src/app/main/ui/auth/verify_token.cljs,
diff --git a/frontend/translations/fin_FI.po b/frontend/translations/fin_FI.po
index 48be1617c..d16237c51 100644
--- a/frontend/translations/fin_FI.po
+++ b/frontend/translations/fin_FI.po
@@ -81,7 +81,7 @@ msgid "auth.new-password"
 msgstr "Syötä uusi salasana"
 
 #: src/app/main/ui/auth/recovery.cljs
-msgid "auth.notifications.invalid-token-error"
+msgid "errors.invalid-recovery-token"
 msgstr "Palautustunnus on virheellinen."
 
 #: src/app/main/ui/auth/recovery.cljs
diff --git a/frontend/translations/fr.po b/frontend/translations/fr.po
index 788254172..25217c5df 100644
--- a/frontend/translations/fr.po
+++ b/frontend/translations/fr.po
@@ -76,20 +76,12 @@ msgstr "LDAP"
 msgid "auth.login-with-oidc-submit"
 msgstr "OpenID"
 
-#: src/app/main/ui/settings/team-form.cljs, src/app/main/ui/auth/register.cljs, src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/onboarding/team_choice.cljs, src/app/main/ui/settings/access_tokens.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/settings/profile.cljs, src/app/main/ui/workspace/sidebar/assets.cljs
-msgid "auth.name.not-all-space"
-msgstr "Le nom doit contenir au moins un caractère autre que l'espace."
-
-#: src/app/main/ui/auth/register.cljs, src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/onboarding/team_choice.cljs, src/app/main/ui/settings/access_tokens.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/settings/profile.cljs, src/app/main/ui/workspace/sidebar/assets.cljs
-msgid "auth.name.too-long"
-msgstr "Le nom ne doit pas contenir plus de 250 caractères."
-
 #: src/app/main/ui/auth/recovery.cljs
 msgid "auth.new-password"
 msgstr "Saisissez un nouveau mot de passe"
 
 #: src/app/main/ui/auth/recovery.cljs
-msgid "auth.notifications.invalid-token-error"
+msgid "errors.invalid-recovery-token"
 msgstr "Le code de récupération n’est pas valide."
 
 #: src/app/main/ui/auth/recovery.cljs
@@ -116,10 +108,6 @@ msgstr "Mot de passe"
 msgid "auth.password-length-hint"
 msgstr "Au moins 8 caractères"
 
-#: src/app/main/ui/auth/register.cljs
-msgid "auth.password-not-empty"
-msgstr "Le mot de passe doit contenir au moins un caractère autre que l'espace."
-
 msgid "auth.privacy-policy"
 msgstr "Politique de confidentialité"
 
@@ -271,7 +259,7 @@ msgid "dashboard.access-tokens.create"
 msgstr "Générer un nouveau jeton"
 
 #: src/app/main/ui/settings/access-tokens.cljs
-msgid "dashboard.access-tokens.create.success"
+msgid "dashboard.access-tokens.create-success"
 msgstr "Jeton d'accès créé avec succès."
 
 #: src/app/main/ui/settings/access-tokens.cljs
@@ -282,10 +270,6 @@ msgstr "Pressez le bouton \"Générer un nouveau jeton\" pour en générer un."
 msgid "dashboard.access-tokens.empty.no-access-tokens"
 msgstr "Vous n'avez pas encore de jeton."
 
-#: src/app/main/ui/settings/access-tokens.cljs
-msgid "dashboard.access-tokens.errors-required-name"
-msgstr "Le nom est requis"
-
 #: src/app/main/ui/settings/access-tokens.cljs
 msgid "dashboard.access-tokens.expiration-180-days"
 msgstr "180 jours"
@@ -874,11 +858,11 @@ msgid "errors.email-has-permanent-bounces"
 msgstr "L'adresse e-mail « %s » a un taux de rebond trop élevé."
 
 #: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs, src/app/main/ui/auth/recovery_request.cljs
-msgid "errors.email-invalid"
+msgid "errors.invalid-email"
 msgstr "Veuillez entrer une adresse mail valide"
 
 #: src/app/main/ui/settings/change_email.cljs
-msgid "errors.email-invalid-confirmation"
+msgid "errors.invalid-email-confirmation"
 msgstr "L’adresse e‑mail de confirmation doit correspondre"
 
 msgid "errors.email-spam-or-permanent-bounces"
@@ -2930,10 +2914,6 @@ msgstr "Augmenter le zoom"
 msgid "shortcuts.zoom-selected"
 msgstr "Zoomer sur la sélection"
 
-#: src/app/main/ui/dashboard/team.cljs
-msgid "team.webhooks.max-length"
-msgstr "Le nom du webhook ne peut pas contenir plus de 2048 caractères."
-
 #: src/app/main/ui/dashboard/files.cljs
 msgid "title.dashboard.files"
 msgstr "%s - Penpot"
diff --git a/frontend/translations/gl.po b/frontend/translations/gl.po
index 3055ed093..3d89251d0 100644
--- a/frontend/translations/gl.po
+++ b/frontend/translations/gl.po
@@ -81,7 +81,7 @@ msgid "auth.new-password"
 msgstr "Escribe un contrasinal novo"
 
 #: src/app/main/ui/auth/recovery.cljs
-msgid "auth.notifications.invalid-token-error"
+msgid "errors.invalid-recovery-token"
 msgstr "O código de recuperación non é correcto."
 
 #: src/app/main/ui/auth/recovery.cljs
diff --git a/frontend/translations/ha.po b/frontend/translations/ha.po
index 858165e3c..594d24229 100644
--- a/frontend/translations/ha.po
+++ b/frontend/translations/ha.po
@@ -76,20 +76,12 @@ msgstr "LDAP"
 msgid "auth.login-with-oidc-submit"
 msgstr "shaidar buxewa"
 
-#: src/app/main/ui/settings/team-form.cljs, src/app/main/ui/auth/register.cljs, src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/onboarding/team_choice.cljs, src/app/main/ui/settings/access_tokens.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/settings/profile.cljs, src/app/main/ui/workspace/sidebar/assets.cljs
-msgid "auth.name.not-all-space"
-msgstr "dole suna ya qumshi waxansu alamimon rubutu, sannan tazara."
-
-#: src/app/main/ui/auth/register.cljs, src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/onboarding/team_choice.cljs, src/app/main/ui/settings/access_tokens.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/settings/profile.cljs, src/app/main/ui/workspace/sidebar/assets.cljs
-msgid "auth.name.too-long"
-msgstr "suna dole ya qunshi alamomin rubutu 250."
-
 #: src/app/main/ui/auth/recovery.cljs
 msgid "auth.new-password"
 msgstr "sanya sabuwar lambar tsaro"
 
 #: src/app/main/ui/auth/recovery.cljs
-msgid "auth.notifications.invalid-token-error"
+msgid "errors.invalid-recovery-token"
 msgstr "lambar tsaron da ka sanya ba daidai ba ce."
 
 #: src/app/main/ui/auth/recovery.cljs
@@ -252,7 +244,7 @@ msgid "dashboard.access-tokens.create"
 msgstr "samo sabuwar lambar tsaro"
 
 #: src/app/main/ui/settings/access-tokens.cljs
-msgid "dashboard.access-tokens.create.success"
+msgid "dashboard.access-tokens.create-success"
 msgstr "ka sami lambar tsaron da aka yi."
 
 #: src/app/main/ui/settings/access-tokens.cljs
@@ -265,10 +257,6 @@ msgstr ""
 msgid "dashboard.access-tokens.empty.no-access-tokens"
 msgstr "ba ka da wasu lambobin tsaro yanzu."
 
-#: src/app/main/ui/settings/access-tokens.cljs
-msgid "dashboard.access-tokens.errors-required-name"
-msgstr "ana buqatar suna"
-
 #: src/app/main/ui/settings/access-tokens.cljs
 msgid "dashboard.access-tokens.expiration-180-days"
 msgstr "kwanaki 180"
@@ -819,11 +807,11 @@ msgid "errors.email-has-permanent-bounces"
 msgstr "imel «%s» na da bayanan matsaloli na dindindin."
 
 #: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs, src/app/main/ui/auth/recovery_request.cljs
-msgid "errors.email-invalid"
+msgid "errors.invalid-email"
 msgstr "sanya imel mai amfani"
 
 #: src/app/main/ui/settings/change_email.cljs
-msgid "errors.email-invalid-confirmation"
+msgid "errors.invalid-email-confirmation"
 msgstr "tabbata imel xinka ya yi daidai"
 
 msgid "errors.email-spam-or-permanent-bounces"
@@ -2790,10 +2778,6 @@ msgstr "Zuko karuwar ido"
 msgid "shortcuts.zoom-selected"
 msgstr "Zuko wanda aka zaba"
 
-#: src/app/main/ui/dashboard/team.cljs
-msgid "team.webhooks.max-length"
-msgstr "Sunan shafin yanar gizon zai kunshi a mafi yawa haruffa 2048."
-
 #: src/app/main/ui/dashboard/files.cljs
 msgid "title.dashboard.files"
 msgstr "%s - Tukunyar aje biro"
diff --git a/frontend/translations/he.po b/frontend/translations/he.po
index abdf5f5b3..85de785ed 100644
--- a/frontend/translations/he.po
+++ b/frontend/translations/he.po
@@ -83,32 +83,12 @@ msgstr "LDAP"
 msgid "auth.login-with-oidc-submit"
 msgstr "‎OpenID Connect"
 
-#: src/app/main/ui/settings/team-form.cljs, src/app/main/ui/auth/register.cljs,
-#: src/app/main/ui/dashboard/team_form.cljs,
-#: src/app/main/ui/onboarding/team_choice.cljs,
-#: src/app/main/ui/settings/access_tokens.cljs,
-#: src/app/main/ui/settings/feedback.cljs,
-#: src/app/main/ui/settings/profile.cljs,
-#: src/app/main/ui/workspace/sidebar/assets.cljs
-msgid "auth.name.not-all-space"
-msgstr "השם חייב להכיל תווים שאינם רווחים."
-
-#: src/app/main/ui/auth/register.cljs,
-#: src/app/main/ui/dashboard/team_form.cljs,
-#: src/app/main/ui/onboarding/team_choice.cljs,
-#: src/app/main/ui/settings/access_tokens.cljs,
-#: src/app/main/ui/settings/feedback.cljs,
-#: src/app/main/ui/settings/profile.cljs,
-#: src/app/main/ui/workspace/sidebar/assets.cljs
-msgid "auth.name.too-long"
-msgstr "השם חייב להכיל 250 תווים לכל היותר."
-
 #: src/app/main/ui/auth/recovery.cljs
 msgid "auth.new-password"
 msgstr "נא להקליד סיסמה חדשה"
 
 #: src/app/main/ui/auth/recovery.cljs
-msgid "auth.notifications.invalid-token-error"
+msgid "errors.invalid-recovery-token"
 msgstr "אסימון השחזור שגוי."
 
 #: src/app/main/ui/auth/recovery.cljs
@@ -135,10 +115,6 @@ msgstr "סיסמה"
 msgid "auth.password-length-hint"
 msgstr "8 תווים לפחות"
 
-#: src/app/main/ui/auth/register.cljs
-msgid "auth.password-not-empty"
-msgstr "הסיסמה חייבת להכיל תווים שאינם רווחים."
-
 msgid "auth.privacy-policy"
 msgstr "מדיניות פרטיות"
 
@@ -293,7 +269,7 @@ msgid "dashboard.access-tokens.create"
 msgstr "יצירת אסימון חדש"
 
 #: src/app/main/ui/settings/access-tokens.cljs
-msgid "dashboard.access-tokens.create.success"
+msgid "dashboard.access-tokens.create-success"
 msgstr "אסימון הגישה נוצר בהצלחה."
 
 #: src/app/main/ui/settings/access-tokens.cljs
@@ -304,10 +280,6 @@ msgstr "נא ללחוץ על הכפתור „יצירת אסימון חדש” 
 msgid "dashboard.access-tokens.empty.no-access-tokens"
 msgstr "אין לך אסימונים עדיין."
 
-#: src/app/main/ui/settings/access-tokens.cljs
-msgid "dashboard.access-tokens.errors-required-name"
-msgstr "השם הוא בגדר חובה"
-
 #: src/app/main/ui/settings/access-tokens.cljs
 msgid "dashboard.access-tokens.expiration-180-days"
 msgstr "180 יום"
@@ -889,11 +861,11 @@ msgstr "לכתובת הדוא״ל „%s” יש יותר מדי דוחות הח
 
 #: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs,
 #: src/app/main/ui/auth/recovery_request.cljs
-msgid "errors.email-invalid"
+msgid "errors.invalid-email"
 msgstr "נא למלא כתובת דוא״ל תקפה בבקשה"
 
 #: src/app/main/ui/settings/change_email.cljs
-msgid "errors.email-invalid-confirmation"
+msgid "errors.invalid-email-confirmation"
 msgstr "כתובת הדוא״ל לאימות חייבת להיות תואמת"
 
 msgid "errors.email-spam-or-permanent-bounces"
@@ -3009,10 +2981,6 @@ msgstr "הגדלת עדשת תקריב"
 msgid "shortcuts.zoom-selected"
 msgstr "התמקדות על הנבחר"
 
-#: src/app/main/ui/dashboard/team.cljs
-msgid "team.webhooks.max-length"
-msgstr "אורך שם ההתליה הוא עד 2048 תווים."
-
 #: src/app/main/ui/dashboard/files.cljs
 msgid "title.dashboard.files"
 msgstr "%s‏ - Penpot"
diff --git a/frontend/translations/hr.po b/frontend/translations/hr.po
index b29b453ad..c551e954b 100644
--- a/frontend/translations/hr.po
+++ b/frontend/translations/hr.po
@@ -80,7 +80,7 @@ msgid "auth.new-password"
 msgstr "Unesi novu lozinku"
 
 #: src/app/main/ui/auth/recovery.cljs
-msgid "auth.notifications.invalid-token-error"
+msgid "errors.invalid-recovery-token"
 msgstr "Token za oporavak je nevažeći."
 
 #: src/app/main/ui/auth/recovery.cljs
@@ -715,7 +715,7 @@ msgid "errors.email-has-permanent-bounces"
 msgstr "E-pmail «%s» ima mnogo trajnih izvješća o odbijanju."
 
 #: src/app/main/ui/settings/change_email.cljs
-msgid "errors.email-invalid-confirmation"
+msgid "errors.invalid-email-confirmation"
 msgstr "E-mail za potvrdu mora odgovarati"
 
 msgid "errors.email-spam-or-permanent-bounces"
diff --git a/frontend/translations/id.po b/frontend/translations/id.po
index 9e6bef4d6..2014d95aa 100644
--- a/frontend/translations/id.po
+++ b/frontend/translations/id.po
@@ -76,32 +76,12 @@ msgstr "LDAP"
 msgid "auth.login-with-oidc-submit"
 msgstr "OpenID Connect"
 
-#: src/app/main/ui/settings/team-form.cljs, src/app/main/ui/auth/register.cljs,
-#: src/app/main/ui/dashboard/team_form.cljs,
-#: src/app/main/ui/onboarding/team_choice.cljs,
-#: src/app/main/ui/settings/access_tokens.cljs,
-#: src/app/main/ui/settings/feedback.cljs,
-#: src/app/main/ui/settings/profile.cljs,
-#: src/app/main/ui/workspace/sidebar/assets.cljs
-msgid "auth.name.not-all-space"
-msgstr "Nama harus berisi beberapa karakter selain spasi."
-
-#: src/app/main/ui/auth/register.cljs,
-#: src/app/main/ui/dashboard/team_form.cljs,
-#: src/app/main/ui/onboarding/team_choice.cljs,
-#: src/app/main/ui/settings/access_tokens.cljs,
-#: src/app/main/ui/settings/feedback.cljs,
-#: src/app/main/ui/settings/profile.cljs,
-#: src/app/main/ui/workspace/sidebar/assets.cljs
-msgid "auth.name.too-long"
-msgstr "Nama harus berisi setidaknya 250 karakter."
-
 #: src/app/main/ui/auth/recovery.cljs
 msgid "auth.new-password"
 msgstr "Ketik kata sandi baru"
 
 #: src/app/main/ui/auth/recovery.cljs
-msgid "auth.notifications.invalid-token-error"
+msgid "errors.invalid-recovery-token"
 msgstr "Token pemulihan tidak sah."
 
 #: src/app/main/ui/auth/recovery.cljs
@@ -129,10 +109,6 @@ msgstr "Kata sandi"
 msgid "auth.password-length-hint"
 msgstr "Setidaknya 8 karakter"
 
-#: src/app/main/ui/auth/register.cljs
-msgid "auth.password-not-empty"
-msgstr "Kata sandi harus berisi beberapa karakter selain spasi."
-
 msgid "auth.privacy-policy"
 msgstr "Kebijakan privasi"
 
@@ -291,7 +267,7 @@ msgid "dashboard.access-tokens.create"
 msgstr "Buat token baru"
 
 #: src/app/main/ui/settings/access-tokens.cljs
-msgid "dashboard.access-tokens.create.success"
+msgid "dashboard.access-tokens.create-success"
 msgstr "Token akses berhasil dibuat."
 
 #: src/app/main/ui/settings/access-tokens.cljs
@@ -302,10 +278,6 @@ msgstr "Tekan tombol \"Buat token baru\" untuk membuat token."
 msgid "dashboard.access-tokens.empty.no-access-tokens"
 msgstr "Anda belum memiliki token."
 
-#: src/app/main/ui/settings/access-tokens.cljs
-msgid "dashboard.access-tokens.errors-required-name"
-msgstr "Nama diperlukan"
-
 #: src/app/main/ui/settings/access-tokens.cljs
 msgid "dashboard.access-tokens.expiration-180-days"
 msgstr "180 hari"
@@ -878,11 +850,11 @@ msgstr "Surel “%s” memiliki banyak laporan lompatan permanen."
 
 #: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs,
 #: src/app/main/ui/auth/recovery_request.cljs
-msgid "errors.email-invalid"
+msgid "errors.invalid-email"
 msgstr "Silakan menyediakan surel yang valid"
 
 #: src/app/main/ui/settings/change_email.cljs
-msgid "errors.email-invalid-confirmation"
+msgid "errors.invalid-email-confirmation"
 msgstr "Surel konfirmasi harus cocok"
 
 msgid "errors.email-spam-or-permanent-bounces"
@@ -2964,10 +2936,6 @@ msgstr "Tambahkan lensa zum"
 msgid "shortcuts.zoom-selected"
 msgstr "Zum ke terpilih"
 
-#: src/app/main/ui/dashboard/team.cljs
-msgid "team.webhooks.max-length"
-msgstr "Nama webhook berisi sampai 2048 karakter."
-
 #: src/app/main/ui/dashboard/files.cljs
 msgid "title.dashboard.files"
 msgstr "%s - Penpot"
diff --git a/frontend/translations/ig.po b/frontend/translations/ig.po
index 6eb4ae1cb..fd0da7b89 100644
--- a/frontend/translations/ig.po
+++ b/frontend/translations/ig.po
@@ -70,20 +70,12 @@ msgstr "LDAP"
 msgid "auth.login-with-oidc-submit"
 msgstr "Mepe ID"
 
-#: src/app/main/ui/settings/team-form.cljs, src/app/main/ui/auth/register.cljs, src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/onboarding/team_choice.cljs, src/app/main/ui/settings/access_tokens.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/settings/profile.cljs, src/app/main/ui/workspace/sidebar/assets.cljs
-msgid "auth.name.not-all-space"
-msgstr "Aha ga-enweriri ụfọdụ mkpụrụ edemede karịa oghere ."
-
-#: src/app/main/ui/auth/register.cljs, src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/onboarding/team_choice.cljs, src/app/main/ui/settings/access_tokens.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/settings/profile.cljs, src/app/main/ui/workspace/sidebar/assets.cljs
-msgid "auth.name.too-long"
-msgstr "Aha ga-enweriri ọ karịa mkpụrụ okwu narị abụọ na iri ise"
-
 #: src/app/main/ui/auth/recovery.cljs
 msgid "auth.new-password"
 msgstr "Pinye akara mpịbanye ọhụrụ"
 
 #: src/app/main/ui/auth/recovery.cljs
-msgid "auth.notifications.invalid-token-error"
+msgid "errors.invalid-recovery-token"
 msgstr "Ọdịmara e nweghachitere adabaghị ."
 
 #: src/app/main/ui/auth/recovery.cljs
@@ -106,10 +98,6 @@ msgstr "Sonyere n'otu nke ọma"
 msgid "auth.password-length-hint"
 msgstr "Ọ karịa mkpụrụ ederede asatọ"
 
-#: src/app/main/ui/auth/register.cljs
-msgid "auth.password-not-empty"
-msgstr "Akara mpịbanye ga-enweriri ụfọdụ leta/akara mpị karịa oghere ."
-
 msgid "auth.privacy-policy"
 msgstr "Iwu oñiño onwe"
 
@@ -245,17 +233,13 @@ msgid "dashboard.access-tokens.create"
 msgstr "Mepụta ọdịmara ọhụrụ"
 
 #: src/app/main/ui/settings/access-tokens.cljs
-msgid "dashboard.access-tokens.create.success"
+msgid "dashboard.access-tokens.create-success"
 msgstr "Mmepụtara ọdịmara nnweta gara nke ọma ."
 
 #: src/app/main/ui/settings/access-tokens.cljs
 msgid "dashboard.access-tokens.empty.add-one"
 msgstr "Pịa mpi \"Nweta ọdịmara ọhụrụ \" inweta otu ."
 
-#: src/app/main/ui/settings/access-tokens.cljs
-msgid "dashboard.access-tokens.errors-required-name"
-msgstr "A chọrọ aha"
-
 #: src/app/main/ui/settings/access-tokens.cljs
 msgid "dashboard.access-tokens.expiration-180-days"
 msgstr "Mkpụrụ ụbọchị narị na iri asatọ"
@@ -724,11 +708,11 @@ msgid "errors.email-has-permanent-bounces"
 msgstr "Ozi-n «%s» nwere ọtụtụ ozi nkọwa mbịaghachigide."
 
 #: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs, src/app/main/ui/auth/recovery_request.cljs
-msgid "errors.email-invalid"
+msgid "errors.invalid-email"
 msgstr "Debanye aha ozi-n dabara adaba"
 
 #: src/app/main/ui/settings/change_email.cljs
-msgid "errors.email-invalid-confirmation"
+msgid "errors.invalid-email-confirmation"
 msgstr "Ozi-n nnabata ga-adabrịrị"
 
 msgid "errors.email-spam-or-permanent-bounces"
diff --git a/frontend/translations/it.po b/frontend/translations/it.po
index 47ea14afa..5c1a9fb18 100644
--- a/frontend/translations/it.po
+++ b/frontend/translations/it.po
@@ -81,7 +81,7 @@ msgid "auth.new-password"
 msgstr "Inserisci una nuova password"
 
 #: src/app/main/ui/auth/recovery.cljs
-msgid "auth.notifications.invalid-token-error"
+msgid "errors.invalid-recovery-token"
 msgstr "Il codice di recupero non è valido."
 
 #: src/app/main/ui/auth/recovery.cljs
@@ -706,7 +706,7 @@ msgid "errors.email-as-password"
 msgstr "Non è possibile utilizzare il tuo indirizzo e-mail come password"
 
 #: src/app/main/ui/settings/change_email.cljs
-msgid "errors.email-invalid-confirmation"
+msgid "errors.invalid-email-confirmation"
 msgstr "L'indirizzo e-mail di conferma deve corrispondere"
 
 msgid "errors.email-spam-or-permanent-bounces"
diff --git a/frontend/translations/jpn_JP.po b/frontend/translations/jpn_JP.po
index 9b325ce04..0dc3c920d 100644
--- a/frontend/translations/jpn_JP.po
+++ b/frontend/translations/jpn_JP.po
@@ -77,7 +77,7 @@ msgid "auth.new-password"
 msgstr "新しいパスワードを入力"
 
 #: src/app/main/ui/auth/recovery.cljs
-msgid "auth.notifications.invalid-token-error"
+msgid "errors.invalid-recovery-token"
 msgstr "リカバリコードが無効です。"
 
 #: src/app/main/ui/auth/recovery.cljs
@@ -519,7 +519,7 @@ msgid "errors.email-has-permanent-bounces"
 msgstr "メールアドレス «%s» には多くの受信失敗レポートがあります。"
 
 #: src/app/main/ui/settings/change_email.cljs
-msgid "errors.email-invalid-confirmation"
+msgid "errors.invalid-email-confirmation"
 msgstr "メールアドレスは同じものを入力する必要があります"
 
 msgid "errors.email-spam-or-permanent-bounces"
diff --git a/frontend/translations/ko.po b/frontend/translations/ko.po
index 3bd56f6d2..cf99805fe 100644
--- a/frontend/translations/ko.po
+++ b/frontend/translations/ko.po
@@ -683,7 +683,7 @@ msgstr "데모 서비스입니다. 실제 작업에 사용하지 마십시오. 
 "주기적으로 삭제될 것입니다."
 
 #: src/app/main/ui/auth/recovery.cljs
-msgid "auth.notifications.invalid-token-error"
+msgid "errors.invalid-recovery-token"
 msgstr "복구 토큰이 유효하지 않습니다."
 
 msgid "common.share-link.destroy-link"
@@ -717,14 +717,6 @@ msgstr "개인용 엑세스 토큰"
 msgid "dashboard.access-tokens.expired-on"
 msgstr "%s에 만료되었습니다"
 
-#: src/app/main/ui/auth/register.cljs, src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/onboarding/team_choice.cljs, src/app/main/ui/settings/access_tokens.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/settings/profile.cljs, src/app/main/ui/workspace/sidebar/assets.cljs
-msgid "auth.name.too-long"
-msgstr "이름은 최대 250자까지만 입력 가능합니다."
-
-#: src/app/main/ui/settings/access-tokens.cljs
-msgid "dashboard.access-tokens.errors-required-name"
-msgstr "이름을 입력하십시오"
-
 #: src/app/main/ui/settings/access-tokens.cljs
 msgid "dashboard.access-tokens.copied-success"
 msgstr "복사된 토큰"
@@ -754,8 +746,10 @@ msgid "auth.login-tagline"
 msgstr "펜팟은 디자인과 코딩의 협업을 위한 무료 오픈소스 디자인 도구입니다"
 
 #: src/app/main/ui/auth/register.cljs
-msgid "auth.password-not-empty"
-msgstr "비밀번호는 공백 이외의 글자를 포함해야 합니다."
+#, markdown
+msgid "auth.terms-privacy-agreement-md"
+msgstr "새로운 계정을 생성하시면, 사용자는 펜팟의 [서비스 정책](%s)과 [개인 정보 "
+"정책](%s)에 동의하는 것으로 간주됩니다."
 
 #: src/app/main/ui/dashboard/projects.cljs
 msgid "dasboard.team-hero.text"
@@ -774,16 +768,12 @@ msgstr "실습용 튜토리얼"
 msgid "auth.login-account-title"
 msgstr "내 계정에 로그인하기"
 
-#: src/app/main/ui/settings/team-form.cljs, src/app/main/ui/auth/register.cljs, src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/onboarding/team_choice.cljs, src/app/main/ui/settings/access_tokens.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/settings/profile.cljs, src/app/main/ui/workspace/sidebar/assets.cljs
-msgid "auth.name.not-all-space"
-msgstr "이름은 공백 이외의 글자를 포함해야 합니다."
-
 #: src/app/main/ui/dashboard/projects.cljs
 msgid "dasboard.tutorial-hero.info"
 msgstr "본 실습용 튜토리얼을 통해 펜팟의 기본 기능에 대하여 재미있게 학습하십시오."
 
 #: src/app/main/ui/settings/access-tokens.cljs
-msgid "dashboard.access-tokens.create.success"
+msgid "dashboard.access-tokens.create-success"
 msgstr "엑세스 토큰이 성공적으로 생성되었습니다."
 
 #: src/app/main/ui/settings/access-tokens.cljs
diff --git a/frontend/translations/lt.po b/frontend/translations/lt.po
index 707e94a2b..08113cf5c 100644
--- a/frontend/translations/lt.po
+++ b/frontend/translations/lt.po
@@ -83,7 +83,7 @@ msgstr "Įveskite naują slaptažodį"
 
 #: src/app/main/ui/auth/recovery.cljs
 #, fuzzy
-msgid "auth.notifications.invalid-token-error"
+msgid "errors.invalid-recovery-token"
 msgstr "Atkūrimo prieigos raktas neteisingas."
 
 #: src/app/main/ui/auth/recovery.cljs
diff --git a/frontend/translations/lv.po b/frontend/translations/lv.po
index bb7a0d34b..d759cd7bd 100644
--- a/frontend/translations/lv.po
+++ b/frontend/translations/lv.po
@@ -85,20 +85,12 @@ msgstr "LDAP"
 msgid "auth.login-with-oidc-submit"
 msgstr "AtvērtoID (OpenID)"
 
-#: src/app/main/ui/settings/team-form.cljs, src/app/main/ui/auth/register.cljs, src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/onboarding/team_choice.cljs, src/app/main/ui/settings/access_tokens.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/settings/profile.cljs, src/app/main/ui/workspace/sidebar/assets.cljs
-msgid "auth.name.not-all-space"
-msgstr "Nosaukumam jāsatur simboli, kas nav atstarpe."
-
-#: src/app/main/ui/auth/register.cljs, src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/onboarding/team_choice.cljs, src/app/main/ui/settings/access_tokens.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/settings/profile.cljs, src/app/main/ui/workspace/sidebar/assets.cljs
-msgid "auth.name.too-long"
-msgstr "Nosaukumus nedrīkst pārsniegt 250 simbolus."
-
 #: src/app/main/ui/auth/recovery.cljs
 msgid "auth.new-password"
 msgstr "Ierakstiet jaunu paroli"
 
 #: src/app/main/ui/auth/recovery.cljs
-msgid "auth.notifications.invalid-token-error"
+msgid "errors.invalid-recovery-token"
 msgstr "Atkopšanas tekstvienība nav derīga."
 
 #: src/app/main/ui/auth/recovery.cljs
@@ -127,10 +119,6 @@ msgstr "Parole"
 msgid "auth.password-length-hint"
 msgstr "Vismaz 8 rakstzīmes"
 
-#: src/app/main/ui/auth/register.cljs
-msgid "auth.password-not-empty"
-msgstr "Parolē ir jābūt arī citām rakstzīmēm bez atstarpes."
-
 msgid "auth.privacy-policy"
 msgstr "Privātuma politika"
 
@@ -287,7 +275,7 @@ msgid "dashboard.access-tokens.create"
 msgstr "Izveidot jaunu pilnvaru"
 
 #: src/app/main/ui/settings/access-tokens.cljs
-msgid "dashboard.access-tokens.create.success"
+msgid "dashboard.access-tokens.create-success"
 msgstr "Piekļuves pilnvara ir veiksmīgi izveidota."
 
 #: src/app/main/ui/settings/access-tokens.cljs
@@ -298,10 +286,6 @@ msgstr "Jānospiež poga \"Izveidot jaunu pilnvaru\", lai izveidotu kādu."
 msgid "dashboard.access-tokens.empty.no-access-tokens"
 msgstr "Pagaidām vēl nav pilnvaru."
 
-#: src/app/main/ui/settings/access-tokens.cljs
-msgid "dashboard.access-tokens.errors-required-name"
-msgstr "Nosaukums ir obligāts"
-
 #: src/app/main/ui/settings/access-tokens.cljs
 msgid "dashboard.access-tokens.expiration-180-days"
 msgstr "180 dienas"
@@ -890,11 +874,11 @@ msgstr "E-pastam “%s” ir daudz pastāvīgu atlēcienu atskaišu."
 
 #: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs,
 #: src/app/main/ui/auth/recovery_request.cljs
-msgid "errors.email-invalid"
+msgid "errors.invalid-email"
 msgstr "Lūgums ievadīt derīgu e-pasta adresi"
 
 #: src/app/main/ui/settings/change_email.cljs
-msgid "errors.email-invalid-confirmation"
+msgid "errors.invalid-email-confirmation"
 msgstr "Apstiprinājuma e-pastam jāatbilst"
 
 msgid "errors.email-spam-or-permanent-bounces"
@@ -3062,10 +3046,6 @@ msgstr "Tālummaiņas palielinājums"
 msgid "shortcuts.zoom-selected"
 msgstr "Tālummainīt uz atlasi"
 
-#: src/app/main/ui/dashboard/team.cljs
-msgid "team.webhooks.max-length"
-msgstr "Tīmekļa aizķeres nosaukumā drīkst būt ne vairāk kā 2048 rakstzīmes."
-
 #: src/app/main/ui/dashboard/files.cljs
 msgid "title.dashboard.files"
 msgstr "%s - Penpot"
diff --git a/frontend/translations/ml.po b/frontend/translations/ml.po
index f67e468d9..2502c2010 100644
--- a/frontend/translations/ml.po
+++ b/frontend/translations/ml.po
@@ -81,7 +81,7 @@ msgid "auth.new-password"
 msgstr "പുതിയൊരു പാസ്‌വേഡ് ചേർക്കുക"
 
 #: src/app/main/ui/auth/recovery.cljs
-msgid "auth.notifications.invalid-token-error"
+msgid "errors.invalid-recovery-token"
 msgstr "റിക്കവറി ടോക്കൺ അസാധുവാണ്."
 
 #: src/app/main/ui/auth/recovery.cljs
diff --git a/frontend/translations/ms.po b/frontend/translations/ms.po
index 3e07cb73f..08db4e6f9 100644
--- a/frontend/translations/ms.po
+++ b/frontend/translations/ms.po
@@ -62,20 +62,12 @@ msgstr "LDAP"
 msgid "auth.login-with-oidc-submit"
 msgstr "OpenID Connect"
 
-#: src/app/main/ui/settings/team-form.cljs, src/app/main/ui/auth/register.cljs, src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/onboarding/team_choice.cljs, src/app/main/ui/settings/access_tokens.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/settings/profile.cljs, src/app/main/ui/workspace/sidebar/assets.cljs
-msgid "auth.name.not-all-space"
-msgstr "Nama mesti mengandungi beberapa aksara selain ruang."
-
-#: src/app/main/ui/auth/register.cljs, src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/onboarding/team_choice.cljs, src/app/main/ui/settings/access_tokens.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/settings/profile.cljs, src/app/main/ui/workspace/sidebar/assets.cljs
-msgid "auth.name.too-long"
-msgstr "Nama mesti mengandungi paling banyak 250 aksara."
-
 #: src/app/main/ui/auth/recovery.cljs
 msgid "auth.new-password"
 msgstr "Taip kata laluan baharu"
 
 #: src/app/main/ui/auth/recovery.cljs
-msgid "auth.notifications.invalid-token-error"
+msgid "errors.invalid-recovery-token"
 msgstr "Token pemulihan adalah tidak sah."
 
 #: src/app/main/ui/auth/recovery.cljs
@@ -102,10 +94,6 @@ msgstr "Kata laluan"
 msgid "auth.password-length-hint"
 msgstr "Sekurang-kurangnya 8 aksara"
 
-#: src/app/main/ui/auth/register.cljs
-msgid "auth.password-not-empty"
-msgstr "Kata laluan mesti mengandungi beberapa aksara selain daripada ruang."
-
 msgid "auth.privacy-policy"
 msgstr "Dasar privasi"
 
@@ -263,7 +251,7 @@ msgid "dashboard.access-tokens.create"
 msgstr "Jana token baru"
 
 #: src/app/main/ui/settings/access-tokens.cljs
-msgid "dashboard.access-tokens.create.success"
+msgid "dashboard.access-tokens.create-success"
 msgstr "Token capaian berjaya dihasilkan."
 
 #: src/app/main/ui/settings/access-tokens.cljs
@@ -274,10 +262,6 @@ msgstr "Tekan butang \"Jana token baharu\" untuk menjana token."
 msgid "dashboard.access-tokens.empty.no-access-tokens"
 msgstr "Anda tidak mempunyai token setakat ini."
 
-#: src/app/main/ui/settings/access-tokens.cljs
-msgid "dashboard.access-tokens.errors-required-name"
-msgstr "Nama diperlukan"
-
 #: src/app/main/ui/settings/access-tokens.cljs
 msgid "dashboard.access-tokens.expiration-180-days"
 msgstr "180 hari"
@@ -840,11 +824,11 @@ msgid "errors.email-has-permanent-bounces"
 msgstr "E-mel «%s» mempunyai banyak laporan lantunan kekal."
 
 #: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs, src/app/main/ui/auth/recovery_request.cljs
-msgid "errors.email-invalid"
+msgid "errors.invalid-email"
 msgstr "Sila masukkan e-mel yang sah"
 
 #: src/app/main/ui/settings/change_email.cljs
-msgid "errors.email-invalid-confirmation"
+msgid "errors.invalid-email-confirmation"
 msgstr "E-mel pengesahan mesti sepadan"
 
 msgid "errors.email-spam-or-permanent-bounces"
diff --git a/frontend/translations/nb_NO.po b/frontend/translations/nb_NO.po
index 990f3b2c1..76d20969b 100644
--- a/frontend/translations/nb_NO.po
+++ b/frontend/translations/nb_NO.po
@@ -28,7 +28,7 @@ msgid "auth.new-password"
 msgstr "Skriv inn et nytt passord"
 
 #: src/app/main/ui/auth/recovery.cljs
-msgid "auth.notifications.invalid-token-error"
+msgid "errors.invalid-recovery-token"
 msgstr "Gjenopprettelsessymbolet er ugyldig."
 
 #: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs
diff --git a/frontend/translations/nl.po b/frontend/translations/nl.po
index 7e3bf1d07..139c96ddf 100644
--- a/frontend/translations/nl.po
+++ b/frontend/translations/nl.po
@@ -86,32 +86,12 @@ msgstr "LDAP"
 msgid "auth.login-with-oidc-submit"
 msgstr "OpenID"
 
-#: src/app/main/ui/settings/team-form.cljs, src/app/main/ui/auth/register.cljs,
-#: src/app/main/ui/dashboard/team_form.cljs,
-#: src/app/main/ui/onboarding/team_choice.cljs,
-#: src/app/main/ui/settings/access_tokens.cljs,
-#: src/app/main/ui/settings/feedback.cljs,
-#: src/app/main/ui/settings/profile.cljs,
-#: src/app/main/ui/workspace/sidebar/assets.cljs
-msgid "auth.name.not-all-space"
-msgstr "De naam mag geen spatie bevatten."
-
-#: src/app/main/ui/auth/register.cljs,
-#: src/app/main/ui/dashboard/team_form.cljs,
-#: src/app/main/ui/onboarding/team_choice.cljs,
-#: src/app/main/ui/settings/access_tokens.cljs,
-#: src/app/main/ui/settings/feedback.cljs,
-#: src/app/main/ui/settings/profile.cljs,
-#: src/app/main/ui/workspace/sidebar/assets.cljs
-msgid "auth.name.too-long"
-msgstr "De naam mag maximaal 250 tekens bevatten."
-
 #: src/app/main/ui/auth/recovery.cljs
 msgid "auth.new-password"
 msgstr "Typ een nieuw wachtwoord"
 
 #: src/app/main/ui/auth/recovery.cljs
-msgid "auth.notifications.invalid-token-error"
+msgid "errors.invalid-recovery-token"
 msgstr "De herstelbewijsstuk is ongeldig."
 
 #: src/app/main/ui/auth/recovery.cljs
@@ -138,10 +118,6 @@ msgstr "Wachtwoord"
 msgid "auth.password-length-hint"
 msgstr "Minimaal 8 tekens"
 
-#: src/app/main/ui/auth/register.cljs
-msgid "auth.password-not-empty"
-msgstr "Het wachtwoord mag geen spatie bevatten."
-
 msgid "auth.privacy-policy"
 msgstr "Privacybeleid"
 
@@ -301,7 +277,7 @@ msgid "dashboard.access-tokens.create"
 msgstr "Nieuw toegangsbewijs aanmaken"
 
 #: src/app/main/ui/settings/access-tokens.cljs
-msgid "dashboard.access-tokens.create.success"
+msgid "dashboard.access-tokens.create-success"
 msgstr "Toegangsbewijs is succesvol aangemaakt."
 
 #: src/app/main/ui/settings/access-tokens.cljs
@@ -312,10 +288,6 @@ msgstr "Klik op de knop \"Nieuw toegangsbewijs aanmaken\" om er een aan te maken
 msgid "dashboard.access-tokens.empty.no-access-tokens"
 msgstr "Je hebt nog geen toegangsbewijzen."
 
-#: src/app/main/ui/settings/access-tokens.cljs
-msgid "dashboard.access-tokens.errors-required-name"
-msgstr "De naam is verplicht"
-
 #: src/app/main/ui/settings/access-tokens.cljs
 msgid "dashboard.access-tokens.expiration-180-days"
 msgstr "180 dagen"
@@ -909,11 +881,11 @@ msgstr "Het emailadres «%s» heeft veel permanente bounce-rapporten."
 
 #: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs,
 #: src/app/main/ui/auth/recovery_request.cljs
-msgid "errors.email-invalid"
+msgid "errors.invalid-email"
 msgstr "Voer een geldig e-mailadres in"
 
 #: src/app/main/ui/settings/change_email.cljs
-msgid "errors.email-invalid-confirmation"
+msgid "errors.invalid-email-confirmation"
 msgstr "Bevestigingsmail moet overeenkomen"
 
 msgid "errors.email-spam-or-permanent-bounces"
@@ -3056,10 +3028,6 @@ msgstr "Zoomlens vergroten"
 msgid "shortcuts.zoom-selected"
 msgstr "Zoomen naar selectie"
 
-#: src/app/main/ui/dashboard/team.cljs
-msgid "team.webhooks.max-length"
-msgstr "De webhooknaam mag maximaal 2048 tekens bevatten."
-
 #: src/app/main/ui/dashboard/files.cljs
 msgid "title.dashboard.files"
 msgstr "%s - Penpot"
diff --git a/frontend/translations/pl.po b/frontend/translations/pl.po
index 505571f23..30db46257 100644
--- a/frontend/translations/pl.po
+++ b/frontend/translations/pl.po
@@ -82,7 +82,7 @@ msgid "auth.new-password"
 msgstr "Wpisz nowe hasło"
 
 #: src/app/main/ui/auth/recovery.cljs
-msgid "auth.notifications.invalid-token-error"
+msgid "errors.invalid-recovery-token"
 msgstr "Token odzyskiwania jest nieprawidłowy."
 
 #: src/app/main/ui/auth/recovery.cljs
@@ -732,11 +732,11 @@ msgid "errors.email-has-permanent-bounces"
 msgstr "Email «%s» zawiera wiele stałych raportów o odrzuceniu."
 
 #: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs, src/app/main/ui/auth/recovery_request.cljs
-msgid "errors.email-invalid"
+msgid "errors.invalid-email"
 msgstr "Podaj prawidłowy adres e-mail"
 
 #: src/app/main/ui/settings/change_email.cljs
-msgid "errors.email-invalid-confirmation"
+msgid "errors.invalid-email-confirmation"
 msgstr "E-mail potwierdzający musi być zgodny"
 
 msgid "errors.email-spam-or-permanent-bounces"
diff --git a/frontend/translations/pt_BR.po b/frontend/translations/pt_BR.po
index e61276ac6..0e7625482 100644
--- a/frontend/translations/pt_BR.po
+++ b/frontend/translations/pt_BR.po
@@ -81,7 +81,7 @@ msgid "auth.new-password"
 msgstr "Digite uma nova senha"
 
 #: src/app/main/ui/auth/recovery.cljs
-msgid "auth.notifications.invalid-token-error"
+msgid "errors.invalid-recovery-token"
 msgstr "O código de recuperação é inválido."
 
 #: src/app/main/ui/auth/recovery.cljs
@@ -731,11 +731,11 @@ msgid "errors.email-has-permanent-bounces"
 msgstr "O e-mail «%s» tem muitos relatórios de devolução permanentes."
 
 #: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs, src/app/main/ui/auth/recovery_request.cljs
-msgid "errors.email-invalid"
+msgid "errors.invalid-email"
 msgstr "Por favor, insira um email válido"
 
 #: src/app/main/ui/settings/change_email.cljs
-msgid "errors.email-invalid-confirmation"
+msgid "errors.invalid-email-confirmation"
 msgstr "E-mail de confirmação deve ser o mesmo"
 
 msgid "errors.email-spam-or-permanent-bounces"
diff --git a/frontend/translations/pt_PT.po b/frontend/translations/pt_PT.po
index 011a7e5ba..20968b6b3 100644
--- a/frontend/translations/pt_PT.po
+++ b/frontend/translations/pt_PT.po
@@ -76,32 +76,12 @@ msgstr "LDAP"
 msgid "auth.login-with-oidc-submit"
 msgstr "OpenID Connect"
 
-#: src/app/main/ui/settings/team-form.cljs, src/app/main/ui/auth/register.cljs,
-#: src/app/main/ui/dashboard/team_form.cljs,
-#: src/app/main/ui/onboarding/team_choice.cljs,
-#: src/app/main/ui/settings/access_tokens.cljs,
-#: src/app/main/ui/settings/feedback.cljs,
-#: src/app/main/ui/settings/profile.cljs,
-#: src/app/main/ui/workspace/sidebar/assets.cljs
-msgid "auth.name.not-all-space"
-msgstr "O nome deve conter pelo menos um caractere que não seja um espaço."
-
-#: src/app/main/ui/auth/register.cljs,
-#: src/app/main/ui/dashboard/team_form.cljs,
-#: src/app/main/ui/onboarding/team_choice.cljs,
-#: src/app/main/ui/settings/access_tokens.cljs,
-#: src/app/main/ui/settings/feedback.cljs,
-#: src/app/main/ui/settings/profile.cljs,
-#: src/app/main/ui/workspace/sidebar/assets.cljs
-msgid "auth.name.too-long"
-msgstr "O nome deve conter um máximo de 250 caracteres."
-
 #: src/app/main/ui/auth/recovery.cljs
 msgid "auth.new-password"
 msgstr "Escreve uma nova palavra-passe"
 
 #: src/app/main/ui/auth/recovery.cljs
-msgid "auth.notifications.invalid-token-error"
+msgid "errors.invalid-recovery-token"
 msgstr "O token de recuperação é inválido."
 
 #: src/app/main/ui/auth/recovery.cljs
@@ -128,11 +108,6 @@ msgstr "Palavra-passe"
 msgid "auth.password-length-hint"
 msgstr "Mínimo de 8 caracteres"
 
-#: src/app/main/ui/auth/register.cljs
-msgid "auth.password-not-empty"
-msgstr ""
-"A palavra-passe deve conter pelo menos um caractere que não seja um espaço."
-
 msgid "auth.privacy-policy"
 msgstr "Política de privacidade"
 
@@ -291,7 +266,7 @@ msgid "dashboard.access-tokens.create"
 msgstr "Gerar novo token"
 
 #: src/app/main/ui/settings/access-tokens.cljs
-msgid "dashboard.access-tokens.create.success"
+msgid "dashboard.access-tokens.create-success"
 msgstr "Token de acesso criado com sucesso."
 
 #: src/app/main/ui/settings/access-tokens.cljs
@@ -302,10 +277,6 @@ msgstr "Clica no botão \"Gerar novo token\" para gerar um."
 msgid "dashboard.access-tokens.empty.no-access-tokens"
 msgstr "Ainda não tens nenhum token."
 
-#: src/app/main/ui/settings/access-tokens.cljs
-msgid "dashboard.access-tokens.errors-required-name"
-msgstr "O nome é obrigatório"
-
 #: src/app/main/ui/settings/access-tokens.cljs
 msgid "dashboard.access-tokens.expiration-180-days"
 msgstr "180 dias"
@@ -887,11 +858,11 @@ msgstr "O e-mail «%s» tem muitos relatórios de rejeição permanentes."
 
 #: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs,
 #: src/app/main/ui/auth/recovery_request.cljs
-msgid "errors.email-invalid"
+msgid "errors.invalid-email"
 msgstr "Por favor introduz um email válido"
 
 #: src/app/main/ui/settings/change_email.cljs
-msgid "errors.email-invalid-confirmation"
+msgid "errors.invalid-email-confirmation"
 msgstr "O e-mail de confirmação deve combinar"
 
 msgid "errors.email-spam-or-permanent-bounces"
@@ -2989,10 +2960,6 @@ msgstr "Aumentar zoom na lupa"
 msgid "shortcuts.zoom-selected"
 msgstr "Zoom para selecionados"
 
-#: src/app/main/ui/dashboard/team.cljs
-msgid "team.webhooks.max-length"
-msgstr "O nome do webhook deve conter um máximo de 2048 caracteres."
-
 #: src/app/main/ui/dashboard/files.cljs
 msgid "title.dashboard.files"
 msgstr "%s - Penpot"
diff --git a/frontend/translations/ro.po b/frontend/translations/ro.po
index 76fd8d962..5fe6786aa 100644
--- a/frontend/translations/ro.po
+++ b/frontend/translations/ro.po
@@ -77,20 +77,12 @@ msgstr "LDAP"
 msgid "auth.login-with-oidc-submit"
 msgstr "OpenID"
 
-#: src/app/main/ui/settings/team-form.cljs, src/app/main/ui/auth/register.cljs, src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/onboarding/team_choice.cljs, src/app/main/ui/settings/access_tokens.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/settings/profile.cljs, src/app/main/ui/workspace/sidebar/assets.cljs
-msgid "auth.name.not-all-space"
-msgstr "Numele trebuie să conțină un caracter altul decât spațiu."
-
-#: src/app/main/ui/auth/register.cljs, src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/onboarding/team_choice.cljs, src/app/main/ui/settings/access_tokens.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/settings/profile.cljs, src/app/main/ui/workspace/sidebar/assets.cljs
-msgid "auth.name.too-long"
-msgstr "Numele trebuie să conțină cel mult 250 caractere."
-
 #: src/app/main/ui/auth/recovery.cljs
 msgid "auth.new-password"
 msgstr "Introduceți o parolă nouă"
 
 #: src/app/main/ui/auth/recovery.cljs
-msgid "auth.notifications.invalid-token-error"
+msgid "errors.invalid-recovery-token"
 msgstr "Codul de recuperare nu este valid."
 
 #: src/app/main/ui/auth/recovery.cljs
@@ -119,10 +111,6 @@ msgstr "Parola"
 msgid "auth.password-length-hint"
 msgstr "Cel puțin 8 caractere"
 
-#: src/app/main/ui/auth/register.cljs
-msgid "auth.password-not-empty"
-msgstr "Parola trebuie să conțină un caracter altul decât spațiu."
-
 msgid "auth.privacy-policy"
 msgstr "Politica de Confidențialitate"
 
@@ -277,7 +265,7 @@ msgid "dashboard.access-tokens.create"
 msgstr "Generați jeton nou"
 
 #: src/app/main/ui/settings/access-tokens.cljs
-msgid "dashboard.access-tokens.create.success"
+msgid "dashboard.access-tokens.create-success"
 msgstr "Jeton de acces creat cu succes."
 
 #: src/app/main/ui/settings/access-tokens.cljs
@@ -288,10 +276,6 @@ msgstr "Apăsați butonul 'Generați jeton nou' pentru a genera unul."
 msgid "dashboard.access-tokens.empty.no-access-tokens"
 msgstr "Nu aveți încă jetoane."
 
-#: src/app/main/ui/settings/access-tokens.cljs
-msgid "dashboard.access-tokens.errors-required-name"
-msgstr "Numele este obligatoriu"
-
 #: src/app/main/ui/settings/access-tokens.cljs
 msgid "dashboard.access-tokens.expiration-180-days"
 msgstr "180 zile"
@@ -872,11 +856,11 @@ msgstr "Adresa de email «%s» are multe rapoarte permanente de respingere."
 
 #: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs,
 #: src/app/main/ui/auth/recovery_request.cljs
-msgid "errors.email-invalid"
+msgid "errors.invalid-email"
 msgstr "Vă rugăm să introduceți un e-mail valid"
 
 #: src/app/main/ui/settings/change_email.cljs
-msgid "errors.email-invalid-confirmation"
+msgid "errors.invalid-email-confirmation"
 msgstr "E-mailul de confirmare trebuie să se potrivească"
 
 msgid "errors.email-spam-or-permanent-bounces"
@@ -2893,10 +2877,6 @@ msgstr "Creștere obiectiv de zoom"
 msgid "shortcuts.zoom-selected"
 msgstr "Mărește la selecție"
 
-#: src/app/main/ui/dashboard/team.cljs
-msgid "team.webhooks.max-length"
-msgstr "Numele webhook-ului trebuie să conțină maxim 2048 caractere."
-
 #: src/app/main/ui/dashboard/files.cljs
 msgid "title.dashboard.files"
 msgstr "%s - Penpot"
diff --git a/frontend/translations/ru.po b/frontend/translations/ru.po
index 1b961ddca..fd26ca494 100644
--- a/frontend/translations/ru.po
+++ b/frontend/translations/ru.po
@@ -79,7 +79,7 @@ msgid "auth.new-password"
 msgstr "Введите новый пароль"
 
 #: src/app/main/ui/auth/recovery.cljs
-msgid "auth.notifications.invalid-token-error"
+msgid "errors.invalid-recovery-token"
 msgstr "Неверный код восстановления."
 
 #: src/app/main/ui/auth/recovery.cljs
@@ -745,7 +745,7 @@ msgid "errors.email-has-permanent-bounces"
 msgstr "Эл. почта «%s» постоянно недоступна."
 
 #: src/app/main/ui/settings/change_email.cljs
-msgid "errors.email-invalid-confirmation"
+msgid "errors.invalid-email-confirmation"
 msgstr "Эл. почта для подтверждения должна совпадать"
 
 msgid "errors.email-spam-or-permanent-bounces"
diff --git a/frontend/translations/ta.po b/frontend/translations/ta.po
index 6ed7b5118..b340c7d5d 100644
--- a/frontend/translations/ta.po
+++ b/frontend/translations/ta.po
@@ -81,7 +81,7 @@ msgid "auth.new-password"
 msgstr "புதிய கடவுச்சொல்லை உள்ளிடவும்"
 
 #: src/app/main/ui/auth/recovery.cljs
-msgid "auth.notifications.invalid-token-error"
+msgid "errors.invalid-recovery-token"
 msgstr "மீட்பு டோக்கன் செல்லுபடியாகாது."
 
 #: src/app/main/ui/auth/recovery.cljs
diff --git a/frontend/translations/tr.po b/frontend/translations/tr.po
index 92d9dfe7e..947bec4e6 100644
--- a/frontend/translations/tr.po
+++ b/frontend/translations/tr.po
@@ -86,20 +86,12 @@ msgstr "LDAP"
 msgid "auth.login-with-oidc-submit"
 msgstr "OpenID"
 
-#: src/app/main/ui/settings/team-form.cljs, src/app/main/ui/auth/register.cljs, src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/onboarding/team_choice.cljs, src/app/main/ui/settings/access_tokens.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/settings/profile.cljs, src/app/main/ui/workspace/sidebar/assets.cljs
-msgid "auth.name.not-all-space"
-msgstr "İsim boşluk dışında bir karakter içermelidir."
-
-#: src/app/main/ui/auth/register.cljs, src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/onboarding/team_choice.cljs, src/app/main/ui/settings/access_tokens.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/settings/profile.cljs, src/app/main/ui/workspace/sidebar/assets.cljs
-msgid "auth.name.too-long"
-msgstr "İsim en fazla 250 karakter içermelidir."
-
 #: src/app/main/ui/auth/recovery.cljs
 msgid "auth.new-password"
 msgstr "Yeni bir parola gir"
 
 #: src/app/main/ui/auth/recovery.cljs
-msgid "auth.notifications.invalid-token-error"
+msgid "errors.invalid-recovery-token"
 msgstr "Kurtarma jetonu geçerli değil."
 
 #: src/app/main/ui/auth/recovery.cljs
@@ -126,10 +118,6 @@ msgstr "Parola"
 msgid "auth.password-length-hint"
 msgstr "En az 8 karakter"
 
-#: src/app/main/ui/auth/register.cljs
-msgid "auth.password-not-empty"
-msgstr "Parola boşluk dışında bir karakter içermelidir."
-
 msgid "auth.privacy-policy"
 msgstr "Gizlilik politikası"
 
@@ -289,7 +277,7 @@ msgid "dashboard.access-tokens.create"
 msgstr "Yeni belirteç oluştur"
 
 #: src/app/main/ui/settings/access-tokens.cljs
-msgid "dashboard.access-tokens.create.success"
+msgid "dashboard.access-tokens.create-success"
 msgstr "Erişim belirteci başarıyla oluşturuldu."
 
 #: src/app/main/ui/settings/access-tokens.cljs
@@ -300,10 +288,6 @@ msgstr "Bir belirteç oluşturmak için \"Yeni belirteç oluştur\" düğmesine
 msgid "dashboard.access-tokens.empty.no-access-tokens"
 msgstr "Şu ana kadar hiç belirteciniz yok."
 
-#: src/app/main/ui/settings/access-tokens.cljs
-msgid "dashboard.access-tokens.errors-required-name"
-msgstr "İsim gereklidir"
-
 #: src/app/main/ui/settings/access-tokens.cljs
 msgid "dashboard.access-tokens.expiration-180-days"
 msgstr "180 gün"
@@ -895,11 +879,11 @@ msgstr "«%s» adresi için çok fazla geri dönme raporu var."
 
 #: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs,
 #: src/app/main/ui/auth/recovery_request.cljs
-msgid "errors.email-invalid"
+msgid "errors.invalid-email"
 msgstr "Lütfen geçerli bir e-posta adresi girin"
 
 #: src/app/main/ui/settings/change_email.cljs
-msgid "errors.email-invalid-confirmation"
+msgid "errors.invalid-email-confirmation"
 msgstr "Doğrulama e-postası eşleşmiyor"
 
 msgid "errors.email-spam-or-permanent-bounces"
@@ -3048,10 +3032,6 @@ msgstr "Görüntüyü büyült"
 msgid "shortcuts.zoom-selected"
 msgstr "Seçilene yakınlaştır"
 
-#: src/app/main/ui/dashboard/team.cljs
-msgid "team.webhooks.max-length"
-msgstr "Webhook adı en fazla 2048 karakter içermelidir."
-
 #: src/app/main/ui/dashboard/files.cljs
 msgid "title.dashboard.files"
 msgstr "%s - Penpot"
diff --git a/frontend/translations/ukr_UA.po b/frontend/translations/ukr_UA.po
index 3c7f50b16..5793f61d4 100644
--- a/frontend/translations/ukr_UA.po
+++ b/frontend/translations/ukr_UA.po
@@ -58,7 +58,7 @@ msgid "auth.new-password"
 msgstr "Введіть новий пароль"
 
 #: src/app/main/ui/auth/recovery.cljs
-msgid "auth.notifications.invalid-token-error"
+msgid "errors.invalid-recovery-token"
 msgstr "Невірний код відновлення."
 
 #: src/app/main/ui/auth/recovery.cljs
diff --git a/frontend/translations/yo.po b/frontend/translations/yo.po
index ab6e5baed..b0ef4b3b6 100644
--- a/frontend/translations/yo.po
+++ b/frontend/translations/yo.po
@@ -72,16 +72,12 @@ msgstr "LDAP"
 msgid "auth.login-with-oidc-submit"
 msgstr "ṣílẹ̀kuǹ ìdánimọ̀"
 
-#: src/app/main/ui/auth/register.cljs, src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/onboarding/team_choice.cljs, src/app/main/ui/settings/access_tokens.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/settings/profile.cljs, src/app/main/ui/workspace/sidebar/assets.cljs
-msgid "auth.name.too-long"
-msgstr "orúkọ kò gbọdọ̀ ju àádọ́jọ́ lẹ́tà lọ."
-
 #: src/app/main/ui/auth/recovery.cljs
 msgid "auth.new-password"
 msgstr "tẹ ọ̀rọ̀ ìgbaniwọlé tuntun"
 
 #: src/app/main/ui/auth/recovery.cljs
-msgid "auth.notifications.invalid-token-error"
+msgid "errors.invalid-recovery-token"
 msgstr "àmì àtúnwárí ti díbàjẹ́."
 
 #: src/app/main/ui/auth/recovery.cljs
@@ -110,10 +106,6 @@ msgstr "ọ̀rọ̀- ìgbaniwọlé"
 msgid "auth.password-length-hint"
 msgstr "kò gbọdọ̀ ju ohun kíkọ mẹ́jọ lọ"
 
-#: src/app/main/ui/auth/register.cljs
-msgid "auth.password-not-empty"
-msgstr "ọ̀rọ̀-ìgbaniwọlé gbọ́dọ̀ ní nǹkan kíkọ láìsí àlàfo."
-
 msgid "auth.privacy-policy"
 msgstr "ìpamọ ètò ìmúló"
 
@@ -252,7 +244,7 @@ msgid "dashboard.access-tokens.create"
 msgstr "ṣe ìpilẹ̀sẹ̀ àmì tókìnnì"
 
 #: src/app/main/ui/settings/access-tokens.cljs
-msgid "dashboard.access-tokens.create.success"
+msgid "dashboard.access-tokens.create-success"
 msgstr "ṣe àyẹ̀wò àmì tókìnnì tí o ṣẹ̀dá bó ṣeyẹ."
 
 #: src/app/main/ui/settings/access-tokens.cljs
@@ -263,10 +255,6 @@ msgstr "tẹ bọ́tìnnì \" ṣe ìpilẹ̀sẹ̀ àmì tókìnnì tuntun\"  l
 msgid "dashboard.access-tokens.empty.no-access-tokens"
 msgstr "o kò tí ì ní àmì tókínnì títí di ìsinsìn yìí."
 
-#: src/app/main/ui/settings/access-tokens.cljs
-msgid "dashboard.access-tokens.errors-required-name"
-msgstr "a nílò orúkọ"
-
 #: src/app/main/ui/settings/access-tokens.cljs
 msgid "dashboard.access-tokens.expiration-180-days"
 msgstr "ọgọ́saǹ-ań ọjọ́"
@@ -793,11 +781,11 @@ msgid "errors.email-has-permanent-bounces"
 msgstr "Ímeèlì  «%s» ti ní ìjábọ̀ ọ̀pọ̀ọlọpọ̀ ìta-bọn-ọ̀n ti pẹ́."
 
 #: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs, src/app/main/ui/auth/recovery_request.cljs
-msgid "errors.email-invalid"
+msgid "errors.invalid-email"
 msgstr "Tẹ àti wọlé pẹ̀lú ímeèlì tó wúlo jọ̀wọ́"
 
 #: src/app/main/ui/settings/change_email.cljs
-msgid "errors.email-invalid-confirmation"
+msgid "errors.invalid-email-confirmation"
 msgstr "Ímeèlì tí a ti mọ̀dájú gbọ́dọ̀ báramu"
 
 msgid "errors.email-spam-or-permanent-bounces"
@@ -2643,10 +2631,6 @@ msgstr "Lílọ̀soké lẹnsi sisun"
 msgid "shortcuts.zoom-selected"
 msgstr "Yiyan pelu sun-un"
 
-#: src/app/main/ui/dashboard/team.cljs
-msgid "team.webhooks.max-length"
-msgstr "Orúkọ̀ webhook kò gbọ́dọ̀ kọjà awọ́n óhun kíkọ́ 2048."
-
 #: src/app/main/ui/dashboard/files.cljs
 msgid "title.dashboard.files"
 msgstr "%s - Penpot"
diff --git a/frontend/translations/zh_CN.po b/frontend/translations/zh_CN.po
index 3abd8eddf..5603a8ae7 100644
--- a/frontend/translations/zh_CN.po
+++ b/frontend/translations/zh_CN.po
@@ -72,32 +72,12 @@ msgstr "LDAP登录"
 msgid "auth.login-with-oidc-submit"
 msgstr "OpenID登录"
 
-#: src/app/main/ui/settings/team-form.cljs, src/app/main/ui/auth/register.cljs,
-#: src/app/main/ui/dashboard/team_form.cljs,
-#: src/app/main/ui/onboarding/team_choice.cljs,
-#: src/app/main/ui/settings/access_tokens.cljs,
-#: src/app/main/ui/settings/feedback.cljs,
-#: src/app/main/ui/settings/profile.cljs,
-#: src/app/main/ui/workspace/sidebar/assets.cljs
-msgid "auth.name.not-all-space"
-msgstr "姓名必须包含一些空格以外的字符。"
-
-#: src/app/main/ui/auth/register.cljs,
-#: src/app/main/ui/dashboard/team_form.cljs,
-#: src/app/main/ui/onboarding/team_choice.cljs,
-#: src/app/main/ui/settings/access_tokens.cljs,
-#: src/app/main/ui/settings/feedback.cljs,
-#: src/app/main/ui/settings/profile.cljs,
-#: src/app/main/ui/workspace/sidebar/assets.cljs
-msgid "auth.name.too-long"
-msgstr "姓名最多包含250个字符。"
-
 #: src/app/main/ui/auth/recovery.cljs
 msgid "auth.new-password"
 msgstr "输入新的密码"
 
 #: src/app/main/ui/auth/recovery.cljs
-msgid "auth.notifications.invalid-token-error"
+msgid "errors.invalid-recovery-token"
 msgstr "恢复令牌无效。"
 
 #: src/app/main/ui/auth/recovery.cljs
@@ -124,10 +104,6 @@ msgstr "密码"
 msgid "auth.password-length-hint"
 msgstr "至少8位字符"
 
-#: src/app/main/ui/auth/register.cljs
-msgid "auth.password-not-empty"
-msgstr "密码必须包含一些空格以外的字符。"
-
 msgid "auth.privacy-policy"
 msgstr "隐私政策"
 
@@ -279,7 +255,7 @@ msgid "dashboard.access-tokens.create"
 msgstr "生成新令牌"
 
 #: src/app/main/ui/settings/access-tokens.cljs
-msgid "dashboard.access-tokens.create.success"
+msgid "dashboard.access-tokens.create-success"
 msgstr "成功创建访问令牌。"
 
 #: src/app/main/ui/settings/access-tokens.cljs
@@ -290,10 +266,6 @@ msgstr "点击“生成新令牌”按钮来生成一个。"
 msgid "dashboard.access-tokens.empty.no-access-tokens"
 msgstr "你目前还没有令牌。"
 
-#: src/app/main/ui/settings/access-tokens.cljs
-msgid "dashboard.access-tokens.errors-required-name"
-msgstr "名称是必填项"
-
 #: src/app/main/ui/settings/access-tokens.cljs
 msgid "dashboard.access-tokens.expiration-180-days"
 msgstr "180天"
@@ -850,11 +822,11 @@ msgstr "电子邮件“%s”收到了非常多的永久退信报告。"
 
 #: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs,
 #: src/app/main/ui/auth/recovery_request.cljs
-msgid "errors.email-invalid"
+msgid "errors.invalid-email"
 msgstr "请输入有效的电子邮件"
 
 #: src/app/main/ui/settings/change_email.cljs
-msgid "errors.email-invalid-confirmation"
+msgid "errors.invalid-email-confirmation"
 msgstr "确认电子邮件必须保持一致"
 
 msgid "errors.email-spam-or-permanent-bounces"
@@ -2877,10 +2849,6 @@ msgstr "变焦镜头放大"
 msgid "shortcuts.zoom-selected"
 msgstr "缩放到选定对象"
 
-#: src/app/main/ui/dashboard/team.cljs
-msgid "team.webhooks.max-length"
-msgstr "Webhook的名称最多包含2048个字符。"
-
 #: src/app/main/ui/dashboard/files.cljs
 msgid "title.dashboard.files"
 msgstr "%s - Penpot"
diff --git a/frontend/translations/zh_Hant.po b/frontend/translations/zh_Hant.po
index 724bddba8..0172859f3 100644
--- a/frontend/translations/zh_Hant.po
+++ b/frontend/translations/zh_Hant.po
@@ -77,7 +77,7 @@ msgid "auth.new-password"
 msgstr "輸入新密碼"
 
 #: src/app/main/ui/auth/recovery.cljs
-msgid "auth.notifications.invalid-token-error"
+msgid "errors.invalid-recovery-token"
 msgstr "此 Recovery token 是無效的。"
 
 #: src/app/main/ui/auth/recovery.cljs
@@ -684,11 +684,11 @@ msgid "errors.email-has-permanent-bounces"
 msgstr "電子郵件«%s»有許多永久退件報告。"
 
 #: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs, src/app/main/ui/auth/recovery_request.cljs
-msgid "errors.email-invalid"
+msgid "errors.invalid-email"
 msgstr "請輸入一個有效的電郵地址"
 
 #: src/app/main/ui/settings/change_email.cljs
-msgid "errors.email-invalid-confirmation"
+msgid "errors.invalid-email-confirmation"
 msgstr "電郵地址必須相同"
 
 msgid "errors.email-spam-or-permanent-bounces"
@@ -2343,7 +2343,7 @@ msgid "dashboard.access-tokens.empty.add-one"
 msgstr "按下\"產生新 Token\" 按鈕來產生一個。"
 
 #: src/app/main/ui/settings/access-tokens.cljs
-msgid "dashboard.access-tokens.create.success"
+msgid "dashboard.access-tokens.create-success"
 msgstr "已成功建立 Access Token。"
 
 #: src/app/main/ui/settings/access-tokens.cljs
@@ -2354,10 +2354,6 @@ msgstr "沒有到期時間"
 msgid "dashboard.access-tokens.copied-success"
 msgstr "已複製 Token"
 
-#: src/app/main/ui/settings/access-tokens.cljs
-msgid "dashboard.access-tokens.errors-required-name"
-msgstr "名稱是必填的"
-
 #: src/app/main/ui/settings/access-tokens.cljs
 msgid "dashboard.access-tokens.token-will-expire"
 msgstr "權杖將於 %s 到期"
@@ -2365,14 +2361,6 @@ msgstr "權杖將於 %s 到期"
 msgid "dashboard.export.options.merge.title"
 msgstr "將共享資料庫的內容加入檔案資料庫"
 
-#: src/app/main/ui/settings/team-form.cljs, src/app/main/ui/auth/register.cljs, src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/onboarding/team_choice.cljs, src/app/main/ui/settings/access_tokens.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/settings/profile.cljs, src/app/main/ui/workspace/sidebar/assets.cljs
-msgid "auth.name.not-all-space"
-msgstr "名稱內必須包含空白以外的文字。"
-
-#: src/app/main/ui/auth/register.cljs, src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/onboarding/team_choice.cljs, src/app/main/ui/settings/access_tokens.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/settings/profile.cljs, src/app/main/ui/workspace/sidebar/assets.cljs
-msgid "auth.name.too-long"
-msgstr "名稱最多包含 250 個字元。"
-
 #: src/app/main/ui/settings/access-tokens.cljs
 msgid "dashboard.access-tokens.expiration-90-days"
 msgstr "90 天"
@@ -2408,10 +2396,6 @@ msgstr "Penpot 是用於設計與開發協作,免費且開源的設計工具"
 msgid "branding-illustrations-marketing-pieces"
 msgstr "...品牌設計、插畫、行銷素材等。"
 
-#: src/app/main/ui/auth/register.cljs
-msgid "auth.password-not-empty"
-msgstr "密碼必須包含空白以外的字元。"
-
 #: src/app/main/ui/settings/access-tokens.cljs
 msgid "dashboard.access-tokens.personal"
 msgstr "個人存取權杖"

From 0fa8aca6e212cce6bed469cfab5704753239b3ea Mon Sep 17 00:00:00 2001
From: Andrey Antukh <niwi@niwi.nz>
Date: Thu, 20 Jun 2024 14:35:55 +0200
Subject: [PATCH 3/3] :sparkles: Add minor improvements to common.schema ns

---
 common/src/app/common/schema.cljc        | 105 +++++++++++++++++++++--
 frontend/src/app/main/ui/auth/login.cljs |  11 ---
 2 files changed, 97 insertions(+), 19 deletions(-)

diff --git a/common/src/app/common/schema.cljc b/common/src/app/common/schema.cljc
index 37636d5b2..425525ca3 100644
--- a/common/src/app/common/schema.cljc
+++ b/common/src/app/common/schema.cljc
@@ -21,6 +21,7 @@
    [cuerdas.core :as str]
    [malli.core :as m]
    [malli.dev.pretty :as mdp]
+   [malli.dev.virhe :as v]
    [malli.error :as me]
    [malli.generator :as mg]
    [malli.registry :as mr]
@@ -104,6 +105,10 @@
   [exp]
   (malli.error/error-value exp {:malli.error/mask-valid-values '...}))
 
+(defn optional-keys
+  [schema]
+  (mu/optional-keys schema default-options))
+
 (def default-transformer
   (let [default-decoder
         {:compile (fn [s _registry]
@@ -190,9 +195,10 @@
     (fn [v] (@vfn v))))
 
 (defn lazy-decoder
-  [s transformer]
-  (let [vfn (delay (decoder (if (delay? s) (deref s) s) transformer))]
-    (fn [v] (@vfn v))))
+  ([s] (lazy-decoder s default-transformer))
+  ([s transformer]
+   (let [vfn (delay (decoder (if (delay? s) (deref s) s) transformer))]
+     (fn [v] (@vfn v)))))
 
 (defn humanize-explain
   [{:keys [schema errors value]} & {:keys [length level]}]
@@ -207,9 +213,29 @@
                                      :level (d/nilv level 8)
                                      :length (d/nilv length 12)})))))
 
+
+(defmethod v/-format ::schemaless-explain
+  [_ {:keys [schema] :as explanation} printer]
+  {:body [:group
+          (v/-block "Value" (v/-visit (me/error-value explanation printer) printer) printer) :break :break
+          (v/-block "Errors" (v/-visit (me/humanize (me/with-spell-checking explanation)) printer) printer) :break :break
+          (v/-block "Schema" (v/-visit schema printer) printer)]})
+
+
+(defmethod v/-format ::explain
+  [_ {:keys [schema] :as explanation} printer]
+  {:body [:group
+          (v/-block "Value" (v/-visit (me/error-value explanation printer) printer) printer) :break :break
+          (v/-block "Errors" (v/-visit (me/humanize (me/with-spell-checking explanation)) printer) printer) :break :break
+          (v/-block "Schema" (v/-visit schema printer) printer)]})
+
+
 (defn pretty-explain
-  [s d]
-  (mdp/explain (schema s) d))
+  [explain & {:keys [variant message]
+              :or {variant ::explain
+                   message "Validation Error"}}]
+  (let [explain (fn [] (me/with-error-messages explain))]
+    ((mdp/prettifier variant message explain default-options))))
 
 (defmacro ignoring
   [expr]
@@ -297,7 +323,7 @@
        (throw (ex-info hint options))))))
 
 (defn validate-fn
-  "Create a predefined validate function"
+  "Create a predefined validate function that raises an expception"
   [s]
   (let [schema (if (lazy-schema? s) s (define s))]
     (partial fast-validate! schema)))
@@ -317,6 +343,7 @@
              hint    (get options :hint "schema validation error")]
          (throw (ex-info hint options)))))))
 
+;; FIXME: revisit
 (defn conform!
   [schema value]
   (assert (lazy-schema? schema) "expected `schema` to satisfy ILazySchema protocol")
@@ -476,11 +503,14 @@
 
 (define! ::set
   {:type :set
+   :min 0
+   :max 1
    :compile
-   (fn [{:keys [coerce kind max min] :as props} _ _]
+   (fn [{:keys [coerce kind max min] :as props} children _]
      (let [xform (if coerce
                    (comp non-empty-strings-xf (map coerce))
                    non-empty-strings-xf)
+           kind  (or (last children) kind)
            pred  (cond
                    (fn? kind)  kind
                    (nil? kind) any?
@@ -509,7 +539,8 @@
                            (every? pred value))))
 
                   :else
-                  pred)]
+                  (fn [value]
+                    (every? pred value)))]
 
        {:pred pred
         :type-properties
@@ -525,6 +556,64 @@
                          (let [v (if (string? v) (str/split v #"[\s,]+") v)]
                            (into #{} xform v)))}}))})
 
+
+(define! ::vec
+  {:type :vector
+   :min 0
+   :max 1
+   :compile
+   (fn [{:keys [coerce kind max min] :as props} children _]
+     (let [xform (if coerce
+                   (comp non-empty-strings-xf (map coerce))
+                   non-empty-strings-xf)
+
+           kind  (or (last children) kind)
+           pred  (cond
+                   (fn? kind)  kind
+                   (nil? kind) any?
+                   :else       (validator kind))
+
+           pred (cond
+                  (and max min)
+                  (fn [value]
+                    (let [size (count value)]
+                      (and (set? value)
+                           (<= min size max)
+                           (every? pred value))))
+
+                  min
+                  (fn [value]
+                    (let [size (count value)]
+                      (and (set? value)
+                           (<= min size)
+                           (every? pred value))))
+
+                  max
+                  (fn [value]
+                    (let [size (count value)]
+                      (and (set? value)
+                           (<= size max)
+                           (every? pred value))))
+
+                  :else
+                  (fn [value]
+                    (every? pred value)))]
+
+       {:pred pred
+        :type-properties
+        {:title "set"
+         :description "Set of Strings"
+         :error/message "should be a set of strings"
+         :gen/gen (-> kind sg/generator sg/set)
+         ::oapi/type "array"
+         ::oapi/format "set"
+         ::oapi/items {:type "string"}
+         ::oapi/unique-items true
+         ::oapi/decode (fn [v]
+                         (let [v (if (string? v) (str/split v #"[\s,]+") v)]
+                           (into [] xform v)))}}))})
+
+
 (define! ::set-of-strings
   {:type ::set-of-strings
    :pred #(and (set? %) (every? string? %))
diff --git a/frontend/src/app/main/ui/auth/login.cljs b/frontend/src/app/main/ui/auth/login.cljs
index 7324c7ec1..27add1c2e 100644
--- a/frontend/src/app/main/ui/auth/login.cljs
+++ b/frontend/src/app/main/ui/auth/login.cljs
@@ -9,7 +9,6 @@
   (:require
    [app.common.logging :as log]
    [app.common.schema :as sm]
-   [app.common.spec :as us]
    [app.config :as cf]
    [app.main.data.messages :as msg]
    [app.main.data.users :as du]
@@ -25,7 +24,6 @@
    [app.util.keyboard :as k]
    [app.util.router :as rt]
    [beicon.v2.core :as rx]
-   [cljs.spec.alpha :as s]
    [rumext.v2 :as mf]))
 
 (def show-alt-login-buttons?
@@ -64,14 +62,6 @@
                      :else
                      (st/emit! (msg/error (tr "errors.generic"))))))))
 
-(s/def ::email ::us/email)
-(s/def ::password ::us/not-empty-string)
-(s/def ::invitation-token ::us/not-empty-string)
-
-(s/def ::login-form
-  (s/keys :req-un [::email ::password]
-          :opt-un [::invitation-token]))
-
 (def ^:private schema:login-form
   [:map {:title "LoginForm"}
    [:email [::sm/email {:error/code "errors.invalid-email"}]]
@@ -84,7 +74,6 @@
   (let [initial (mf/with-memo [params] params)
         error   (mf/use-state false)
         form    (fm/use-form :schema schema:login-form
-                             ;; :validators [handle-error-messages]
                              :initial initial)
 
         on-error