diff --git a/.circleci/config.yml b/.circleci/config.yml index ef402408fb..0fc2473ce9 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -13,7 +13,7 @@ jobs: environment: POSTGRES_USER: penpot_test POSTGRES_PASSWORD: penpot_test - POSTGRES_DB: penpot + POSTGRES_DB: penpot_test - image: circleci/redis:6.0.8 @@ -45,10 +45,10 @@ jobs: name: backend test command: "clojure -M:dev:tests" environment: - PENPOT_DATABASE_URI: "postgresql://localhost/penpot" - PENPOT_DATABASE_USERNAME: penpot_test - PENPOT_DATABASE_PASSWORD: penpot_test - PENPOT_REDIS_URI: "redis://localhost/1" + PENPOT_TEST_DATABASE_URI: "postgresql://localhost/penpot_test" + PENPOT_TEST_DATABASE_USERNAME: penpot_test + PENPOT_TEST_DATABASE_PASSWORD: penpot_test + PENPOT_TEST_REDIS_URI: "redis://localhost/1" - run: working_directory: "./frontend" @@ -57,7 +57,8 @@ jobs: yarn install npx shadow-cljs compile tests environment: - PATH: /usr/local/nodejs/bin/:/usr/local/bin:/bin:/usr/bin + JAVA_HOME: /usr/lib/jvm/openjdk16 + PATH: /usr/local/nodejs/bin/:/usr/local/bin:/bin:/usr/bin:/usr/lib/jvm/openjdk16/bin - save_cache: paths: diff --git a/.gitignore b/.gitignore index f848ed7cc5..0bf57fa623 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,7 @@ node_modules /backend/dist/ /backend/logs/ /backend/- +/telemetry/ /frontend/npm-debug.log /frontend/target/ /frontend/dist/ diff --git a/.gitpod.yml b/.gitpod.yml new file mode 100644 index 0000000000..0735ba2dd8 --- /dev/null +++ b/.gitpod.yml @@ -0,0 +1,105 @@ +image: + file: docker/gitpod/Dockerfile + +ports: + # nginx + - port: 3449 + onOpen: open-preview + + # frontend nREPL + - port: 3447 + onOpen: ignore + visibility: private + + # frontend shadow server + - port: 3448 + onOpen: ignore + visibility: private + + # backend + - port: 6060 + onOpen: ignore + + # exporter shadow server + - port: 9630 + onOpen: ignore + visibility: private + + # exporter http server + - port: 6061 + onOpen: ignore + + # mailhog web interface + - port: 8025 + onOpen: ignore + + # mailhog postfix + - port: 1025 + onOpen: ignore + + # postgres + - port: 5432 + onOpen: ignore + + # redis + - port: 6379 + onOpen: ignore + + # openldap + - port: 389 + onOpen: ignore + +tasks: + # https://github.com/gitpod-io/gitpod/issues/666#issuecomment-534347856 + - name: gulp + command: > + cd $GITPOD_REPO_ROOT/frontend/; + yarn && gp sync-done 'frontend-yarn'; + npx gulp --theme=${PENPOT_THEME} watch + + - name: frontend shadow watch + command: > + cd $GITPOD_REPO_ROOT/frontend/; + gp sync-await 'frontend-yarn'; + npx shadow-cljs watch main + + - init: gp await-port 5432 && psql -f $GITPOD_REPO_ROOT/docker/gitpod/files/postgresql_init.sql + name: backend + command: > + cd $GITPOD_REPO_ROOT/backend/; + ./scripts/start-dev + + - name: exporter shadow watch + command: + cd $GITPOD_REPO_ROOT/exporter/; + gp sync-await 'frontend-yarn'; + yarn && npx shadow-cljs watch main + + - name: exporter web server + command: > + cd $GITPOD_REPO_ROOT/exporter/; + ./scripts/wait-and-start.sh + + - name: signed terminal + before: > + [[ ! -z ${GNUGPG} ]] && + cd ~ && + rm -rf .gnupg && + echo ${GNUGPG} | base64 -d | tar --no-same-owner -xzvf - + init: > + [[ ! -z ${GNUGPG_KEY} ]] && + git config --global commit.gpgsign true && + git config --global user.signingkey ${GNUGPG_KEY} + command: cd $GITPOD_REPO_ROOT + + - name: redis + command: redis-server + + - before: go get github.com/mailhog/MailHog + name: mailhog + command: MailHog + + - name: Nginx + command: > + nginx && + multitail /var/log/nginx/access.log -I /var/log/nginx/error.log diff --git a/CHANGES.md b/CHANGES.md index 65cd4e7f42..d45f28d5c1 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -3,13 +3,57 @@ ## :rocket: Next ### :sparkles: New features +### :bug: Bugs fixed +### :arrow_up: Deps updates +### :boom: Breaking changes +### :heart: Community contributions by (Thank you!) + + +## 1.5.0-alpha + +### :sparkles: New features + +- Add integration with gitpod.io (an online IDE) [#807](https://github.com/penpot/penpot/pull/807) +- Allow basic math operations in inputs [Taiga 1383](https://tree.taiga.io/project/penpot/us/1383) +- Autocomplete color names in hex inputs [Taiga 1596](https://tree.taiga.io/project/penpot/us/1596) +- Allow to group assets (components and graphics) [Taiga #1289](https://tree.taiga.io/project/penpot/us/1289) +- Change icon of pinned projects [Taiga 1298](https://tree.taiga.io/project/penpot/us/1298) +- Internal: refactor of http client, replace internal xhr usage with more modern Fetch API. +- New features for paths: snap points on edition, add/remove nodes, merge/join/split nodes. [Taiga #907](https://tree.taiga.io/project/penpot/us/907) +- Add OpenID-Connect support. +- Reimplement social auth providers on top of the generic openid impl. ### :bug: Bugs fixed +- Fix problem with pan and space [#811](https://github.com/penpot/penpot/issues/811) +- Fix issue when parsing exponential numbers in paths +- Remove legacy system user and team [#843](https://github.com/penpot/penpot/issues/843) +- Fix ordering of copy pasted objects [Taiga #1618](https://tree.taiga.io/project/penpot/issue/1617) +- Fix problems with blending modes [#837](https://github.com/penpot/penpot/issues/837) +- Fix problem with zoom an selection rect [#845](https://github.com/penpot/penpot/issues/845) +- Fix problem displaying team statistics [#859](https://github.com/penpot/penpot/issues/859) +- Fix problems with text editor selection [Taiga #1546](https://tree.taiga.io/project/penpot/issue/1546) +- Fix problem when opening the context menu in dashboard at the bottom [#856](https://github.com/penpot/penpot/issues/856) +- Fix problem when clicking an interactive group in view mode [#863](https://github.com/penpot/penpot/issues/863) +- Fix visibility of pages in sitemap when changing page [Taiga #1618](https://tree.taiga.io/project/penpot/issue/1618) +- Fix visual problem with group invite [Taiga #1290](https://tree.taiga.io/project/penpot/issue/1290) +- Fix issues with promote owner panel [Taiga #763](https://tree.taiga.io/project/penpot/issue/763) +- Allow use library colors when defining gradients [Taiga #1614](https://tree.taiga.io/project/penpot/issue/1614) +- Fix group selrect not updating after alignment [#895](https://github.com/penpot/penpot/issues/895) + ### :arrow_up: Deps updates +### :boom: Breaking changes + +- Translations refactor: now penpot uses gettext instead of a custom + JSON, and each locale has its own separated file. All translations + should be contributed via the weblate.org service. + ### :heart: Community contributions by (Thank you!) +- madmath03 (by [Monogramm](https://github.com/Monogramm)) [#807](https://github.com/penpot/penpot/pull/807) +- zzkt [#814](https://github.com/penpot/penpot/pull/814) + ## 1.4.1-alpha diff --git a/README.md b/README.md index 6d842a8d8c..29d90c1dd6 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ [![License: MPL-2.0][uri_license_image]][uri_license] [![Gitter](https://badges.gitter.im/sereno-xyz/community.svg)](https://gitter.im/penpot/community) [![Managed with Taiga.io](https://img.shields.io/badge/managed%20with-TAIGA.io-709f14.svg)](https://tree.taiga.io/project/penpot/ "Managed with Taiga.io") +[![Gitpod ready-to-code](https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/penpot/penpot) # PENPOT # @@ -39,4 +40,6 @@ Please refer to the [help center](https://help.penpot.app). This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +Copyright (c) UXBOX Labs SL ``` diff --git a/backend/deps.edn b/backend/deps.edn index 196bb99208..5659d1a62c 100644 --- a/backend/deps.edn +++ b/backend/deps.edn @@ -4,10 +4,10 @@ "jcenter" {:url "https://jcenter.bintray.com/"}} :deps {org.clojure/clojure {:mvn/version "1.10.3"} - org.clojure/clojurescript {:mvn/version "1.10.773"} - org.clojure/data.json {:mvn/version "1.1.0"} + org.clojure/data.json {:mvn/version "2.2.1"} org.clojure/core.async {:mvn/version "1.3.610"} org.clojure/tools.cli {:mvn/version "1.0.206"} + org.clojure/clojurescript {:mvn/version "1.10.844"} ;; Logging org.clojure/tools.logging {:mvn/version "1.1.0"} @@ -15,12 +15,12 @@ org.apache.logging.log4j/log4j-core {:mvn/version "2.14.1"} org.apache.logging.log4j/log4j-web {:mvn/version "2.14.1"} org.apache.logging.log4j/log4j-jul {:mvn/version "2.14.1"} - org.apache.logging.log4j/log4j-slf4j-impl {:mvn/version "2.14.1"} - org.slf4j/slf4j-api {:mvn/version "1.7.30"} + org.apache.logging.log4j/log4j-slf4j18-impl {:mvn/version "2.14.1"} + org.slf4j/slf4j-api {:mvn/version "2.0.0-alpha1"} org.zeromq/jeromq {:mvn/version "0.5.2"} com.taoensso/nippy {:mvn/version "3.1.1"} - com.github.luben/zstd-jni {:mvn/version "1.4.9-1"} + com.github.luben/zstd-jni {:mvn/version "1.4.9-5"} ;; NOTE: don't upgrade to latest version, breaking change is ;; introduced on 0.10.0 that suffixes counters with _total if they @@ -36,10 +36,10 @@ expound/expound {:mvn/version "0.8.9"} com.cognitect/transit-clj {:mvn/version "1.0.324"} - io.lettuce/lettuce-core {:mvn/version "6.0.2.RELEASE"} + io.lettuce/lettuce-core {:mvn/version "6.1.1.RELEASE"} java-http-clj/java-http-clj {:mvn/version "0.4.2"} - info.sunng/ring-jetty9-adapter {:mvn/version "0.15.0"} + info.sunng/ring-jetty9-adapter {:mvn/version "0.15.1"} com.github.seancorfield/next.jdbc {:mvn/version "1.1.646"} metosin/reitit-ring {:mvn/version "0.5.12"} metosin/jsonista {:mvn/version "0.3.1"} @@ -64,12 +64,12 @@ org.im4java/im4java {:mvn/version "1.4.0"} org.lz4/lz4-java {:mvn/version "1.7.1"} commons-io/commons-io {:mvn/version "2.8.0"} - com.sun.mail/jakarta.mail {:mvn/version "2.0.0"} + com.sun.mail/jakarta.mail {:mvn/version "2.0.1"} org.clojars.pntblnk/clj-ldap {:mvn/version "0.0.17"} integrant/integrant {:mvn/version "0.8.0"} - software.amazon.awssdk/s3 {:mvn/version "2.16.19"} + software.amazon.awssdk/s3 {:mvn/version "2.16.44"} ;; exception printing io.aviso/pretty {:mvn/version "0.1.37"} @@ -96,7 +96,7 @@ :main-opts ["-m" "kaocha.runner"]} :outdated - {:extra-deps {com.github.liquidz/antq {:mvn/version "0.12.0"}} + {:extra-deps {com.github.liquidz/antq {:mvn/version "RELEASE"}} :main-opts ["-m" "antq.core"]} :jmx-remote diff --git a/backend/dev/user.clj b/backend/dev/user.clj index d7ee62abf9..fc3fe94bd8 100644 --- a/backend/dev/user.clj +++ b/backend/dev/user.clj @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2016-2020 Andrey Antukh +;; Copyright (c) UXBOX Labs SL (ns user (:require @@ -70,7 +67,7 @@ [] (alter-var-root #'system (fn [sys] (when sys (ig/halt! sys)) - (-> (main/build-system-config cfg/config) + (-> main/system-config (ig/prep) (ig/init)))) :started) diff --git a/backend/resources/emails/invite-to-team/en.subj b/backend/resources/emails/invite-to-team/en.subj index 1ff46884ac..d4d00f7316 100644 --- a/backend/resources/emails/invite-to-team/en.subj +++ b/backend/resources/emails/invite-to-team/en.subj @@ -1 +1 @@ -Inviation to join {{team}} \ No newline at end of file +Invitation to join {{team}} diff --git a/backend/resources/log4j2-devenv.xml b/backend/resources/log4j2-devenv.xml index 3c18f6bd3f..eb38cb183b 100644 --- a/backend/resources/log4j2-devenv.xml +++ b/backend/resources/log4j2-devenv.xml @@ -6,7 +6,7 @@ - + @@ -32,6 +32,10 @@ + + + + diff --git a/backend/resources/log4j2.xml b/backend/resources/log4j2.xml index 660f8075ab..96750c0456 100644 --- a/backend/resources/log4j2.xml +++ b/backend/resources/log4j2.xml @@ -14,6 +14,10 @@ + + + + diff --git a/backend/scripts/build b/backend/scripts/build index ec65f4ea34..f1d9e03a47 100755 --- a/backend/scripts/build +++ b/backend/scripts/build @@ -4,9 +4,6 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; ;; Copyright (c) UXBOX Labs SL (ns build diff --git a/backend/scripts/repl b/backend/scripts/repl index 6a21835b12..32fd12d42f 100755 --- a/backend/scripts/repl +++ b/backend/scripts/repl @@ -2,7 +2,7 @@ export PENPOT_ASSERTS_ENABLED=true -export OPTIONS="-A:jmx-remote:dev -J-Dclojure.tools.logging.factory=clojure.tools.logging.impl/log4j2-factory -J-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager -J-Xms512m -J-Xmx512m -J-Dlog4j2.configurationFile=log4j2-devenv.xml" +export OPTIONS="-A:jmx-remote:dev -J-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager -J-Xms512m -J-Xmx512m -J-Dlog4j2.configurationFile=log4j2-devenv.xml" export OPTIONS_EVAL="nil" # export OPTIONS_EVAL="(set! *warn-on-reflection* true)" diff --git a/backend/src/app/cli/fixtures.clj b/backend/src/app/cli/fixtures.clj index 414ea90ae1..cd09769cbe 100644 --- a/backend/src/app/cli/fixtures.clj +++ b/backend/src/app/cli/fixtures.clj @@ -2,23 +2,19 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.cli.fixtures "A initial fixtures." (:require [app.common.pages :as cp] [app.common.uuid :as uuid] - [app.config :as cfg] [app.db :as db] [app.main :as main] [app.rpc.mutations.profile :as profile] [app.util.blob :as blob] + [app.util.logging :as l] [buddy.hashers :as hashers] - [clojure.tools.logging :as log] [integrant.core :as ig])) (defn- mk-uuid @@ -75,7 +71,9 @@ (let [rng (java.util.Random. 1)] (letfn [(create-profile [conn index] (let [id (mk-uuid "profile" index) - _ (log/info "create profile" index id) + _ (l/info :action "create profile" + :index index + :id id) prof (register-profile conn {:id id @@ -91,20 +89,22 @@ prof)) (create-profiles [conn] - (log/info "create profiles") + (l/info :action "create profiles") (collect (partial create-profile conn) (range (:num-profiles opts)))) (create-team [conn index] (let [id (mk-uuid "team" index) name (str "Team" index)] - (log/info "create team" index id) + (l/info :action "create team" + :index index + :id id) (db/insert! conn :team {:id id :name name}) id)) (create-teams [conn] - (log/info "create teams") + (l/info :action "create teams") (collect (partial create-team conn) (range (:num-teams opts)))) @@ -112,7 +112,9 @@ (let [id (mk-uuid "file" project-id index) name (str "file" index) data (cp/make-file-data id)] - (log/info "create file" index id) + (l/info :action "create file" + :index index + :id id) (db/insert! conn :file {:id id :data (blob/encode data) @@ -127,7 +129,7 @@ id)) (create-files [conn owner-id project-id] - (log/info "create files") + (l/info :action "create files") (run! (partial create-file conn owner-id project-id) (range (:num-files-per-project opts)))) @@ -139,7 +141,9 @@ (str "project " index) "Drafts") is-default (nil? index)] - (log/info "create project" index id) + (l/info :action "create project" + :index index + :id id) (db/insert! conn :project {:id id :team-id team-id @@ -154,7 +158,7 @@ id)) (create-projects [conn team-id profile-ids] - (log/info "create projects") + (l/info :action "create projects") (let [owner-id (rng-nth rng profile-ids) project-ids (conj (collect (partial create-project conn team-id owner-id) @@ -171,14 +175,16 @@ :can-edit true})) (setup-team [conn team-id profile-ids] - (log/info "setup team" team-id profile-ids) + (l/info :action "setup team" + :team-id team-id + :profile-ids (pr-str profile-ids)) (assign-profile-to-team conn team-id true (first profile-ids)) (run! (partial assign-profile-to-team conn team-id false) (rest profile-ids)) (create-projects conn team-id profile-ids)) (assign-teams-and-profiles [conn teams profiles] - (log/info "assign teams and profiles") + (l/info :action "assign teams and profiles") (loop [team-id (first teams) teams (rest teams)] (when-not (nil? team-id) @@ -195,7 +201,9 @@ project-id (:default-project-id owner) data (cp/make-file-data id)] - (log/info "create draft file" index id) + (l/info :action "create draft file" + :index index + :id id) (db/insert! conn :file {:id id :data (blob/encode data) @@ -233,7 +241,7 @@ (defn run [{:keys [preset] :or {preset :small}}] - (let [config (select-keys (main/build-system-config cfg/config) + (let [config (select-keys main/system-config [:app.db/pool :app.telemetry/migrations :app.migrations/migrations @@ -245,6 +253,6 @@ (try (run-in-system system preset) (catch Exception e - (log/errorf e "unhandled exception")) + (l/error :hint "unhandled exception" :cause e)) (finally (ig/halt! system))))) diff --git a/backend/src/app/cli/manage.clj b/backend/src/app/cli/manage.clj index 36093375e4..29975d0a9d 100644 --- a/backend/src/app/cli/manage.clj +++ b/backend/src/app/cli/manage.clj @@ -2,22 +2,18 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2021 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.cli.manage "A manage cli api." (:require - [app.config :as cfg] [app.db :as db] [app.main :as main] [app.rpc.mutations.profile :as profile] [app.rpc.queries.profile :refer [retrieve-profile-data-by-email]] + [app.util.logging :as l] [clojure.string :as str] [clojure.tools.cli :refer [parse-opts]] - [clojure.tools.logging :as log] [integrant.core :as ig]) (:import java.io.Console)) @@ -26,7 +22,7 @@ (defn init-system [] - (let [data (-> (main/build-system-config cfg/config) + (let [data (-> main/system-config (select-keys [:app.db/pool :app.metrics/metrics]) (assoc :app.migrations/all {}))] (-> data ig/prep ig/init))) @@ -35,7 +31,7 @@ [{:keys [label type] :or {type :text}}] (let [^Console console (System/console)] (when-not console - (log/error "no console found, can proceed") + (l/error :hint "no console found, can proceed") (System/exit 1)) (binding [*out* (.writer console)] diff --git a/backend/src/app/cli/migrate_media.clj b/backend/src/app/cli/migrate_media.clj index 4afaf79b2e..0d627afdc9 100644 --- a/backend/src/app/cli/migrate_media.clj +++ b/backend/src/app/cli/migrate_media.clj @@ -2,19 +2,16 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.cli.migrate-media (:require [app.common.media :as cm] - [app.config :as cfg] + [app.config :as cf] [app.db :as db] [app.main :as main] [app.storage :as sto] - [clojure.tools.logging :as log] + [app.util.logging :as l] [cuerdas.core :as str] [datoteka.core :as fs] [integrant.core :as ig])) @@ -34,7 +31,7 @@ (defn run [] - (let [config (select-keys (main/build-system-config cfg/config) + (let [config (select-keys main/system-config [:app.db/pool :app.migrations/migrations :app.metrics/metrics @@ -49,7 +46,7 @@ (run-in-system) (ig/halt!)) (catch Exception e - (log/errorf e "Unhandled exception."))))) + (l/error :hint "unhandled exception" :cause e))))) ;; --- IMPL @@ -60,7 +57,7 @@ (->> (db/exec! conn ["select * from profile"]) (filter #(not (str/empty? (:photo %)))) (seq)))] - (let [base (fs/path (:storage-fs-old-directory cfg/config)) + (let [base (fs/path (cf/get :storage-fs-old-directory)) storage (-> (:app.storage/storage system) (assoc :conn conn))] (doseq [profile (retrieve-profiles conn)] @@ -81,7 +78,7 @@ (->> (db/exec! conn ["select * from team"]) (filter #(not (str/empty? (:photo %)))) (seq)))] - (let [base (fs/path (:storage-fs-old-directory cfg/config)) + (let [base (fs/path (cf/get :storage-fs-old-directory)) storage (-> (:app.storage/storage system) (assoc :conn conn))] (doseq [team (retrieve-teams conn)] @@ -105,7 +102,7 @@ from file_media_object as fmo join file_media_thumbnail as fth on (fth.media_object_id = fmo.id)"]) (seq)))] - (let [base (fs/path (:storage-fs-old-directory cfg/config)) + (let [base (fs/path (cf/get :storage-fs-old-directory)) storage (-> (:app.storage/storage system) (assoc :conn conn))] (doseq [mobj (retrieve-media-objects conn)] diff --git a/backend/src/app/config.clj b/backend/src/app/config.clj index 51a7347644..0e9d4ac5dd 100644 --- a/backend/src/app/config.clj +++ b/backend/src/app/config.clj @@ -2,9 +2,6 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; ;; Copyright (c) UXBOX Labs SL (ns app.config @@ -16,10 +13,19 @@ [app.util.time :as dt] [clojure.core :as c] [clojure.java.io :as io] + [clojure.pprint :as pprint] [clojure.spec.alpha :as s] [cuerdas.core :as str] [environ.core :refer [env]])) +(prefer-method print-method + clojure.lang.IRecord + clojure.lang.IDeref) + +(prefer-method pprint/simple-dispatch + clojure.lang.IPersistentMap + clojure.lang.IDeref) + (def defaults {:http-server-port 6060 :host "devenv" @@ -99,9 +105,17 @@ (s/def ::gitlab-client-secret ::us/string) (s/def ::google-client-id ::us/string) (s/def ::google-client-secret ::us/string) +(s/def ::oidc-client-id ::us/string) +(s/def ::oidc-client-secret ::us/string) +(s/def ::oidc-base-uri ::us/string) +(s/def ::oidc-token-uri ::us/string) +(s/def ::oidc-auth-uri ::us/string) +(s/def ::oidc-user-uri ::us/string) +(s/def ::oidc-scopes ::us/set-of-str) +(s/def ::oidc-roles ::us/set-of-str) +(s/def ::oidc-roles-attr ::us/keyword) (s/def ::host ::us/string) (s/def ::http-server-port ::us/integer) -(s/def ::http-session-cookie-name ::us/string) (s/def ::http-session-idle-max-age ::dt/duration) (s/def ::http-session-updater-batch-max-age ::dt/duration) (s/def ::http-session-updater-batch-max-size ::us/integer) @@ -172,6 +186,15 @@ ::gitlab-client-secret ::google-client-id ::google-client-secret + ::oidc-client-id + ::oidc-client-secret + ::oidc-base-uri + ::oidc-token-uri + ::oidc-auth-uri + ::oidc-user-uri + ::oidc-scopes + ::oidc-roles-attr + ::oidc-roles ::host ::http-server-port ::http-session-idle-max-age @@ -222,42 +245,33 @@ ::telemetry-server-enabled ::telemetry-server-port ::telemetry-uri + ::telemetry-referer ::telemetry-with-taiga ::tenant])) -(defn- env->config - [env] - (reduce-kv - (fn [acc k v] - (cond-> acc - (str/starts-with? (name k) "penpot-") - (assoc (keyword (subs (name k) 7)) v) - - (str/starts-with? (name k) "app-") - (assoc (keyword (subs (name k) 4)) v))) - {} - env)) +(defn read-env + [prefix] + (let [prefix (str prefix "-") + len (count prefix)] + (reduce-kv + (fn [acc k v] + (cond-> acc + (str/starts-with? (name k) prefix) + (assoc (keyword (subs (name k) len)) v))) + {} + env))) (defn- read-config - [env] - (->> (env->config env) + [] + (->> (read-env "penpot") (merge defaults) (us/conform ::config))) -(defn- read-test-config - [env] - (merge {:redis-uri "redis://redis/1" - :database-uri "postgresql://postgres/penpot_test" - :storage-fs-directory "/tmp/app/storage" - :migrations-verbose false} - (read-config env))) - (def version (v/parse (or (some-> (io/resource "version.txt") (slurp) (str/trim)) "%version%"))) -(def config (read-config env)) -(def test-config (read-test-config env)) +(def config (atom (read-config))) (def deletion-delay (dt/duration {:days 7})) @@ -265,6 +279,9 @@ (defn get "A configuration getter. Helps code be more testable." ([key] - (c/get config key)) + (c/get @config key)) ([key default] - (c/get config key default))) + (c/get @config key default))) + +;; Set value for all new threads bindings. +(alter-var-root #'*assert* (constantly (get :asserts-enabled))) diff --git a/backend/src/app/db.clj b/backend/src/app/db.clj index 7791190c23..32a91e8d66 100644 --- a/backend/src/app/db.clj +++ b/backend/src/app/db.clj @@ -2,25 +2,23 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.db (:require + [app.common.data :as d] [app.common.exceptions :as ex] [app.common.geom.point :as gpt] [app.common.spec :as us] [app.db.sql :as sql] [app.metrics :as mtx] [app.util.json :as json] + [app.util.logging :as l] [app.util.migrations :as mg] [app.util.time :as dt] [app.util.transit :as t] [clojure.java.io :as io] [clojure.spec.alpha :as s] - [clojure.tools.logging :as log] [integrant.core :as ig] [next.jdbc :as jdbc] [next.jdbc.date-time :as jdbc-dt]) @@ -48,8 +46,8 @@ (declare instrument-jdbc!) +(s/def ::name keyword?) (s/def ::uri ::us/not-empty-string) -(s/def ::name ::us/not-empty-string) (s/def ::min-pool-size ::us/integer) (s/def ::max-pool-size ::us/integer) (s/def ::migrations map?) @@ -59,14 +57,16 @@ (defmethod ig/init-key ::pool [_ {:keys [migrations metrics] :as cfg}] - (log/infof "initialize connection pool '%s' with uri '%s'" (:name cfg) (:uri cfg)) + (l/info :action "initialize connection pool" + :name (d/name (:name cfg)) + :uri (:uri cfg)) (instrument-jdbc! (:registry metrics)) (let [pool (create-pool cfg)] (when (seq migrations) (with-open [conn ^AutoCloseable (open pool)] (mg/setup! conn) - (doseq [[mname steps] migrations] - (mg/migrate! conn {:name (name mname) :steps steps})))) + (doseq [[name steps] migrations] + (mg/migrate! conn {:name (d/name name) :steps steps})))) pool)) (defmethod ig/halt-key! ::pool @@ -100,7 +100,7 @@ mtf (PrometheusMetricsTrackerFactory. (:registry metrics))] (doto config (.setJdbcUrl (str "jdbc:" dburi)) - (.setPoolName (:name cfg "default")) + (.setPoolName (d/name (:name cfg))) (.setAutoCommit true) (.setReadOnly false) (.setConnectionTimeout 8000) ;; 8seg diff --git a/backend/src/app/db/sql.clj b/backend/src/app/db/sql.clj index e82d8e0773..d2c92db383 100644 --- a/backend/src/app/db/sql.clj +++ b/backend/src/app/db/sql.clj @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.db.sql (:refer-clojure :exclude [update]) diff --git a/backend/src/app/emails.clj b/backend/src/app/emails.clj index 74fbaf84bf..9033ca7145 100644 --- a/backend/src/app/emails.clj +++ b/backend/src/app/emails.clj @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020-2021 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.emails "Main api for send emails." @@ -14,18 +11,13 @@ [app.config :as cfg] [app.db :as db] [app.db.sql :as sql] - [app.tasks :as tasks] [app.util.emails :as emails] - [clojure.spec.alpha :as s])) + [app.util.logging :as l] + [app.worker :as wrk] + [clojure.spec.alpha :as s] + [integrant.core :as ig])) -;; --- Defaults - -(defn default-context - [] - {:assets-uri (:assets-uri cfg/config) - :public-uri (:public-uri cfg/config)}) - -;; --- Public API +;; --- PUBLIC API (defn render [email-factory context] @@ -33,17 +25,20 @@ (defn send! "Schedule the email for sending." - [conn email-factory context] - (us/verify fn? email-factory) - (us/verify map? context) - (let [email (email-factory context)] - (tasks/submit! conn {:name "sendmail" - :delay 0 - :max-retries 1 - :priority 200 - :props email}))) + [{:keys [::conn ::factory] :as context}] + (us/verify fn? factory) + (us/verify some? conn) + (let [email (factory context)] + (wrk/submit! (assoc email + ::wrk/task :sendmail + ::wrk/delay 0 + ::wrk/max-retries 1 + ::wrk/priority 200 + ::wrk/conn conn)))) +;; --- BOUNCE/COMPLAINS HANDLING + (def sql:profile-complaint-report "select (select count(*) from profile_complaint_report @@ -91,7 +86,7 @@ (>= (count reports) threshold)))) -;; --- Emails +;; --- EMAIL FACTORIES (s/def ::subject ::us/string) (s/def ::content ::us/string) @@ -101,7 +96,7 @@ (def feedback "A profile feedback email." - (emails/template-factory ::feedback default-context)) + (emails/template-factory ::feedback)) (s/def ::name ::us/string) (s/def ::register @@ -109,7 +104,7 @@ (def register "A new profile registration welcome email." - (emails/template-factory ::register default-context)) + (emails/template-factory ::register)) (s/def ::token ::us/string) (s/def ::password-recovery @@ -117,7 +112,7 @@ (def password-recovery "A password recovery notification email." - (emails/template-factory ::password-recovery default-context)) + (emails/template-factory ::password-recovery)) (s/def ::pending-email ::us/email) (s/def ::change-email @@ -125,7 +120,7 @@ (def change-email "Password change confirmation email" - (emails/template-factory ::change-email default-context)) + (emails/template-factory ::change-email)) (s/def :internal.emails.invite-to-team/invited-by ::us/string) (s/def :internal.emails.invite-to-team/team ::us/string) @@ -138,4 +133,50 @@ (def invite-to-team "Teams member invitation email." - (emails/template-factory ::invite-to-team default-context)) + (emails/template-factory ::invite-to-team)) + + +;; --- SENDMAIL TASK + +(declare send-console!) + +(s/def ::username ::cfg/smtp-username) +(s/def ::password ::cfg/smtp-password) +(s/def ::tls ::cfg/smtp-tls) +(s/def ::ssl ::cfg/smtp-ssl) +(s/def ::host ::cfg/smtp-host) +(s/def ::port ::cfg/smtp-port) +(s/def ::default-reply-to ::cfg/smtp-default-reply-to) +(s/def ::default-from ::cfg/smtp-default-from) +(s/def ::enabled ::cfg/smtp-enabled) + +(defmethod ig/pre-init-spec ::sendmail-handler [_] + (s/keys :req-un [::enabled] + :opt-un [::username + ::password + ::tls + ::ssl + ::host + ::port + ::default-from + ::default-reply-to])) + +(defmethod ig/init-key ::sendmail-handler + [_ cfg] + (fn [{:keys [props] :as task}] + (if (:enabled cfg) + (emails/send! cfg props) + (send-console! cfg props)))) + +(defn- send-console! + [cfg email] + (let [baos (java.io.ByteArrayOutputStream.) + mesg (emails/smtp-message cfg email)] + (.writeTo mesg baos) + (let [out (with-out-str + (println "email console dump:") + (println "******** start email" (:id email) "**********") + (println (.toString baos)) + (println "******** end email "(:id email) "**********"))] + (l/info :email out)))) + diff --git a/backend/src/app/http.clj b/backend/src/app/http.clj index 1b2c52516b..a3fbc02c62 100644 --- a/backend/src/app/http.clj +++ b/backend/src/app/http.clj @@ -2,22 +2,18 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020-2021 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.http (:require [app.common.data :as d] + [app.common.exceptions :as ex] [app.common.spec :as us] - [app.config :as cfg] [app.http.errors :as errors] [app.http.middleware :as middleware] [app.metrics :as mtx] - [app.util.log4j :refer [update-thread-context!]] + [app.util.logging :as l] [clojure.spec.alpha :as s] - [clojure.tools.logging :as log] [integrant.core :as ig] [reitit.ring :as rr] [ring.adapter.jetty9 :as jetty]) @@ -26,30 +22,32 @@ org.eclipse.jetty.server.handler.ErrorHandler org.eclipse.jetty.server.handler.StatisticsHandler)) +(declare router-handler) + (s/def ::handler fn?) +(s/def ::router some?) (s/def ::ws (s/map-of ::us/string fn?)) -(s/def ::port ::cfg/http-server-port) +(s/def ::port ::us/integer) (s/def ::name ::us/string) (defmethod ig/pre-init-spec ::server [_] - (s/keys :req-un [::handler ::port] - :opt-un [::ws ::name ::mtx/metrics])) + (s/keys :req-un [::port] + :opt-un [::ws ::name ::mtx/metrics ::router ::handler])) (defmethod ig/prep-key ::server [_ cfg] - (merge {:name "http"} - (d/without-nils cfg))) + (merge {:name "http"} (d/without-nils cfg))) (defmethod ig/init-key ::server - [_ {:keys [handler ws port name metrics] :as opts}] - (log/infof "starting '%s' server on port %s." name port) + [_ {:keys [handler router ws port name metrics] :as opts}] + (l/info :msg "starting http server" :port port :name name) (let [pre-start (fn [^Server server] (let [handler (doto (ErrorHandler.) (.setShowStacks true) (.setServer server))] (.setErrorHandler server ^ErrorHandler handler) (when metrics - (let [stats (new StatisticsHandler)] + (let [stats (StatisticsHandler.)] (.setHandler ^StatisticsHandler stats (.getHandler server)) (.setHandler server stats) (mtx/instrument-jetty! (:registry metrics) stats))))) @@ -63,61 +61,71 @@ (when (seq ws) {:websockets ws})) + handler (cond + (fn? handler) handler + (some? router) (router-handler router) + :else (ex/raise :type :internal + :code :invalid-argument + :hint "Missing `handler` or `router` option.")) + server (jetty/run-jetty handler options)] (assoc opts :server server))) (defmethod ig/halt-key! ::server [_ {:keys [server name port] :as opts}] - (log/infof "stoping '%s' server on port %s." name port) + (l/info :msg "stoping http server" + :name name + :port port) (jetty/stop-server server)) -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; Http Main Handler (Router) -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - -(declare create-router) - -(s/def ::rpc map?) -(s/def ::session map?) -(s/def ::metrics map?) -(s/def ::oauth map?) -(s/def ::storage map?) -(s/def ::assets map?) -(s/def ::feedback fn?) - -(defmethod ig/pre-init-spec ::router [_] - (s/keys :req-un [::rpc ::session ::metrics ::oauth ::storage ::assets ::feedback])) - -(defmethod ig/init-key ::router - [_ cfg] - (let [handler (rr/ring-handler - (create-router cfg) - (rr/routes - (rr/create-resource-handler {:path "/"}) - (rr/create-default-handler)) - {:middleware [middleware/server-timing]})] +(defn- router-handler + [router] + (let [handler (rr/ring-handler router + (rr/routes + (rr/create-resource-handler {:path "/"}) + (rr/create-default-handler)) + {:middleware [middleware/server-timing]})] (fn [request] (try (handler request) (catch Throwable e (try (let [cdata (errors/get-error-context request e)] - (update-thread-context! cdata) - (log/errorf e "unhandled exception: %s (id: %s)" (ex-message e) (str (:id cdata))) - {:status 500 - :body "internal server error"}) + (l/update-thread-context! cdata) + (l/error :hint "unhandled exception" + :message (ex-message e) + :error-id (str (:id cdata)) + :cause e)) + {:status 500 :body "internal server error"} (catch Throwable e - (log/errorf e "unhandled exception: %s" (ex-message e)) - {:status 500 - :body "internal server error"}))))))) + (l/error :hint "unhandled exception" + :message (ex-message e) + :cause e) + {:status 500 :body "internal server error"}))))))) -(defn- create-router - [{:keys [session rpc oauth metrics svgparse assets feedback] :as cfg}] + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Http Main Handler (Router) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(s/def ::rpc map?) +(s/def ::session map?) +(s/def ::oauth map?) +(s/def ::storage map?) +(s/def ::assets map?) +(s/def ::feedback fn?) + +(defmethod ig/pre-init-spec ::router [_] + (s/keys :req-un [::rpc ::session ::mtx/metrics ::oauth ::storage ::assets ::feedback])) + +(defmethod ig/init-key ::router + [_ {:keys [session rpc oauth metrics assets feedback] :as cfg}] (rr/router [["/metrics" {:get (:handler metrics)}] - ["/assets" {:middleware [[middleware/format-response-body] - [middleware/errors errors/handle]]} + [middleware/errors errors/handle] + [middleware/cookies] + (:middleware session)]} ["/by-id/:id" {:get (:objects-handler assets)}] ["/by-file-media-id/:id" {:get (:file-objects-handler assets)}] ["/by-file-media-id/:id/thumbnail" {:get (:file-thumbnails-handler assets)}]] @@ -137,20 +145,13 @@ [middleware/errors errors/handle] [middleware/cookies]]} - ["/svg/parse" {:post svgparse}] ["/feedback" {:middleware [(:middleware session)] :post feedback}] - ["/oauth" - ["/google" {:post (get-in oauth [:google :handler])}] - ["/google/callback" {:get (get-in oauth [:google :callback-handler])}] - - ["/gitlab" {:post (get-in oauth [:gitlab :handler])}] - ["/gitlab/callback" {:get (get-in oauth [:gitlab :callback-handler])}] - - ["/github" {:post (get-in oauth [:github :handler])}] - ["/github/callback" {:get (get-in oauth [:github :callback-handler])}]] + ["/auth/oauth/:provider" {:post (:handler oauth)}] + ["/auth/oauth/:provider/callback" {:get (:callback-handler oauth)}] ["/rpc" {:middleware [(:middleware session)]} - ["/query/:type" {:get (:query-handler rpc)}] + ["/query/:type" {:get (:query-handler rpc) + :post (:query-handler rpc)}] ["/mutation/:type" {:post (:mutation-handler rpc)}]]]])) diff --git a/backend/src/app/http/assets.clj b/backend/src/app/http/assets.clj index d5eb459941..678c4bdddd 100644 --- a/backend/src/app/http/assets.clj +++ b/backend/src/app/http/assets.clj @@ -2,23 +2,20 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020-2021 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.http.assets "Assets related handlers." (:require [app.common.exceptions :as ex] [app.common.spec :as us] + [app.common.uri :as u] [app.db :as db] [app.metrics :as mtx] [app.storage :as sto] [app.util.time :as dt] [clojure.spec.alpha :as s] - [integrant.core :as ig] - [lambdaisland.uri :as u])) + [integrant.core :as ig])) (def ^:private cache-max-age (dt/duration {:hours 24})) diff --git a/backend/src/app/http/awsns.clj b/backend/src/app/http/awsns.clj index ea47131ce9..d5c29a979b 100644 --- a/backend/src/app/http/awsns.clj +++ b/backend/src/app/http/awsns.clj @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020 Andrey Antukh +;; Copyright (c) UXBOX Labs SL (ns app.http.awsns "AWS SNS webhook handler for bounces." @@ -14,9 +11,8 @@ [app.db :as db] [app.db.sql :as sql] [app.util.http :as http] - [clojure.pprint :refer [pprint]] + [app.util.logging :as l] [clojure.spec.alpha :as s] - [clojure.tools.logging :as log] [cuerdas.core :as str] [integrant.core :as ig] [jsonista.core :as j])) @@ -25,11 +21,6 @@ (declare parse-notification) (declare process-report) -(defn- pprint-report - [message] - (binding [clojure.pprint/*print-right-margin* 120] - (with-out-str (pprint message)))) - (defmethod ig/pre-init-spec ::handler [_] (s/keys :req-un [::db/pool])) @@ -42,19 +33,17 @@ (= mtype "SubscriptionConfirmation") (let [surl (get body "SubscribeURL") stopic (get body "TopicArn")] - (log/infof "subscription received (topic=%s, url=%s)" stopic surl) + (l/info :action "subscription received" :topic stopic :url surl) (http/send! {:uri surl :method :post :timeout 10000})) (= mtype "Notification") (when-let [message (parse-json (get body "Message"))] - ;; (log/infof "Received: %s" (pr-str message)) (let [notification (parse-notification cfg message)] (process-report cfg notification))) :else - (log/warn (str "unexpected data received\n" - (pprint-report body)))) - + (l/warn :hint "unexpected data received" + :report (pr-str body))) {:status 200 :body ""}))) (defn- parse-bounce @@ -184,15 +173,15 @@ (defn- process-report [cfg {:keys [type profile-id] :as report}] - (log/trace (str "procesing report:\n" (pprint-report report))) + (l/trace :action "procesing report" :report (pr-str report)) (cond ;; In this case we receive a bounce/complaint notification without ;; confirmed identity, we just emit a warning but do nothing about ;; it because this is not a normal case. All notifications should ;; come with profile identity. (nil? profile-id) - (log/warn (str "a notification without identity recevied from AWS\n" - (pprint-report report))) + (l/warn :msg "a notification without identity recevied from AWS" + :report (pr-str report)) (= "bounce" type) (register-bounce-for-profile cfg report) @@ -201,7 +190,7 @@ (register-complaint-for-profile cfg report) :else - (log/warn (str "unrecognized report received from AWS\n" - (pprint-report report))))) + (l/warn :msg "unrecognized report received from AWS" + :report (pr-str report)))) diff --git a/backend/src/app/http/errors.clj b/backend/src/app/http/errors.clj index e5e7d6af20..d2e45ed010 100644 --- a/backend/src/app/http/errors.clj +++ b/backend/src/app/http/errors.clj @@ -2,18 +2,14 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.http.errors "A errors handling for the http server." (:require [app.common.exceptions :as ex] [app.common.uuid :as uuid] - [app.util.log4j :refer [update-thread-context!]] - [clojure.tools.logging :as log] + [app.util.logging :as l] [cuerdas.core :as str] [expound.alpha :as expound])) @@ -73,8 +69,11 @@ [error request] (let [edata (ex-data error) cdata (get-error-context request error)] - (update-thread-context! cdata) - (log/errorf error "internal error: assertion (id: %s)" (str (:id cdata))) + (l/update-thread-context! cdata) + (l/error :hint "internal error: assertion" + :error-id (str (:id cdata)) + :cause error) + {:status 500 :body {:type :server-error :data (-> edata @@ -97,10 +96,11 @@ (ex/exception? (:handling edata))) (handle-exception (:handling edata) request) (let [cdata (get-error-context request error)] - (update-thread-context! cdata) - (log/errorf error "internal error: %s (id: %s)" - (ex-message error) - (str (:id cdata))) + (l/update-thread-context! cdata) + (l/error :hint "internal error" + :error-message (ex-message error) + :error-id (str (:id cdata)) + :cause error) {:status 500 :body {:type :server-error :hint (ex-message error) @@ -111,11 +111,11 @@ (let [cdata (get-error-context request error) state (.getSQLState ^java.sql.SQLException error)] - (update-thread-context! cdata) - (log/errorf error "PSQL Exception: %s (id: %s, state: %s)" - (ex-message error) - (str (:id cdata)) - state) + (l/update-thread-context! cdata) + (l/error :hint "psql exception" + :error-message (ex-message error) + :error-id (str (:id cdata)) + :sql-state state) (cond (= state "57014") diff --git a/backend/src/app/http/feedback.clj b/backend/src/app/http/feedback.clj index 0d3fa49591..dd78cfd575 100644 --- a/backend/src/app/http/feedback.clj +++ b/backend/src/app/http/feedback.clj @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2021 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.http.feedback "A general purpose feedback module." @@ -15,7 +12,7 @@ [app.common.spec :as us] [app.config :as cfg] [app.db :as db] - [app.emails :as emails] + [app.emails :as eml] [app.rpc.queries.profile :as profile] [clojure.spec.alpha :as s] [integrant.core :as ig])) @@ -62,11 +59,12 @@ [pool profile params] (let [params (us/conform ::feedback params) destination (cfg/get :feedback-destination)] - (emails/send! pool emails/feedback - {:to destination - :profile profile - :reply-to (:from params) - :email (:from params) - :subject (:subject params) - :content (:content params)}) + (eml/send! {::eml/conn pool + ::eml/factory eml/feedback + :to destination + :profile profile + :reply-to (:from params) + :email (:from params) + :subject (:subject params) + :content (:content params)}) nil)) diff --git a/backend/src/app/http/middleware.clj b/backend/src/app/http/middleware.clj index 1cfc7459d4..16d7429edf 100644 --- a/backend/src/app/http/middleware.clj +++ b/backend/src/app/http/middleware.clj @@ -2,15 +2,13 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020-2021 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.http.middleware (:require [app.metrics :as mtx] [app.util.json :as json] + [app.util.logging :as l] [app.util.transit :as t] [buddy.core.codecs :as bc] [buddy.core.hash :as bh] @@ -165,3 +163,18 @@ (def etag {:name ::etag :compile (constantly wrap-etag)}) + +(defn activity-logger + [handler] + (let [logger "penpot.profile-activity"] + (fn [{:keys [headers] :as request}] + (let [ip-addr (get headers "x-forwarded-for") + profile-id (:profile-id request) + qstring (:query-string request)] + (l/info ::l/async true + ::l/logger logger + :ip-addr ip-addr + :profile-id profile-id + :uri (str (:uri request) (when qstring (str "?" qstring))) + :method (name (:request-method request))) + (handler request))))) diff --git a/backend/src/app/http/oauth.clj b/backend/src/app/http/oauth.clj new file mode 100644 index 0000000000..60e0a90c88 --- /dev/null +++ b/backend/src/app/http/oauth.clj @@ -0,0 +1,298 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) UXBOX Labs SL + +(ns app.http.oauth + (:require + [app.common.exceptions :as ex] + [app.common.spec :as us] + [app.common.uri :as u] + [app.config :as cf] + [app.util.http :as http] + [app.util.logging :as l] + [app.util.time :as dt] + [clojure.data.json :as json] + [clojure.set :as set] + [clojure.spec.alpha :as s] + [cuerdas.core :as str] + [integrant.core :as ig])) + +(defn redirect-response + [uri] + {:status 302 + :headers {"location" (str uri)} + :body ""}) + +(defn generate-error-redirect-uri + [cfg] + (-> (u/uri (:public-uri cfg)) + (assoc :path "/#/auth/login") + (assoc :query (u/map->query-string {:error "unable-to-auth"})))) + +(defn register-profile + [{:keys [rpc] :as cfg} info] + (let [method-fn (get-in rpc [:methods :mutation :login-or-register]) + profile (method-fn info)] + (cond-> profile + (some? (:invitation-token info)) + (assoc :invitation-token (:invitation-token info))))) + +(defn generate-redirect-uri + [{:keys [tokens] :as cfg} profile] + (let [token (or (:invitation-token profile) + (tokens :generate {:iss :auth + :exp (dt/in-future "15m") + :profile-id (:id profile)}))] + (-> (u/uri (:public-uri cfg)) + (assoc :path "/#/auth/verify-token") + (assoc :query (u/map->query-string {:token token}))))) + +(defn- build-redirect-uri + [{:keys [provider] :as cfg}] + (let [public (u/uri (:public-uri cfg))] + (str (assoc public :path (str "/api/auth/oauth/" (:name provider) "/callback"))))) + +(defn- build-auth-uri + [{:keys [provider] :as cfg} state] + (let [params {:client_id (:client-id provider) + :redirect_uri (build-redirect-uri cfg) + :response_type "code" + :state state + :scope (str/join " " (:scopes provider []))} + query (u/map->query-string params)] + (-> (u/uri (:auth-uri provider)) + (assoc :query query) + (str)))) + +(defn retrieve-access-token + [{:keys [provider] :as cfg} code] + (try + (let [params {:client_id (:client-id provider) + :client_secret (:client-secret provider) + :code code + :grant_type "authorization_code" + :redirect_uri (build-redirect-uri cfg)} + req {:method :post + :headers {"content-type" "application/x-www-form-urlencoded"} + :uri (:token-uri provider) + :body (u/map->query-string params)} + res (http/send! req)] + (when (= 200 (:status res)) + (let [data (json/read-str (:body res))] + {:token (get data "access_token") + :type (get data "token_type")}))) + (catch Exception e + (l/error :hint "unexpected error on retrieve-access-token" + :cause e) + nil))) + +(defn- retrieve-user-info + [{:keys [provider] :as cfg} tdata] + (try + (let [req {:uri (:user-uri provider) + :headers {"Authorization" (str (:type tdata) " " (:token tdata))} + :timeout 6000 + :method :get} + res (http/send! req)] + + (when (= 200 (:status res)) + (let [{:keys [name] :as data} (json/read-str (:body res) :key-fn keyword)] + (-> data + (assoc :backend (:name provider)) + (assoc :fullname name))))) + + (catch Exception e + (l/error :hint "unexpected exception on retrieve-user-info" + :cause e) + nil))) + +(defn retrieve-info + [{:keys [tokens provider] :as cfg} request] + (let [state (get-in request [:params :state]) + state (tokens :verify {:token state :iss :oauth}) + info (some->> (get-in request [:params :code]) + (retrieve-access-token cfg) + (retrieve-user-info cfg))] + (when-not info + (ex/raise :type :internal + :code :unable-to-auth)) + + ;; If the provider is OIDC, we can proceed to check + ;; roles if they are defined. + (when (and (= "oidc" (:name provider)) + (seq (:roles provider))) + (let [provider-roles (into #{} (:roles provider)) + profile-roles (let [attr (cf/get :oidc-roles-attr :roles) + roles (get info attr)] + (cond + (string? roles) (into #{} (str/words roles)) + (vector? roles) (into #{} roles) + :else #{}))] + ;; check if profile has a configured set of roles + (when-not (set/subset? provider-roles profile-roles) + (ex/raise :type :internal + :code :unable-to-auth + :hint "not enought permissions")))) + + (cond-> info + (some? (:invitation-token state)) + (assoc :invitation-token (:invitation-token state))))) + +;; --- HTTP HANDLERS + +(defn- auth-handler + [{:keys [tokens] :as cfg} request] + (let [invitation (get-in request [:params :invitation-token]) + state (tokens :generate + {:iss :oauth + :invitation-token invitation + :exp (dt/in-future "15m")}) + uri (build-auth-uri cfg state)] + {:status 200 + :body {:redirect-uri uri}})) + +(defn- callback-handler + [{:keys [session] :as cfg} request] + (try + (let [info (retrieve-info cfg request) + profile (register-profile cfg info) + uri (generate-redirect-uri cfg profile) + sxf ((:create session) (:id profile))] + (->> (redirect-response uri) + (sxf request))) + (catch Exception _e + (-> (generate-error-redirect-uri cfg) + (redirect-response))))) + +;; --- INIT + +(declare initialize) + +(s/def ::public-uri ::us/not-empty-string) +(s/def ::session map?) +(s/def ::tokens fn?) +(s/def ::rpc map?) + +(defmethod ig/pre-init-spec :app.http.oauth/handlers [_] + (s/keys :req-un [::public-uri ::session ::tokens ::rpc])) + +(defn wrap-handler + [cfg handler] + (fn [request] + (let [provider (get-in request [:path-params :provider]) + provider (get-in @cfg [:providers provider])] + (when-not provider + (ex/raise :type :not-found + :context {:provider provider} + :hint "provider not configured")) + (-> (assoc @cfg :provider provider) + (handler request))))) + +(defmethod ig/init-key :app.http.oauth/handlers + [_ cfg] + (let [cfg (initialize cfg)] + {:handler (wrap-handler cfg auth-handler) + :callback-handler (wrap-handler cfg callback-handler)})) + +(defn- discover-oidc-config + [{:keys [base-uri] :as opts}] + (let [discovery-uri (u/join base-uri ".well-known/openid-configuration") + response (http/send! {:method :get :uri (str discovery-uri)})] + (when (= 200 (:status response)) + (let [data (json/read-str (:body response))] + (assoc opts + :token-uri (get data "token_endpoint") + :auth-uri (get data "authorization_endpoint") + :user-uri (get data "userinfo_endpoint")))))) + +(defn- initialize-oidc-provider + [cfg] + (let [opts {:base-uri (cf/get :oidc-base-uri) + :client-id (cf/get :oidc-client-id) + :client-secret (cf/get :oidc-client-secret) + :token-uri (cf/get :oidc-token-uri) + :auth-uri (cf/get :oidc-auth-uri) + :user-uri (cf/get :oidc-user-uri) + :scopes (into #{"openid" "profile" "email" "name"} + (cf/get :oidc-scopes #{})) + :roles-attr (cf/get :oidc-roles-attr) + :roles (cf/get :oidc-roles) + :name "oidc"}] + (if (and (string? (:base-uri opts)) + (string? (:client-id opts)) + (string? (:client-secret opts))) + (if (and (string? (:token-uri opts)) + (string? (:user-uri opts)) + (string? (:auth-uri opts))) + (do + (l/info :action "initialize" :provider "oid" :method "static") + (assoc-in cfg [:providers "oidc"] opts)) + (let [opts (discover-oidc-config opts)] + (l/info :action "initialize" :provider "oid" :method "discover") + (assoc-in cfg [:providers "oidc"] opts))) + cfg))) + +(defn- initialize-google-provider + [cfg] + (let [opts {:client-id (cf/get :google-client-id) + :client-secret (cf/get :google-client-secret) + :scopes #{"email" "profile" "openid" + "https://www.googleapis.com/auth/userinfo.email" + "https://www.googleapis.com/auth/userinfo.profile"} + :auth-uri "https://accounts.google.com/o/oauth2/v2/auth" + :token-uri "https://oauth2.googleapis.com/token" + :user-uri "https://openidconnect.googleapis.com/v1/userinfo" + :name "google"}] + (if (and (string? (:client-id opts)) + (string? (:client-secret opts))) + (do + (l/info :action "initialize" :provider "google") + (assoc-in cfg [:providers "google"] opts)) + cfg))) + +(defn- initialize-github-provider + [cfg] + (let [opts {:client-id (cf/get :github-client-id) + :client-secret (cf/get :github-client-secret) + :scopes #{"read:user" + "user:email"} + :auth-uri "https://github.com/login/oauth/authorize" + :token-uri "https://github.com/login/oauth/access_token" + :user-uri "https://api.github.com/user" + :name "github"}] + (if (and (string? (:client-id opts)) + (string? (:client-secret opts))) + (do + (l/info :action "initialize" :provider "github") + (assoc-in cfg [:providers "github"] opts)) + cfg))) + + +(defn- initialize-gitlab-provider + [cfg] + (let [base (cf/get :gitlab-base-uri "https://gitlab.com") + opts {:base-uri base + :client-id (cf/get :gitlab-client-id) + :client-secret (cf/get :gitlab-client-secret) + :scopes #{"read_user"} + :auth-uri (str base "/oauth/authorize") + :token-uri (str base "/oauth/token") + :user-uri (str base "/api/v4/user") + :name "gitlab"}] + (if (and (string? (:client-id opts)) + (string? (:client-secret opts))) + (do + (l/info :action "initialize" :provider "gitlab") + (assoc-in cfg [:providers "gitlab"] opts)) + cfg))) + +(defn- initialize + [cfg] + (let [cfg (agent cfg :error-mode :continue)] + (send-off cfg initialize-google-provider) + (send-off cfg initialize-gitlab-provider) + (send-off cfg initialize-github-provider) + (send-off cfg initialize-oidc-provider) + cfg)) diff --git a/backend/src/app/http/oauth/github.clj b/backend/src/app/http/oauth/github.clj deleted file mode 100644 index bfa4c3c148..0000000000 --- a/backend/src/app/http/oauth/github.clj +++ /dev/null @@ -1,159 +0,0 @@ -;; This Source Code Form is subject to the terms of the Mozilla Public -;; License, v. 2.0. If a copy of the MPL was not distributed with this -;; file, You can obtain one at http://mozilla.org/MPL/2.0/. -;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020-2021 UXBOX Labs SL - -(ns app.http.oauth.github - (:require - [app.common.exceptions :as ex] - [app.common.spec :as us] - [app.config :as cfg] - [app.http.oauth.google :as gg] - [app.util.http :as http] - [app.util.time :as dt] - [clojure.data.json :as json] - [clojure.spec.alpha :as s] - [clojure.tools.logging :as log] - [integrant.core :as ig] - [lambdaisland.uri :as u])) - -(def base-github-uri - (u/uri "https://github.com")) - -(def base-api-github-uri - (u/uri "https://api.github.com")) - -(def authorize-uri - (assoc base-github-uri :path "/login/oauth/authorize")) - -(def token-url - (assoc base-github-uri :path "/login/oauth/access_token")) - -(def user-info-url - (assoc base-api-github-uri :path "/user")) - -(def scope "user:email") - -(defn- build-redirect-url - [cfg] - (let [public (u/uri (:public-uri cfg))] - (str (assoc public :path "/api/oauth/github/callback")))) - -(defn- get-access-token - [cfg state code] - (try - (let [params {:client_id (:client-id cfg) - :client_secret (:client-secret cfg) - :code code - :state state - :redirect_uri (build-redirect-url cfg)} - req {:method :post - :headers {"content-type" "application/x-www-form-urlencoded" - "accept" "application/json"} - :uri (str token-url) - :timeout 6000 - :body (u/map->query-string params)} - res (http/send! req)] - - (when (= 200 (:status res)) - (-> (json/read-str (:body res)) - (get "access_token")))) - - (catch Exception e - (log/error e "unexpected error on get-access-token") - nil))) - -(defn- get-user-info - [_ token] - (try - (let [req {:uri (str user-info-url) - :headers {"authorization" (str "token " token)} - :timeout 6000 - :method :get} - res (http/send! req)] - (when (= 200 (:status res)) - (let [data (json/read-str (:body res))] - {:email (get data "email") - :backend "github" - :fullname (get data "name")}))) - (catch Exception e - (log/error e "unexpected exception on get-user-info") - nil))) - -(defn- retrieve-info - [{:keys [tokens] :as cfg} request] - (let [token (get-in request [:params :state]) - state (tokens :verify {:token token :iss :github-oauth}) - info (some->> (get-in request [:params :code]) - (get-access-token cfg state) - (get-user-info cfg))] - (when-not info - (ex/raise :type :internal - :code :unable-to-auth)) - - (cond-> info - (some? (:invitation-token state)) - (assoc :invitation-token (:invitation-token state))))) - -(defn auth-handler - [{:keys [tokens] :as cfg} request] - (let [invitation (get-in request [:params :invitation-token]) - state (tokens :generate {:iss :github-oauth - :invitation-token invitation - :exp (dt/in-future "15m")}) - params {:client_id (:client-id cfg/config) - :redirect_uri (build-redirect-url cfg) - :state state - :scope scope} - query (u/map->query-string params) - uri (-> authorize-uri - (assoc :query query))] - {:status 200 - :body {:redirect-uri (str uri)}})) - -(defn- callback-handler - [{:keys [session] :as cfg} request] - (try - (let [info (retrieve-info cfg request) - profile (gg/register-profile cfg info) - uri (gg/generate-redirect-uri cfg profile) - sxf ((:create session) (:id profile))] - (->> (gg/redirect-response uri) - (sxf request))) - (catch Exception _e - (-> (gg/generate-error-redirect-uri cfg) - (gg/redirect-response))))) - - -;; --- ENTRY POINT - -(s/def ::client-id ::us/not-empty-string) -(s/def ::client-secret ::us/not-empty-string) -(s/def ::public-uri ::us/not-empty-string) -(s/def ::session map?) -(s/def ::tokens fn?) - -(defmethod ig/pre-init-spec :app.http.oauth/github [_] - (s/keys :req-un [::public-uri - ::session - ::tokens] - :opt-un [::client-id - ::client-secret])) - -(defn- default-handler - [_] - (ex/raise :type :not-found)) - -(defmethod ig/init-key :app.http.oauth/github - [_ cfg] - (if (and (:client-id cfg) - (:client-secret cfg)) - {:handler #(auth-handler cfg %) - :callback-handler #(callback-handler cfg %)} - {:handler default-handler - :callback-handler default-handler})) - diff --git a/backend/src/app/http/oauth/gitlab.clj b/backend/src/app/http/oauth/gitlab.clj deleted file mode 100644 index 4a8b0a7c2c..0000000000 --- a/backend/src/app/http/oauth/gitlab.clj +++ /dev/null @@ -1,167 +0,0 @@ -;; This Source Code Form is subject to the terms of the Mozilla Public -;; License, v. 2.0. If a copy of the MPL was not distributed with this -;; file, You can obtain one at http://mozilla.org/MPL/2.0/. -;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020 UXBOX Labs SL - -(ns app.http.oauth.gitlab - (:require - [app.common.data :as d] - [app.common.exceptions :as ex] - [app.common.spec :as us] - [app.http.oauth.google :as gg] - [app.util.http :as http] - [app.util.time :as dt] - [clojure.data.json :as json] - [clojure.spec.alpha :as s] - [clojure.tools.logging :as log] - [integrant.core :as ig] - [lambdaisland.uri :as u])) - -(def scope "read_user") - -(defn- build-redirect-url - [cfg] - (let [public (u/uri (:public-uri cfg))] - (str (assoc public :path "/api/oauth/gitlab/callback")))) - -(defn- build-oauth-uri - [cfg] - (let [base-uri (u/uri (:base-uri cfg))] - (assoc base-uri :path "/oauth/authorize"))) - -(defn- build-token-url - [cfg] - (let [base-uri (u/uri (:base-uri cfg))] - (str (assoc base-uri :path "/oauth/token")))) - -(defn- build-user-info-url - [cfg] - (let [base-uri (u/uri (:base-uri cfg))] - (str (assoc base-uri :path "/api/v4/user")))) - -(defn- get-access-token - [cfg code] - (try - (let [params {:client_id (:client-id cfg) - :client_secret (:client-secret cfg) - :code code - :grant_type "authorization_code" - :redirect_uri (build-redirect-url cfg)} - req {:method :post - :headers {"content-type" "application/x-www-form-urlencoded"} - :uri (build-token-url cfg) - :body (u/map->query-string params)} - res (http/send! req)] - - (when (= 200 (:status res)) - (-> (json/read-str (:body res)) - (get "access_token")))) - - (catch Exception e - (log/error e "unexpected error on get-access-token") - nil))) - -(defn- get-user-info - [cfg token] - (try - (let [req {:uri (build-user-info-url cfg) - :headers {"Authorization" (str "Bearer " token)} - :timeout 6000 - :method :get} - res (http/send! req)] - - (when (= 200 (:status res)) - (let [data (json/read-str (:body res))] - {:email (get data "email") - :backend "gitlab" - :fullname (get data "name")}))) - - (catch Exception e - (log/error e "unexpected exception on get-user-info") - nil))) - - -(defn- retrieve-info - [{:keys [tokens] :as cfg} request] - (let [token (get-in request [:params :state]) - state (tokens :verify {:token token :iss :gitlab-oauth}) - info (some->> (get-in request [:params :code]) - (get-access-token cfg) - (get-user-info cfg))] - (when-not info - (ex/raise :type :internal - :code :unable-to-auth)) - - (cond-> info - (some? (:invitation-token state)) - (assoc :invitation-token (:invitation-token state))))) - - -(defn- auth-handler - [{:keys [tokens] :as cfg} request] - (let [invitation (get-in request [:params :invitation-token]) - state (tokens :generate - {:iss :gitlab-oauth - :invitation-token invitation - :exp (dt/in-future "15m")}) - - params {:client_id (:client-id cfg) - :redirect_uri (build-redirect-url cfg) - :response_type "code" - :state state - :scope scope} - query (u/map->query-string params) - uri (-> (build-oauth-uri cfg) - (assoc :query query))] - {:status 200 - :body {:redirect-uri (str uri)}})) - -(defn- callback-handler - [{:keys [session] :as cfg} request] - (try - (let [info (retrieve-info cfg request) - profile (gg/register-profile cfg info) - uri (gg/generate-redirect-uri cfg profile) - sxf ((:create session) (:id profile))] - (->> (gg/redirect-response uri) - (sxf request))) - (catch Exception _e - (-> (gg/generate-error-redirect-uri cfg) - (gg/redirect-response))))) - -(s/def ::client-id ::us/not-empty-string) -(s/def ::client-secret ::us/not-empty-string) -(s/def ::base-uri ::us/not-empty-string) -(s/def ::public-uri ::us/not-empty-string) -(s/def ::session map?) -(s/def ::tokens fn?) - -(defmethod ig/pre-init-spec :app.http.oauth/gitlab [_] - (s/keys :req-un [::public-uri - ::session - ::tokens] - :opt-un [::base-uri - ::client-id - ::client-secret])) - -(defmethod ig/prep-key :app.http.oauth/gitlab - [_ cfg] - (d/merge {:base-uri "https://gitlab.com"} - (d/without-nils cfg))) - -(defn- default-handler - [_] - (ex/raise :type :not-found)) - -(defmethod ig/init-key :app.http.oauth/gitlab - [_ cfg] - (if (and (:client-id cfg) - (:client-secret cfg)) - {:handler #(auth-handler cfg %) - :callback-handler #(callback-handler cfg %)} - {:handler default-handler - :callback-handler default-handler})) diff --git a/backend/src/app/http/oauth/google.clj b/backend/src/app/http/oauth/google.clj deleted file mode 100644 index ce92116892..0000000000 --- a/backend/src/app/http/oauth/google.clj +++ /dev/null @@ -1,182 +0,0 @@ -;; This Source Code Form is subject to the terms of the Mozilla Public -;; License, v. 2.0. If a copy of the MPL was not distributed with this -;; file, You can obtain one at http://mozilla.org/MPL/2.0/. -;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020-2021 UXBOX Labs SL - -(ns app.http.oauth.google - (:require - [app.common.exceptions :as ex] - [app.common.spec :as us] - [app.util.http :as http] - [app.util.time :as dt] - [clojure.data.json :as json] - [clojure.spec.alpha :as s] - [clojure.tools.logging :as log] - [integrant.core :as ig] - [lambdaisland.uri :as u])) - -(def base-goauth-uri "https://accounts.google.com/o/oauth2/v2/auth") - -(def scope - (str "email profile " - "https://www.googleapis.com/auth/userinfo.email " - "https://www.googleapis.com/auth/userinfo.profile " - "openid")) - -(defn- build-redirect-url - [cfg] - (let [public (u/uri (:public-uri cfg))] - (str (assoc public :path "/api/oauth/google/callback")))) - -(defn- get-access-token - [cfg code] - (try - (let [params {:code code - :client_id (:client-id cfg) - :client_secret (:client-secret cfg) - :redirect_uri (build-redirect-url cfg) - :grant_type "authorization_code"} - req {:method :post - :headers {"content-type" "application/x-www-form-urlencoded"} - :uri "https://oauth2.googleapis.com/token" - :timeout 6000 - :body (u/map->query-string params)} - res (http/send! req)] - - (when (= 200 (:status res)) - (-> (json/read-str (:body res)) - (get "access_token")))) - (catch Exception e - (log/error e "unexpected error on get-access-token") - nil))) - -(defn- get-user-info - [_ token] - (try - (let [req {:uri "https://openidconnect.googleapis.com/v1/userinfo" - :headers {"Authorization" (str "Bearer " token)} - :timeout 6000 - :method :get} - res (http/send! req)] - - (when (= 200 (:status res)) - (let [data (json/read-str (:body res))] - {:email (get data "email") - :backend "google" - :fullname (get data "name")}))) - (catch Exception e - (log/error e "unexpected exception on get-user-info") - nil))) - -(defn- retrieve-info - [{:keys [tokens] :as cfg} request] - (let [token (get-in request [:params :state]) - state (tokens :verify {:token token :iss :google-oauth}) - info (some->> (get-in request [:params :code]) - (get-access-token cfg) - (get-user-info cfg))] - - - (when-not info - (ex/raise :type :internal - :code :unable-to-auth)) - - (cond-> info - (some? (:invitation-token state)) - (assoc :invitation-token (:invitation-token state))))) - -(defn register-profile - [{:keys [rpc] :as cfg} info] - (let [method-fn (get-in rpc [:methods :mutation :login-or-register]) - profile (method-fn {:email (:email info) - :backend (:backend info) - :fullname (:fullname info)})] - (cond-> profile - (some? (:invitation-token info)) - (assoc :invitation-token (:invitation-token info))))) - -(defn generate-redirect-uri - [{:keys [tokens] :as cfg} profile] - (let [token (or (:invitation-token profile) - (tokens :generate {:iss :auth - :exp (dt/in-future "15m") - :profile-id (:id profile)}))] - (-> (u/uri (:public-uri cfg)) - (assoc :path "/#/auth/verify-token") - (assoc :query (u/map->query-string {:token token}))))) - -(defn generate-error-redirect-uri - [cfg] - (-> (u/uri (:public-uri cfg)) - (assoc :path "/#/auth/login") - (assoc :query (u/map->query-string {:error "unable-to-auth"})))) - -(defn redirect-response - [uri] - {:status 302 - :headers {"location" (str uri)} - :body ""}) - -(defn- auth-handler - [{:keys [tokens] :as cfg} request] - (let [invitation (get-in request [:params :invitation-token]) - state (tokens :generate - {:iss :google-oauth - :invitation-token invitation - :exp (dt/in-future "15m")}) - params {:scope scope - :access_type "offline" - :include_granted_scopes true - :state state - :response_type "code" - :redirect_uri (build-redirect-url cfg) - :client_id (:client-id cfg)} - query (u/map->query-string params) - uri (-> (u/uri base-goauth-uri) - (assoc :query query))] - - {:status 200 - :body {:redirect-uri (str uri)}})) - -(defn- callback-handler - [{:keys [session] :as cfg} request] - (try - (let [info (retrieve-info cfg request) - profile (register-profile cfg info) - uri (generate-redirect-uri cfg profile) - sxf ((:create session) (:id profile))] - (->> (redirect-response uri) - (sxf request))) - (catch Exception _e - (-> (generate-error-redirect-uri cfg) - (redirect-response))))) - -(s/def ::client-id ::us/not-empty-string) -(s/def ::client-secret ::us/not-empty-string) -(s/def ::public-uri ::us/not-empty-string) -(s/def ::session map?) -(s/def ::tokens fn?) - -(defmethod ig/pre-init-spec :app.http.oauth/google [_] - (s/keys :req-un [::public-uri - ::session - ::tokens] - :opt-un [::client-id - ::client-secret])) - -(defn- default-handler - [_] - (ex/raise :type :not-found)) - -(defmethod ig/init-key :app.http.oauth/google - [_ cfg] - (if (and (:client-id cfg) - (:client-secret cfg)) - {:handler #(auth-handler cfg %) - :callback-handler #(callback-handler cfg %)} - {:handler default-handler - :callback-handler default-handler})) diff --git a/backend/src/app/http/session.clj b/backend/src/app/http/session.clj index 316558257b..af0fd6d8f6 100644 --- a/backend/src/app/http/session.clj +++ b/backend/src/app/http/session.clj @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.http.session (:require @@ -15,101 +12,98 @@ [app.db :as db] [app.metrics :as mtx] [app.util.async :as aa] - [app.util.log4j :refer [update-thread-context!]] + [app.util.logging :as l] [app.util.time :as dt] [app.worker :as wrk] - [buddy.core.codecs :as bc] - [buddy.core.nonce :as bn] [clojure.core.async :as a] [clojure.spec.alpha :as s] - [clojure.tools.logging :as log] [integrant.core :as ig])) +;; A default cookie name for storing the session. We don't allow +;; configure it. +(def cookie-name "auth-token") + ;; --- IMPL -(defn- next-session-id - ([] (next-session-id 96)) - ([n] - (-> (bn/random-nonce n) - (bc/bytes->b64u) - (bc/bytes->str)))) +(defn- create-session + [{:keys [conn tokens] :as cfg} {:keys [profile-id headers] :as request}] + (let [token (tokens :generate {:iss "authentication" + :iat (dt/now) + :uid profile-id}) + params {:user-agent (get headers "user-agent") + :profile-id profile-id + :id token}] + (db/insert! conn :http-session params))) -(defn- create - [{:keys [conn] :as cfg} {:keys [profile-id user-agent]}] - (let [id (next-session-id)] - (db/insert! conn :http-session {:id id - :profile-id profile-id - :user-agent user-agent}) - id)) - -(defn- delete - [{:keys [conn cookie-name] :as cfg} {:keys [cookies] :as request}] +(defn- delete-session + [{:keys [conn] :as cfg} {:keys [cookies] :as request}] (when-let [token (get-in cookies [cookie-name :value])] (db/delete! conn :http-session {:id token})) nil) -(defn- retrieve - [{:keys [conn] :as cfg} token] - (when token - (db/exec-one! conn ["select id, profile_id from http_session where id = ?" token]))) +(defn- retrieve-session + [{:keys [conn] :as cfg} id] + (when id + (db/exec-one! conn ["select id, profile_id from http_session where id = ?" id]))) (defn- retrieve-from-request - [{:keys [cookie-name] :as cfg} {:keys [cookies] :as request}] + [cfg {:keys [cookies] :as request}] (->> (get-in cookies [cookie-name :value]) - (retrieve cfg))) + (retrieve-session cfg))) -(defn- cookies - [{:keys [cookie-name] :as cfg} vals] - {cookie-name (merge vals {:path "/" :http-only true})}) +(defn- add-cookies + [response {:keys [id] :as session}] + (assoc response :cookies {cookie-name {:path "/" :http-only true :value id}})) + +(defn- clear-cookies + [response] + (assoc response :cookies {cookie-name {:value "" :max-age -1}})) (defn- middleware [cfg handler] (fn [request] (if-let [{:keys [id profile-id] :as session} (retrieve-from-request cfg request)] - (let [ech (::events-ch cfg)] - (a/>!! ech id) - (update-thread-context! {:profile-id profile-id}) + (do + (a/>!! (::events-ch cfg) id) + (l/update-thread-context! {:profile-id profile-id}) (handler (assoc request :profile-id profile-id))) (handler request)))) ;; --- STATE INIT: SESSION -(s/def ::cookie-name ::cfg/http-session-cookie-name) - (defmethod ig/pre-init-spec ::session [_] - (s/keys :req-un [::db/pool] - :opt-un [::cookie-name])) + (s/keys :req-un [::db/pool])) (defmethod ig/prep-key ::session [_ cfg] - (merge {:cookie-name "auth-token" - :buffer-size 64} - (d/without-nils cfg))) + (d/merge {:buffer-size 64} + (d/without-nils cfg))) (defmethod ig/init-key ::session [_ {:keys [pool] :as cfg}] (let [events (a/chan (a/dropping-buffer (:buffer-size cfg))) - cfg (assoc cfg - :conn pool - ::events-ch events)] + cfg (-> cfg + (assoc :conn pool) + (assoc ::events-ch events))] (-> cfg (assoc :middleware #(middleware cfg %)) (assoc :create (fn [profile-id] (fn [request response] - (let [uagent (get-in request [:headers "user-agent"]) - value (create cfg {:profile-id profile-id :user-agent uagent})] - (assoc response :cookies (cookies cfg {:value value})))))) + (let [request (assoc request :profile-id profile-id) + session (create-session cfg request)] + (add-cookies response session))))) (assoc :delete (fn [request response] - (delete cfg request) - (assoc response - :status 204 - :body "" - :cookies (cookies cfg {:value "" :max-age -1}))))))) + (delete-session cfg request) + (-> response + (assoc :status 204) + (assoc :body "") + (clear-cookies))))))) (defmethod ig/halt-key! ::session [_ data] (a/close! (::events-ch data))) + ;; --- STATE INIT: SESSION UPDATER (declare batch-events) @@ -132,9 +126,9 @@ (defmethod ig/init-key ::updater [_ {:keys [session metrics] :as cfg}] - (log/infof "initialize session updater (max-batch-age=%s, max-batch-size=%s)" - (str (:max-batch-age cfg)) - (str (:max-batch-size cfg))) + (l/info :action "initialize session updater" + :max-batch-age (str (:max-batch-age cfg)) + :max-batch-size (str (:max-batch-size cfg))) (let [input (batch-events cfg (::events-ch session)) mcnt (mtx/create {:name "http_session_update_total" @@ -146,8 +140,13 @@ (let [result (a/ system-config - (ig/prep) - (ig/init)))) - (log/infof "welcome to penpot (version: '%s')" - (:full cfg/version)))) + (ig/load-namespaces system-config) + (alter-var-root #'system (fn [sys] + (when sys (ig/halt! sys)) + (-> system-config + (ig/prep) + (ig/init)))) + (l/info :msg "welcome to penpot" + :version (:full cf/version))) (defn stop [] @@ -380,14 +319,6 @@ (when sys (ig/halt! sys)) nil))) -(prefer-method print-method - clojure.lang.IRecord - clojure.lang.IDeref) - -(prefer-method pprint/simple-dispatch - clojure.lang.IPersistentMap - clojure.lang.IDeref) - (defn -main [& _args] (start)) diff --git a/backend/src/app/media.clj b/backend/src/app/media.clj index 1886ac6267..6d035a9c82 100644 --- a/backend/src/app/media.clj +++ b/backend/src/app/media.clj @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.media "Media postprocessing." @@ -15,7 +12,7 @@ [app.common.media :as cm] [app.common.spec :as us] [app.rlimits :as rlm] - [app.svgparse :as svg] + [app.rpc.queries.svg :as svg] [clojure.spec.alpha :as s] [cuerdas.core :as str] [datoteka.core :as fs]) diff --git a/backend/src/app/metrics.clj b/backend/src/app/metrics.clj index e71283ca23..52567eb821 100644 --- a/backend/src/app/metrics.clj +++ b/backend/src/app/metrics.clj @@ -2,16 +2,13 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020-2021 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.metrics (:require [app.common.exceptions :as ex] + [app.util.logging :as l] [clojure.spec.alpha :as s] - [clojure.tools.logging :as log] [integrant.core :as ig]) (:import io.prometheus.client.CollectorRegistry @@ -50,7 +47,7 @@ (defmethod ig/init-key ::metrics [_ {:keys [definitions] :as cfg}] - (log/infof "Initializing prometheus registry and instrumentation.") + (l/info :action "initialize metrics") (let [registry (create-registry) definitions (reduce-kv (fn [res k v] (->> (assoc v :registry registry) diff --git a/backend/src/app/migrations.clj b/backend/src/app/migrations.clj index 3f9f032e47..18a8917c4c 100644 --- a/backend/src/app/migrations.clj +++ b/backend/src/app/migrations.clj @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.migrations (:require @@ -166,6 +163,9 @@ {:name "0051-mod-file-library-rel-table" :fn (mg/resource "app/migrations/sql/0051-mod-file-library-rel-table.sql")} + + {:name "0052-del-legacy-user-and-team" + :fn (mg/resource "app/migrations/sql/0052-del-legacy-user-and-team.sql")} ]) diff --git a/backend/src/app/migrations/migration_0023.clj b/backend/src/app/migrations/migration_0023.clj index b5ce1d3b84..6f66a79989 100644 --- a/backend/src/app/migrations/migration_0023.clj +++ b/backend/src/app/migrations/migration_0023.clj @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.migrations.migration-0023 (:require diff --git a/backend/src/app/migrations/sql/0052-del-legacy-user-and-team.sql b/backend/src/app/migrations/sql/0052-del-legacy-user-and-team.sql new file mode 100644 index 0000000000..6410b13c64 --- /dev/null +++ b/backend/src/app/migrations/sql/0052-del-legacy-user-and-team.sql @@ -0,0 +1,2 @@ +DELETE FROM team WHERE id = '00000000-0000-0000-0000-000000000000'; +DELETE FROM profile WHERE id = '00000000-0000-0000-0000-000000000000'; diff --git a/backend/src/app/msgbus.clj b/backend/src/app/msgbus.clj index c6d7c217bb..8058c6577d 100644 --- a/backend/src/app/msgbus.clj +++ b/backend/src/app/msgbus.clj @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2021 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.msgbus "The msgbus abstraction implemented using redis as underlying backend." @@ -14,10 +11,10 @@ [app.common.spec :as us] [app.config :as cfg] [app.util.blob :as blob] + [app.util.logging :as l] [app.util.time :as dt] [clojure.core.async :as a] [clojure.spec.alpha :as s] - [clojure.tools.logging :as log] [integrant.core :as ig] [promesa.core :as p]) (:import @@ -60,7 +57,8 @@ (defmethod ig/init-key ::msgbus [_ {:keys [backend buffer-size] :as cfg}] - (log/debugf "initializing msgbus (backend=%s)" (name backend)) + (l/debug :action "initialize msgbus" + :backend (name backend)) (let [cfg (init-backend cfg) ;; Channel used for receive publications from the application. @@ -165,13 +163,14 @@ (when-let [val (a/ pending: %s" (pr-str pending)) (some->> (seq pending) (send-off chans unsubscribe-channels)) diff --git a/backend/src/app/notifications.clj b/backend/src/app/notifications.clj index 9897b7a324..75b7de0c62 100644 --- a/backend/src/app/notifications.clj +++ b/backend/src/app/notifications.clj @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020-2021 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.notifications "A websocket based notifications mechanism." @@ -14,12 +11,12 @@ [app.db :as db] [app.metrics :as mtx] [app.util.async :as aa] + [app.util.logging :as l] [app.util.time :as dt] [app.util.transit :as t] [app.worker :as wrk] [clojure.core.async :as a] [clojure.spec.alpha :as s] - [clojure.tools.logging :as log] [integrant.core :as ig] [ring.adapter.jetty9 :as jetty] [ring.middleware.cookies :refer [wrap-cookies]] @@ -149,7 +146,7 @@ :out-ch out-ch :sub-ch sub-ch)] - (log/tracef "on-connect %s" (:session-id cfg)) + (l/trace :event "connect" :session (:session-id cfg)) ;; Forward all messages from out-ch to the websocket ;; connection @@ -171,20 +168,22 @@ ;; close subscription (a/close! sub-ch)))) - (on-error [_conn e] - (log/tracef "on-error %s (%s)" (:session-id cfg) (ex-message e)) + (on-error [_conn _e] + (l/trace :event "error" :session (:session-id cfg)) + (a/close! out-ch) (a/close! rcv-ch)) (on-close [_conn _status _reason] - (log/tracef "on-close %s" (:session-id cfg)) + (l/trace :event "close" :session (:session-id cfg)) + (a/close! out-ch) (a/close! rcv-ch)) (on-message [_ws message] (let [message (t/decode-str message)] (when-not (a/offer! rcv-ch message) - (log/warn "droping ws input message, channe full"))))] + (l/warn :msg "drop messages"))))] {:on-connect on-connect :on-error on-error @@ -254,12 +253,10 @@ (defmethod handle-message :connect [cfg _] - ;; (log/debugf "profile '%s' is connected to file '%s'" profile-id file-id) (send-presence cfg :connect)) (defmethod handle-message :disconnect [cfg _] - ;; (log/debugf "profile '%s' is disconnected from '%s'" profile-id file-id) (send-presence cfg :disconnect)) (defmethod handle-message :keepalive @@ -277,5 +274,7 @@ (defmethod handle-message :default [_ws message] (a/go - (log/warnf "received unexpected message: %s" message))) + (l/log :level :warn + :msg "received unexpected message" + :message message))) diff --git a/backend/src/app/rlimits.clj b/backend/src/app/rlimits.clj index 703fae5074..86a3903dba 100644 --- a/backend/src/app/rlimits.clj +++ b/backend/src/app/rlimits.clj @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020-2021 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.rlimits "Resource usage limits (in other words: semaphores)." diff --git a/backend/src/app/rpc.clj b/backend/src/app/rpc.clj index de202964bb..a0c1a4bda4 100644 --- a/backend/src/app/rpc.clj +++ b/backend/src/app/rpc.clj @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020-2021 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.rpc (:require @@ -15,9 +12,9 @@ [app.db :as db] [app.metrics :as mtx] [app.rlimits :as rlm] + [app.util.logging :as l] [app.util.services :as sv] [clojure.spec.alpha :as s] - [clojure.tools.logging :as log] [cuerdas.core :as str] [integrant.core :as ig])) @@ -33,10 +30,15 @@ (defn- rpc-query-handler [methods {:keys [profile-id] :as request}] (let [type (keyword (get-in request [:path-params :type])) - data (assoc (:params request) ::type type) + + data (d/merge (:params request) + (:body-params request) + (:uploads request)) + data (if profile-id (assoc data :profile-id profile-id) (dissoc data :profile-id)) + result ((get methods type default-handler) data) mdata (meta result)] @@ -76,7 +78,8 @@ (ex/raise :type :internal :code :rlimit-not-configured :hint (str/fmt "%s rlimit not configured" key))) - (log/tracef "adding rlimit to '%s' rpc handler" (::sv/name mdata)) + (l/trace :action "add rlimit" + :handler (::sv/name mdata)) (fn [cfg params] (rlm/execute rlinst (f cfg params)))) f)) @@ -86,7 +89,8 @@ (let [f (wrap-with-rlimits cfg f mdata) f (wrap-with-metrics cfg f mdata) spec (or (::sv/spec mdata) (s/spec any?))] - (log/tracef "registering '%s' command to rpc service" (::sv/name mdata)) + (l/trace :action "register" + :name (::sv/name mdata)) (fn [params] (when (and (:auth mdata true) (not (uuid? (:profile-id params)))) (ex/raise :type :authentication @@ -115,7 +119,8 @@ 'app.rpc.queries.comments 'app.rpc.queries.profile 'app.rpc.queries.recent-files - 'app.rpc.queries.viewer) + 'app.rpc.queries.viewer + 'app.rpc.queries.svg) (map (partial process-method cfg)) (into {})))) diff --git a/backend/src/app/rpc/mutations/comments.clj b/backend/src/app/rpc/mutations/comments.clj index 3008b87331..033c31ce25 100644 --- a/backend/src/app/rpc/mutations/comments.clj +++ b/backend/src/app/rpc/mutations/comments.clj @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.rpc.mutations.comments (:require diff --git a/backend/src/app/rpc/mutations/demo.clj b/backend/src/app/rpc/mutations/demo.clj index 80658a5da4..2f0c583c27 100644 --- a/backend/src/app/rpc/mutations/demo.clj +++ b/backend/src/app/rpc/mutations/demo.clj @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020-2021 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.rpc.mutations.demo "A demo specific mutations." @@ -16,8 +13,8 @@ [app.db :as db] [app.rpc.mutations.profile :as profile] [app.setup.initial-data :as sid] - [app.tasks :as tasks] [app.util.services :as sv] + [app.worker :as wrk] [buddy.core.codecs :as bc] [buddy.core.nonce :as bn] [clojure.spec.alpha :as s])) @@ -40,7 +37,7 @@ :password password :props {:onboarding-viewed true}}] - (when-not (:allow-demo-users cfg/config) + (when-not (cfg/get :allow-demo-users) (ex/raise :type :validation :code :demo-users-not-allowed :hint "Demo users are disabled by config.")) @@ -51,9 +48,10 @@ (sid/load-initial-project! conn)) ;; Schedule deletion of the demo profile - (tasks/submit! conn {:name "delete-profile" - :delay cfg/deletion-delay - :props {:profile-id id}}) + (wrk/submit! {::wrk/task :delete-profile + ::wrk/delay cfg/deletion-delay + ::wrk/conn conn + :profile-id id}) {:email email :password password}))) diff --git a/backend/src/app/rpc/mutations/files.clj b/backend/src/app/rpc/mutations/files.clj index 0631429d51..493be817b5 100644 --- a/backend/src/app/rpc/mutations/files.clj +++ b/backend/src/app/rpc/mutations/files.clj @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.rpc.mutations.files (:require @@ -19,10 +16,10 @@ [app.rpc.permissions :as perms] [app.rpc.queries.files :as files] [app.rpc.queries.projects :as proj] - [app.tasks :as tasks] [app.util.blob :as blob] [app.util.services :as sv] [app.util.time :as dt] + [app.worker :as wrk] [clojure.spec.alpha :as s])) ;; --- Helpers & Specs @@ -126,9 +123,11 @@ (files/check-edition-permissions! conn profile-id id) ;; Schedule object deletion - (tasks/submit! conn {:name "delete-object" - :delay cfg/deletion-delay - :props {:id id :type :file}}) + (wrk/submit! {::wrk/task :delete-object + ::wrk/delay cfg/deletion-delay + ::wrk/conn conn + :id id + :type :file}) (mark-file-deleted conn params))) diff --git a/backend/src/app/rpc/mutations/ldap.clj b/backend/src/app/rpc/mutations/ldap.clj index 2799f9d667..8b5f93ff04 100644 --- a/backend/src/app/rpc/mutations/ldap.clj +++ b/backend/src/app/rpc/mutations/ldap.clj @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020-2021 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.rpc.mutations.ldap (:require diff --git a/backend/src/app/rpc/mutations/management.clj b/backend/src/app/rpc/mutations/management.clj index ee885d0aa1..177d029640 100644 --- a/backend/src/app/rpc/mutations/management.clj +++ b/backend/src/app/rpc/mutations/management.clj @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2021 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.rpc.mutations.management "Move & Duplicate RPC methods for files and projects." diff --git a/backend/src/app/rpc/mutations/media.clj b/backend/src/app/rpc/mutations/media.clj index 2ae6ef4dc4..26dbac324d 100644 --- a/backend/src/app/rpc/mutations/media.clj +++ b/backend/src/app/rpc/mutations/media.clj @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020-2021 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.rpc.mutations.media (:require diff --git a/backend/src/app/rpc/mutations/profile.clj b/backend/src/app/rpc/mutations/profile.clj index 3e27d4e5e2..7e719b6c23 100644 --- a/backend/src/app/rpc/mutations/profile.clj +++ b/backend/src/app/rpc/mutations/profile.clj @@ -2,28 +2,26 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020-2021 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.rpc.mutations.profile (:require + [app.common.data :as d] [app.common.exceptions :as ex] [app.common.spec :as us] [app.common.uuid :as uuid] [app.config :as cfg] [app.db :as db] - [app.emails :as emails] + [app.emails :as eml] [app.media :as media] [app.rpc.mutations.projects :as projects] [app.rpc.mutations.teams :as teams] [app.rpc.queries.profile :as profile] [app.setup.initial-data :as sid] [app.storage :as sto] - [app.tasks :as tasks] [app.util.services :as sv] [app.util.time :as dt] + [app.worker :as wrk] [buddy.hashers :as hashers] [clojure.spec.alpha :as s] [cuerdas.core :as str])) @@ -117,16 +115,19 @@ ;; Don't allow proceed in register page if the email is ;; already reported as permanent bounced - (when (emails/has-bounce-reports? conn (:email profile)) + (when (eml/has-bounce-reports? conn (:email profile)) (ex/raise :type :validation :code :email-has-permanent-bounces :hint "looks like the email has one or many bounces reported")) - (emails/send! conn emails/register - {:to (:email profile) - :name (:fullname profile) - :token vtoken - :extra-data ptoken}) + (eml/send! {::eml/conn conn + ::eml/factory eml/register + :public-uri (:public-uri cfg) + :to (:email profile) + :name (:fullname profile) + :token vtoken + :extra-data ptoken}) + (with-meta profile {:before-complete (annotate-profile-register metrics profile)}))))) @@ -303,16 +304,35 @@ (defn login-or-register [{:keys [conn] :as cfg} {:keys [email backend] :as params}] - (letfn [(create-profile [conn {:keys [fullname email]}] + (letfn [(info->props [info] + (dissoc info :name :fullname :email :backend)) + + (info->lang [{:keys [locale] :as info}] + (when (and (string? locale) + (not (str/blank? locale))) + locale)) + + (create-profile [conn {:keys [email] :as info}] (db/insert! conn :profile {:id (uuid/next) - :fullname fullname + :fullname (:fullname info) :email (str/lower email) + :lang (info->lang info) :auth-backend backend :is-active true :password "!" + :props (db/tjson (info->props info)) :is-demo false})) + (update-profile [conn info profile] + (let [props (d/merge (:props profile) + (info->props info))] + (db/update! conn :profile + {:props (db/tjson props) + :modified-at (dt/now)} + {:id (:id profile)}) + (assoc profile :props props))) + (register-profile [conn params] (let [profile (->> (create-profile conn params) (create-profile-relations conn))] @@ -321,7 +341,9 @@ (let [profile (profile/retrieve-profile-data-by-email conn email) profile (if profile - (profile/populate-additional-data conn profile) + (->> profile + (update-profile conn params) + (profile/populate-additional-data conn)) (register-profile conn params))] (profile/strip-private-attrs profile)))) @@ -346,7 +368,6 @@ (update-profile conn params) nil)) - ;; --- Mutation: Update Password (declare validate-password!) @@ -439,7 +460,7 @@ {:changed true}) (defn- request-email-change - [{:keys [conn tokens]} {:keys [profile email] :as params}] + [{:keys [conn tokens] :as cfg} {:keys [profile email] :as params}] (let [token (tokens :generate {:iss :change-email :exp (dt/in-future "15m") @@ -452,22 +473,24 @@ (when (not= email (:email profile)) (check-profile-existence! conn params)) - (when-not (emails/allow-send-emails? conn profile) + (when-not (eml/allow-send-emails? conn profile) (ex/raise :type :validation :code :profile-is-muted :hint "looks like the profile has reported repeatedly as spam or has permanent bounces.")) - (when (emails/has-bounce-reports? conn email) + (when (eml/has-bounce-reports? conn email) (ex/raise :type :validation :code :email-has-permanent-bounces :hint "looks like the email you invite has been repeatedly reported as spam or permanent bounce")) - (emails/send! conn emails/change-email - {:to (:email profile) - :name (:fullname profile) - :pending-email email - :token token - :extra-data ptoken}) + (eml/send! {::eml/conn conn + ::eml/factory eml/change-email + :public-uri (:public-uri cfg) + :to (:email profile) + :name (:fullname profile) + :pending-email email + :token token + :extra-data ptoken}) nil)) @@ -493,16 +516,18 @@ (let [ptoken (tokens :generate-predefined {:iss :profile-identity :profile-id (:id profile)})] - (emails/send! conn emails/password-recovery - {:to (:email profile) - :token (:token profile) - :name (:fullname profile) - :extra-data ptoken}) + (eml/send! {::eml/conn conn + ::eml/factory eml/password-recovery + :public-uri (:public-uri cfg) + :to (:email profile) + :token (:token profile) + :name (:fullname profile) + :extra-data ptoken}) nil))] (db/with-atomic [conn pool] (when-let [profile (profile/retrieve-profile-data-by-email conn email)] - (when-not (emails/allow-send-emails? conn profile) + (when-not (eml/allow-send-emails? conn profile) (ex/raise :type :validation :code :profile-is-muted :hint "looks like the profile has reported repeatedly as spam or has permanent bounces.")) @@ -512,7 +537,7 @@ :code :profile-not-verified :hint "the user need to validate profile before recover password")) - (when (emails/has-bounce-reports? conn (:email profile)) + (when (eml/has-bounce-reports? conn (:email profile)) (ex/raise :type :validation :code :email-has-permanent-bounces :hint "looks like the email you invite has been repeatedly reported as spam or permanent bounce")) @@ -579,9 +604,10 @@ (check-can-delete-profile! conn profile-id) ;; Schedule a complete deletion of profile - (tasks/submit! conn {:name "delete-profile" - :delay cfg/deletion-delay - :props {:profile-id profile-id}}) + (wrk/submit! {::wrk/task :delete-profile + ::wrk/dalay cfg/deletion-delay + ::wrk/conn conn + :profile-id profile-id}) (db/update! conn :profile {:deleted-at (dt/now)} diff --git a/backend/src/app/rpc/mutations/projects.clj b/backend/src/app/rpc/mutations/projects.clj index 3dbdef8f00..bc25ef8ff3 100644 --- a/backend/src/app/rpc/mutations/projects.clj +++ b/backend/src/app/rpc/mutations/projects.clj @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.rpc.mutations.projects (:require @@ -16,9 +13,9 @@ [app.rpc.permissions :as perms] [app.rpc.queries.projects :as proj] [app.rpc.queries.teams :as teams] - [app.tasks :as tasks] [app.util.services :as sv] [app.util.time :as dt] + [app.worker :as wrk] [clojure.spec.alpha :as s])) ;; --- Helpers & Specs @@ -128,9 +125,11 @@ (proj/check-edition-permissions! conn profile-id id) ;; Schedule object deletion - (tasks/submit! conn {:name "delete-object" - :delay cfg/deletion-delay - :props {:id id :type :project}}) + (wrk/submit! {::wrk/task :delete-object + ::wrk/delay cfg/deletion-delay + ::wrk/conn conn + :id id + :type :project}) (db/update! conn :project {:deleted-at (dt/now)} diff --git a/backend/src/app/rpc/mutations/teams.clj b/backend/src/app/rpc/mutations/teams.clj index c1934de9d5..701833162f 100644 --- a/backend/src/app/rpc/mutations/teams.clj +++ b/backend/src/app/rpc/mutations/teams.clj @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020-2021 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.rpc.mutations.teams (:require @@ -15,16 +12,16 @@ [app.common.uuid :as uuid] [app.config :as cfg] [app.db :as db] - [app.emails :as emails] + [app.emails :as eml] [app.media :as media] [app.rpc.mutations.projects :as projects] [app.rpc.permissions :as perms] [app.rpc.queries.profile :as profile] [app.rpc.queries.teams :as teams] [app.storage :as sto] - [app.tasks :as tasks] [app.util.services :as sv] [app.util.time :as dt] + [app.worker :as wrk] [clojure.spec.alpha :as s] [datoteka.core :as fs])) @@ -139,9 +136,11 @@ :code :only-owner-can-delete-team)) ;; Schedule object deletion - (tasks/submit! conn {:name "delete-object" - :delay cfg/deletion-delay - :props {:id id :type :team}}) + (wrk/submit! {::wrk/task :delete-object + ::wrk/delay cfg/deletion-delay + ::wrk/conn conn + :id id + :type :team}) (db/update! conn :team {:deleted-at (dt/now)} @@ -323,27 +322,29 @@ :code :insufficient-permissions)) ;; First check if the current profile is allowed to send emails. - (when-not (emails/allow-send-emails? conn profile) + (when-not (eml/allow-send-emails? conn profile) (ex/raise :type :validation :code :profile-is-muted :hint "looks like the profile has reported repeatedly as spam or has permanent bounces")) - (when (and member (not (emails/allow-send-emails? conn member))) + (when (and member (not (eml/allow-send-emails? conn member))) (ex/raise :type :validation :code :member-is-muted :hint "looks like the profile has reported repeatedly as spam or has permanent bounces")) ;; Secondly check if the invited member email is part of the ;; global spam/bounce report. - (when (emails/has-bounce-reports? conn email) + (when (eml/has-bounce-reports? conn email) (ex/raise :type :validation :code :email-has-permanent-bounces :hint "looks like the email you invite has been repeatedly reported as spam or permanent bounce")) - (emails/send! conn emails/invite-to-team - {:to email - :invited-by (:fullname profile) - :team (:name team) - :token itoken - :extra-data ptoken}) + (eml/send! {::eml/conn conn + ::eml/factory eml/invite-to-team + :public-uri (:public-uri cfg) + :to email + :invited-by (:fullname profile) + :team (:name team) + :token itoken + :extra-data ptoken}) nil))) diff --git a/backend/src/app/rpc/mutations/verify_token.clj b/backend/src/app/rpc/mutations/verify_token.clj index 4c7938e85e..e294cfb739 100644 --- a/backend/src/app/rpc/mutations/verify_token.clj +++ b/backend/src/app/rpc/mutations/verify_token.clj @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020-2021 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.rpc.mutations.verify-token (:require diff --git a/backend/src/app/rpc/mutations/viewer.clj b/backend/src/app/rpc/mutations/viewer.clj index 85d3aa245c..85beafa969 100644 --- a/backend/src/app/rpc/mutations/viewer.clj +++ b/backend/src/app/rpc/mutations/viewer.clj @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.rpc.mutations.viewer (:require diff --git a/backend/src/app/rpc/permissions.clj b/backend/src/app/rpc/permissions.clj index 9448f71ffa..221b883632 100644 --- a/backend/src/app/rpc/permissions.clj +++ b/backend/src/app/rpc/permissions.clj @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.rpc.permissions "A permission checking helper factories." diff --git a/backend/src/app/rpc/queries/comments.clj b/backend/src/app/rpc/queries/comments.clj index 56a3a8f192..1894022f91 100644 --- a/backend/src/app/rpc/queries/comments.clj +++ b/backend/src/app/rpc/queries/comments.clj @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.rpc.queries.comments (:require @@ -131,7 +128,6 @@ (-> (db/exec-one! conn [sql profile-id file-id id]) (decode-row))))) - ;; --- Query: Comments (declare retrieve-comments) diff --git a/backend/src/app/rpc/queries/files.clj b/backend/src/app/rpc/queries/files.clj index b96903ddd0..9f0a0b3ff6 100644 --- a/backend/src/app/rpc/queries/files.clj +++ b/backend/src/app/rpc/queries/files.clj @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.rpc.queries.files (:require diff --git a/backend/src/app/rpc/queries/profile.clj b/backend/src/app/rpc/queries/profile.clj index 29fb1124b5..8016b3d94f 100644 --- a/backend/src/app/rpc/queries/profile.clj +++ b/backend/src/app/rpc/queries/profile.clj @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.rpc.queries.profile (:require diff --git a/backend/src/app/rpc/queries/projects.clj b/backend/src/app/rpc/queries/projects.clj index be4697d44e..29195ceb08 100644 --- a/backend/src/app/rpc/queries/projects.clj +++ b/backend/src/app/rpc/queries/projects.clj @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.rpc.queries.projects (:require diff --git a/backend/src/app/rpc/queries/recent_files.clj b/backend/src/app/rpc/queries/recent_files.clj index e4f9b6b1f9..51c1bbe3f3 100644 --- a/backend/src/app/rpc/queries/recent_files.clj +++ b/backend/src/app/rpc/queries/recent_files.clj @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.rpc.queries.recent-files (:require diff --git a/backend/src/app/rpc/queries/svg.clj b/backend/src/app/rpc/queries/svg.clj new file mode 100644 index 0000000000..cd244aafdb --- /dev/null +++ b/backend/src/app/rpc/queries/svg.clj @@ -0,0 +1,58 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) UXBOX Labs SL + +(ns app.rpc.queries.svg + (:require + [app.common.exceptions :as ex] + [app.common.spec :as us] + [app.util.logging :as l] + [app.util.services :as sv] + [clojure.spec.alpha :as s] + [clojure.xml :as xml] + [cuerdas.core :as str]) + (:import + javax.xml.XMLConstants + javax.xml.parsers.SAXParserFactory + org.apache.commons.io.IOUtils)) + +(defn- secure-parser-factory + [s ch] + (.. (doto (SAXParserFactory/newInstance) + (.setFeature XMLConstants/FEATURE_SECURE_PROCESSING true) + (.setFeature "http://apache.org/xml/features/disallow-doctype-decl" true)) + (newSAXParser) + (parse s ch))) + +(defn parse + [data] + (try + (with-open [istream (IOUtils/toInputStream data "UTF-8")] + (xml/parse istream secure-parser-factory)) + (catch Exception e + (l/warn :hint "error on processing svg" + :message (ex-message e)) + (ex/raise :type :validation + :code :invalid-svg-file + :cause e)))) + +(declare pre-process) + +(s/def ::data ::us/string) +(s/def ::parsed-svg (s/keys :req-un [::data])) + +(sv/defmethod ::parsed-svg + [_ {:keys [data] :as params}] + (->> data pre-process parse)) + +;; --- PROCESSORS + +(defn strip-doctype + [data] + (cond-> data + (str/includes? data "]+>" ""))) + +(def pre-process strip-doctype) diff --git a/backend/src/app/rpc/queries/teams.clj b/backend/src/app/rpc/queries/teams.clj index a414f8d7af..7bad8ba598 100644 --- a/backend/src/app/rpc/queries/teams.clj +++ b/backend/src/app/rpc/queries/teams.clj @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.rpc.queries.teams (:require diff --git a/backend/src/app/rpc/queries/viewer.clj b/backend/src/app/rpc/queries/viewer.clj index 4cb957f209..3ed2bbbc5c 100644 --- a/backend/src/app/rpc/queries/viewer.clj +++ b/backend/src/app/rpc/queries/viewer.clj @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.rpc.queries.viewer (:require diff --git a/backend/src/app/setup.clj b/backend/src/app/setup.clj index 21853d79b1..ec2c10a962 100644 --- a/backend/src/app/setup.clj +++ b/backend/src/app/setup.clj @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020-2021 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.setup "Initial data setup of instance." diff --git a/backend/src/app/setup/initial_data.clj b/backend/src/app/setup/initial_data.clj index 6c84996970..5514e71032 100644 --- a/backend/src/app/setup/initial_data.clj +++ b/backend/src/app/setup/initial_data.clj @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2021 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.setup.initial-data (:refer-clojure :exclude [load]) diff --git a/backend/src/app/srepl.clj b/backend/src/app/srepl.clj index 5fa1314d38..71ef20a6d7 100644 --- a/backend/src/app/srepl.clj +++ b/backend/src/app/srepl.clj @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.srepl "Server Repl." diff --git a/backend/src/app/storage.clj b/backend/src/app/storage.clj index 94e7e3b703..b67c2b215a 100644 --- a/backend/src/app/storage.clj +++ b/backend/src/app/storage.clj @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.storage "File Storage abstraction layer." @@ -19,10 +16,10 @@ [app.storage.fs :as sfs] [app.storage.impl :as impl] [app.storage.s3 :as ss3] + [app.util.logging :as l] [app.util.time :as dt] [app.worker :as wrk] [clojure.spec.alpha :as s] - [clojure.tools.logging :as log] [cuerdas.core :as str] [datoteka.core :as fs] [integrant.core :as ig] @@ -310,7 +307,9 @@ (run! (partial delete-in-bulk conn) groups) (recur (+ n ^long total))) (do - (log/infof "gc-deleted: processed %s items" n) + (l/info :task "gc-deleted" + :action "permanently delete items" + :count n) {:deleted n}))))))) (def sql:retrieve-deleted-objects @@ -382,7 +381,12 @@ (recur (+ cntf (count to-freeze)) (+ cntd (count to-delete)))) (do - (log/infof "gc-touched: %s objects marked as freeze and %s marked to be deleted" cntf cntd) + (l/info :task "gc-touched" + :action "mark freeze" + :count cntf) + (l/info :task "gc-touched" + :action "mark for deletion" + :count cntd) {:freeze cntf :delete cntd}))))))) (def sql:retrieve-touched-objects @@ -459,7 +463,10 @@ (recur (+ n (count all)) (+ d (count to-delete)))) (do - (log/infof "recheck: processed %s items, %s deleted" n d) + (l/info :task "recheck" + :action "recheck items" + :processed n + :deleted n) {:processed n :deleted d}))))))) (def sql:retrieve-pending-to-recheck diff --git a/backend/src/app/storage/db.clj b/backend/src/app/storage/db.clj index 9dee0b59c5..a7ed7adbc1 100644 --- a/backend/src/app/storage/db.clj +++ b/backend/src/app/storage/db.clj @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020-2021 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.storage.db (:require diff --git a/backend/src/app/storage/fs.clj b/backend/src/app/storage/fs.clj index 9a0e52b4cd..4f9d059693 100644 --- a/backend/src/app/storage/fs.clj +++ b/backend/src/app/storage/fs.clj @@ -2,22 +2,19 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020-2021 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.storage.fs (:require [app.common.exceptions :as ex] [app.common.spec :as us] + [app.common.uri :as u] [app.storage.impl :as impl] [clojure.java.io :as io] [clojure.spec.alpha :as s] [cuerdas.core :as str] [datoteka.core :as fs] - [integrant.core :as ig] - [lambdaisland.uri :as u]) + [integrant.core :as ig]) (:import java.io.InputStream java.io.OutputStream @@ -43,7 +40,7 @@ :uri (u/uri (str "file://" dir)))))) (s/def ::type ::us/keyword) -(s/def ::uri #(instance? lambdaisland.uri.URI %)) +(s/def ::uri u/uri?) (s/def ::backend (s/keys :req-un [::type ::directory ::uri])) diff --git a/backend/src/app/storage/impl.clj b/backend/src/app/storage/impl.clj index 00f356f569..27a44cbedb 100644 --- a/backend/src/app/storage/impl.clj +++ b/backend/src/app/storage/impl.clj @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020-2021 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.storage.impl "Storage backends abstraction layer." diff --git a/backend/src/app/storage/s3.clj b/backend/src/app/storage/s3.clj index 7f0499138a..da7928bdff 100644 --- a/backend/src/app/storage/s3.clj +++ b/backend/src/app/storage/s3.clj @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.storage.s3 "Storage backends abstraction layer." @@ -13,12 +10,12 @@ [app.common.data :as d] [app.common.exceptions :as ex] [app.common.spec :as us] + [app.common.uri :as u] [app.storage.impl :as impl] [app.util.time :as dt] [clojure.java.io :as io] [clojure.spec.alpha :as s] - [integrant.core :as ig] - [lambdaisland.uri :as u]) + [integrant.core :as ig]) (:import java.time.Duration java.util.Collection diff --git a/backend/src/app/svgparse.clj b/backend/src/app/svgparse.clj deleted file mode 100644 index 0dd3621f9d..0000000000 --- a/backend/src/app/svgparse.clj +++ /dev/null @@ -1,72 +0,0 @@ -;; This Source Code Form is subject to the terms of the Mozilla Public -;; License, v. 2.0. If a copy of the MPL was not distributed with this -;; file, You can obtain one at http://mozilla.org/MPL/2.0/. -;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) UXBOX Labs SL - -(ns app.svgparse - (:require - [app.common.exceptions :as ex] - [app.metrics :as mtx] - [clojure.spec.alpha :as s] - [clojure.tools.logging :as log] - [clojure.xml :as xml] - [integrant.core :as ig]) - (:import - org.apache.commons.io.IOUtils)) - -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; Handler -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - -(declare handler) -(declare process-request) - -(defmethod ig/pre-init-spec ::handler [_] - (s/keys :req-un [::mtx/metrics])) - -(defmethod ig/init-key ::handler - [_ {:keys [metrics] :as cfg}] - (let [handler #(handler cfg %)] - (->> {:registry (:registry metrics) - :type :summary - :name "http_handler_svgparse_timing" - :help "svg parse timings"} - (mtx/instrument handler)))) - -(defn- handler - [_ {:keys [headers body] :as request}] - (when (not= "image/svg+xml" (get headers "content-type")) - (ex/raise :type :validation - :code :unsupported-mime-type - :mime (get headers "content-type"))) - {:status 200 - :body (process-request body)}) - -(defn secure-factory - [s ch] - (.. (doto (javax.xml.parsers.SAXParserFactory/newInstance) - (.setFeature javax.xml.XMLConstants/FEATURE_SECURE_PROCESSING true) - (.setFeature "http://apache.org/xml/features/disallow-doctype-decl" true)) - (newSAXParser) - (parse s ch))) - -(defn parse - [data] - (try - (with-open [istream (IOUtils/toInputStream data "UTF-8")] - (xml/parse istream secure-factory)) - (catch Exception e - (log/warnf "error on processing svg: %s" (ex-message e)) - (ex/raise :type :validation - :code :invalid-svg-file - :cause e)))) - -(defn process-request - [body] - (let [data (slurp body)] - (parse data))) - diff --git a/backend/src/app/tasks.clj b/backend/src/app/tasks.clj deleted file mode 100644 index 5ef8f0d4f8..0000000000 --- a/backend/src/app/tasks.clj +++ /dev/null @@ -1,110 +0,0 @@ -;; This Source Code Form is subject to the terms of the Mozilla Public -;; License, v. 2.0. If a copy of the MPL was not distributed with this -;; file, You can obtain one at http://mozilla.org/MPL/2.0/. -;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020-2021 UXBOX Labs SL - -(ns app.tasks - (:require - [app.common.spec :as us] - [app.common.uuid :as uuid] - [app.db :as db] - [app.metrics :as mtx] - [app.util.time :as dt] - [app.worker] - [clojure.spec.alpha :as s] - [clojure.tools.logging :as log] - [integrant.core :as ig])) - -(s/def ::name ::us/string) -(s/def ::delay - (s/or :int ::us/integer - :duration dt/duration?)) -(s/def ::queue ::us/string) - -(s/def ::task-options - (s/keys :req-un [::name] - :opt-un [::delay ::props ::queue])) - -(def ^:private sql:insert-new-task - "insert into task (id, name, props, queue, priority, max_retries, scheduled_at) - values (?, ?, ?, ?, ?, ?, clock_timestamp() + ?) - returning id") - -(defn submit! - [conn {:keys [name delay props queue priority max-retries] - :or {delay 0 props {} queue "default" priority 100 max-retries 3} - :as options}] - (us/verify ::task-options options) - (let [duration (dt/duration delay) - interval (db/interval duration) - props (db/tjson props) - id (uuid/next)] - (log/debugf "submit task '%s' to be executed in '%s'" name (str duration)) - (db/exec-one! conn [sql:insert-new-task id name props queue priority max-retries interval]) - id)) - -(defn- instrument! - [registry] - (mtx/instrument-vars! - [#'submit!] - {:registry registry - :type :counter - :labels ["name"] - :name "tasks_submit_total" - :help "A counter of task submissions." - :wrap (fn [rootf mobj] - (let [mdata (meta rootf) - origf (::original mdata rootf)] - (with-meta - (fn [conn params] - (let [tname (:name params)] - (mobj :inc [tname]) - (origf conn params))) - {::original origf})))}) - - (mtx/instrument-vars! - [#'app.worker/run-task] - {:registry registry - :type :summary - :quantiles [] - :name "tasks_checkout_timing" - :help "Latency measured between scheduld_at and execution time." - :wrap (fn [rootf mobj] - (let [mdata (meta rootf) - origf (::original mdata rootf)] - (with-meta - (fn [tasks item] - (let [now (inst-ms (dt/now)) - sat (inst-ms (:scheduled-at item))] - (mobj :observe (- now sat)) - (origf tasks item))) - {::original origf})))})) - -;; --- STATE INIT: REGISTRY - -(s/def ::tasks - (s/map-of keyword? fn?)) - -(defmethod ig/pre-init-spec ::registry [_] - (s/keys :req-un [::mtx/metrics ::tasks])) - -(defmethod ig/init-key ::registry - [_ {:keys [metrics tasks]}] - (instrument! (:registry metrics)) - (let [mobj (mtx/create - {:registry (:registry metrics) - :type :summary - :labels ["name"] - :quantiles [] - :name "tasks_timing" - :help "Background task execution timing."})] - (reduce-kv (fn [res k v] - (let [tname (name k)] - (log/debugf "registring task '%s'" tname) - (assoc res tname (mtx/wrap-summary v mobj [tname])))) - {} - tasks))) diff --git a/backend/src/app/tasks/delete_object.clj b/backend/src/app/tasks/delete_object.clj index 78fd470071..bcd6e4a480 100644 --- a/backend/src/app/tasks/delete_object.clj +++ b/backend/src/app/tasks/delete_object.clj @@ -2,18 +2,16 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.tasks.delete-object "Generic task for permanent deletion of objects." (:require + [app.common.data :as d] [app.common.spec :as us] [app.db :as db] + [app.util.logging :as l] [clojure.spec.alpha :as s] - [clojure.tools.logging :as log] [integrant.core :as ig])) (declare handle-deletion) @@ -37,7 +35,8 @@ (defmethod handle-deletion :default [_conn {:keys [type]}] - (log/warnf "no handler found for '%s'" type)) + (l/warn :hint "no handler found" + :type (d/name type))) (defmethod handle-deletion :file [conn {:keys [id] :as props}] diff --git a/backend/src/app/tasks/delete_profile.clj b/backend/src/app/tasks/delete_profile.clj index 923ccf8146..17e2facb4c 100644 --- a/backend/src/app/tasks/delete_profile.clj +++ b/backend/src/app/tasks/delete_profile.clj @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020-2021 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.tasks.delete-profile "Task for permanent deletion of profiles." @@ -13,8 +10,8 @@ [app.common.spec :as us] [app.db :as db] [app.db.sql :as sql] + [app.util.logging :as l] [clojure.spec.alpha :as s] - [clojure.tools.logging :as log] [integrant.core :as ig])) (declare delete-profile-data) @@ -47,7 +44,8 @@ (if (or (:is-demo profile) (:deleted-at profile)) (delete-profile-data conn id) - (log/warnf "profile '%s' does not match constraints for deletion" id)))))) + (l/warn :hint "profile does not match constraints for deletion" + :profile-id id)))))) ;; --- IMPL @@ -70,7 +68,8 @@ (defn- delete-profile-data [conn profile-id] - (log/debugf "proceding to delete all data related to profile '%s'" profile-id) + (l/debug :action "delete profile" + :profile-id profile-id) (delete-teams conn profile-id) (delete-profile conn profile-id) true) diff --git a/backend/src/app/tasks/file_media_gc.clj b/backend/src/app/tasks/file_media_gc.clj index eebd434b50..d1bdc6751c 100644 --- a/backend/src/app/tasks/file_media_gc.clj +++ b/backend/src/app/tasks/file_media_gc.clj @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020-2021 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.tasks.file-media-gc "A maintenance task that is responsible to purge the unused media @@ -15,9 +12,9 @@ [app.common.pages.migrations :as pmg] [app.db :as db] [app.util.blob :as blob] + [app.util.logging :as l] [app.util.time :as dt] [clojure.spec.alpha :as s] - [clojure.tools.logging :as log] [integrant.core :as ig])) (declare process-file) @@ -40,7 +37,7 @@ (run! (partial process-file cfg) files) (recur (+ n (count files)))) (do - (log/debugf "finalized with total of %s processed files" n) + (l/debug :msg "finished processing files" :processed n) {:processed n})))))))) (def ^:private @@ -88,7 +85,10 @@ unused (->> (db/query conn :file-media-object {:file-id id}) (remove #(contains? used (:id %))))] - (log/debugf "processing file: id='%s' age='%s' to-delete=%s" id age (count unused)) + (l/debug :action "processing file" + :id id + :age age + :to-delete (count unused)) ;; Mark file as trimmed (db/update! conn :file @@ -96,8 +96,10 @@ {:id id}) (doseq [mobj unused] - (log/debugf "deleting media object: id='%s' media-id='%s' thumb-id='%s'" - (:id mobj) (:media-id mobj) (:thumbnail-id mobj)) + (l/debug :action "deleting media object" + :id (:id mobj) + :media-id (:media-id mobj) + :thumbnail-id (:thumbnail-id mobj)) ;; NOTE: deleting the file-media-object in the database ;; automatically marks as toched the referenced storage objects. (db/delete! conn :file-media-object {:id (:id mobj)})) diff --git a/backend/src/app/tasks/file_xlog_gc.clj b/backend/src/app/tasks/file_xlog_gc.clj index d333f2ac52..36fb71cf39 100644 --- a/backend/src/app/tasks/file_xlog_gc.clj +++ b/backend/src/app/tasks/file_xlog_gc.clj @@ -2,19 +2,16 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020-2021 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.tasks.file-xlog-gc "A maintenance task that performs a garbage collection of the file change (transaction) log." (:require [app.db :as db] + [app.util.logging :as l] [app.util.time :as dt] [clojure.spec.alpha :as s] - [clojure.tools.logging :as log] [integrant.core :as ig])) (declare sql:delete-files-xlog) @@ -31,7 +28,7 @@ (let [interval (db/interval max-age) result (db/exec-one! conn [sql:delete-files-xlog interval]) result (:next.jdbc/update-count result)] - (log/debugf "removed %s rows from file-change table" result) + (l/debug :action "trim file-change table" :removed result) result)))) (def ^:private diff --git a/backend/src/app/tasks/sendmail.clj b/backend/src/app/tasks/sendmail.clj deleted file mode 100644 index 0619b75a23..0000000000 --- a/backend/src/app/tasks/sendmail.clj +++ /dev/null @@ -1,58 +0,0 @@ -;; This Source Code Form is subject to the terms of the Mozilla Public -;; License, v. 2.0. If a copy of the MPL was not distributed with this -;; file, You can obtain one at http://mozilla.org/MPL/2.0/. -;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020-2021 UXBOX Labs SL - -(ns app.tasks.sendmail - (:require - [app.config :as cfg] - [app.util.emails :as emails] - [clojure.spec.alpha :as s] - [clojure.tools.logging :as log] - [integrant.core :as ig])) - -(declare send-console!) - -(s/def ::username ::cfg/smtp-username) -(s/def ::password ::cfg/smtp-password) -(s/def ::tls ::cfg/smtp-tls) -(s/def ::ssl ::cfg/smtp-ssl) -(s/def ::host ::cfg/smtp-host) -(s/def ::port ::cfg/smtp-port) -(s/def ::default-reply-to ::cfg/smtp-default-reply-to) -(s/def ::default-from ::cfg/smtp-default-from) -(s/def ::enabled ::cfg/smtp-enabled) - -(defmethod ig/pre-init-spec ::handler [_] - (s/keys :req-un [::enabled] - :opt-un [::username - ::password - ::tls - ::ssl - ::host - ::port - ::default-from - ::default-reply-to])) - -(defmethod ig/init-key ::handler - [_ cfg] - (fn [{:keys [props] :as task}] - (if (:enabled cfg) - (emails/send! cfg props) - (send-console! cfg props)))) - -(defn- send-console! - [cfg email] - (let [baos (java.io.ByteArrayOutputStream.) - mesg (emails/smtp-message cfg email)] - (.writeTo mesg baos) - (let [out (with-out-str - (println "email console dump:") - (println "******** start email" (:id email) "**********") - (println (.toString baos)) - (println "******** end email "(:id email) "**********"))] - (log/info out)))) diff --git a/backend/src/app/tasks/tasks_gc.clj b/backend/src/app/tasks/tasks_gc.clj index 3ff4e8db05..a3560f0e17 100644 --- a/backend/src/app/tasks/tasks_gc.clj +++ b/backend/src/app/tasks/tasks_gc.clj @@ -2,19 +2,16 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020-2021 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.tasks.tasks-gc "A maintenance task that performs a cleanup of already executed tasks from the database table." (:require [app.db :as db] + [app.util.logging :as l] [app.util.time :as dt] [clojure.spec.alpha :as s] - [clojure.tools.logging :as log] [integrant.core :as ig])) (declare sql:delete-completed-tasks) @@ -31,7 +28,7 @@ (let [interval (db/interval max-age) result (db/exec-one! conn [sql:delete-completed-tasks interval]) result (:next.jdbc/update-count result)] - (log/debugf "removed %s rows from tasks-completed table" result) + (l/debug :action "trim completed tasks table" :removed result) result)))) (def ^:private diff --git a/backend/src/app/tasks/telemetry.clj b/backend/src/app/tasks/telemetry.clj index 64ca03c8e9..564021c3e6 100644 --- a/backend/src/app/tasks/telemetry.clj +++ b/backend/src/app/tasks/telemetry.clj @@ -2,16 +2,14 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.tasks.telemetry "A task that is reponsible to collect anonymous statistical information about the current instance and send it to the telemetry server." (:require + [app.common.data :as d] [app.common.exceptions :as ex] [app.common.spec :as us] [app.config :as cfg] @@ -32,7 +30,6 @@ (s/def ::sprops (s/keys :req-un [::instance-id])) - (defmethod ig/pre-init-spec ::handler [_] (s/keys :req-un [::db/pool ::version ::uri ::sprops])) @@ -128,11 +125,16 @@ (defn- retrieve-stats [{:keys [conn version]}] - (merge - {:version version - :with-taiga (:telemetry-with-taiga cfg/config false) - :total-teams (retrieve-num-teams conn) - :total-projects (retrieve-num-projects conn) - :total-files (retrieve-num-files conn)} - (retrieve-team-averages conn) - (retrieve-jvm-stats))) + (let [referer (if (cfg/get :telemetry-with-taiga) + "taiga" + (cfg/get :telemetry-referer))] + (-> {:version version + :referer referer + :total-teams (retrieve-num-teams conn) + :total-projects (retrieve-num-projects conn) + :total-files (retrieve-num-files conn)} + (d/merge + (retrieve-team-averages conn) + (retrieve-jvm-stats)) + (d/without-nils)))) + diff --git a/backend/src/app/telemetry.clj b/backend/src/app/telemetry.clj deleted file mode 100644 index a5268ef23d..0000000000 --- a/backend/src/app/telemetry.clj +++ /dev/null @@ -1,121 +0,0 @@ -;; This Source Code Form is subject to the terms of the Mozilla Public -;; License, v. 2.0. If a copy of the MPL was not distributed with this -;; file, You can obtain one at http://mozilla.org/MPL/2.0/. -;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020-2021 UXBOX Labs SL - -(ns app.telemetry - (:require - [app.common.spec :as us] - [app.db :as db] - [app.http.middleware :refer [wrap-parse-request-body]] - [clojure.pprint :refer [pprint]] - [clojure.spec.alpha :as s] - [clojure.tools.logging :as log] - [integrant.core :as ig] - [promesa.exec :as px] - [ring.middleware.keyword-params :refer [wrap-keyword-params]] - [ring.middleware.params :refer [wrap-params]])) - - -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; Migrations -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - -(def sql:create-instance-table - "CREATE TABLE IF NOT EXISTS telemetry.instance ( - id uuid PRIMARY KEY, - created_at timestamptz NOT NULL DEFAULT now() - );") - -(def sql:create-info-table - "CREATE TABLE telemetry.info ( - instance_id uuid, - created_at timestamptz NOT NULL DEFAULT clock_timestamp(), - data jsonb NOT NULL, - - PRIMARY KEY (instance_id, created_at) - ) PARTITION BY RANGE(created_at); - - CREATE TABLE telemetry.info_default (LIKE telemetry.info INCLUDING ALL); - - ALTER TABLE telemetry.info - ATTACH PARTITION telemetry.info_default DEFAULT;") - -(def migrations - [{:name "0001-add-telemetry-schema" - :fn #(db/exec! % ["CREATE SCHEMA IF NOT EXISTS telemetry;"])} - - {:name "0002-add-instance-table" - :fn #(db/exec! % [sql:create-instance-table])} - - {:name "0003-add-info-table" - :fn #(db/exec! % [sql:create-info-table])} - - {:name "0004-del-instance-table" - :fn #(db/exec! % ["DROP TABLE telemetry.instance;"])}]) - -(defmethod ig/init-key ::migrations [_ _] migrations) - -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; Router Handler -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - -(declare handler) -(declare process-request) - -(defmethod ig/init-key ::handler - [_ cfg] - (-> (partial handler cfg) - (wrap-keyword-params) - (wrap-params) - (wrap-parse-request-body))) - -(s/def ::instance-id ::us/uuid) -(s/def ::params (s/keys :req-un [::instance-id])) - -(defn handler - [{:keys [executor] :as cfg} {:keys [params] :as request}] - (try - (let [params (us/conform ::params params) - cfg (assoc cfg - :instance-id (:instance-id params) - :data (dissoc params :instance-id))] - (px/run! executor (partial process-request cfg))) - (catch Exception e - ;; We don't want notify user of a error, just log it for posible - ;; future investigation. - (log/warn e (str "unexpected error on telemetry:\n" - (when-let [edata (ex-data e)] - (str "ex-data: \n" - (with-out-str (pprint edata)))) - (str "params: \n" - (with-out-str (pprint params))))))) - {:status 200 - :body "OK\n"}) - - -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; Request Processing -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - -(def sql:insert-instance-info - "insert into telemetry.info (instance_id, data, created_at) - values (?, ?, date_trunc('day', now())) - on conflict (instance_id, created_at) - do update set data = ?") - -(defn- process-request - [{:keys [pool instance-id data]}] - (try - (db/with-atomic [conn pool] - (let [data (db/json data)] - (db/exec! conn [sql:insert-instance-info - instance-id - data - data]))) - (catch Exception e - (log/errorf e "error on procesing request")))) diff --git a/backend/src/app/tokens.clj b/backend/src/app/tokens.clj index 4abbca8550..ecf7c84a4e 100644 --- a/backend/src/app/tokens.clj +++ b/backend/src/app/tokens.clj @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020-2021 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.tokens "Tokens generation service." diff --git a/backend/src/app/util/async.clj b/backend/src/app/util/async.clj index c78ab70e2b..fb17e6a7e3 100644 --- a/backend/src/app/util/async.clj +++ b/backend/src/app/util/async.clj @@ -2,7 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; Copyright (c) 2020 Andrey Antukh +;; Copyright (c) UXBOX Labs SL (ns app.util.async (:require diff --git a/backend/src/app/util/blob.clj b/backend/src/app/util/blob.clj index b478f9de22..d70d12ab0b 100644 --- a/backend/src/app/util/blob.clj +++ b/backend/src/app/util/blob.clj @@ -2,16 +2,13 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2016-2020 Andrey Antukh +;; Copyright (c) UXBOX Labs SL (ns app.util.blob - "A generic blob storage encoding. Mainly used for - page data, page options and txlog payload storage." + "A generic blob storage encoding. Mainly used for page data, page + options and txlog payload storage." (:require - [app.config :as cfg] + [app.config :as cf] [app.util.transit :as t] [taoensso.nippy :as n]) (:import @@ -33,17 +30,15 @@ (declare encode-v2) (declare encode-v3) -(def default-version - (:default-blob-version cfg/config 1)) - (defn encode ([data] (encode data nil)) - ([data {:keys [version] :or {version default-version}}] - (case (long version) - 1 (encode-v1 data) - 2 (encode-v2 data) - 3 (encode-v3 data) - (throw (ex-info "unsupported version" {:version version}))))) + ([data {:keys [version]}] + (let [version (or version (cf/get :default-blob-version 1))] + (case (long version) + 1 (encode-v1 data) + 2 (encode-v2 data) + 3 (encode-v3 data) + (throw (ex-info "unsupported version" {:version version})))))) (defn decode "A function used for decode persisted blobs in the database." diff --git a/backend/src/app/util/closeable.clj b/backend/src/app/util/closeable.clj index 78da0565fd..2cad6d1728 100644 --- a/backend/src/app/util/closeable.clj +++ b/backend/src/app/util/closeable.clj @@ -2,7 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; Copyright (c) 2016 Andrey Antukh +;; Copyright (c) UXBOX Labs SL (ns app.util.closeable "A closeable abstraction. A drop in replacement for diff --git a/backend/src/app/util/data.clj b/backend/src/app/util/data.clj deleted file mode 100644 index 32bd107ee1..0000000000 --- a/backend/src/app/util/data.clj +++ /dev/null @@ -1,54 +0,0 @@ -;; This Source Code Form is subject to the terms of the Mozilla Public -;; License, v. 2.0. If a copy of the MPL was not distributed with this -;; file, You can obtain one at http://mozilla.org/MPL/2.0/. -;; -;; Copyright (c) 2016 Andrey Antukh - -(ns app.util.data - "Data transformations utils." - (:require [clojure.walk :as walk] - [cuerdas.core :as str])) - -;; TODO: move to app.common.helpers - -(defn dissoc-in - [m [k & ks]] - (if ks - (if-let [nextmap (get m k)] - (let [newmap (dissoc-in nextmap ks)] - (if (seq newmap) - (assoc m k newmap) - (dissoc m k))) - m) - (dissoc m k))) - -(defn normalize-attrs - "Recursively transforms all map keys from strings to keywords." - [m] - (letfn [(tf [[k v]] - (let [ks (-> (name k) - (str/replace "_" "-"))] - [(keyword ks) v])) - (walker [x] - (if (map? x) - (into {} (map tf) x) - x))] - (walk/postwalk walker m))) - -(defn strip-delete-attrs - [m] - (dissoc m :deleted-at)) - -(defn normalize - "Perform a common normalization transformation - for a entity (database retrieved) data structure." - [m] - (-> m normalize-attrs strip-delete-attrs)) - -(defn deep-merge - [& maps] - (letfn [(merge' [& maps] - (if (every? map? maps) - (apply merge-with merge' maps) - (last maps)))] - (apply merge' (remove nil? maps)))) diff --git a/backend/src/app/util/dispatcher.clj b/backend/src/app/util/dispatcher.clj deleted file mode 100644 index e86ae408c5..0000000000 --- a/backend/src/app/util/dispatcher.clj +++ /dev/null @@ -1,95 +0,0 @@ -;; This Source Code Form is subject to the terms of the Mozilla Public -;; License, v. 2.0. If a copy of the MPL was not distributed with this -;; file, You can obtain one at http://mozilla.org/MPL/2.0/. -;; -;; Copyright (c) 2019 Andrey Antukh - -(ns app.util.dispatcher - "A generic service dispatcher implementation." - (:refer-clojure :exclude [defmethod]) - (:require - [app.common.exceptions :as ex] - [clojure.spec.alpha :as s]) - (:import - java.util.HashMap - java.util.Map)) - -(definterface IDispatcher - (^void add [key f])) - -(deftype Dispatcher [reg attr wrap] - IDispatcher - (add [this key f] - (.put ^Map reg key (wrap f)) - this) - - - clojure.lang.IDeref - (deref [_] - {:registry reg - :attr attr - :wrap wrap}) - - clojure.lang.IFn - (invoke [_ params] - (let [key (get params attr) - f (.get ^Map reg key)] - (when (nil? f) - (ex/raise :type :method-not-found - :hint "No method found for the current request." - :context {:key key})) - (f params)))) - -(defn dispatcher? - [v] - (instance? IDispatcher v)) - -(defmacro defservice - [sname & {:keys [dispatch-by wrap]}] - `(def ~sname (Dispatcher. (HashMap.) ~dispatch-by ~wrap))) - -(defn parse-defmethod - [args] - (loop [r {} - s 0 - v (first args) - n (rest args)] - (case s - 0 (if (symbol? v) - (recur (assoc r :sym v) 1 (first n) (rest n)) - (throw (ex-info "first arg to `defmethod` should be a symbol" {}))) - 1 (if (qualified-keyword? v) - (recur (-> r - (assoc :key (keyword (name v))) - (assoc :meta {:spec v :doc nil})) - 3 (first n) (rest n)) - (recur r (inc s) v n)) - 2 (if (simple-keyword? v) - (recur (-> r - (assoc :key v) - (assoc :meta {:doc nil})) - 3 (first n) (rest n)) - (throw (ex-info "second arg to `defmethod` should be a keyword" {}))) - 3 (if (string? v) - (recur (update r :meta assoc :doc v) (inc s) (first n) (rest n)) - (recur r 4 v n)) - 4 (if (map? v) - (recur (update r :meta merge v) (inc s) (first n) (rest n)) - (recur r 5 v n)) - 5 (if (vector? v) - (assoc r :args v :body n) - (throw (ex-info "missing arguments vector" {})))))) - -(defn add-method - [^Dispatcher dsp key f meta] - (let [f (with-meta f meta)] - (.add dsp key f) - dsp)) - -(defmacro defmethod - [& args] - (let [{:keys [key meta sym args body]} (parse-defmethod args) - f `(fn ~args ~@body)] - `(do - (s/assert dispatcher? ~sym) - (add-method ~sym ~key ~f ~meta)))) diff --git a/backend/src/app/util/emails.clj b/backend/src/app/util/emails.clj index a2111d6f80..948ebe437e 100644 --- a/backend/src/app/util/emails.clj +++ b/backend/src/app/util/emails.clj @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.util.emails (:require diff --git a/backend/src/app/util/http.clj b/backend/src/app/util/http.clj index 068a03bb25..9fa6b90865 100644 --- a/backend/src/app/util/http.clj +++ b/backend/src/app/util/http.clj @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.util.http "Http client abstraction layer." diff --git a/backend/src/app/util/json.clj b/backend/src/app/util/json.clj index 042517c62d..0ffd859d1e 100644 --- a/backend/src/app/util/json.clj +++ b/backend/src/app/util/json.clj @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.util.json (:refer-clojure :exclude [read]) diff --git a/backend/src/app/util/log4j.clj b/backend/src/app/util/log4j.clj deleted file mode 100644 index cd41bae78e..0000000000 --- a/backend/src/app/util/log4j.clj +++ /dev/null @@ -1,27 +0,0 @@ -;; This Source Code Form is subject to the terms of the Mozilla Public -;; License, v. 2.0. If a copy of the MPL was not distributed with this -;; file, You can obtain one at http://mozilla.org/MPL/2.0/. -;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2021 UXBOX Labs SL - -(ns app.util.log4j - (:require - [clojure.pprint :refer [pprint]]) - (:import - org.apache.logging.log4j.ThreadContext)) - -(defn update-thread-context! - [data] - (run! (fn [[key val]] - (ThreadContext/put - (name key) - (cond - (coll? val) - (binding [clojure.pprint/*print-right-margin* 120] - (with-out-str (pprint val))) - (instance? clojure.lang.Named val) (name val) - :else (str val)))) - data)) diff --git a/backend/src/app/util/logging.clj b/backend/src/app/util/logging.clj new file mode 100644 index 0000000000..5aaa409a8b --- /dev/null +++ b/backend/src/app/util/logging.clj @@ -0,0 +1,106 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) UXBOX Labs SL + +(ns app.util.logging + (:require + [clojure.pprint :refer [pprint]]) + (:import + org.apache.logging.log4j.Level + org.apache.logging.log4j.LogManager + org.apache.logging.log4j.Logger + org.apache.logging.log4j.ThreadContext + org.apache.logging.log4j.message.MapMessage + org.apache.logging.log4j.spi.LoggerContext)) + +(defn build-map-message + [m] + (let [message (MapMessage. (count m))] + (reduce-kv #(.with ^MapMessage %1 (name %2) %3) message m))) + +(defprotocol ILogger + (-enabled? [logger level]) + (-write! [logger level throwable message])) + +(def logger-context + (LogManager/getContext false)) + +(def logging-agent + (agent nil :error-mode :continue)) + +(defn get-logger + [lname] + (.getLogger ^LoggerContext logger-context ^String lname)) + +(defn get-level + [level] + (case level + :trace Level/TRACE + :debug Level/DEBUG + :info Level/INFO + :warn Level/WARN + :error Level/ERROR + :fatal Level/FATAL)) + +(defn enabled? + [logger level] + (.isEnabled ^Logger logger ^Level level)) + +(defn write-log! + [logger level e msg] + (if e + (.log ^Logger logger + ^Level level + ^Object msg + ^Throwable e) + (.log ^Logger logger + ^Level level + ^Object msg))) + +(defmacro log + [& {:keys [level cause ::logger ::async] :as props}] + (let [props (dissoc props :level :cause ::logger ::async) + logger (or logger (str *ns*)) + logger-sym (gensym "log") + level-sym (gensym "log")] + `(let [~logger-sym (get-logger ~logger) + ~level-sym (get-level ~level)] + (if (enabled? ~logger-sym ~level-sym) + ~(if async + `(send-off logging-agent (fn [_#] (write-log! ~logger-sym ~level-sym ~cause (build-map-message ~props)))) + `(write-log! ~logger-sym ~level-sym ~cause (build-map-message ~props))))))) + +(defmacro info + [& params] + `(log :level :info ~@params)) + +(defmacro error + [& params] + `(log :level :error ~@params)) + +(defmacro warn + [& params] + `(log :level :warn ~@params)) + +(defmacro debug + [& params] + `(log :level :debug ~@params)) + +(defmacro trace + [& params] + `(log :level :trace ~@params)) + +(defn update-thread-context! + [data] + (run! (fn [[key val]] + (ThreadContext/put + (name key) + (cond + (coll? val) + (binding [clojure.pprint/*print-right-margin* 120] + (with-out-str (pprint val))) + (instance? clojure.lang.Named val) (name val) + :else (str val)))) + data)) diff --git a/backend/src/app/util/migrations.clj b/backend/src/app/util/migrations.clj index 9255657ab1..f79c50b2b7 100644 --- a/backend/src/app/util/migrations.clj +++ b/backend/src/app/util/migrations.clj @@ -2,17 +2,13 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.util.migrations (:require + [app.util.logging :as l] [clojure.java.io :as io] [clojure.spec.alpha :as s] - [clojure.tools.logging :as log] - [cuerdas.core :as str] [next.jdbc :as jdbc])) (s/def ::name string?) @@ -40,7 +36,7 @@ (defn- impl-migrate-single [pool modname {:keys [name] :as migration}] (when-not (registered? pool modname (:name migration)) - (log/info (str/format "applying migration %s/%s" modname name)) + (l/info :action "apply migration" :module modname :name name) (register! pool modname name) ((:fn migration) pool))) diff --git a/backend/src/app/util/services.clj b/backend/src/app/util/services.clj index edc8c1074e..1621ad7606 100644 --- a/backend/src/app/util/services.clj +++ b/backend/src/app/util/services.clj @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020-2021 Andrey Antukh +;; Copyright (c) UXBOX Labs SL (ns app.util.services "A helpers and macros for define rpc like registry based services." diff --git a/backend/src/app/util/sql.clj b/backend/src/app/util/sql.clj deleted file mode 100644 index 61274db155..0000000000 --- a/backend/src/app/util/sql.clj +++ /dev/null @@ -1,198 +0,0 @@ -;; Copyright (c) 2019 Andrey Antukh -;; All rights reserved. -;; -;; Redistribution and use in source and binary forms, with or without -;; modification, are permitted provided that the following conditions are met: -;; -;; * Redistributions of source code must retain the above copyright notice, this -;; list of conditions and the following disclaimer. -;; -;; * Redistributions in binary form must reproduce the above copyright notice, -;; this list of conditions and the following disclaimer in the documentation -;; and/or other materials provided with the distribution. -;; -;; THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -;; AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -;; IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -;; DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -;; FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -;; DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -;; SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -;; CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -;; OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -;; OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -(ns app.util.sql - "A composable sql helpers." - (:refer-clojure :exclude [test update set format]) - (:require [clojure.core :as c] - [cuerdas.core :as str])) - -;; --- Low Level Helpers - -(defn raw-expr - [m] - (cond - (string? m) - {::type :raw-expr - :sql m - :params []} - - (vector? m) - {::type :raw-expr - :sql (first m) - :params (vec (rest m))} - - (and (map? m) - (= :raw-expr (::type m))) - m - - :else - (throw (ex-info "unexpected input" {:m m})))) - -(defn alias-expr - [m] - (cond - (string? m) - {::type :alias-expr - :sql m - :alias nil - :params []} - - (vector? m) - {::type :alias-expr - :sql (first m) - :alias (second m) - :params (vec (drop 2 m))} - - :else - (throw (ex-info "unexpected input" {:m m})))) - -;; --- SQL API (Select only) - -(defn from - [name] - {::type :query - ::from [(alias-expr name)] - ::order [] - ::select [] - ::join [] - ::where []}) - -(defn select - [m & fields] - (c/update m ::select into (map alias-expr fields))) - -(defn limit - [m n] - (assoc m ::limit [(raw-expr ["LIMIT ?" n])])) - -(defn offset - [m n] - (assoc m ::offset [(raw-expr ["OFFSET ?" n])])) - -(defn order - [m e] - (c/update m ::order conj (raw-expr e))) - -(defn- join* - [m type table condition] - (c/update m ::join conj - {::type :join-expr - :type type - :table (alias-expr table) - :condition (raw-expr condition)})) - -(defn join - [m table condition] - (join* m :inner table condition)) - -(defn ljoin - [m table condition] - (join* m :left table condition)) - -(defn rjoin - [m table condition] - (join* m :right table condition)) - -(defn where - [m & conditions] - (->> (filter identity conditions) - (reduce #(c/update %1 ::where conj (raw-expr %2)) m))) - -;; --- Formating - -(defmulti format-expr ::type) - -(defmethod format-expr :raw-expr - [{:keys [sql params]}] - [sql params]) - -(defmethod format-expr :alias-expr - [{:keys [sql alias params]}] - (if alias - [(str sql " AS " alias) params] - [sql params])) - -(defmethod format-expr :join-expr - [{:keys [table type condition]}] - (let [[csql cparams] (format-expr condition) - [tsql tparams] (format-expr table) - prefix (str/upper (name type))] - [(str prefix " JOIN " tsql " ON (" csql ")") (into cparams tparams)])) - -(defn- format-exprs - ([items] (format-exprs items {})) - ([items {:keys [prefix suffix join-with] - :or {prefix "" - suffix "" - join-with ","}}] - (loop [rs [] - rp [] - v (first items) - n (rest items)] - (if v - (let [[s p] (format-expr v)] - (recur (conj rs s) - (into rp p) - (first n) - (rest n))) - (if (empty? rs) - ["" []] - [(str prefix (str/join join-with rs) suffix) rp]))))) - -(defn- process-param-tokens - [sql] - (let [cnt (java.util.concurrent.atomic.AtomicInteger. 1)] - (str/replace sql #"\?" (fn [& _args] - (str "$" (.getAndIncrement cnt)))))) - -(def ^:private select-formatters - [#(format-exprs (::select %) {:prefix "SELECT "}) - #(format-exprs (::from %) {:prefix "FROM "}) - #(format-exprs (::join %) {:join-with " "}) - #(format-exprs (::where %) {:prefix "WHERE (" - :join-with ") AND (" - :suffix ")"}) - #(format-exprs (::order %) {:prefix "ORDER BY "} ) - #(format-exprs (::limit %)) - #(format-exprs (::offset %))]) - -(defn- collect - [formatters qdata] - (loop [sqls [] - params [] - f (first formatters) - r (rest formatters)] - (if (fn? f) - (let [[s p] (f qdata)] - (recur (conj sqls s) - (into params p) - (first r) - (rest r))) - [(str/join " " sqls) params]))) - -(defn fmt - [qdata] - (let [[sql params] (collect select-formatters qdata)] - (into [(process-param-tokens sql)] params))) diff --git a/backend/src/app/util/template.clj b/backend/src/app/util/template.clj index cd5e00f0aa..2aa8c324d3 100644 --- a/backend/src/app/util/template.clj +++ b/backend/src/app/util/template.clj @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.util.template (:require diff --git a/backend/src/app/util/time.clj b/backend/src/app/util/time.clj index 51d0f89dda..c2839f483d 100644 --- a/backend/src/app/util/time.clj +++ b/backend/src/app/util/time.clj @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020-2021 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.util.time (:require @@ -60,7 +57,6 @@ [t1 t2] (Duration/between t1 t2)) - (letfn [(conformer [v] (cond (duration? v) v diff --git a/backend/src/app/util/transit.clj b/backend/src/app/util/transit.clj index 7347eddb19..e200f5e695 100644 --- a/backend/src/app/util/transit.clj +++ b/backend/src/app/util/transit.clj @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.util.transit (:require diff --git a/backend/src/app/worker.clj b/backend/src/app/worker.clj index fe078ba02b..e33a114648 100644 --- a/backend/src/app/worker.clj +++ b/backend/src/app/worker.clj @@ -2,24 +2,22 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020 Andrey Antukh +;; Copyright (c) UXBOX Labs SL (ns app.worker "Async tasks abstraction (impl)." (:require + [app.common.data :as d] [app.common.exceptions :as ex] [app.common.spec :as us] [app.common.uuid :as uuid] [app.db :as db] + [app.metrics :as mtx] [app.util.async :as aa] - [app.util.log4j :refer [update-thread-context!]] + [app.util.logging :as l] [app.util.time :as dt] [clojure.core.async :as a] [clojure.spec.alpha :as s] - [clojure.tools.logging :as log] [cuerdas.core :as str] [integrant.core :as ig] [promesa.exec :as px]) @@ -35,21 +33,13 @@ ;; Executor ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(s/def ::name ::us/string) +(s/def ::name keyword?) (s/def ::min-threads ::us/integer) (s/def ::max-threads ::us/integer) (s/def ::idle-timeout ::us/integer) (defmethod ig/pre-init-spec ::executor [_] - (s/keys :opt-un [::min-threads ::max-threads ::idle-timeout ::name])) - -(defmethod ig/prep-key ::executor - [_ cfg] - (merge {:min-threads 0 - :max-threads 256 - :idle-timeout 60000 - :name "worker"} - cfg)) + (s/keys :req-un [::min-threads ::max-threads ::idle-timeout ::name])) (defmethod ig/init-key ::executor [_ {:keys [min-threads max-threads idle-timeout name]}] @@ -57,28 +47,29 @@ (int min-threads) (int idle-timeout)) (.setStopTimeout 500) - (.setName name) + (.setName (d/name name)) (.start))) (defmethod ig/halt-key! ::executor [_ instance] (.stop ^QueuedThreadPool instance)) - ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Worker ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (declare event-loop-fn) +(declare instrument-tasks) -(s/def ::queue ::us/string) +(s/def ::queue keyword?) (s/def ::parallelism ::us/integer) (s/def ::batch-size ::us/integer) -(s/def ::tasks (s/map-of string? fn?)) +(s/def ::tasks (s/map-of keyword? fn?)) (s/def ::poll-interval ::dt/duration) (defmethod ig/pre-init-spec ::worker [_] (s/keys :req-un [::executor + ::mtx/metrics ::db/pool ::batch-size ::name @@ -88,47 +79,50 @@ (defmethod ig/prep-key ::worker [_ cfg] - (merge {:batch-size 2 - :name "worker" - :poll-interval (dt/duration {:seconds 5}) - :queue "default"} - cfg)) + (d/merge {:batch-size 2 + :name :worker + :poll-interval (dt/duration {:seconds 5}) + :queue :default} + (d/without-nils cfg))) (defmethod ig/init-key ::worker [_ {:keys [pool poll-interval name queue] :as cfg}] - (log/infof "starting worker '%s' on queue '%s'" name queue) - (let [cch (a/chan 1) - poll-ms (inst-ms poll-interval)] + (l/info :action "start worker" + :name (d/name name) + :queue (d/name queue)) + (let [close-ch (a/chan 1) + poll-ms (inst-ms poll-interval)] (a/go-loop [] - (let [[val port] (a/alts! [cch (event-loop-fn cfg)] :priority true)] + (let [[val port] (a/alts! [close-ch (event-loop-fn cfg)] :priority true)] (cond ;; Terminate the loop if close channel is closed or ;; event-loop-fn returns nil. - (or (= port cch) (nil? val)) - (log/infof "stop condition found; shutdown worker: '%s'" name) + (or (= port close-ch) (nil? val)) + (l/debug :msg "stop condition found") (db/pool-closed? pool) (do - (log/info "worker eventloop is aborted because pool is closed") - (a/close! cch)) + (l/debug :msg "eventloop aborted because pool is closed") + (a/close! close-ch)) (and (instance? java.sql.SQLException val) (contains? #{"08003" "08006" "08001" "08004"} (.getSQLState ^java.sql.SQLException val))) (do - (log/error "connection error, trying resume in some instants") + (l/error :hint "connection error, trying resume in some instants") (a/ res + (not (qualified-keyword? k)) + (assoc! k v))) + (transient {}) + options))) + +(defn submit! + [{:keys [::task ::delay ::queue ::priority ::max-retries ::conn] + :or {delay 0 queue :default priority 100 max-retries 3} + :as options}] + (us/verify ::submit-options options) + (let [duration (dt/duration delay) + interval (db/interval duration) + props (-> options extract-props db/tjson) + id (uuid/next)] + (l/debug :action "submit task" + :name (d/name task) + :in duration) + (db/exec-one! conn [sql:insert-new-task id (d/name task) props (d/name queue) priority max-retries interval]) + id)) + + +;; --- RUNNER (def ^:private sql:mark-as-retry @@ -194,17 +232,19 @@ nil)) (defn- decode-task-row - [{:keys [props] :as row}] + [{:keys [props name] :as row}] (when row (cond-> row - (db/pgobject? props) (assoc :props (db/decode-transit-pgobject props))))) + (db/pgobject? props) (assoc :props (db/decode-transit-pgobject props)) + (string? name) (assoc :name (keyword name))))) (defn- handle-task [tasks {:keys [name] :as item}] (let [task-fn (get tasks name)] (if task-fn (task-fn item) - (log/warnf "no task handler found for '%s'" (pr-str name))) + (l/warn :msg "no task handler found" + :name (d/name name))) {:status :completed :task item})) (defn get-error-context @@ -228,21 +268,32 @@ (assoc :inc-by 0)) (let [cdata (get-error-context error item)] - (update-thread-context! cdata) - (log/errorf error "unhandled exception on task (id: '%s')" (:id cdata)) + (l/update-thread-context! cdata) + (l/error :cause error + :hint "unhandled exception on task" + :id (:id cdata)) + (if (>= (:retry-num item) (:max-retries item)) {:status :failed :task item :error error} {:status :retry :task item :error error}))))) (defn- run-task [{:keys [tasks]} item] - (try - (log/debugf "started task '%s/%s/%s'" (:name item) (:id item) (:retry-num item)) - (handle-task tasks item) - (catch Exception e - (handle-exception e item)) - (finally - (log/debugf "finished task '%s/%s/%s'" (:name item) (:id item) (:retry-num item))))) + (let [name (d/name (:name item))] + (try + (l/debug :action "start task" + :name name + :id (:id item) + :retry (:retry-num item)) + + (handle-task tasks item) + (catch Exception e + (handle-exception e item)) + (finally + (l/debug :action "end task" + :name name + :id (:id item) + :retry (:retry-num item)))))) (def sql:select-next-tasks "select * from task as t @@ -256,7 +307,7 @@ (defn- event-loop-fn* [{:keys [pool executor batch-size] :as cfg}] (db/with-atomic [conn pool] - (let [queue (:queue cfg) + (let [queue (name (:queue cfg)) items (->> (db/exec! conn [sql:select-next-tasks queue batch-size]) (map decode-task-row) (seq)) @@ -288,16 +339,16 @@ (declare synchronize-schedule) (s/def ::fn (s/or :var var? :fn fn?)) -(s/def ::id ::us/string) +(s/def ::id keyword?) (s/def ::cron dt/cron?) (s/def ::props (s/nilable map?)) (s/def ::task keyword?) -(s/def ::scheduled-task-spec - (s/keys :req-un [::id ::cron ::task] - :opt-un [::props])) +(s/def ::scheduled-task + (s/keys :req-un [::cron ::task] + :opt-un [::props ::id])) -(s/def ::schedule (s/coll-of (s/nilable ::scheduled-task-spec))) +(s/def ::schedule (s/coll-of (s/nilable ::scheduled-task))) (defmethod ig/pre-init-spec ::scheduler [_] (s/keys :req-un [::executor ::db/pool ::schedule ::tasks])) @@ -307,8 +358,13 @@ (let [scheduler (Executors/newScheduledThreadPool (int 1)) schedule (->> schedule (filter some?) + ;; If id is not defined, use the task as id. + (map (fn [{:keys [id task] :as item}] + (if (some? id) + (assoc item :id (d/name id)) + (assoc item :id (d/name task))))) (map (fn [{:keys [task] :as item}] - (let [f (get tasks (name task))] + (let [f (get tasks task)] (when-not f (ex/raise :type :internal :code :task-not-found @@ -342,7 +398,7 @@ (defn- synchronize-schedule-item [conn {:keys [id cron]}] (let [cron (str cron)] - (log/infof "initialize scheduled task '%s' (cron: '%s')" id cron) + (l/debug :action "initialize scheduled task" :id id :cron cron) (db/exec-one! conn [sql:upsert-scheduled-task id cron cron]))) (defn- synchronize-schedule @@ -362,8 +418,8 @@ [{:keys [executor pool] :as cfg} {:keys [id] :as task}] (letfn [(run-task [conn] (try - (when (db/exec-one! conn [sql:lock-scheduled-task id]) - (log/debugf "executing scheduled task '%s'" id) + (when (db/exec-one! conn [sql:lock-scheduled-task (d/name id)]) + (l/debug :action "execute scheduled task" :id id) ((:fn task) task)) (catch Throwable e e))) @@ -372,7 +428,9 @@ (db/with-atomic [conn pool] (let [result (run-task conn)] (when (ex/exception? result) - (log/errorf result "unhandled exception on scheduled task '%s'" id)))))] + (l/error :cause result + :hint "unhandled exception on scheduled task" + :id id)))))] (try (px/run! executor handle-task) @@ -390,3 +448,62 @@ [{:keys [scheduler] :as cfg} {:keys [cron] :as task}] (let [ms (ms-until-valid cron)] (px/schedule! scheduler ms (partial execute-scheduled-task cfg task)))) + +;; --- INSTRUMENTATION + +(defn instrument! + [registry] + (mtx/instrument-vars! + [#'submit!] + {:registry registry + :type :counter + :labels ["name"] + :name "tasks_submit_total" + :help "A counter of task submissions." + :wrap (fn [rootf mobj] + (let [mdata (meta rootf) + origf (::original mdata rootf)] + (with-meta + (fn [conn params] + (let [tname (:name params)] + (mobj :inc [tname]) + (origf conn params))) + {::original origf})))}) + + (mtx/instrument-vars! + [#'app.worker/run-task] + {:registry registry + :type :summary + :quantiles [] + :name "tasks_checkout_timing" + :help "Latency measured between scheduld_at and execution time." + :wrap (fn [rootf mobj] + (let [mdata (meta rootf) + origf (::original mdata rootf)] + (with-meta + (fn [tasks item] + (let [now (inst-ms (dt/now)) + sat (inst-ms (:scheduled-at item))] + (mobj :observe (- now sat)) + (origf tasks item))) + {::original origf})))})) + + +(defmethod ig/pre-init-spec ::registry [_] + (s/keys :req-un [::mtx/metrics ::tasks])) + +(defmethod ig/init-key ::registry + [_ {:keys [metrics tasks]}] + (let [mobj (mtx/create + {:registry (:registry metrics) + :type :summary + :labels ["name"] + :quantiles [] + :name "tasks_timing" + :help "Background task execution timing."})] + (reduce-kv (fn [res k v] + (let [tname (name k)] + (l/debug :action "register task" :name tname) + (assoc res k (mtx/wrap-summary v mobj [tname])))) + {} + tasks))) diff --git a/backend/tests/app/tests/helpers.clj b/backend/tests/app/tests/helpers.clj index 2c4584573e..bf6de1ff11 100644 --- a/backend/tests/app/tests/helpers.clj +++ b/backend/tests/app/tests/helpers.clj @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.tests.helpers (:require @@ -13,7 +10,7 @@ [app.common.pages :as cp] [app.common.spec :as us] [app.common.uuid :as uuid] - [app.config :as cfg] + [app.config :as cf] [app.db :as db] [app.main :as main] [app.media] @@ -38,16 +35,23 @@ (def ^:dynamic *system* nil) (def ^:dynamic *pool* nil) +(def defaults + {:database-uri "postgresql://postgres/penpot_test" + :redis-uri "redis://redis/1"}) + (def config - (merge {:redis-uri "redis://redis/1" - :database-uri "postgresql://postgres/penpot_test" - :storage-fs-directory "/tmp/app/storage" - :migrations-verbose false} - cfg/config)) + (->> (cf/read-env "penpot-test") + (merge cf/defaults defaults) + (us/conform ::cf/config))) (defn state-init [next] - (let [config (-> (main/build-system-config config) + (let [config (-> main/system-config + (assoc-in [:app.msgbus/msgbus :redis-uri] (:redis-uri config)) + (assoc-in [:app.db/pool :uri] (:database-uri config)) + (assoc-in [:app.db/pool :username] (:database-username config)) + (assoc-in [:app.db/pool :password] (:database-password config)) + (assoc-in [[:app.main/main :app.storage.fs/backend] :directory] "/tmp/app/storage") (dissoc :app.srepl/server :app.http/server :app.http/router @@ -328,8 +332,10 @@ "Helper for mock app.config/get" [data] (fn - ([key] (get (merge config data) key)) - ([key default] (get (merge config data) key default)))) + ([key] + (get data key (get @cf/config key))) + ([key default] + (get data key (get @cf/config key default))))) (defn reset-mock! [m] diff --git a/backend/tests/app/tests/test_bounces_handling.clj b/backend/tests/app/tests/test_bounces_handling.clj index 065ada03f0..57e838af53 100644 --- a/backend/tests/app/tests/test_bounces_handling.clj +++ b/backend/tests/app/tests/test_bounces_handling.clj @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2021 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.tests.test-bounces-handling (:require @@ -21,10 +18,6 @@ (t/use-fixtures :once th/state-init) (t/use-fixtures :each th/database-reset) -;; (with-mocks [mock {:target 'app.tasks/submit! :return nil}] -;; Right now we have many different scenarios what can cause a -;; bounce/complain report. - (defn- decode-row [{:keys [content] :as row}] (cond-> row diff --git a/backend/tests/app/tests/test_common_geom.clj b/backend/tests/app/tests/test_common_geom.clj index 4c7acd75c7..4656f716c3 100644 --- a/backend/tests/app/tests/test_common_geom.clj +++ b/backend/tests/app/tests/test_common_geom.clj @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.tests.test-common-geom (:require diff --git a/backend/tests/app/tests/test_common_geom_shapes.clj b/backend/tests/app/tests/test_common_geom_shapes.clj index 860f05ea44..b53d3ebc59 100644 --- a/backend/tests/app/tests/test_common_geom_shapes.clj +++ b/backend/tests/app/tests/test_common_geom_shapes.clj @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.tests.test-common-geom-shapes (:require diff --git a/backend/tests/app/tests/test_common_pages.clj b/backend/tests/app/tests/test_common_pages.clj index f6b84cc674..80e3ea7e2f 100644 --- a/backend/tests/app/tests/test_common_pages.clj +++ b/backend/tests/app/tests/test_common_pages.clj @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.tests.test-common-pages (:require diff --git a/backend/tests/app/tests/test_emails.clj b/backend/tests/app/tests/test_emails.clj index 7381f510ac..7cc62966ef 100644 --- a/backend/tests/app/tests/test_emails.clj +++ b/backend/tests/app/tests/test_emails.clj @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.tests.test-emails (:require diff --git a/backend/tests/app/tests/test_services_files.clj b/backend/tests/app/tests/test_services_files.clj index 4de3d8c5f2..68f34eacb2 100644 --- a/backend/tests/app/tests/test_services_files.clj +++ b/backend/tests/app/tests/test_services_files.clj @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.tests.test-services-files (:require diff --git a/backend/tests/app/tests/test_services_management.clj b/backend/tests/app/tests/test_services_management.clj index fd0d582336..f662b43810 100644 --- a/backend/tests/app/tests/test_services_management.clj +++ b/backend/tests/app/tests/test_services_management.clj @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2021 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.tests.test-services-management (:require diff --git a/backend/tests/app/tests/test_services_media.clj b/backend/tests/app/tests/test_services_media.clj index 0385e47191..112eed7795 100644 --- a/backend/tests/app/tests/test_services_media.clj +++ b/backend/tests/app/tests/test_services_media.clj @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.tests.test-services-media (:require diff --git a/backend/tests/app/tests/test_services_profile.clj b/backend/tests/app/tests/test_services_profile.clj index f527a3d314..7ac20c849c 100644 --- a/backend/tests/app/tests/test_services_profile.clj +++ b/backend/tests/app/tests/test_services_profile.clj @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.tests.test-services-profile (:require @@ -132,7 +129,7 @@ (t/is (nil? result))) ;; Request profile to be deleted - (with-mocks [mock {:target 'app.tasks/submit! :return nil}] + (with-mocks [mock {:target 'app.worker/submit! :return nil}] (let [params {::th/type :delete-profile :profile-id (:id prof)} out (th/mutation! params)] @@ -140,11 +137,11 @@ ;; check the mock (let [mock (deref mock) - mock-params (second (:call-args mock))] + mock-params (first (:call-args mock))] (t/is (:called? mock)) (t/is (= 1 (:call-count mock))) - (t/is (= "delete-profile" (:name mock-params))) - (t/is (= (:id prof) (get-in mock-params [:props :profile-id])))))) + (t/is (= :delete-profile (:app.worker/task mock-params))) + (t/is (= (:id prof) (:profile-id mock-params)))))) ;; query files after profile soft deletion (let [params {::th/type :files @@ -257,8 +254,8 @@ :terms-privacy true} out (th/mutation! data)] ;; (th/print-result! out) - (let [mock (deref mock) - [_ _ params] (:call-args mock)] + (let [mock (deref mock) + [params] (:call-args mock)] ;; (clojure.pprint/pprint params) (t/is (:called? mock)) (t/is (= (:email data) (:to params))) diff --git a/backend/tests/app/tests/test_services_projects.clj b/backend/tests/app/tests/test_services_projects.clj index 0d4fabf8d8..5c791fe74e 100644 --- a/backend/tests/app/tests/test_services_projects.clj +++ b/backend/tests/app/tests/test_services_projects.clj @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.tests.test-services-projects (:require diff --git a/backend/tests/app/tests/test_services_teams.clj b/backend/tests/app/tests/test_services_teams.clj index da6ddb6884..c64f7922d2 100644 --- a/backend/tests/app/tests/test_services_teams.clj +++ b/backend/tests/app/tests/test_services_teams.clj @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.tests.test-services-teams (:require diff --git a/backend/tests/app/tests/test_services_viewer.clj b/backend/tests/app/tests/test_services_viewer.clj index 54d2ce93c8..ce638a19eb 100644 --- a/backend/tests/app/tests/test_services_viewer.clj +++ b/backend/tests/app/tests/test_services_viewer.clj @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.tests.test-services-viewer (:require diff --git a/backend/tests/app/tests/test_storage.clj b/backend/tests/app/tests/test_storage.clj index f967c491d2..7c0f49e8fb 100644 --- a/backend/tests/app/tests/test_storage.clj +++ b/backend/tests/app/tests/test_storage.clj @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.tests.test-storage (:require diff --git a/common/app/common/attrs.cljc b/common/app/common/attrs.cljc index f341a9adb9..ce6af91313 100644 --- a/common/app/common/attrs.cljc +++ b/common/app/common/attrs.cljc @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.common.attrs (:refer-clojure :exclude [merge])) diff --git a/common/app/common/data.cljc b/common/app/common/data.cljc index b6e2eb0547..a46b306f2d 100644 --- a/common/app/common/data.cljc +++ b/common/app/common/data.cljc @@ -2,7 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; Copyright (c) 2016-2019 Andrey Antukh +;; Copyright (c) UXBOX Labs SL (ns app.common.data "Data manipulation and query helper functions." @@ -403,6 +403,9 @@ (keyword? maybe-keyword) (core/name maybe-keyword) + (string? maybe-keyword) + maybe-keyword + (nil? maybe-keyword) default-value :else diff --git a/common/app/common/data/undo_stack.cljc b/common/app/common/data/undo_stack.cljc new file mode 100644 index 0000000000..57f71d1286 --- /dev/null +++ b/common/app/common/data/undo_stack.cljc @@ -0,0 +1,60 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) UXBOX Labs SL + +(ns app.common.data.undo-stack + (:refer-clojure :exclude [peek]) + (:require + #?(:cljs [cljs.core :as core] + :clj [clojure.core :as core]))) + +(defonce MAX-UNDO-SIZE 100) + +(defn make-stack + [] + {:index -1 + :items []}) + +(defn peek + [{index :index items :items :as stack}] + (when (and (>= index 0) (< index (count items))) + (nth items index))) + +(defn append + [{index :index items :items :as stack} value] + + (if (and (some? stack) (not= value (peek stack))) + (let [items (cond-> items + (> index 0) + (subvec 0 (inc index)) + + (> (+ index 2) MAX-UNDO-SIZE) + (subvec 1 (inc index)) + + :always + (conj value)) + + index (min (dec MAX-UNDO-SIZE) (inc index))] + {:index index + :items items}) + stack)) + +(defn fixup + [{index :index :as stack} value] + (assoc-in stack [:items index] value)) + +(defn undo + [{index :index items :items :as stack}] + (update stack :index dec)) + +(defn redo + [{index :index items :items :as stack}] + (cond-> stack + (< index (dec (count items))) + (update :index inc))) + +(defn size + [{index :index items :items :as stack}] + (inc index)) diff --git a/common/app/common/exceptions.cljc b/common/app/common/exceptions.cljc index 96782de956..4fe202efdc 100644 --- a/common/app/common/exceptions.cljc +++ b/common/app/common/exceptions.cljc @@ -2,7 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; Copyright (c) 2016 Andrey Antukh +;; Copyright (c) UXBOX Labs SL (ns app.common.exceptions "A helpers for work with exceptions." diff --git a/common/app/common/geom/align.cljc b/common/app/common/geom/align.cljc index f06bcb4bd4..4a14e4b1e4 100644 --- a/common/app/common/geom/align.cljc +++ b/common/app/common/geom/align.cljc @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.common.geom.align (:require diff --git a/common/app/common/geom/matrix.cljc b/common/app/common/geom/matrix.cljc index 8b9c9a59dc..04aa8651d8 100644 --- a/common/app/common/geom/matrix.cljc +++ b/common/app/common/geom/matrix.cljc @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.common.geom.matrix (:require diff --git a/common/app/common/geom/point.cljc b/common/app/common/geom/point.cljc index 11fff0f4b2..0d3feeb063 100644 --- a/common/app/common/geom/point.cljc +++ b/common/app/common/geom/point.cljc @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.common.geom.point (:refer-clojure :exclude [divide min max]) @@ -41,7 +38,7 @@ ([v] (cond (point? v) - v + (Point. (:x v) (:y v)) (number? v) (point v v) @@ -165,6 +162,8 @@ (mth/precision 6))] (if (mth/nan? d) 0 d))))) +(defn angle-sign [v1 v2] + (if (> (* (:y v1) (:x v2)) (* (:x v1) (:y v2))) -1 1)) (defn update-angle "Update the angle of the point." @@ -216,14 +215,16 @@ (let [v-length (length v)] (divide v (point v-length v-length)))) -(defn project [v1 v2] +(defn project + "V1 perpendicular projection on vector V2" + [v1 v2] (let [v2-unit (unit v2) - scalar-projection (dot v1 (unit v2))] - (multiply - v2-unit - (point scalar-projection scalar-projection)))) + scalar-proj (dot v1 v2-unit)] + (scale v2-unit scalar-proj))) -(defn center-points [points] +(defn center-points + "Centroid of a group of points" + [points] (let [k (point (count points))] (reduce #(add %1 (divide %2 k)) (point) points))) @@ -256,7 +257,43 @@ (and (mth/almost-zero? x) (mth/almost-zero? y))) +(defn line-val + "Given a line with two points p1-p2 and a 'percent'. Returns the point in the vector + generated by these two points. For example: for p1=(0,0) p2=(1,1) and v=0.25 will return + the point (0.25, 0.25)" + [p1 p2 v] + (let [v (-> (to-vec p1 p2) + (scale v))] + (add p1 v))) + + +(defn rotate + "Rotates the point around center with an angle" + [{px :x py :y} {cx :x cy :y} angle] + (let [angle (mth/radians angle) + + x (+ (* (mth/cos angle) (- px cx)) + (* (mth/sin angle) (- py cy) -1) + cx) + + y (+ (* (mth/sin angle) (- px cx)) + (* (mth/cos angle) (- py cy)) + cy)] + (point x y))) + + +(defn scale-from + "Moves a point in the vector that creates with center with a scale + value" + [point center value] + (add point + (-> (to-vec center point) + (unit) + (scale value)))) + ;; --- Debug (defmethod pp/simple-dispatch Point [obj] (pr obj)) + + diff --git a/common/app/common/geom/proportions.cljc b/common/app/common/geom/proportions.cljc index 15e6bfffd3..0fd8dccc3f 100644 --- a/common/app/common/geom/proportions.cljc +++ b/common/app/common/geom/proportions.cljc @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.common.geom.proportions) diff --git a/common/app/common/geom/shapes.cljc b/common/app/common/geom/shapes.cljc index a71e0792a0..a93c20ad39 100644 --- a/common/app/common/geom/shapes.cljc +++ b/common/app/common/geom/shapes.cljc @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.common.geom.shapes (:require @@ -20,28 +17,6 @@ [app.common.geom.shapes.intersect :as gin] [app.common.spec :as us])) -;; --- Relative Movement - -(defn move - "Move the shape relativelly to its current - position applying the provided delta." - [shape {dx :x dy :y}] - (let [dx (d/check-num dx) - dy (d/check-num dy)] - (-> shape - (assoc-in [:modifiers :displacement] (gmt/translate-matrix (gpt/point dx dy))) - (gtr/transform-shape)))) - -;; --- Absolute Movement - -(declare absolute-move-rect) - -(defn absolute-move - "Move the shape to the exactly specified position." - [shape {:keys [x y]}] - (let [dx (- (d/check-num x) (-> shape :selrect :x)) - dy (- (d/check-num y) (-> shape :selrect :y))] - (move shape (gpt/point dx dy)))) ;; --- Resize (Dimensions) (defn resize-modifiers @@ -123,38 +98,8 @@ (gpr/join-selrects))) (defn translate-to-frame - [{:keys [type x y] :as shape} {:keys [x y]}] - (let [move-point - (fn [point] - (-> point - (update :x - x) - (update :y - y))) - - move-segment - (fn [segment] - (-> segment - (d/update-in-when [:params :x] - x) - (d/update-in-when [:params :y] - y) - (d/update-in-when [:params :c1x] - x) - (d/update-in-when [:params :c1y] - y) - (d/update-in-when [:params :c2x] - x) - (d/update-in-when [:params :c2y] - y)))] - - (-> shape - (d/update-when :x - x) - (d/update-when :y - y) - (update-in [:selrect :x] - x) - (update-in [:selrect :y] - y) - (update-in [:selrect :x1] - x) - (update-in [:selrect :y1] - y) - (update-in [:selrect :x2] - x) - (update-in [:selrect :y2] - y) - - (d/update-when :points #(mapv move-point %)) - - (cond-> (= :path type) - (d/update-when :content #(mapv move-segment %)))))) - + [shape {:keys [x y]}] + (gtr/move shape (gpt/negate (gpt/point x y))) ) ;; --- Helpers @@ -247,6 +192,8 @@ (d/export gtr/update-group-selrect) (d/export gtr/transform-points) (d/export gtr/calculate-adjust-matrix) +(d/export gtr/move) +(d/export gtr/absolute-move) ;; PATHS (d/export gsp/content->points) @@ -256,3 +203,4 @@ ;; Intersection (d/export gin/overlaps?) (d/export gin/has-point?) +(d/export gin/has-point-rect?) diff --git a/common/app/common/geom/shapes/common.cljc b/common/app/common/geom/shapes/common.cljc index 07b17ad2b3..9b5c6d1b3e 100644 --- a/common/app/common/geom/shapes/common.cljc +++ b/common/app/common/geom/shapes/common.cljc @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.common.geom.shapes.common (:require diff --git a/common/app/common/geom/shapes/intersect.cljc b/common/app/common/geom/shapes/intersect.cljc index 72ed180fb3..0b6fbcd6f5 100644 --- a/common/app/common/geom/shapes/intersect.cljc +++ b/common/app/common/geom/shapes/intersect.cljc @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.common.geom.shapes.intersect (:require @@ -288,6 +285,11 @@ (or (not path?) (overlaps-path? shape rect)) (or (not circle?) (overlaps-ellipse? shape rect)))))) +(defn has-point-rect? + [rect point] + (let [lines (gpr/rect->lines rect)] + (is-point-inside-evenodd? point lines))) + (defn has-point? "Check if the shape contains a point" [shape point] diff --git a/common/app/common/geom/shapes/path.cljc b/common/app/common/geom/shapes/path.cljc index f38467b337..0339cd652e 100644 --- a/common/app/common/geom/shapes/path.cljc +++ b/common/app/common/geom/shapes/path.cljc @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.common.geom.shapes.path (:require @@ -44,6 +41,20 @@ (gpt/point (coord-v :x) (coord-v :y)))) +(defn curve-split + "Splits a curve into two at the given parametric value `t`. + Calculates the Casteljau's algorithm intermediate points" + [start end h1 h2 t] + + (let [p1 (gpt/line-val start h1 t) + p2 (gpt/line-val h1 h2 t) + p3 (gpt/line-val h2 end t) + p4 (gpt/line-val p1 p2 t) + p5 (gpt/line-val p2 p3 t) + sp (gpt/line-val p4 p5 t)] + [[start sp p1 p4] + [sp end p5 p3]])) + ;; https://pomax.github.io/bezierinfo/#extremities (defn curve-extremities "Given a cubic bezier cube finds its roots in t. This are the extremities @@ -128,6 +139,23 @@ (update :width #(if (mth/almost-zero? %) 1 %)) (update :height #(if (mth/almost-zero? %) 1 %))))) +(defn move-content [content move-vec] + (let [set-tr (fn [params px py] + (let [tr-point (-> (gpt/point (get params px) (get params py)) + (gpt/add move-vec))] + (assoc params + px (:x tr-point) + py (:y tr-point)))) + + transform-params + (fn [{:keys [x c1x c2x] :as params}] + (cond-> params + (not (nil? x)) (set-tr :x :y) + (not (nil? c1x)) (set-tr :c1x :c1y) + (not (nil? c2x)) (set-tr :c2x :c2y)))] + + (mapv #(update % :params transform-params) content))) + (defn transform-content [content transform] (let [set-tr (fn [params px py] (let [tr-point (-> (gpt/point (get params px) (get params py)) @@ -214,3 +242,91 @@ point)) (conj result [prev-point last-start])))) + +(defonce path-closest-point-accuracy 0.01) +(defn curve-closest-point + [position start end h1 h2] + (let [d (memoize (fn [t] (gpt/distance position (curve-values start end h1 h2 t))))] + (loop [t1 0 + t2 1] + (if (<= (mth/abs (- t1 t2)) path-closest-point-accuracy) + (-> (curve-values start end h1 h2 t1) + ;; store the segment info + (with-meta {:t t1 :from-p start :to-p end})) + + (let [ht (+ t1 (/ (- t2 t1) 2)) + ht1 (+ t1 (/ (- t2 t1) 4)) + ht2 (+ t1 (/ (* 3 (- t2 t1)) 4)) + + [t1 t2] (cond + (< (d ht1) (d ht2)) + [t1 ht] + + (< (d ht2) (d ht1)) + [ht t2] + + (and (< (d ht) (d t1)) (< (d ht) (d t2))) + [ht1 ht2] + + (< (d t1) (d t2)) + [t1 ht] + + :else + [ht t2])] + (recur t1 t2)))))) + +(defn line-closest-point + "Point on line" + [position from-p to-p] + + (let [e1 (gpt/to-vec from-p to-p ) + e2 (gpt/to-vec from-p position) + + len2 (+ (mth/sq (:x e1)) (mth/sq (:y e1))) + t (/ (gpt/dot e1 e2) len2)] + + (if (and (>= t 0) (<= t 1) (not (mth/almost-zero? len2))) + (-> (gpt/add from-p (gpt/scale e1 t)) + (with-meta {:t t + :from-p from-p + :to-p to-p})) + + ;; There is no perpendicular projection in the line so the closest + ;; point will be one of the extremes + (if (<= (gpt/distance position from-p) (gpt/distance position to-p)) + from-p + to-p)))) + +(defn path-closest-point + "Given a path and a position" + [shape position] + + (let [point+distance (fn [[cur-cmd prev-cmd]] + (let [from-p (command->point prev-cmd) + to-p (command->point cur-cmd) + h1 (gpt/point (get-in cur-cmd [:params :c1x]) + (get-in cur-cmd [:params :c1y])) + h2 (gpt/point (get-in cur-cmd [:params :c2x]) + (get-in cur-cmd [:params :c2y])) + point + (case (:command cur-cmd) + :line-to + (line-closest-point position from-p to-p) + + :curve-to + (curve-closest-point position from-p to-p h1 h2) + + nil)] + (when point + [point (gpt/distance point position)]))) + + find-min-point (fn [[min-p min-dist :as acc] [cur-p cur-dist :as cur]] + (if (and (some? acc) (or (not cur) (<= min-dist cur-dist))) + [min-p min-dist] + [cur-p cur-dist]))] + + (->> (:content shape) + (d/with-prev) + (map point+distance) + (reduce find-min-point) + (first)))) diff --git a/common/app/common/geom/shapes/rect.cljc b/common/app/common/geom/shapes/rect.cljc index 80e06ef110..91e7d18a9a 100644 --- a/common/app/common/geom/shapes/rect.cljc +++ b/common/app/common/geom/shapes/rect.cljc @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.common.geom.shapes.rect (:require @@ -22,6 +19,12 @@ (gpt/point (+ x width) (+ y height)) (gpt/point x (+ y height))]) +(defn rect->lines [{:keys [x y width height]}] + [[(gpt/point x y) (gpt/point (+ x width) y)] + [(gpt/point (+ x width) y) (gpt/point (+ x width) (+ y height))] + [(gpt/point (+ x width) (+ y height)) (gpt/point x (+ y height))] + [(gpt/point x (+ y height)) (gpt/point x y)]]) + (defn points->rect [points] (let [minx (transduce gco/map-x-xf min ##Inf points) diff --git a/common/app/common/geom/shapes/transforms.cljc b/common/app/common/geom/shapes/transforms.cljc index 3162d233d0..cf98457411 100644 --- a/common/app/common/geom/shapes/transforms.cljc +++ b/common/app/common/geom/shapes/transforms.cljc @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.common.geom.shapes.transforms (:require @@ -17,6 +14,49 @@ [app.common.math :as mth] [app.common.data :as d])) +;; --- Relative Movement + +(defn move-selrect [selrect {dx :x dy :y}] + (-> selrect + (d/update-when :x + dx) + (d/update-when :y + dy) + (d/update-when :x1 + dx) + (d/update-when :y1 + dy) + (d/update-when :x2 + dx) + (d/update-when :y2 + dy))) + +(defn move-points [points move-vec] + (->> points + (mapv #(gpt/add % move-vec)))) + +(defn move + "Move the shape relativelly to its current + position applying the provided delta." + [shape {dx :x dy :y}] + (let [dx (d/check-num dx) + dy (d/check-num dy) + move-vec (gpt/point dx dy)] + + (-> shape + (update :selrect move-selrect move-vec) + (update :points move-points move-vec) + (d/update-when :x + dx) + (d/update-when :y + dy) + (cond-> (= :path (:type shape)) + (update :content gpa/move-content move-vec))))) + +;; --- Absolute Movement + +(declare absolute-move-rect) + +(defn absolute-move + "Move the shape to the exactly specified position." + [shape {:keys [x y]}] + (let [dx (- (d/check-num x) (-> shape :selrect :x)) + dy (- (d/check-num y) (-> shape :selrect :y))] + (move shape (gpt/point dx dy)))) + + (defn- modif-rotation [shape] (let [cur-rotation (d/check-num (:rotation shape)) delta-angle (d/check-num (get-in shape [:modifiers :rotation]))] @@ -164,7 +204,7 @@ v2 (gpt/to-vec center p2) rot-angle (gpt/angle-with-other v1 v2) - rot-sign (if (> (* (:y v1) (:x v2)) (* (:x v1) (:y v2))) -1 1)] + rot-sign (gpt/angle-sign v1 v2)] (* rot-sign rot-angle))) (defn- calculate-dimensions @@ -275,12 +315,27 @@ (and rx (< rx 0)) (update :flip-x not) (and ry (< ry 0)) (update :flip-y not)))) -(defn transform-shape [shape] - (let [center (gco/center-shape shape)] - (if (and (:modifiers shape) center) - (let [transform (modifiers->transform center (:modifiers shape))] +(defn apply-displacement [shape] + (let [modifiers (:modifiers shape)] + (if (contains? modifiers :displacement) + (let [mov-vec (-> (gpt/point 0 0) + (gpt/transform (:displacement modifiers))) + shape (move shape mov-vec) + modifiers (dissoc modifiers :displacement)] (-> shape - (set-flip (:modifiers shape)) + (assoc :modifiers modifiers) + (cond-> (empty? modifiers) + (dissoc :modifiers)))) + shape))) + +(defn transform-shape [shape] + (let [shape (apply-displacement shape) + center (gco/center-shape shape) + modifiers (:modifiers shape)] + (if (and modifiers center) + (let [transform (modifiers->transform center modifiers)] + (-> shape + (set-flip modifiers) (apply-transform transform) (dissoc :modifiers))) shape))) diff --git a/common/app/common/math.cljc b/common/app/common/math.cljc index e676c3f34f..c81db2a392 100644 --- a/common/app/common/math.cljc +++ b/common/app/common/math.cljc @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.common.math "A collection of math utils." diff --git a/common/app/common/media.cljc b/common/app/common/media.cljc index ca33567514..df3a556caf 100644 --- a/common/app/common/media.cljc +++ b/common/app/common/media.cljc @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.common.media (:require diff --git a/common/app/common/pages.cljc b/common/app/common/pages.cljc index 3ed7a4485f..2776e81686 100644 --- a/common/app/common/pages.cljc +++ b/common/app/common/pages.cljc @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.common.pages "A common (clj/cljs) functions and specs for pages." @@ -65,6 +62,10 @@ (d/export helpers/get-index-in-parent) (d/export helpers/calculate-z-index) (d/export helpers/generate-child-all-parents-index) +(d/export helpers/parse-path-name) +(d/export helpers/merge-path-item) +(d/export helpers/compact-path) +(d/export helpers/compact-name) ;; Process changes (d/export changes/process-changes) diff --git a/common/app/common/pages/changes.cljc b/common/app/common/pages/changes.cljc index 7f1da9a456..0a792233cb 100644 --- a/common/app/common/pages/changes.cljc +++ b/common/app/common/pages/changes.cljc @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.common.pages.changes (:require @@ -34,7 +31,7 @@ ;; When verify? false we spec the schema validation. Currently used to make just ;; 1 validation even if the changes are applied twice (when verify? - (us/verify ::spec/changes items)) + (us/assert ::spec/changes items)) (let [pages (into #{} (map :page-id) items) result (->> items @@ -367,21 +364,25 @@ ;; -- Components (defmethod process-change :add-component - [data {:keys [id name shapes]}] + [data {:keys [id name path shapes]}] (assoc-in data [:components id] {:id id :name name + :path path :objects (d/index-by :id shapes)})) (defmethod process-change :mod-component - [data {:keys [id name objects]}] + [data {:keys [id name path objects]}] (update-in data [:components id] #(cond-> % - (some? name) - (assoc :name name) + (some? name) + (assoc :name name) - (some? objects) - (assoc :objects objects)))) + (some? path) + (assoc :path path) + + (some? objects) + (assoc :objects objects)))) (defmethod process-change :del-component [data {:keys [id]}] diff --git a/common/app/common/pages/common.cljc b/common/app/common/pages/common.cljc index 784896e3b3..db1e9341c3 100644 --- a/common/app/common/pages/common.cljc +++ b/common/app/common/pages/common.cljc @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.common.pages.common (:require diff --git a/common/app/common/pages/helpers.cljc b/common/app/common/pages/helpers.cljc index 2a5ba99aa2..39597788d7 100644 --- a/common/app/common/pages/helpers.cljc +++ b/common/app/common/pages/helpers.cljc @@ -2,17 +2,15 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.common.pages.helpers (:require [app.common.data :as d] [app.common.geom.shapes :as gsh] [app.common.spec :as us] - [app.common.uuid :as uuid])) + [app.common.uuid :as uuid] + [cuerdas.core :as str])) (defn walk-pages "Go through all pages of a file and apply a function to each one" @@ -335,7 +333,6 @@ (d/concat new-children new-child-objects) (d/concat updated-children updated-child-objects)))))))) - (defn indexed-shapes "Retrieves a list with the indexes for each element in the layer tree. This will be used for shift+selection." @@ -462,3 +459,55 @@ [parent-idx _] (d/seek (fn [[idx child-id]] (= child-id shape-id)) (d/enumerate (:shapes parent)))] parent-idx)) + +(defn split-path + [path] + "Decompose a string in the form 'one / two / three' into + an array of strings, normalizing spaces." + (->> (str/split path "/") + (map str/trim) + (remove str/empty?))) + +(defn parse-path-name + "Parse a string in the form 'group / subgroup / name'. + Retrieve the path and the name in separated values, normalizing spaces." + [path-name] + (let [path-name-split (split-path path-name) + path (str/join " / " (butlast path-name-split)) + name (last path-name-split)] + [path name])) + +(defn merge-path-item + "Put the item at the end of the path." + [path name] + (if-not (empty? path) + (str path " / " name) + name)) + +(defn compact-path + "Separate last item of the path, and truncate the others if too long: + 'one' -> ['' 'one' false] + 'one / two / three' -> ['one / two' 'three' false] + 'one / two / three / four' -> ['one / two / ...' 'four' true] + 'one-item-but-very-long / two' -> ['...' 'two' true] " + [path max-length] + (let [path-split (split-path path) + last-item (last path-split)] + (loop [other-items (seq (butlast path-split)) + other-path ""] + (if-let [item (first other-items)] + (let [full-path (-> other-path + (merge-path-item item) + (merge-path-item last-item))] + (if (> (count full-path) max-length) + [(merge-path-item other-path "...") last-item true] + (recur (next other-items) + (merge-path-item other-path item)))) + [other-path last-item false])))) + +(defn compact-name + "Append the first item of the path and the name." + [path name] + (let [path-split (split-path path)] + (merge-path-item (first path-split) name))) + diff --git a/common/app/common/pages/init.cljc b/common/app/common/pages/init.cljc index 38267aaf53..79e0b50de9 100644 --- a/common/app/common/pages/init.cljc +++ b/common/app/common/pages/init.cljc @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.common.pages.init (:require diff --git a/common/app/common/pages/migrations.cljc b/common/app/common/pages/migrations.cljc index 28aa8682ed..588da5d958 100644 --- a/common/app/common/pages/migrations.cljc +++ b/common/app/common/pages/migrations.cljc @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.common.pages.migrations (:require diff --git a/common/app/common/pages/spec.cljc b/common/app/common/pages/spec.cljc index d99f44ef42..ffea433313 100644 --- a/common/app/common/pages/spec.cljc +++ b/common/app/common/pages/spec.cljc @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.common.pages.spec (:require @@ -19,6 +16,7 @@ (s/def ::frame-id uuid?) (s/def ::id uuid?) (s/def ::name string?) +(s/def ::path (s/nilable string?)) (s/def ::page-id uuid?) (s/def ::parent-id uuid?) (s/def ::string string?) @@ -550,7 +548,8 @@ (s/coll-of ::shape)) (defmethod change-spec :add-component [_] - (s/keys :req-un [::id ::name :internal.changes.add-component/shapes])) + (s/keys :req-un [::id ::name :internal.changes.add-component/shapes] + :opt-un [::path])) (defmethod change-spec :mod-component [_] (s/keys :req-un [::id] diff --git a/common/app/common/spec.cljc b/common/app/common/spec.cljc index 81fc5195db..eabce91ff9 100644 --- a/common/app/common/spec.cljc +++ b/common/app/common/spec.cljc @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.common.spec "Data manipulation and query helper functions." @@ -136,6 +133,20 @@ ::s/invalid))] (s/def ::email (s/conformer cfn str))) + +;; --- SPEC: set-of-str +(letfn [(conformer [s] + (cond + (string? s) (into #{} (str/split s #"\s*,\s*")) + (set? s) (if (every? string? s) + s + ::s/invalid) + :else ::s/invalid)) + + (unformer [s] + (str/join "," s))] + (s/def ::set-of-str (s/conformer conformer unformer))) + ;; --- Macros (defn spec-assert* diff --git a/common/app/common/text.cljc b/common/app/common/text.cljc index d8123c0393..a2864a79e1 100644 --- a/common/app/common/text.cljc +++ b/common/app/common/text.cljc @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020-2021 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.common.text (:require diff --git a/common/app/common/uri.cljc b/common/app/common/uri.cljc new file mode 100644 index 0000000000..d381ee7b0a --- /dev/null +++ b/common/app/common/uri.cljc @@ -0,0 +1,40 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) UXBOX Labs SL + +(ns app.common.uri + (:refer-clojure :exclude [uri?]) + (:require + [app.common.data :as d] + [lambdaisland.uri :as u] + [lambdaisland.uri.normalize :as un])) + +(d/export u/uri) +(d/export u/join) +(d/export u/query-encode) +(d/export un/percent-encode) + +(defn uri? + [o] + (instance? lambdaisland.uri.URI o)) + +(defn query-string->map + [s] + (u/query-string->map s)) + +(defn default-encode-value + [v] + (if (keyword? v) (name v) v)) + +(defn map->query-string + ([params] (map->query-string params nil)) + ([params {:keys [value-fn key-fn] + :or {value-fn default-encode-value + key-fn identity}}] + (->> params + (into {} (comp + (remove #(nil? (second %))) + (map (fn [[k v]] [(key-fn k) (value-fn v)])))) + (u/map->query-string)))) diff --git a/common/app/common/uuid.cljc b/common/app/common/uuid.cljc index 3dad2eb0e4..0cc0106dfd 100644 --- a/common/app/common/uuid.cljc +++ b/common/app/common/uuid.cljc @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.common.uuid (:refer-clojure :exclude [next uuid zero?]) diff --git a/common/app/common/uuid_impl.js b/common/app/common/uuid_impl.js index 40c944f10b..d276ce5167 100644 --- a/common/app/common/uuid_impl.js +++ b/common/app/common/uuid_impl.js @@ -3,10 +3,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. * - * This Source Code Form is "Incompatible With Secondary Licenses", as - * defined by the Mozilla Public License, v. 2.0. - * - * Copyright (c) 2020 UXBOX Labs SL + * Copyright (c) UXBOX Labs SL */ "use strict"; diff --git a/common/app/common/version.cljc b/common/app/common/version.cljc index b3dd591a05..aafad2d384 100644 --- a/common/app/common/version.cljc +++ b/common/app/common/version.cljc @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020 Andrey Antukh +;; Copyright (c) UXBOX Labs SL (ns app.common.version "A version parsing helper." diff --git a/docker/devenv/Dockerfile b/docker/devenv/Dockerfile index 34285df042..9e90ed04dc 100644 --- a/docker/devenv/Dockerfile +++ b/docker/devenv/Dockerfile @@ -3,10 +3,10 @@ LABEL maintainer="Andrey Antukh " ARG DEBIAN_FRONTEND=noninteractive -ENV NODE_VERSION=v14.16.0 \ +ENV NODE_VERSION=v14.16.1 \ CLOJURE_VERSION=1.10.3.822 \ - CLJKONDO_VERSION=2021.03.31 \ - BABASHKA_VERSION=0.3.2 \ + CLJKONDO_VERSION=2021.04.23 \ + BABASHKA_VERSION=0.3.5 \ LANG=en_US.UTF-8 \ LC_ALL=en_US.UTF-8 @@ -50,6 +50,9 @@ RUN set -ex; \ nginx \ jq \ redis-tools \ + woff-tools \ + woff2 \ + fontforge \ ; \ rm -rf /var/lib/apt/lists/*; @@ -95,7 +98,7 @@ RUN set -ex; \ rm -rf /var/lib/apt/lists/*; RUN set -ex; \ - curl -LfsSo /tmp/openjdk.tar.gz https://github.com/AdoptOpenJDK/openjdk16-binaries/releases/download/jdk-16%2B36/OpenJDK16-jdk_x64_linux_hotspot_16_36.tar.gz; \ + curl -LfsSo /tmp/openjdk.tar.gz https://github.com/AdoptOpenJDK/openjdk16-binaries/releases/download/jdk-16.0.1%2B9/OpenJDK16U-jdk_x64_linux_hotspot_16.0.1_9.tar.gz; \ mkdir -p /usr/lib/jvm/openjdk16; \ cd /usr/lib/jvm/openjdk16; \ tar -xf /tmp/openjdk.tar.gz --strip-components=1; \ diff --git a/docker/devenv/files/nginx.conf b/docker/devenv/files/nginx.conf index 6fa8e666b6..eefd05cd09 100644 --- a/docker/devenv/files/nginx.conf +++ b/docker/devenv/files/nginx.conf @@ -111,6 +111,10 @@ http { proxy_pass http://127.0.0.1:6061; } + location /telemetry { + proxy_pass http://127.0.0.1:6070/inbox; + } + location /playground { alias /home/penpot/penpot/experiments/; add_header Cache-Control "no-cache, max-age=0"; @@ -125,6 +129,7 @@ http { location / { add_header Cache-Control "no-cache, max-age=0"; + proxy_pass http://127.0.0.1:8888; } } } diff --git a/docker/devenv/files/postgresql_init.sql b/docker/devenv/files/postgresql_init.sql index 3f174e897e..c36960e258 100644 --- a/docker/devenv/files/postgresql_init.sql +++ b/docker/devenv/files/postgresql_init.sql @@ -1 +1,2 @@ CREATE DATABASE penpot_test; +CREATE DATABASE penpot_telemetry; diff --git a/docker/gitpod/Dockerfile b/docker/gitpod/Dockerfile new file mode 100644 index 0000000000..7db973a46f --- /dev/null +++ b/docker/gitpod/Dockerfile @@ -0,0 +1,118 @@ +FROM gitpod/workspace-postgres + +# Install custom tools, runtimes, etc. +# For example "bastet", a command-line tetris clone: +# RUN brew install bastet +# +# More information: https://www.gitpod.io/docs/config-docker/ + +RUN set -ex; \ + brew install redis; \ + brew install imagemagick; \ + brew install mailhog; \ + brew install openldap; \ + sudo mkdir -p /var/log/nginx; \ + sudo chown gitpod:gitpod /var/log/nginx + +COPY docker/gitpod/files/nginx.conf /etc/nginx/nginx.conf + +USER root + +ENV CLOJURE_VERSION=1.10.3.822 \ + CLJKONDO_VERSION=2021.03.31 \ + BABASHKA_VERSION=0.3.2 \ + LANG=en_US.UTF-8 \ + LC_ALL=en_US.UTF-8 + +RUN set -ex; \ + useradd -m -g users -s /bin/bash penpot; \ + passwd penpot -d; \ + echo "penpot ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers + +RUN set -ex; \ + apt-get -qq update; \ + apt-get -qqy install \ + gconf-service \ + libasound2 \ + libatk1.0-0 \ + libatk-bridge2.0-0 \ + libcairo2 \ + libcups2 \ + libdbus-1-3 \ + libexpat1 \ + libfontconfig1 \ + libgcc1 \ + libgconf-2-4 \ + libgdk-pixbuf2.0-0 \ + libglib2.0-0 \ + libgtk-3-0 \ + libnspr4 \ + libpango-1.0-0 \ + libpangocairo-1.0-0 \ + libx11-6 \ + libx11-xcb1 \ + libxcb1 \ + libxcomposite1 \ + libxcursor1 \ + libxdamage1 \ + libxext6 \ + libxfixes3 \ + libxi6 \ + libxrandr2 \ + libxrender1 \ + libxshmfence1 \ + libxss1 \ + libxtst6 \ + fonts-liberation \ + libappindicator1 \ + libnss3 \ + libgbm1 \ + ; \ + rm -rf /var/lib/apt/lists/*; + +RUN set -ex; \ + curl -LfsSo /tmp/openjdk.tar.gz https://github.com/AdoptOpenJDK/openjdk16-binaries/releases/download/jdk-16%2B36/OpenJDK16-jdk_x64_linux_hotspot_16_36.tar.gz; \ + mkdir -p /usr/lib/jvm/openjdk16; \ + cd /usr/lib/jvm/openjdk16; \ + tar -xf /tmp/openjdk.tar.gz --strip-components=1; \ + rm -rf /tmp/openjdk.tar.gz; + +# Install clojure cli +RUN set -ex; \ + curl -LfsSo /tmp/clojure.sh https://download.clojure.org/install/linux-install-$CLOJURE_VERSION.sh; \ + chmod +x /tmp/clojure.sh; \ + /tmp/clojure.sh; \ + rm -rf /tmp/clojure.sh; + +# Install clj-kondo +RUN set -ex; \ + curl -LfsSo /tmp/clj-kondo.zip https://github.com/borkdude/clj-kondo/releases/download/v$CLJKONDO_VERSION/clj-kondo-$CLJKONDO_VERSION-linux-amd64.zip; \ + cd /usr/local/bin; \ + unzip /tmp/clj-kondo.zip; \ + rm /tmp/clj-kondo.zip; + +# Install babashka +RUN set -ex; \ + cd /tmp; \ + curl -LfsSo /tmp/babashka.tar.gz https://github.com/babashka/babashka/releases/download/v$BABASHKA_VERSION/babashka-$BABASHKA_VERSION-linux-amd64.tar.gz; \ + cd /usr/local/bin; \ + tar -xf /tmp/babashka.tar.gz; \ + rm -rf /tmp/babashka.tar.gz; + +USER gitpod + +ENV PATH="/usr/lib/jvm/openjdk16/bin:/usr/local/nodejs/bin:$PATH" \ + JAVA_HOME=/usr/lib/jvm/openjdk16 \ + PENPOT_SMTP_ENABLED=true \ + PENPOT_SMTP_HOST=localhost \ + PENPOT_SMTP_PORT=1025 \ + PENPOT_SMTP_USER= \ + PENPOT_SMTP_PASSWORD= \ + PENPOT_SMTP_SSL=false \ + PENPOT_SMTP_TLS=false \ + PENPOT_SMTP_DEFAULT_REPLY_TO=no-reply@example.com \ + PENPOT_SMTP_DEFAULT_FROM=no-reply@example.com \ + PENPOT_DATABASE_URI="postgresql://localhost/penpot" + PENPOT_REDIS_URI="redis://localhost/0" + +# TODO Retrieve OpenLDAP from rroemhild/docker-test-openldap diff --git a/docker/gitpod/files/nginx.conf b/docker/gitpod/files/nginx.conf new file mode 100644 index 0000000000..319b4bcbb5 --- /dev/null +++ b/docker/gitpod/files/nginx.conf @@ -0,0 +1,136 @@ +# This Nginx config file is optional. +# If you don't have this file, the Nginx config from the workspace-full docker image will be taken. + +worker_processes auto; +pid /var/run/nginx/nginx.pid; +include /etc/nginx/modules-enabled/*.conf; + +env GITPOD_REPO_ROOT; + +events { + worker_connections 768; + # multi_accept on; +} + +http { + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + # server_tokens off; + + # server_names_hash_bucket_size 64; + # server_name_in_redirect off; + + include /etc/nginx/mime.types; + default_type application/octet-stream; + + access_log /var/log/nginx/access.log; + error_log /var/log/nginx/error.log; + + gzip on; + + gzip_vary on; + gzip_proxied any; + gzip_comp_level 3; + gzip_buffers 16 8k; + gzip_http_version 1.1; + + gzip_types text/plain text/css text/javascript application/javascript application/json application/transit+json; + + map $http_upgrade $connection_upgrade { + default upgrade; + '' close; + } + + # include /etc/nginx/sites-enabled/*; + + server { + set_by_lua $gitpod_repo_root 'return os.getenv("GITPOD_REPO_ROOT")'; + + listen 0.0.0.0:3449 default_server; + server_name _; + + client_max_body_size 5M; + charset utf-8; + + proxy_http_version 1.1; + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Scheme $scheme; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + resolver 8.8.8.8; + + etag off; + + root $gitpod_repo_root/frontend/resources/public; + + location @handle_redirect { + set $redirect_uri "$upstream_http_location"; + set $redirect_host "$upstream_http_x_host"; + set $redirect_cache_control "$upstream_http_cache_control"; + + proxy_buffering off; + + proxy_set_header Host "$redirect_host"; + proxy_hide_header etag; + proxy_hide_header x-amz-id-2; + proxy_hide_header x-amz-request-id; + proxy_hide_header x-amz-meta-server-side-encryption; + proxy_hide_header x-amz-server-side-encryption; + proxy_pass $redirect_uri; + + add_header x-internal-redirect "$redirect_uri"; + add_header x-cache-control "$redirect_cache_control"; + add_header cache-control "$redirect_cache_control"; + } + + location /assets { + proxy_pass http://127.0.0.1:6060/assets; + recursive_error_pages on; + proxy_intercept_errors on; + error_page 301 302 307 = @handle_redirect; + } + + location /internal/assets { + internal; + alias $gitpod_repo_root/backend/assets; + add_header x-internal-redirect "$upstream_http_x_accel_redirect"; + } + + location /api { + proxy_pass http://127.0.0.1:6060/api; + } + + location /webhooks { + proxy_pass http://127.0.0.1:6060/webhooks; + } + + location /dbg { + proxy_pass http://127.0.0.1:6060/dbg; + } + + location /export { + proxy_pass http://127.0.0.1:6061; + } + + location /playground { + alias $gitpod_repo_root/experiments/; + add_header Cache-Control "no-cache, max-age=0"; + autoindex on; + } + + location /ws/notifications { + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_pass http://127.0.0.1:6060/ws/notifications; + } + + location / { + add_header Cache-Control "no-cache, max-age=0"; + } + } +} diff --git a/docker/gitpod/files/postgresql_init.sql b/docker/gitpod/files/postgresql_init.sql new file mode 100644 index 0000000000..1766a643d2 --- /dev/null +++ b/docker/gitpod/files/postgresql_init.sql @@ -0,0 +1,3 @@ +CREATE DATABASE penpot; +CREATE USER penpot PASSWORD 'penpot'; +ALTER ROLE penpot SUPERUSER; diff --git a/docker/images/files/config.js b/docker/images/files/config.js index 0ac491bbb8..aac4fb7092 100644 --- a/docker/images/files/config.js +++ b/docker/images/files/config.js @@ -6,5 +6,6 @@ //var penpotGoogleClientID = ""; //var penpotGitlabClientID = ""; //var penpotGithubClientID = ""; +//var penpotOIDCClientID = ""; //var penpotLoginWithLDAP = ; //var penpotRegistrationEnabled = ; diff --git a/docker/images/files/nginx-entrypoint.sh b/docker/images/files/nginx-entrypoint.sh index e99e697872..b341649a75 100644 --- a/docker/images/files/nginx-entrypoint.sh +++ b/docker/images/files/nginx-entrypoint.sh @@ -69,6 +69,14 @@ update_github_client_id() { fi } +update_oidc_client_id() { + if [ -n "$PENPOT_OIDC_CLIENT_ID" ]; then + log "Updating Oidc Client Id: $PENPOT_OIDC_CLIENT_ID" + sed -i \ + -e "s|^//var penpotOIDCClientID = \".*\";|var penpotOIDCClientID = \"$PENPOT_OIDC_CLIENT_ID\";|g" \ + "$1" + fi +} update_login_with_ldap() { if [ -n "$PENPOT_LOGIN_WITH_LDAP" ]; then @@ -95,6 +103,7 @@ update_allow_demo_users /var/www/app/js/config.js update_google_client_id /var/www/app/js/config.js update_gitlab_client_id /var/www/app/js/config.js update_github_client_id /var/www/app/js/config.js +update_oidc_client_id /var/www/app/js/config.js update_login_with_ldap /var/www/app/js/config.js update_registration_enabled /var/www/app/js/config.js diff --git a/exporter/deps.edn b/exporter/deps.edn new file mode 100644 index 0000000000..86c907f65c --- /dev/null +++ b/exporter/deps.edn @@ -0,0 +1,9 @@ +{:paths [] + :deps {} + :aliases + {:outdated + {:extra-deps {com.github.liquidz/antq {:mvn/version "RELEASE"} + org.slf4j/slf4j-nop {:mvn/version "RELEASE"}} + :main-opts ["-m" "antq.core"]} + }} + diff --git a/exporter/package.json b/exporter/package.json index 38998f4332..fb08208872 100644 --- a/exporter/package.json +++ b/exporter/package.json @@ -12,15 +12,14 @@ "inflation": "^2.0.0", "jszip": "^3.6.0", "koa": "^2.13.0", - "puppeteer": "^8.0.0", + "puppeteer": "^9.1.0", "puppeteer-cluster": "^0.22.0", "raw-body": "^2.4.1", - "svgo": "^1.3.2", "xml-js": "^1.6.11", - "xregexp": "^5.0.1" + "xregexp": "^5.0.2" }, "devDependencies": { - "shadow-cljs": "^2.11.20", + "shadow-cljs": "^2.12.5", "source-map-support": "^0.5.19" } } diff --git a/exporter/scripts/wait-and-start.sh b/exporter/scripts/wait-and-start.sh index 730ed16bca..c049353672 100755 --- a/exporter/scripts/wait-and-start.sh +++ b/exporter/scripts/wait-and-start.sh @@ -2,4 +2,5 @@ bb -i '(babashka.wait/wait-for-port "localhost" 9630)'; bb -i '(babashka.wait/wait-for-path "target/app.js")'; +sleep 2; node target/app.js diff --git a/exporter/shadow-cljs.edn b/exporter/shadow-cljs.edn index c8a8a5f5e1..3a81cff0f2 100644 --- a/exporter/shadow-cljs.edn +++ b/exporter/shadow-cljs.edn @@ -1,14 +1,14 @@ {:dependencies [[funcool/promesa "6.0.0"] [danlentz/clj-uuid "0.1.9"] - [funcool/cuerdas "2020.03.26-3"] - [lambdaisland/glogi "1.0.74"] - [metosin/reitit-core "0.5.9"] - [com.cognitect/transit-cljs "0.8.264"] + [funcool/cuerdas "2021.05.02-0"] + [lambdaisland/glogi "1.0.106"] + [metosin/reitit-core "0.5.13"] + [com.cognitect/transit-cljs "0.8.269"] [frankiesardo/linked "1.3.0"]] - :source-paths ["src" "../common"] - :jvm-opts ["-Xmx512m" "-Xms512m"] + :source-paths ["src" "vendor" "../common"] + :jvm-opts ["-Xmx512m" "-Xms50m" "-XX:+UseSerialGC"] :builds {:main @@ -33,5 +33,3 @@ :pretty-print true :anon-fn-naming-policy :off :source-map-detail-level :all}}}}} - - diff --git a/exporter/src/app/browser.cljs b/exporter/src/app/browser.cljs index 5fb1bdefe1..374464420c 100644 --- a/exporter/src/app/browser.cljs +++ b/exporter/src/app/browser.cljs @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020-2021 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.browser (:require diff --git a/exporter/src/app/config.cljs b/exporter/src/app/config.cljs index 993c9555c4..e9b83c23ba 100644 --- a/exporter/src/app/config.cljs +++ b/exporter/src/app/config.cljs @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020-2021 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.config (:require diff --git a/exporter/src/app/core.cljs b/exporter/src/app/core.cljs index 2a20b0f67c..6bc6994766 100644 --- a/exporter/src/app/core.cljs +++ b/exporter/src/app/core.cljs @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020-2021 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.core (:require diff --git a/exporter/src/app/http.cljs b/exporter/src/app/http.cljs index cc63a7d539..d0d32ebf8d 100644 --- a/exporter/src/app/http.cljs +++ b/exporter/src/app/http.cljs @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020-2021 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.http (:require diff --git a/exporter/src/app/http/export.cljs b/exporter/src/app/http/export.cljs index a3ff36dbdd..15ac3e8bae 100644 --- a/exporter/src/app/http/export.cljs +++ b/exporter/src/app/http/export.cljs @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.http.export (:require diff --git a/exporter/src/app/http/export_bitmap.cljs b/exporter/src/app/http/export_bitmap.cljs index 09eb81e16b..cf17a5e6d3 100644 --- a/exporter/src/app/http/export_bitmap.cljs +++ b/exporter/src/app/http/export_bitmap.cljs @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020-2021 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.http.export-bitmap (:require diff --git a/exporter/src/app/http/export_svg.cljs b/exporter/src/app/http/export_svg.cljs index db9795bb2b..13b6bf946b 100644 --- a/exporter/src/app/http/export_svg.cljs +++ b/exporter/src/app/http/export_svg.cljs @@ -2,9 +2,6 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; ;; Copyright (c) UXBOX Labs SL (ns app.http.export-svg diff --git a/exporter/src/app/http/impl.cljs b/exporter/src/app/http/impl.cljs index f51c703703..15f00ba5b3 100644 --- a/exporter/src/app/http/impl.cljs +++ b/exporter/src/app/http/impl.cljs @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020-2021 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.http.impl (:require diff --git a/exporter/src/app/http/thumbnail.cljs b/exporter/src/app/http/thumbnail.cljs index 3a6d1c8a3b..f351d67a41 100644 --- a/exporter/src/app/http/thumbnail.cljs +++ b/exporter/src/app/http/thumbnail.cljs @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.http.thumbnail (:require diff --git a/exporter/src/app/util/shell.cljs b/exporter/src/app/util/shell.cljs index e4d727a007..f269fe1319 100644 --- a/exporter/src/app/util/shell.cljs +++ b/exporter/src/app/util/shell.cljs @@ -2,9 +2,6 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; ;; Copyright (c) UXBOX Labs SL (ns app.util.shell diff --git a/exporter/src/app/util/transit.cljs b/exporter/src/app/util/transit.cljs index 80ccfea157..38fbe3d4b1 100644 --- a/exporter/src/app/util/transit.cljs +++ b/exporter/src/app/util/transit.cljs @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020-2021 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.util.transit (:require diff --git a/exporter/src/app/zipfile.cljs b/exporter/src/app/zipfile.cljs index 8c9cbeff0e..bc54b327ba 100644 --- a/exporter/src/app/zipfile.cljs +++ b/exporter/src/app/zipfile.cljs @@ -2,10 +2,7 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020-2021 UXBOX Labs SL +;; Copyright (c) UXBOX Labs SL (ns app.zipfile (:require diff --git a/exporter/vendor/cuerdas/impl/xregexp.cljs b/exporter/vendor/cuerdas/impl/xregexp.cljs new file mode 100644 index 0000000000..27a5e7f86d --- /dev/null +++ b/exporter/vendor/cuerdas/impl/xregexp.cljs @@ -0,0 +1,4 @@ +(ns cuerdas.impl.xregexp + (:require ["xregexp" :as XRegExp])) + +(goog/exportSymbol "XRegExp" XRegExp) diff --git a/exporter/yarn.lock b/exporter/yarn.lock index b6d222c511..a122fe7aca 100644 --- a/exporter/yarn.lock +++ b/exporter/yarn.lock @@ -3,22 +3,17 @@ "@babel/runtime-corejs3@^7.12.1": - version "7.13.9" - resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.13.9.tgz#b2fa9a6e5690ef8d4c4f2d30cac3ec1a8bb633ce" - integrity sha512-p6WSr71+5u/VBf1KDS/Y4dK3ZwbV+DD6wQO3X2EbUVluEOiyXUk09DzcwSaUH4WomYXrEPC+i2rqzuthhZhOJw== + version "7.14.0" + resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.14.0.tgz#6bf5fbc0b961f8e3202888cb2cd0fb7a0a9a3f66" + integrity sha512-0R0HTZWHLk6G8jIk0FtoX+AatCtKnswS98VhXwGImFc759PJRp4Tru0PQYZofyijTFUr+gT8Mu7sgXVJLQ0ceg== dependencies: core-js-pure "^3.0.0" regenerator-runtime "^0.13.4" "@types/node@*": - version "14.14.31" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.31.tgz#72286bd33d137aa0d152d47ec7c1762563d34055" - integrity sha512-vFHy/ezP5qI0rFgJ7aQnjDXwAMrG0KqqIH7tQG5PPv3BWBayOPIQNBjVc/P6hhdZfMx51REc6tfDNXHUio893g== - -"@types/q@^1.5.1": - version "1.5.4" - resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.4.tgz#15925414e0ad2cd765bfef58842f7e26a7accb24" - integrity sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug== + version "15.0.1" + resolved "https://registry.yarnpkg.com/@types/node/-/node-15.0.1.tgz#ef34dea0881028d11398be5bf4e856743e3dc35a" + integrity sha512-TMkXt0Ck1y0KKsGr9gJtWGjttxlZnnvDtphxUOSd0bfaR6Q1jle+sPvrzNR1urqYTWMinoKvjKfXUGsumaO1PA== "@types/yauzl@^2.9.1": version "2.9.1" @@ -42,25 +37,11 @@ agent-base@6: dependencies: debug "4" -ansi-styles@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" - integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== - dependencies: - color-convert "^1.9.0" - any-promise@^1.1.0: version "1.3.0" resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" integrity sha1-q8av7tzqUugJzcA3au0845Y10X8= -argparse@^1.0.7: - version "1.0.10" - resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" - integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== - dependencies: - sprintf-js "~1.0.2" - asn1.js@^5.2.0: version "5.4.1" resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.4.1.tgz#11a980b84ebb91781ce35b0fdc2ee294e3783f07" @@ -85,9 +66,9 @@ async-limiter@~1.0.0: integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== balanced-match@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" - integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== base64-js@^1.0.2, base64-js@^1.3.1: version "1.5.1" @@ -113,11 +94,6 @@ bn.js@^5.0.0, bn.js@^5.1.1: resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.0.tgz#358860674396c6997771a9d051fcc1b57d4ae002" integrity sha512-D7iWRBvnZE8ecXiLj/9wbxH7Tk79fAh8IHaTNq1RWRixsS02W+5qS+iE9yq6RYl0asXx5tw0bLhmT5pIfbSquw== -boolbase@^1.0.0, boolbase@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" - integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24= - brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" @@ -242,23 +218,6 @@ cache-content-type@^1.0.0: mime-types "^2.1.18" ylru "^1.2.0" -call-bind@^1.0.0, call-bind@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" - integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== - dependencies: - function-bind "^1.1.1" - get-intrinsic "^1.0.2" - -chalk@^2.4.1: - version "2.4.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" - integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== - dependencies: - ansi-styles "^3.2.1" - escape-string-regexp "^1.0.5" - supports-color "^5.3.0" - chownr@^1.1.1: version "1.1.4" resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" @@ -277,27 +236,6 @@ co@^4.6.0: resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" integrity sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ= -coa@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/coa/-/coa-2.0.2.tgz#43f6c21151b4ef2bf57187db0d73de229e3e7ec3" - integrity sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA== - dependencies: - "@types/q" "^1.5.1" - chalk "^2.4.1" - q "^1.1.2" - -color-convert@^1.9.0: - version "1.9.3" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" - integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== - dependencies: - color-name "1.1.3" - -color-name@1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" - integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= - concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -334,9 +272,9 @@ cookies@~0.8.0: keygrip "~1.1.0" core-js-pure@^3.0.0: - version "3.9.1" - resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.9.1.tgz#677b322267172bd490e4464696f790cbc355bec5" - integrity sha512-laz3Zx0avrw9a4QEIdmIblnVuJz8W51leY9iLThatCsFawWxC3sE4guASC78JbCin+DkwMpCdp1AVAuzL/GN7A== + version "3.11.2" + resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.11.2.tgz#10e3b35788c00f431bc0d601d7551475ec3e792c" + integrity sha512-DQxdEKm+zFsnON7ZGOgUAQXBt1UJJ01tOzN/HgQ7cNf0oEHW1tcBLfCQQd1q6otdLu5gAdvKYxKHAoXGwE/kiQ== core-util-is@~1.0.0: version "1.0.2" @@ -391,49 +329,6 @@ crypto-browserify@^3.11.0: randombytes "^2.0.0" randomfill "^1.0.3" -css-select-base-adapter@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz#3b2ff4972cc362ab88561507a95408a1432135d7" - integrity sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w== - -css-select@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/css-select/-/css-select-2.1.0.tgz#6a34653356635934a81baca68d0255432105dbef" - integrity sha512-Dqk7LQKpwLoH3VovzZnkzegqNSuAziQyNZUcrdDM401iY+R5NkGBXGmtO05/yaXQziALuPogeG0b7UAgjnTJTQ== - dependencies: - boolbase "^1.0.0" - css-what "^3.2.1" - domutils "^1.7.0" - nth-check "^1.0.2" - -css-tree@1.0.0-alpha.37: - version "1.0.0-alpha.37" - resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.0.0-alpha.37.tgz#98bebd62c4c1d9f960ec340cf9f7522e30709a22" - integrity sha512-DMxWJg0rnz7UgxKT0Q1HU/L9BeJI0M6ksor0OgqOnF+aRCDWg/N2641HmVyU9KVIu0OVVWOb2IpC9A+BJRnejg== - dependencies: - mdn-data "2.0.4" - source-map "^0.6.1" - -css-tree@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.1.2.tgz#9ae393b5dafd7dae8a622475caec78d3d8fbd7b5" - integrity sha512-wCoWush5Aeo48GLhfHPbmvZs59Z+M7k5+B1xDnXbdWNcEF423DoFdqSWE0PM5aNk5nI5cp1q7ms36zGApY/sKQ== - dependencies: - mdn-data "2.0.14" - source-map "^0.6.1" - -css-what@^3.2.1: - version "3.4.2" - resolved "https://registry.yarnpkg.com/css-what/-/css-what-3.4.2.tgz#ea7026fcb01777edbde52124e21f327e7ae950e4" - integrity sha512-ACUm3L0/jiZTqfzRM3Hi9Q8eZqd6IK37mMWPLz9PJxkLWllYeRf+EHUSHYEtFop2Eqytaq1FizFVh7XfBnXCDQ== - -csso@^4.0.2: - version "4.2.0" - resolved "https://registry.yarnpkg.com/csso/-/csso-4.2.0.tgz#ea3a561346e8dc9f546d6febedd50187cf389529" - integrity sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA== - dependencies: - css-tree "^1.1.2" - debug@4, debug@^4.1.0, debug@^4.1.1: version "4.3.1" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee" @@ -453,13 +348,6 @@ deep-equal@~1.0.1: resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5" integrity sha1-9dJgKStmDghO/0zbyfCK0yR0SLU= -define-properties@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" - integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ== - dependencies: - object-keys "^1.0.12" - delegates@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" @@ -488,10 +376,10 @@ destroy@^1.0.4: resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= -devtools-protocol@0.0.854822: - version "0.0.854822" - resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.854822.tgz#eac3a5260a6b3b4e729a09fdc0c77b0d322e777b" - integrity sha512-xd4D8kHQtB0KtWW0c9xBZD5LVtm9chkMOfs/3Yn01RhT/sFIsVtzTtypfKoFfWBaL+7xCYLxjOLkhwPXaX/Kcg== +devtools-protocol@0.0.869402: + version "0.0.869402" + resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.869402.tgz#03ade701761742e43ae4de5dc188bcd80f156d8d" + integrity sha512-VvlVYY+VDJe639yHs5PHISzdWTLL3Aw8rO4cvUtwvoxFd6FHbE4OpHHcde52M6096uYYazAmd4l0o5VuFRO2WA== diffie-hellman@^5.0.0: version "5.0.3" @@ -502,37 +390,11 @@ diffie-hellman@^5.0.0: miller-rabin "^4.0.0" randombytes "^2.0.0" -dom-serializer@0: - version "0.2.2" - resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51" - integrity sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g== - dependencies: - domelementtype "^2.0.1" - entities "^2.0.0" - domain-browser@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda" integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA== -domelementtype@1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f" - integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w== - -domelementtype@^2.0.1: - version "2.1.0" - resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.1.0.tgz#a851c080a6d1c3d94344aed151d99f669edf585e" - integrity sha512-LsTgx/L5VpD+Q8lmsXSHW2WpA+eBlZ9HPf3erD1IoPF00/3JKHZ3BknUVA2QGDNu69ZNmyFmCWBSO45XjYKC5w== - -domutils@^1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a" - integrity sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg== - dependencies: - dom-serializer "0" - domelementtype "1" - ee-first@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" @@ -563,57 +425,11 @@ end-of-stream@^1.1.0, end-of-stream@^1.4.1: dependencies: once "^1.4.0" -entities@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" - integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== - -es-abstract@^1.17.2, es-abstract@^1.18.0-next.2: - version "1.18.0" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.0.tgz#ab80b359eecb7ede4c298000390bc5ac3ec7b5a4" - integrity sha512-LJzK7MrQa8TS0ja2w3YNLzUgJCGPdPOV1yVvezjNnS89D+VR08+Szt2mz3YB2Dck/+w5tfIq/RoUAFqJJGM2yw== - dependencies: - call-bind "^1.0.2" - es-to-primitive "^1.2.1" - function-bind "^1.1.1" - get-intrinsic "^1.1.1" - has "^1.0.3" - has-symbols "^1.0.2" - is-callable "^1.2.3" - is-negative-zero "^2.0.1" - is-regex "^1.1.2" - is-string "^1.0.5" - object-inspect "^1.9.0" - object-keys "^1.1.1" - object.assign "^4.1.2" - string.prototype.trimend "^1.0.4" - string.prototype.trimstart "^1.0.4" - unbox-primitive "^1.0.0" - -es-to-primitive@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" - integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== - dependencies: - is-callable "^1.1.4" - is-date-object "^1.0.1" - is-symbol "^1.0.2" - escape-html@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= -escape-string-regexp@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" - integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= - -esprima@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" - integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== - events@^3.0.0: version "3.3.0" resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" @@ -668,20 +484,6 @@ fs.realpath@^1.0.0: resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= -function-bind@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" - integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== - -get-intrinsic@^1.0.2, get-intrinsic@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.1.tgz#15f59f376f855c446963948f0d24cd3637b4abc6" - integrity sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q== - dependencies: - function-bind "^1.1.1" - has "^1.0.3" - has-symbols "^1.0.1" - get-stream@^5.1.0: version "5.2.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" @@ -701,28 +503,6 @@ glob@^7.1.3: once "^1.3.0" path-is-absolute "^1.0.0" -has-bigints@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.1.tgz#64fe6acb020673e3b78db035a5af69aa9d07b113" - integrity sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA== - -has-flag@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" - integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= - -has-symbols@^1.0.0, has-symbols@^1.0.1, has-symbols@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.2.tgz#165d3070c00309752a1236a479331e3ac56f1423" - integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw== - -has@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" - integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== - dependencies: - function-bind "^1.1.1" - hash-base@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.1.0.tgz#55c381d9e06e1d2997a883b4a3fddfe7f0d3af33" @@ -837,63 +617,11 @@ inherits@2.0.3: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= -is-bigint@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.1.tgz#6923051dfcbc764278540b9ce0e6b3213aa5ebc2" - integrity sha512-J0ELF4yHFxHy0cmSxZuheDOz2luOdVvqjwmEcj8H/L1JHeuEDSDbeRP+Dk9kFVk5RTFzbucJ2Kb9F7ixY2QaCg== - -is-boolean-object@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.0.tgz#e2aaad3a3a8fca34c28f6eee135b156ed2587ff0" - integrity sha512-a7Uprx8UtD+HWdyYwnD1+ExtTgqQtD2k/1yJgtXP6wnMm8byhkoTZRl+95LLThpzNZJ5aEvi46cdH+ayMFRwmA== - dependencies: - call-bind "^1.0.0" - -is-callable@^1.1.4, is-callable@^1.2.3: - version "1.2.3" - resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.3.tgz#8b1e0500b73a1d76c70487636f368e519de8db8e" - integrity sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ== - -is-date-object@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.2.tgz#bda736f2cd8fd06d32844e7743bfa7494c3bfd7e" - integrity sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g== - is-generator-function@^1.0.7: version "1.0.8" resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.8.tgz#dfb5c2b120e02b0a8d9d2c6806cd5621aa922f7b" integrity sha512-2Omr/twNtufVZFr1GhxjOMFPAj2sjc/dKaIqBhvo4qciXfJmITGH6ZGd8eZYNHza8t1y0e01AuqRhJwfWp26WQ== -is-negative-zero@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.1.tgz#3de746c18dda2319241a53675908d8f766f11c24" - integrity sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w== - -is-number-object@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.4.tgz#36ac95e741cf18b283fc1ddf5e83da798e3ec197" - integrity sha512-zohwelOAur+5uXtk8O3GPQ1eAcu4ZX3UwxQhUlfFFMNpUd83gXgjbhJh6HmB6LUNV/ieOLQuDwJO3dWJosUeMw== - -is-regex@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.2.tgz#81c8ebde4db142f2cf1c53fc86d6a45788266251" - integrity sha512-axvdhb5pdhEVThqJzYXwMlVuZwC+FF2DpcOhTS+y/8jVq4trxyPgfcwIxIKiyeuLlSQYKkmUaPQJ8ZE4yNKXDg== - dependencies: - call-bind "^1.0.2" - has-symbols "^1.0.1" - -is-string@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.5.tgz#40493ed198ef3ff477b8c7f92f644ec82a5cd3a6" - integrity sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ== - -is-symbol@^1.0.2, is-symbol@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.3.tgz#38e1014b9e6329be0de9d24a414fd7441ec61937" - integrity sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ== - dependencies: - has-symbols "^1.0.1" - isarray@^1.0.0, isarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" @@ -904,14 +632,6 @@ isexe@^2.0.0: resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= -js-yaml@^3.13.1: - version "3.14.1" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" - integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== - dependencies: - argparse "^1.0.7" - esprima "^4.0.0" - jszip@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.6.0.tgz#839b72812e3f97819cc13ac4134ffced95dd6af9" @@ -1001,16 +721,6 @@ md5.js@^1.3.4: inherits "^2.0.1" safe-buffer "^5.1.2" -mdn-data@2.0.14: - version "2.0.14" - resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50" - integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow== - -mdn-data@2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.4.tgz#699b3c38ac6f1d728091a64650b65d388502fd5b" - integrity sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA== - media-typer@0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" @@ -1024,17 +734,17 @@ miller-rabin@^4.0.0: bn.js "^4.0.0" brorand "^1.0.1" -mime-db@1.46.0: - version "1.46.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.46.0.tgz#6267748a7f799594de3cbc8cde91def349661cee" - integrity sha512-svXaP8UQRZ5K7or+ZmfNhg2xX3yKDMUzqadsSqi4NCH/KomcH75MAMYAGVlvXn4+b/xOPhS3I2uHKRUzvjY7BQ== +mime-db@1.47.0: + version "1.47.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.47.0.tgz#8cb313e59965d3c05cfbf898915a267af46a335c" + integrity sha512-QBmA/G2y+IfeS4oktet3qRZ+P5kPhCKRXxXnQEudYqUaEioAU1/Lq2us3D/t1Jfo4hE9REQPrbB7K5sOczJVIw== mime-types@^2.1.18, mime-types@~2.1.24: - version "2.1.29" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.29.tgz#1d4ab77da64b91f5f72489df29236563754bb1b2" - integrity sha512-Y/jMt/S5sR9OaqteJtslsFZKWOIIqMACsJSiHghlCAyhf7jfVYjKBmLiX8OgpWeW+fjJ2b+Az69aPFPkUOY6xQ== + version "2.1.30" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.30.tgz#6e7be8b4c479825f85ed6326695db73f9305d62d" + integrity sha512-crmjA4bLtR8m9qLpHvgxSChT+XoSlZi8J4n/aIdn3z92e/U47Z0V/yl+Wh9W046GgFVAmoNR/fmdbZYcSSIUeg== dependencies: - mime-db "1.46.0" + mime-db "1.47.0" minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: version "1.0.1" @@ -1053,23 +763,11 @@ minimatch@^3.0.4: dependencies: brace-expansion "^1.1.7" -minimist@^1.2.5: - version "1.2.5" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" - integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== - mkdirp-classic@^0.5.2: version "0.5.3" resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== -mkdirp@~0.5.1: - version "0.5.5" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" - integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== - dependencies: - minimist "^1.2.5" - ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" @@ -1119,57 +817,11 @@ node-libs-browser@^2.2.1: util "^0.11.0" vm-browserify "^1.0.1" -nth-check@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c" - integrity sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg== - dependencies: - boolbase "~1.0.0" - object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= -object-inspect@^1.9.0: - version "1.9.0" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.9.0.tgz#c90521d74e1127b67266ded3394ad6116986533a" - integrity sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw== - -object-keys@^1.0.12, object-keys@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" - integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== - -object.assign@^4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.2.tgz#0ed54a342eceb37b38ff76eb831a0e788cb63940" - integrity sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ== - dependencies: - call-bind "^1.0.0" - define-properties "^1.1.3" - has-symbols "^1.0.1" - object-keys "^1.1.1" - -object.getownpropertydescriptors@^2.1.0: - version "2.1.2" - resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.2.tgz#1bd63aeacf0d5d2d2f31b5e393b03a7c601a23f7" - integrity sha512-WtxeKSzfBjlzL+F9b7M7hewDzMwy+C8NRssHd1YrNlzHzIDrXcXiNOMrezdAEM4UXixgV+vvnyBeN7Rygl2ttQ== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - es-abstract "^1.18.0-next.2" - -object.values@^1.1.0: - version "1.1.3" - resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.3.tgz#eaa8b1e17589f02f698db093f7c62ee1699742ee" - integrity sha512-nkF6PfDB9alkOUxpf1HNm/QlkeW3SReqL5WXeBLpEJJnlPSvRaDQpW3gQTksTN3fgJX4hL42RzKyOin6ff3tyw== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - es-abstract "^1.18.0-next.2" - has "^1.0.3" - on-finished@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" @@ -1250,9 +902,9 @@ path-is-absolute@^1.0.0: integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= pbkdf2@^3.0.3: - version "3.1.1" - resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.1.1.tgz#cb8724b0fada984596856d1a6ebafd3584654b94" - integrity sha512-4Ejy1OPxi9f2tt1rRV7Go7zmfDQ+ZectEQz3VGUQhgq62HtIRPDyG/JtnwIxs6x3uNMwo2V7q1fMvKjb+Tnpqg== + version "3.1.2" + resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.1.2.tgz#dd822aa0887580e52f1a039dc3eda108efae3075" + integrity sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA== dependencies: create-hash "^1.1.2" create-hmac "^1.1.4" @@ -1329,13 +981,13 @@ puppeteer-cluster@^0.22.0: dependencies: debug "^4.1.1" -puppeteer@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-8.0.0.tgz#a236669118aa795331c2d0ca19877159e7664705" - integrity sha512-D0RzSWlepeWkxPPdK3xhTcefj8rjah1791GE82Pdjsri49sy11ci/JQsAO8K2NRukqvwEtcI+ImP5F4ZiMvtIQ== +puppeteer@^9.1.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-9.1.0.tgz#0530ed1f595088eefd078c8f1f7618d00f216a56" + integrity sha512-+BWwEKYQ9oBTUcDYwfgnVPlHSEYqD4sXsMqQf70vSlTE6YIuXujc7zKgO3FyZNJYVrdrUppy/LLwGF1IRacQMQ== dependencies: debug "^4.1.0" - devtools-protocol "0.0.854822" + devtools-protocol "0.0.869402" extract-zip "^2.0.0" https-proxy-agent "^5.0.0" node-fetch "^2.6.1" @@ -1347,11 +999,6 @@ puppeteer@^8.0.0: unbzip2-stream "^1.3.3" ws "^7.2.3" -q@^1.1.2: - version "1.5.1" - resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" - integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc= - querystring-es3@^0.2.0: version "0.2.1" resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73" @@ -1449,7 +1096,7 @@ safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -sax@^1.2.4, sax@~1.2.4: +sax@^1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== @@ -1487,10 +1134,10 @@ shadow-cljs-jar@1.3.2: resolved "https://registry.yarnpkg.com/shadow-cljs-jar/-/shadow-cljs-jar-1.3.2.tgz#97273afe1747b6a2311917c1c88d9e243c81957b" integrity sha512-XmeffAZHv8z7451kzeq9oKh8fh278Ak+UIOGGrapyqrFBB773xN8vMQ3O7J7TYLnb9BUwcqadKkmgaq7q6fhZg== -shadow-cljs@^2.11.20: - version "2.11.20" - resolved "https://registry.yarnpkg.com/shadow-cljs/-/shadow-cljs-2.11.20.tgz#fa814b74a7fa7909ff056994a80f8f7435881046" - integrity sha512-TmZp1Hjp49oziqaTdBYwO0qzvoVMYa+O5c7rwdfO334bdYhTPMmJJeG/EbeJpRvLAKErjbxGmY4P28rqfPmZ3w== +shadow-cljs@^2.12.5: + version "2.12.5" + resolved "https://registry.yarnpkg.com/shadow-cljs/-/shadow-cljs-2.12.5.tgz#d3cf29fc1f1e02dd875939549419979e0feadbf4" + integrity sha512-o3xo3coRgnlkI/iI55ccHjj6AU3F1+ovk3hhK86e3P2JGGOpNTAwsGNxUpMC5JAwS9Nz0v6sSk73hWjEOnm6fQ== dependencies: node-libs-browser "^2.2.1" readline-sync "^1.4.7" @@ -1519,21 +1166,11 @@ source-map@^0.5.6: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= -source-map@^0.6.0, source-map@^0.6.1: +source-map@^0.6.0: version "0.6.1" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== -sprintf-js@~1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" - integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= - -stable@^0.1.8: - version "0.1.8" - resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf" - integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w== - "statuses@>= 1.5.0 < 2", statuses@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" @@ -1558,22 +1195,6 @@ stream-http@^2.7.2: to-arraybuffer "^1.0.0" xtend "^4.0.0" -string.prototype.trimend@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz#e75ae90c2942c63504686c18b287b4a0b1a45f80" - integrity sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - -string.prototype.trimstart@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz#b36399af4ab2999b4c9c648bd7a3fb2bb26feeed" - integrity sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - string_decoder@^1.0.0, string_decoder@^1.1.1: version "1.3.0" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" @@ -1588,32 +1209,6 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -supports-color@^5.3.0: - version "5.5.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" - integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== - dependencies: - has-flag "^3.0.0" - -svgo@^1.3.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/svgo/-/svgo-1.3.2.tgz#b6dc511c063346c9e415b81e43401145b96d4167" - integrity sha512-yhy/sQYxR5BkC98CY7o31VGsg014AKLEPxdfhora76l36hD9Rdy5NZA/Ocn6yayNPgSamYdtX2rFJdcv07AYVw== - dependencies: - chalk "^2.4.1" - coa "^2.0.2" - css-select "^2.0.0" - css-select-base-adapter "^0.1.1" - css-tree "1.0.0-alpha.37" - csso "^4.0.2" - js-yaml "^3.13.1" - mkdirp "~0.5.1" - object.values "^1.1.0" - sax "~1.2.4" - stable "^0.1.8" - unquote "~1.1.1" - util.promisify "~1.0.0" - tar-fs@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784" @@ -1680,16 +1275,6 @@ ultron@~1.1.0: resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.1.1.tgz#9fe1536a10a664a65266a1e3ccf85fd36302bc9c" integrity sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og== -unbox-primitive@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.0.tgz#eeacbc4affa28e9b3d36b5eaeccc50b3251b1d3f" - integrity sha512-P/51NX+JXyxK/aigg1/ZgyccdAxm5K1+n8+tvqSntjOivPt19gvm1VC49RWYetsiub8WViUchdxl/KWHHB0kzA== - dependencies: - function-bind "^1.1.1" - has-bigints "^1.0.0" - has-symbols "^1.0.0" - which-boxed-primitive "^1.0.1" - unbzip2-stream@^1.3.3: version "1.4.3" resolved "https://registry.yarnpkg.com/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz#b0da04c4371311df771cdc215e87f2130991ace7" @@ -1703,11 +1288,6 @@ unpipe@1.0.0: resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= -unquote@~1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/unquote/-/unquote-1.1.1.tgz#8fded7324ec6e88a0ff8b905e7c098cdc086d544" - integrity sha1-j97XMk7G6IoP+LkF58CYzcCG1UQ= - url@^0.11.0: version "0.11.0" resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" @@ -1721,16 +1301,6 @@ util-deprecate@^1.0.1, util-deprecate@~1.0.1: resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= -util.promisify@~1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/util.promisify/-/util.promisify-1.0.1.tgz#6baf7774b80eeb0f7520d8b81d07982a59abbaee" - integrity sha512-g9JpC/3He3bm38zsLupWryXHoEcS22YHthuPQSJdMy6KNrzIRzWqcsHzD/WUnqe45whVou4VIsPew37DoXWNrA== - dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.2" - has-symbols "^1.0.1" - object.getownpropertydescriptors "^2.1.0" - util@0.10.3: version "0.10.3" resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9" @@ -1755,17 +1325,6 @@ vm-browserify@^1.0.1: resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0" integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ== -which-boxed-primitive@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" - integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg== - dependencies: - is-bigint "^1.0.1" - is-boolean-object "^1.1.0" - is-number-object "^1.0.4" - is-string "^1.0.5" - is-symbol "^1.0.3" - which@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" @@ -1788,9 +1347,9 @@ ws@^3.0.0: ultron "~1.1.0" ws@^7.2.3: - version "7.4.3" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.3.tgz#1f9643de34a543b8edb124bdcbc457ae55a6e5cd" - integrity sha512-hr6vCR76GsossIRsr8OLR9acVVm1jyfEWvhbNjtgPOrfvAlKzvyeg/P6r8RuDjRyrcQoPQT7K0DGEPc7Ae6jzA== + version "7.4.5" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.5.tgz#a484dd851e9beb6fdb420027e3885e8ce48986c1" + integrity sha512-xzyu3hFvomRfXKH8vOFMU3OguG6oOvhXMo3xsGy3xWExqaM2dxBbVxuD99O7m3ZUFMvvscsZDqxfgMaRr/Nr1g== xml-js@^1.6.11: version "1.6.11" @@ -1799,10 +1358,10 @@ xml-js@^1.6.11: dependencies: sax "^1.2.4" -xregexp@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-5.0.1.tgz#b2f20e87790876585fb4103e91cb163a63cd2547" - integrity sha512-flG0ykxHQLOfF886GpnY26WQkj4/RmcxYvoVcSFbg+1lPMdnXEPoTKuLzw1olKnJ+o2Wc1+RCD1oktV4bYzVlQ== +xregexp@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-5.0.2.tgz#798aa7757836f39cdbdeeba3daf94d75f7a9dcc1" + integrity sha512-JPNfN40YMNSDxZrahMrmtNH1QqPJp0/qNeEJM2nnOlhcBdfCCjekPYFV2OnwKxwvpEYglH1RBotbpRRaEuCG8Q== dependencies: "@babel/runtime-corejs3" "^7.12.1" diff --git a/frontend/deps.edn b/frontend/deps.edn index df5753b1e5..4a3337c6a8 100644 --- a/frontend/deps.edn +++ b/frontend/deps.edn @@ -1,52 +1,9 @@ {:paths ["src" "vendor" "resources" "../common"] - :deps - {org.clojure/clojurescript {:mvn/version "1.10.773"} - org.clojure/clojure {:mvn/version "1.10.3"} - com.cognitect/transit-cljs {:mvn/version "0.8.264"} - - environ/environ {:mvn/version "1.2.0"} - metosin/reitit-core {:mvn/version "0.5.12"} - expound/expound {:mvn/version "0.8.9"} - - danlentz/clj-uuid {:mvn/version "0.1.9"} - frankiesardo/linked {:mvn/version "1.3.0"} - - funcool/beicon {:mvn/version "2021.01.29-1"} - funcool/cuerdas {:mvn/version "2020.03.26-3"} - funcool/okulary {:mvn/version "2020.04.14-0"} - funcool/potok {:mvn/version "3.2.0"} - funcool/promesa {:mvn/version "6.0.0"} - funcool/rumext {:mvn/version "2021.01.26-0"} - - lambdaisland/uri {:mvn/version "1.4.54" - :exclusions [org.clojure/data.json]} - - } + :deps {} :aliases - {:dev - {:extra-paths ["tests" "dev"] - :extra-deps - {com.bhauman/rebel-readline-cljs {:mvn/version "0.1.4"} - com.bhauman/rebel-readline {:mvn/version "0.1.4"} - org.clojure/tools.namespace {:mvn/version "0.3.1"} - hashp/hashp {:mvn/version "RELEASE"} - - metosin/jsonista {:mvn/version "0.2.5"} - funcool/datoteka {:mvn/version "1.2.0"} - binaryage/devtools {:mvn/version "RELEASE"} - - thheller/shadow-cljs {:mvn/version "2.11.20"} - - ;; i18n parsing - carocad/parcera {:mvn/version "0.11.0"} - org.antlr/antlr4-runtime {:mvn/version "4.7"}}} - - :outdated - {:extra-deps {olical/depot {:mvn/version "RELEASE"}} - :main-opts ["-m" "depot.outdated.main"]} - - :repl - {:main-opts ["-m" "rebel-readline.main"]} - + {:outdated + {:extra-deps {com.github.liquidz/antq {:mvn/version "RELEASE"} + org.slf4j/slf4j-nop {:mvn/version "RELEASE"}} + :main-opts ["-m" "antq.core"]} }} diff --git a/frontend/gulpfile.js b/frontend/gulpfile.js index dd8922be6e..ba8cedd86d 100644 --- a/frontend/gulpfile.js +++ b/frontend/gulpfile.js @@ -16,6 +16,7 @@ const clean = require("postcss-clean"); const mkdirp = require("mkdirp"); const rimraf = require("rimraf"); const sass = require("sass"); +const gettext = require("gettext-parser"); const mapStream = require("map-stream"); const paths = {}; @@ -31,16 +32,30 @@ paths.dist = "./target/dist/"; // Templates function readLocales() { - const path = __dirname + "/resources/locales.json"; - const content = JSON.parse(fs.readFileSync(path, {encoding: "utf8"})); + const langs = ["ca", "de", "el", "en", "es", "fr", "tr", "ru", "zh_cn"]; + const result = {}; - let result = {}; - for (let key of Object.keys(content)) { - const item = content[key]; - if (l.isString(item)) { - result[key] = {"en": item}; - } else if (l.isPlainObject(item) && l.isPlainObject(item.translations)) { - result[key] = item.translations; + for (let lang of langs) { + const content = fs.readFileSync(`./translations/${lang}.po`, {encoding:"utf-8"}); + + lang = lang.toLowerCase(); + + const data = gettext.po.parse(content, "utf-8"); + const trdata = data.translations[""]; + + for (let key of Object.keys(trdata)) { + if (key === "") continue; + + if (l.isNil(result[key])) { + result[key] = {}; + } + + const msgstr = trdata[key].msgstr; + if (msgstr.length === 1) { + result[key][lang] = msgstr[0]; + } else { + result[key][lang] = msgstr; + } } } @@ -53,23 +68,23 @@ function readManifest() { const content = JSON.parse(fs.readFileSync(path, {encoding: "utf8"})); const index = { - "config": "/js/config.js?ts=" + Date.now(), + "config": "js/config.js?ts=" + Date.now(), "polyfills": "js/polyfills.js?ts=" + Date.now(), }; for (let item of content) { - index[item.name] = "/js/" + item["output-name"]; + index[item.name] = "js/" + item["output-name"]; }; return index; } catch (e) { console.error("Error on reading manifest, using default."); return { - "config": "/js/config.js", + "config": "js/config.js", "polyfills": "js/polyfills.js", - "main": "/js/main.js", - "shared": "/js/shared.js", - "worker": "/js/worker.js" + "main": "js/main.js", + "shared": "js/shared.js", + "worker": "js/worker.js" }; } } @@ -189,7 +204,7 @@ gulp.task("watch:main", function() { gulp.watch(paths.resources + "images/**/*", gulp.series("copy:assets:images")); gulp.watch([paths.resources + "templates/*.mustache", - paths.resources + "locales.json"], + "translations/*.po"], gulp.series("templates")); }); diff --git a/frontend/locales.clj b/frontend/locales.clj deleted file mode 100644 index a80a0a38a4..0000000000 --- a/frontend/locales.clj +++ /dev/null @@ -1,145 +0,0 @@ -(require '[clojure.pprint :as pp :refer [pprint]]) - -(require '[clojure.edn :as edn] - '[clojure.set :as set] - '[clojure.java.io :as io]) - -(require '[datoteka.core :as fs] - '[jsonista.core :as json] - '[parcera.core :as pa]) - -(import 'java.nio.file.Paths - 'java.nio.file.Path - 'java.nio.file.Files - 'java.nio.file.SimpleFileVisitor - 'java.nio.file.FileVisitResult - 'com.fasterxml.jackson.databind.ObjectMapper - 'com.fasterxml.jackson.databind.SerializationFeature) - -(defmulti task first) - -(defn- find-translations-in-form - [form] - (reduce (fn [messages node] - (let [found (->> node - (filter #(and (seq? %) (= :string (first %)))) - (map (fn [item] - (let [mdata (meta item)] - {:code (edn/read-string (second item)) - :line (get-in mdata [::pa/start :row])}))))] - (into messages found))) - [] - (->> (tree-seq seq? seq form) - (filter #(and (seq? %) - (seq? (second %)) - (= :list (first %)) - (= :symbol (first (second %))) - (or (= "t" (second (second %))) - (= "tr" (second (second %))))))))) - -(defn- find-translations - [path] - (let [forms (pa/ast (slurp path)) - spath (str path)] - (->> forms - (filter #(and (seq? %) (= :list (first %)))) - (reduce (fn [messages form] - (->> (find-translations-in-form form) - (map #(assoc % :file spath)) - (into messages))) [])))) - -(defn- collect-translations - [path] - (let [messages (atom [])] - (->> (proxy [SimpleFileVisitor] [] - (visitFile [path attrs] - (when (= (fs/ext path) "cljs") - (swap! messages into (find-translations path))) - FileVisitResult/CONTINUE) - (postVisitDirectory [dir exc] - FileVisitResult/CONTINUE)) - (Files/walkFileTree (fs/path path))) - @messages)) - -(defn- read-json-file - [path] - (when (fs/regular-file? path) - (let [content (json/read-value (io/as-file path))] - (reduce-kv (fn [res k v] - (let [v (into (sorted-map) v) - v (update v "translations" #(into (sorted-map) %))] - (assoc res k v))) - (sorted-map) - content)))) - -(defn- add-translation - [data {:keys [code file line] :as translation}] - (let [rpath (str file)] - (if (contains? data code) - (update data code (fn [state] - (if (get state "permanent") - state - (-> state - (dissoc "unused") - (update "used-in" conj rpath))))) - (assoc data code {"translations" (sorted-map "en" nil "es" nil) - "used-in" [rpath]})))) - -(defn- clean-removed-translations - [data imported] - (let [existing (into #{} (keys data)) - toremove (set/difference existing imported)] - (reduce (fn [data code] - (if (get-in data [code "permanent"]) - data - (-> data - (update code dissoc "used-in") - (update code assoc "unused" true)))) - data - toremove))) - -(defn- initial-cleanup - [data] - (reduce-kv (fn [data k v] - (if (string? v) - (assoc data k {"used-in" [] - "translations" {:en v}}) - (update data k assoc "used-in" []))) - data - data)) - -(defn- synchronize-translations - [data translations] - (loop [data (initial-cleanup data) - imported #{} - c (first translations) - r (rest translations)] - (if (nil? c) - (clean-removed-translations data imported) - (recur (add-translation data c) - (conj imported (:code c)) - (first r) - (rest r))))) - -(defn- write-result! - [data output-path] - (binding [*out* (io/writer (fs/path output-path))] - (let [mapper (doto (ObjectMapper.) - (.enable SerializationFeature/ORDER_MAP_ENTRIES_BY_KEYS)) - mapper (json/object-mapper {:pretty true :mapper mapper})] - (println (json/write-value-as-string data mapper)) - (flush)))) - -(defn- update-translations - [{:keys [find-directory output-path] :as props}] - (let [data (read-json-file output-path) - translations (collect-translations find-directory) - data (synchronize-translations data translations)] - (write-result! data output-path))) - -(defmethod task "collect" - [[_ in-path out-path]] - (update-translations {:find-directory in-path - :output-path out-path})) - -(task *command-line-args*) diff --git a/frontend/package.json b/frontend/package.json index dd4ff7333e..dbc09258cb 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,10 +12,11 @@ "defaults" ], "scripts": { - "collect-locales": "clojure -M:dev locales.clj collect src/app/main/ resources/locales.json" + "validate-translations": "node ./scripts/validate-translations.js" }, "devDependencies": { "autoprefixer": "^10.2.4", + "gettext-parser": "^4.0.4", "gulp": "4.0.2", "gulp-concat": "^2.6.1", "gulp-gzip": "^1.4.2", @@ -34,18 +35,16 @@ "shadow-cljs": "^2.11.20" }, "dependencies": { - "date-fns": "^2.19.0", + "date-fns": "^2.21.1", "draft-js": "^0.11.7", "highlight.js": "^10.6.0", - "humanize-duration": "~3.25.0", "js-beautify": "^1.13.5", + "luxon": "^1.26.0", "mousetrap": "^1.6.5", "randomcolor": "^0.6.2", "react": "~17.0.1", "react-dom": "~17.0.1", "rxjs": "~7.0.0-beta.12", - "slate": "^0.59.0", - "slate-react": "^0.59.0", "source-map-support": "^0.5.16", "tdigest": "^0.1.1", "xregexp": "^5.0.1" diff --git a/frontend/resources/images/features/assets-organiz.gif b/frontend/resources/images/features/assets-organiz.gif new file mode 100644 index 0000000000..333b7be7d6 Binary files /dev/null and b/frontend/resources/images/features/assets-organiz.gif differ diff --git a/frontend/resources/images/features/path-tool.gif b/frontend/resources/images/features/path-tool.gif new file mode 100644 index 0000000000..34f280652d Binary files /dev/null and b/frontend/resources/images/features/path-tool.gif differ diff --git a/frontend/resources/images/features/smart-inputs.gif b/frontend/resources/images/features/smart-inputs.gif new file mode 100644 index 0000000000..3f2a8dacaa Binary files /dev/null and b/frontend/resources/images/features/smart-inputs.gif differ diff --git a/frontend/resources/images/icons/alignment.svg b/frontend/resources/images/icons/alignment.svg index aaacb23b13..65db1eb382 100644 --- a/frontend/resources/images/icons/alignment.svg +++ b/frontend/resources/images/icons/alignment.svg @@ -1 +1,3 @@ - \ No newline at end of file + + + diff --git a/frontend/resources/images/icons/arrow-down.svg b/frontend/resources/images/icons/arrow-down.svg index a56f66d870..9402aff968 100644 --- a/frontend/resources/images/icons/arrow-down.svg +++ b/frontend/resources/images/icons/arrow-down.svg @@ -1 +1,3 @@ - \ No newline at end of file + + + diff --git a/frontend/resources/images/icons/arrow-end.svg b/frontend/resources/images/icons/arrow-end.svg index fe82545d62..a025ca2c55 100644 --- a/frontend/resources/images/icons/arrow-end.svg +++ b/frontend/resources/images/icons/arrow-end.svg @@ -1 +1,3 @@ - \ No newline at end of file + + + diff --git a/frontend/resources/images/icons/arrow-slide.svg b/frontend/resources/images/icons/arrow-slide.svg index 3f4c3f96c3..754e3e97f0 100644 --- a/frontend/resources/images/icons/arrow-slide.svg +++ b/frontend/resources/images/icons/arrow-slide.svg @@ -1 +1,3 @@ - \ No newline at end of file + + + diff --git a/frontend/resources/images/icons/artboard.svg b/frontend/resources/images/icons/artboard.svg index c913973004..87f4543a3f 100644 --- a/frontend/resources/images/icons/artboard.svg +++ b/frontend/resources/images/icons/artboard.svg @@ -1,3 +1,3 @@ - - + + diff --git a/frontend/resources/images/icons/box.svg b/frontend/resources/images/icons/box.svg index cbed171e63..d8ef1bffe9 100644 --- a/frontend/resources/images/icons/box.svg +++ b/frontend/resources/images/icons/box.svg @@ -1 +1,3 @@ - \ No newline at end of file + + + diff --git a/frontend/resources/images/icons/chain.svg b/frontend/resources/images/icons/chain.svg index a34db111ec..661d8db6b9 100644 --- a/frontend/resources/images/icons/chain.svg +++ b/frontend/resources/images/icons/chain.svg @@ -1 +1,3 @@ - + + + diff --git a/frontend/resources/images/icons/code.svg b/frontend/resources/images/icons/code.svg index d8ae15c9ef..3c80441e35 100644 --- a/frontend/resources/images/icons/code.svg +++ b/frontend/resources/images/icons/code.svg @@ -1,3 +1,3 @@ - - + + diff --git a/frontend/resources/images/icons/component.svg b/frontend/resources/images/icons/component.svg index 2042881f9b..2777568a41 100644 --- a/frontend/resources/images/icons/component.svg +++ b/frontend/resources/images/icons/component.svg @@ -1,3 +1,3 @@ - - + + diff --git a/frontend/resources/images/icons/copy.svg b/frontend/resources/images/icons/copy.svg index 8a65c91c70..d676e2227e 100644 --- a/frontend/resources/images/icons/copy.svg +++ b/frontend/resources/images/icons/copy.svg @@ -1,4 +1,3 @@ - - - + + diff --git a/frontend/resources/images/icons/exit.svg b/frontend/resources/images/icons/exit.svg index 403b4c6754..ce145a3853 100644 --- a/frontend/resources/images/icons/exit.svg +++ b/frontend/resources/images/icons/exit.svg @@ -1 +1,3 @@ - \ No newline at end of file + + + diff --git a/frontend/resources/images/icons/file-html.svg b/frontend/resources/images/icons/file-html.svg index 9427f31d2a..91a9e6c425 100644 --- a/frontend/resources/images/icons/file-html.svg +++ b/frontend/resources/images/icons/file-html.svg @@ -1 +1,3 @@ - \ No newline at end of file + + + diff --git a/frontend/resources/images/icons/file-svg.svg b/frontend/resources/images/icons/file-svg.svg index 5cab59e987..33e5830de6 100644 --- a/frontend/resources/images/icons/file-svg.svg +++ b/frontend/resources/images/icons/file-svg.svg @@ -1 +1,3 @@ - \ No newline at end of file + + + diff --git a/frontend/resources/images/icons/fill.svg b/frontend/resources/images/icons/fill.svg index 7593f603f2..a509069cc6 100644 --- a/frontend/resources/images/icons/fill.svg +++ b/frontend/resources/images/icons/fill.svg @@ -1 +1,3 @@ - + + + diff --git a/frontend/resources/images/icons/folder-zip.svg b/frontend/resources/images/icons/folder-zip.svg index 37e404e8bd..a07dbcb808 100644 --- a/frontend/resources/images/icons/folder-zip.svg +++ b/frontend/resources/images/icons/folder-zip.svg @@ -1 +1,3 @@ - \ No newline at end of file + + + diff --git a/frontend/resources/images/icons/folder.svg b/frontend/resources/images/icons/folder.svg index 64899804d5..cd7c2a4ec8 100644 --- a/frontend/resources/images/icons/folder.svg +++ b/frontend/resources/images/icons/folder.svg @@ -1 +1,3 @@ - \ No newline at end of file + + + diff --git a/frontend/resources/images/icons/grid-snap.svg b/frontend/resources/images/icons/grid-snap.svg index 14fb513c3e..2f03be05ab 100644 --- a/frontend/resources/images/icons/grid-snap.svg +++ b/frontend/resources/images/icons/grid-snap.svg @@ -1 +1,3 @@ - \ No newline at end of file + + + diff --git a/frontend/resources/images/icons/grid.svg b/frontend/resources/images/icons/grid.svg index a42d22149e..be57664c1c 100644 --- a/frontend/resources/images/icons/grid.svg +++ b/frontend/resources/images/icons/grid.svg @@ -1 +1,3 @@ - \ No newline at end of file + + + diff --git a/frontend/resources/images/icons/html.svg b/frontend/resources/images/icons/html.svg index 09616a9baa..a2dd0494ee 100644 --- a/frontend/resources/images/icons/html.svg +++ b/frontend/resources/images/icons/html.svg @@ -1 +1,3 @@ - \ No newline at end of file + + + diff --git a/frontend/resources/images/icons/icon-grid.svg b/frontend/resources/images/icons/icon-grid.svg deleted file mode 100644 index 4c1eafbdff..0000000000 --- a/frontend/resources/images/icons/icon-grid.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend/resources/images/icons/image.svg b/frontend/resources/images/icons/image.svg index dda84b5459..1d3f13f09e 100644 --- a/frontend/resources/images/icons/image.svg +++ b/frontend/resources/images/icons/image.svg @@ -1 +1,3 @@ - \ No newline at end of file + + + diff --git a/frontend/resources/images/icons/import.svg b/frontend/resources/images/icons/import.svg new file mode 100644 index 0000000000..4e0914b057 --- /dev/null +++ b/frontend/resources/images/icons/import.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/images/icons/interaction.svg b/frontend/resources/images/icons/interaction.svg index afc359d843..d95baa8be0 100644 --- a/frontend/resources/images/icons/interaction.svg +++ b/frontend/resources/images/icons/interaction.svg @@ -1,5 +1,3 @@ - - - - + + diff --git a/frontend/resources/images/icons/layers.svg b/frontend/resources/images/icons/layers.svg index 3968ee0dbe..703fd0cbeb 100644 --- a/frontend/resources/images/icons/layers.svg +++ b/frontend/resources/images/icons/layers.svg @@ -1,4 +1,3 @@ - - - + + diff --git a/frontend/resources/images/icons/library.svg b/frontend/resources/images/icons/library.svg index 4b5457f7fc..c4be0d41ff 100644 --- a/frontend/resources/images/icons/library.svg +++ b/frontend/resources/images/icons/library.svg @@ -1,4 +1,3 @@ - - - + + diff --git a/frontend/resources/images/icons/listing-enum.svg b/frontend/resources/images/icons/listing-enum.svg new file mode 100644 index 0000000000..d979e50101 --- /dev/null +++ b/frontend/resources/images/icons/listing-enum.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/images/icons/listing-thumbs.svg b/frontend/resources/images/icons/listing-thumbs.svg new file mode 100644 index 0000000000..ac5d98e47a --- /dev/null +++ b/frontend/resources/images/icons/listing-thumbs.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/images/icons/lock-open.svg b/frontend/resources/images/icons/lock-open.svg deleted file mode 100644 index ed703a641c..0000000000 --- a/frontend/resources/images/icons/lock-open.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/frontend/resources/images/icons/lock.svg b/frontend/resources/images/icons/lock.svg index cbb4a4ec9d..6477336aeb 100644 --- a/frontend/resources/images/icons/lock.svg +++ b/frontend/resources/images/icons/lock.svg @@ -1 +1,3 @@ - \ No newline at end of file + + + diff --git a/frontend/resources/images/icons/mail.svg b/frontend/resources/images/icons/mail.svg index e40e68ed38..0d03f891d1 100644 --- a/frontend/resources/images/icons/mail.svg +++ b/frontend/resources/images/icons/mail.svg @@ -1 +1,3 @@ - \ No newline at end of file + + + diff --git a/frontend/resources/images/icons/minus.svg b/frontend/resources/images/icons/minus.svg index c92d33dd59..d79c63c514 100644 --- a/frontend/resources/images/icons/minus.svg +++ b/frontend/resources/images/icons/minus.svg @@ -1 +1,3 @@ - \ No newline at end of file + + + diff --git a/frontend/resources/images/icons/move.svg b/frontend/resources/images/icons/move.svg deleted file mode 100644 index dcb4e329e3..0000000000 --- a/frontend/resources/images/icons/move.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/resources/images/icons/multi-canvas.svg b/frontend/resources/images/icons/multi-canvas.svg index 8545ec76ac..94025eee1f 100644 --- a/frontend/resources/images/icons/multi-canvas.svg +++ b/frontend/resources/images/icons/multi-canvas.svg @@ -1 +1,3 @@ - \ No newline at end of file + + + diff --git a/frontend/resources/images/icons/navigate.svg b/frontend/resources/images/icons/navigate.svg index 154f70535e..1b13012e4b 100644 --- a/frontend/resources/images/icons/navigate.svg +++ b/frontend/resources/images/icons/navigate.svg @@ -1 +1,3 @@ - + + + diff --git a/frontend/resources/images/icons/options.svg b/frontend/resources/images/icons/options.svg deleted file mode 100644 index 153440bdeb..0000000000 --- a/frontend/resources/images/icons/options.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend/resources/images/icons/palette.svg b/frontend/resources/images/icons/palette.svg index 80ab784018..4984a76b1d 100644 --- a/frontend/resources/images/icons/palette.svg +++ b/frontend/resources/images/icons/palette.svg @@ -1,4 +1,3 @@ - - - + + diff --git a/frontend/resources/images/icons/picker-harmony.svg b/frontend/resources/images/icons/picker-harmony.svg index c108e28127..1f8de0d5c2 100644 --- a/frontend/resources/images/icons/picker-harmony.svg +++ b/frontend/resources/images/icons/picker-harmony.svg @@ -1,2 +1,3 @@ - + + diff --git a/frontend/resources/images/icons/pin-fill.svg b/frontend/resources/images/icons/pin-fill.svg new file mode 100644 index 0000000000..1e11c8c45f --- /dev/null +++ b/frontend/resources/images/icons/pin-fill.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/images/icons/pin.svg b/frontend/resources/images/icons/pin.svg index 4387917436..46d71436a3 100644 --- a/frontend/resources/images/icons/pin.svg +++ b/frontend/resources/images/icons/pin.svg @@ -1 +1,3 @@ - \ No newline at end of file + + + diff --git a/frontend/resources/images/icons/plus.svg b/frontend/resources/images/icons/plus.svg index 716813537e..8368e0234d 100644 --- a/frontend/resources/images/icons/plus.svg +++ b/frontend/resources/images/icons/plus.svg @@ -1 +1,3 @@ - \ No newline at end of file + + + diff --git a/frontend/resources/images/icons/recent.svg b/frontend/resources/images/icons/recent.svg index 590de65173..696757240a 100644 --- a/frontend/resources/images/icons/recent.svg +++ b/frontend/resources/images/icons/recent.svg @@ -1,3 +1,3 @@ - - + + diff --git a/frontend/resources/images/icons/ruler-tool.svg b/frontend/resources/images/icons/ruler-tool.svg index b0ae77a902..0d18c21a50 100644 --- a/frontend/resources/images/icons/ruler-tool.svg +++ b/frontend/resources/images/icons/ruler-tool.svg @@ -1 +1,3 @@ - \ No newline at end of file + + + diff --git a/frontend/resources/images/icons/sort-ascending.svg b/frontend/resources/images/icons/sort-ascending.svg new file mode 100644 index 0000000000..48ed056f4a --- /dev/null +++ b/frontend/resources/images/icons/sort-ascending.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/images/icons/sort-descending.svg b/frontend/resources/images/icons/sort-descending.svg new file mode 100644 index 0000000000..3a5838b956 --- /dev/null +++ b/frontend/resources/images/icons/sort-descending.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/images/icons/sublevel.svg b/frontend/resources/images/icons/sublevel.svg deleted file mode 100644 index da078b4996..0000000000 --- a/frontend/resources/images/icons/sublevel.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend/resources/images/icons/tick.svg b/frontend/resources/images/icons/tick.svg index f92e28e87b..93e3f520bf 100644 --- a/frontend/resources/images/icons/tick.svg +++ b/frontend/resources/images/icons/tick.svg @@ -1,3 +1,3 @@ - - + + diff --git a/frontend/resources/images/icons/toggle.svg b/frontend/resources/images/icons/toggle.svg index 992c33882a..732c7042c3 100644 --- a/frontend/resources/images/icons/toggle.svg +++ b/frontend/resources/images/icons/toggle.svg @@ -1 +1,3 @@ - \ No newline at end of file + + + diff --git a/frontend/resources/images/icons/tree.svg b/frontend/resources/images/icons/tree.svg index cdb26bd89d..ddfffe3e7a 100644 --- a/frontend/resources/images/icons/tree.svg +++ b/frontend/resources/images/icons/tree.svg @@ -1 +1,3 @@ - \ No newline at end of file + + + diff --git a/frontend/resources/images/icons/unchain.svg b/frontend/resources/images/icons/unchain.svg index 5727cc3801..9115608895 100644 --- a/frontend/resources/images/icons/unchain.svg +++ b/frontend/resources/images/icons/unchain.svg @@ -1,7 +1,3 @@ - - - - - - + + diff --git a/frontend/resources/images/icons/undo-history.svg b/frontend/resources/images/icons/undo-history.svg deleted file mode 100644 index 42a4cc1c7b..0000000000 --- a/frontend/resources/images/icons/undo-history.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/frontend/resources/images/icons/ungroup.svg b/frontend/resources/images/icons/ungroup.svg index 6017f0ecdc..5afc3a40e7 100644 --- a/frontend/resources/images/icons/ungroup.svg +++ b/frontend/resources/images/icons/ungroup.svg @@ -1 +1,3 @@ - \ No newline at end of file + + + diff --git a/frontend/resources/images/icons/unlock.svg b/frontend/resources/images/icons/unlock.svg index dd2954b082..b9c1ffa83a 100644 --- a/frontend/resources/images/icons/unlock.svg +++ b/frontend/resources/images/icons/unlock.svg @@ -1 +1,3 @@ - \ No newline at end of file + + + diff --git a/frontend/resources/images/icons/user.svg b/frontend/resources/images/icons/user.svg index eac608035e..228a69876d 100644 --- a/frontend/resources/images/icons/user.svg +++ b/frontend/resources/images/icons/user.svg @@ -1 +1,3 @@ - \ No newline at end of file + + + diff --git a/frontend/resources/locales.json b/frontend/resources/locales.json deleted file mode 100644 index 71fa1f3052..0000000000 --- a/frontend/resources/locales.json +++ /dev/null @@ -1,6598 +0,0 @@ -{ - "auth.already-have-account" : { - "translations" : { - "ca" : "Ja tens un compte?", - "de" : "Sie haben schon ein Konto?", - "en" : "Already have an account?", - "es" : "¿Tienes ya una cuenta?", - "fr" : "Vous avez déjà un compte ?", - "ru" : "Уже есть аккаунт?", - "tr" : "Zaten hesabın var mı?", - "zh_cn" : "已经有账号了?" - }, - "used-in" : [ "src/app/main/ui/auth/register.cljs" ] - }, - "auth.check-your-email" : { - "translations" : { - "ca" : "Revisa el teu email i fes click al link per verificar i començar a utilitzar Penpot.", - "de" : "Überprüfen Sie Ihre E-Mail, klicken Sie auf den Link um sich zu verifizieren und Penpot zu nutzen.", - "en" : "Check your email and click on the link to verify and start using Penpot.", - "fr" : "Vérifiez votre e‑mail et cliquez sur le lien pour vérifier et commencer à utiliser Penpot.", - "tr" : "Penpot hesabını onaylamak ve kullanmaya başlamak için e-postanı kontrol et ve gönderilen bağlantıya tıkla.", - "zh_cn" : "请检查电子邮箱,点击邮件中的超链接来验证,然后开始使用Penpot。" - }, - "used-in" : [ "src/app/main/ui/auth/register.cljs" ] - }, - "auth.confirm-password" : { - "translations" : { - "ca" : "Confirmar contrasenya", - "de" : "Passwort bestätigen", - "en" : "Confirm password", - "es" : "Confirmar contraseña", - "fr" : "Confirmez le mot de passe", - "ru" : "Подтвердите пароль", - "tr" : "Parolayı onayla", - "zh_cn" : "确认密码" - }, - "used-in" : [ "src/app/main/ui/auth/recovery.cljs" ] - }, - "auth.create-demo-account" : { - "translations" : { - "ca" : "Crea un compte de proba", - "de" : "Ein Demo-Konto erstellen", - "en" : "Create demo account", - "es" : "Crear cuenta de prueba", - "fr" : "Créer un compte de démonstration", - "ru" : "Хотите попробовать?", - "tr" : "Demo hesabı oluştur", - "zh_cn" : "创建演示账号" - }, - "used-in" : [ "src/app/main/ui/auth/register.cljs", "src/app/main/ui/auth/login.cljs" ] - }, - "auth.create-demo-profile" : { - "translations" : { - "ca" : "Vols probar-ho?", - "de" : "Einfach testen?", - "en" : "Just wanna try it?", - "es" : "¿Quieres probar?", - "fr" : "Vous voulez juste essayer ?", - "ru" : "Хотите попробовать?", - "tr" : "Sadece denemek mi istiyorsun?", - "zh_cn" : "只是想试试?" - }, - "used-in" : [ "src/app/main/ui/auth/register.cljs", "src/app/main/ui/auth/login.cljs" ] - }, - "auth.demo-warning" : { - "translations" : { - "ca" : "Aquest es un servei de PROBA. NO HO UTILITZIS per feina real, els projectes seran esborrats periòdicament.", - "de" : "Das ist eine DEMO-VERSION, verwenden Sie es NICHT zum Arbeiten, die Projekte werden regelmäßig gelöscht.", - "en" : "This is a DEMO service, DO NOT USE for real work, the projects will be periodicaly wiped.", - "es" : "Este es un servicio de DEMOSTRACIÓN. NO USAR para trabajo real, los proyectos serán borrados periodicamente.", - "fr" : "Il s’agit d’un service DEMO, NE PAS UTILISER pour un travail réel, les projets seront périodiquement supprimés.", - "ru" : "Это ДЕМОНСТРАЦИЯ, НЕ ИСПОЛЬЗУЙТЕ для работы, проекты будут периодически удаляться.", - "tr" : "Bu bir DEMO servis, gerçek işleriniz için KULLANMAYIN, projeler periyodik olarak silinecektir.", - "zh_cn" : "这是一个演示服务,请【不要】用于真实工作,这些项目将被周期性地抹除。" - }, - "used-in" : [ "src/app/main/ui/auth/register.cljs" ] - }, - "auth.email" : { - "translations" : { - "ca" : "Correu electrònic", - "de" : "E-Mail", - "en" : "Email", - "es" : "Correo electrónico", - "fr" : "Adresse e‑mail", - "ru" : "Email", - "tr" : "E-posta", - "zh_cn" : "电子邮件" - }, - "used-in" : [ "src/app/main/ui/auth/register.cljs", "src/app/main/ui/auth/recovery_request.cljs", "src/app/main/ui/auth/login.cljs" ] - }, - "auth.forgot-password" : { - "translations" : { - "ca" : "Has oblidat la contrasenya?", - "de" : "Passwort vergessen?", - "en" : "Forgot password?", - "es" : "¿Olvidaste tu contraseña?", - "fr" : "Mot de passe oublié ?", - "ru" : "Забыли пароль?", - "tr" : "Parolanı mı unuttun?", - "zh_cn" : "忘记密码?" - }, - "used-in" : [ "src/app/main/ui/auth/login.cljs" ] - }, - "auth.fullname" : { - "translations" : { - "ca" : "Nom complet", - "de" : "Vollständiger Name", - "en" : "Full Name", - "es" : "Nombre completo", - "fr" : "Nom complet", - "ru" : "Полное имя", - "tr" : "Tam Adın", - "zh_cn" : "全名" - }, - "used-in" : [ "src/app/main/ui/auth/register.cljs" ] - }, - "auth.go-back-to-login" : { - "translations" : { - "ca" : "Tornar", - "de" : "Zurück!", - "en" : "Go back!", - "es" : "Volver", - "fr" : "Retour !", - "ru" : "Назад!", - "tr" : "Geri dön!", - "zh_cn" : "返回!" - }, - "used-in" : [ "src/app/main/ui/auth/recovery_request.cljs" ] - }, - "auth.login-here" : { - "translations" : { - "ca" : "Inicia sessió aquí", - "de" : "Hier einloggen", - "en" : "Login here", - "es" : "Entra aquí", - "fr" : "Se connecter ici", - "ru" : "Войти здесь", - "tr" : "Buradan giriş yap", - "zh_cn" : "在这里登录" - }, - "used-in" : [ "src/app/main/ui/auth/register.cljs" ] - }, - "auth.login-submit" : { - "translations" : { - "ca" : "Accedir", - "de" : "Anmelden", - "en" : "Sign in", - "es" : "Entrar", - "fr" : "Se connecter", - "ru" : "Вход", - "tr" : "Giriş yap", - "zh_cn" : "登录" - }, - "used-in" : [ "src/app/main/ui/auth/login.cljs" ] - }, - "auth.login-subtitle" : { - "translations" : { - "ca" : "Introdueix les teves dades aquí", - "de" : "Geben Sie unten Ihre Daten ein", - "en" : "Enter your details below", - "es" : "Introduce tus datos aquí", - "fr" : "Entrez vos informations ci‑dessous", - "ru" : "Введите информацию о себе ниже", - "tr" : "Bilgilerini aşağıdaki alana gir", - "zh_cn" : "请在下面输入你的详细信息" - }, - "used-in" : [ "src/app/main/ui/auth/login.cljs" ] - }, - "auth.login-title" : { - "translations" : { - "ca" : "Encantats de tornar a veure't", - "de" : "Schön, Sie wiederzusehen!", - "en" : "Great to see you again!", - "es" : "Encantados de volverte a ver", - "fr" : "Ravi de vous revoir !", - "ru" : "Рады видеть Вас снова!", - "tr" : "Seni tekrar görmek süper!", - "zh_cn" : "很高兴又见到你!" - }, - "used-in" : [ "src/app/main/ui/auth/login.cljs" ] - }, - "auth.login-with-github-submit" : { - "translations" : { - "ca" : "Accedir amb Github", - "de" : "Einloggen mit Github", - "en" : "Login with Github", - "es" : "Entrar con Github", - "fr" : "Se connecter via Github", - "ru" : "Вход через Gitnub", - "tr" : "Github ile giriş yap", - "zh_cn" : "使用Github登录" - }, - "used-in" : [ "src/app/main/ui/auth/register.cljs", "src/app/main/ui/auth/login.cljs" ] - }, - "auth.login-with-gitlab-submit" : { - "translations" : { - "ca" : "Accedir amb Gitlab", - "de" : "Einloggen mit Gitlab", - "en" : "Login with Gitlab", - "es" : "Entrar con Gitlab", - "fr" : "Se connecter via Gitlab", - "ru" : "Вход через Gitlab", - "tr" : "Gitlab ile giriş yap", - "zh_cn" : "使用Gitlab登录" - }, - "used-in" : [ "src/app/main/ui/auth/register.cljs", "src/app/main/ui/auth/login.cljs" ] - }, - "auth.login-with-ldap-submit" : { - "translations" : { - "ca" : "Accedir amb LDAP", - "de" : "Anmelden mit LDAP", - "en" : "Sign in with LDAP", - "es" : "Entrar con LDAP", - "fr" : "Se connecter via LDAP", - "ru" : "Вход через LDAP", - "tr" : "LDAP ile giriş yap", - "zh_cn" : "使用LDAP登录" - }, - "used-in" : [ "src/app/main/ui/auth/login.cljs" ] - }, - "auth.new-password" : { - "translations" : { - "ca" : "Introdueix la nova contrasenya", - "de" : "Geben Sie ein neues Passwort ein", - "en" : "Type a new password", - "es" : "Introduce la nueva contraseña", - "fr" : "Saisissez un nouveau mot de passe", - "ru" : "Введите новый пароль", - "tr" : "Yeni bir parola gir", - "zh_cn" : "输入新的密码" - }, - "used-in" : [ "src/app/main/ui/auth/recovery.cljs" ] - }, - "auth.notifications.invalid-token-error" : { - "translations" : { - "ca" : "El codi de recuperació no és vàlid", - "de" : "Der Wiederherstellungscode ist ungültig.", - "en" : "The recovery token is invalid.", - "es" : "El código de recuperación no es válido.", - "fr" : "Le code de récupération n’est pas valide.", - "ru" : "Неверный код восстановления.", - "tr" : "Kurtarma bağlantısı geçerli değil", - "zh_cn" : "恢复令牌无效。" - }, - "used-in" : [ "src/app/main/ui/auth/recovery.cljs" ] - }, - "auth.notifications.password-changed-succesfully" : { - "translations" : { - "ca" : "La contrasenya s'ha canviat correctament", - "de" : "Passwort erfolgreich geändert", - "en" : "Password successfully changed", - "es" : "La contraseña ha sido cambiada", - "fr" : "Mot de passe changé avec succès", - "ru" : "Пароль изменен успешно", - "tr" : "Parola başarıyla değiştirldi", - "zh_cn" : "密码修改成功" - }, - "used-in" : [ "src/app/main/ui/auth/recovery.cljs" ] - }, - "auth.notifications.profile-not-verified" : { - "translations" : { - "ca" : "El perfil encara no s'ha verificat, si us plau verifica-ho abans de continuar.", - "de" : "Profil ist nicht verifiziert. Bevor Sie fortfahren, verifizieren Sie bitte das Profil.", - "en" : "Profile is not verified, please verify profile before continue.", - "es" : "El perfil aun no ha sido verificado, por favor valida el perfil antes de continuar.", - "fr" : "Le profil n’est pas vérifié. Veuillez vérifier le profil avant de continuer.", - "tr" : "Profil onaylanmamış, devam etmeden önce profili onaylayın.", - "zh_cn" : "个人资料未验证,请于验证后继续。" - }, - "used-in" : [ "src/app/main/ui/auth/recovery_request.cljs" ] - }, - "auth.notifications.recovery-token-sent" : { - "translations" : { - "ca" : "Hem enviat un link de recuperació de contrasenya al teu email.", - "de" : "Der Link zur Wiederherstellung des Passworts wurde an Ihre E-Mail gesendet.", - "en" : "Password recovery link sent to your inbox.", - "es" : "Hemos enviado a tu buzón un enlace para recuperar tu contraseña.", - "fr" : "Lien de récupération de mot de passe envoyé.", - "ru" : "Ссылка для восстановления пароля отправлена на почту.", - "tr" : "Parola kurtarma bağlantısı e-posta kutuna gönderildi.", - "zh_cn" : "找回密码链接已发至你的收件箱。" - }, - "used-in" : [ "src/app/main/ui/auth/recovery_request.cljs" ] - }, - "auth.notifications.team-invitation-accepted" : { - "translations" : { - "ca" : "T'has unit al equip", - "de" : "Sie sind dem Team beigetreten", - "en" : "Joined the team succesfully", - "es" : "Te uniste al equipo", - "fr" : "Vous avez rejoint l’équipe avec succès", - "tr" : "Takıma başarıyla katıldın", - "zh_cn" : "成功加入团队" - }, - "used-in" : [ "src/app/main/ui/auth/verify_token.cljs" ] - }, - "auth.password" : { - "translations" : { - "ca" : "Contrasenya", - "de" : "Passwort", - "en" : "Password", - "es" : "Contraseña", - "fr" : "Mot de passe", - "ru" : "Пароль", - "tr" : "Parola", - "zh_cn" : "密码" - }, - "used-in" : [ "src/app/main/ui/auth/register.cljs", "src/app/main/ui/auth/login.cljs" ] - }, - "auth.password-length-hint" : { - "translations" : { - "ca" : "Com a mínim 8 caràcters", - "de" : "Mindestens 8 Zeichen", - "en" : "At least 8 characters", - "es" : "8 caracteres como mínimo", - "fr" : "Au moins 8 caractères", - "ru" : "Минимум 8 символов", - "tr" : "En az 8 karakter", - "zh_cn" : "至少8位字符" - }, - "used-in" : [ "src/app/main/ui/auth/register.cljs" ] - }, - "auth.recovery-request-submit" : { - "translations" : { - "ca" : "Recuperar contrasenya", - "de" : "Passwort wiederherstellen", - "en" : "Recover Password", - "es" : "Recuperar contraseña", - "fr" : "Récupérer le mot de passe", - "ru" : "Восстановить пароль", - "tr" : "Parolayı kurtar", - "zh_cn" : "找回密码" - }, - "used-in" : [ "src/app/main/ui/auth/recovery_request.cljs" ] - }, - "auth.recovery-request-subtitle" : { - "translations" : { - "ca" : "T'enviarem un correu electrónic amb instruccions", - "de" : "Wir senden Ihnen eine E-Mail mit Anweisungen zu", - "en" : "We'll send you an email with instructions", - "es" : "Te enviaremos un correo electrónico con instrucciones", - "fr" : "Nous vous enverrons un e‑mail avec des instructions", - "ru" : "Письмо с инструкциями отправлено на почту.", - "tr" : "Detayları sana e-posta ile göndereceğiz", - "zh_cn" : "我们将给你发送一封带有说明的电子邮件" - }, - "used-in" : [ "src/app/main/ui/auth/recovery_request.cljs" ] - }, - "auth.recovery-request-title" : { - "translations" : { - "ca" : "Has oblidat la teva contrasenya?", - "de" : "Passwort vergessen?", - "en" : "Forgot password?", - "es" : "¿Olvidaste tu contraseña?", - "fr" : "Mot de passe oublié ?", - "ru" : "Забыли пароль?", - "tr" : "Parolanı mı unuttun?", - "zh_cn" : "忘记密码?" - }, - "used-in" : [ "src/app/main/ui/auth/recovery_request.cljs" ] - }, - "auth.recovery-submit" : { - "translations" : { - "ca" : "Canvia la teva contrasenya", - "de" : "Passwort ändern", - "en" : "Change your password", - "es" : "Cambiar tu contraseña", - "fr" : "Changez votre mot de passe", - "ru" : "Изменить пароль", - "tr" : "Parolanı değiştir", - "zh_cn" : "修改密码" - }, - "used-in" : [ "src/app/main/ui/auth/recovery.cljs" ] - }, - "auth.register" : { - "translations" : { - "ca" : "Encara no tens compte?", - "de" : "Noch kein Konto?", - "en" : "No account yet?", - "es" : "¿No tienes una cuenta?", - "fr" : "Pas encore de compte ?", - "ru" : "Еще нет аккаунта?", - "tr" : "Henüz hesabın yok mu?", - "zh_cn" : "现在还没有账号?" - }, - "used-in" : [ "src/app/main/ui/auth/login.cljs" ] - }, - "auth.register-submit" : { - "translations" : { - "ca" : "Crea un compte", - "de" : "Konto erstellen", - "en" : "Create an account", - "es" : "Crear una cuenta", - "fr" : "Créer un compte", - "ru" : "Создать аккаунт", - "tr" : "Bir hesap oluştur", - "zh_cn" : "创建账号" - }, - "used-in" : [ "src/app/main/ui/auth/register.cljs", "src/app/main/ui/auth/login.cljs" ] - }, - "auth.register-subtitle" : { - "translations" : { - "ca" : "Es gratuit, es Open Source", - "de" : "Kostenlos, es ist Open Source", - "en" : "It's free, it's Open Source", - "es" : "Es gratis, es Open Source", - "fr" : "C’est gratuit, c’est Open Source", - "ru" : "Это бесплатно, это Open Source", - "tr" : "Ücretsiz ve Açık Kaynak", - "zh_cn" : "它免费,它开源" - }, - "used-in" : [ "src/app/main/ui/auth/register.cljs" ] - }, - "auth.register-title" : { - "translations" : { - "ca" : "Crea un compte", - "de" : "Konto erstellen", - "en" : "Create an account", - "es" : "Crear una cuenta", - "fr" : "Créer un compte", - "ru" : "Создать аккаунт", - "tr" : "Bir hesap oluştur", - "zh_cn" : "创建账号" - }, - "used-in" : [ "src/app/main/ui/auth/register.cljs" ] - }, - "auth.sidebar-tagline" : { - "translations" : { - "ca" : "La solució de codi obert per disenyar i prototipar", - "de" : "Die Open-Source-Lösung für Design und Prototyping.", - "en" : "The open-source solution for design and prototyping.", - "es" : "La solución de código abierto para diseñar y prototipar", - "fr" : "La solution Open Source pour la conception et le prototypage.", - "ru" : "Open Source решение для дизайна и прототипирования.", - "tr" : "Tasarım ve prototipleme için açık-kaynak çözüm.", - "zh_cn" : "设计与原型的开源解决方案" - }, - "used-in" : [ "src/app/main/ui/auth.cljs" ] - }, - "auth.terms-privacy-agreement" : { - "translations" : { - "en" : "When creating a new account, you agree to our terms of service and privacy policy.", - "es" : "Al crear una nueva cuenta, aceptas nuestros términos de servicio y política de privacidad.", - "tr" : "Bir hesap oluştururken, koşullarımızı ve gizlilik politikamızı kabul etmiş sayılırsınız." - }, - "used-in" : [ "src/app/main/ui/auth/register.cljs" ] - }, - "auth.verification-email-sent" : { - "translations" : { - "ca" : "Em enviat un correu de verificació a", - "de" : "Wir haben eine Bestätigungs-E-Mail gesendet an", - "en" : "We've sent a verification email to", - "fr" : "Nous avons envoyé un e-mail de vérification à", - "tr" : "Onay e-postanı şu adrese gönderdik", - "zh_cn" : "我们已经发送了一封验证邮件到" - }, - "used-in" : [ "src/app/main/ui/auth/register.cljs" ] - }, - "dashboard.add-shared" : { - "translations" : { - "ca" : "Afegeix una Biblioteca Compartida", - "de" : "Hinzufügen als gemeinsam genutzte Bibliothek", - "en" : "Add as Shared Library", - "es" : "Añadir como Biblioteca Compartida", - "fr" : "Ajouter une Bibliothèque Partagée", - "ru" : "", - "tr" : "Paylaşılan Kitaplık olarak ekle", - "zh_cn" : "添加为共享库" - }, - "used-in" : [ "src/app/main/ui/workspace/header.cljs", "src/app/main/ui/dashboard/file_menu.cljs" ] - }, - "dashboard.change-email" : { - "translations" : { - "ca" : "Canviar correu", - "de" : "E-Mail-Adresse ändern", - "en" : "Change email", - "es" : "Cambiar correo", - "fr" : "Changer adresse e‑mail", - "ru" : "Сменить email адрес", - "tr" : "E-posta adresini değiştir", - "zh_cn" : "修改电子邮件" - }, - "used-in" : [ "src/app/main/ui/settings/profile.cljs" ] - }, - "dashboard.copy-suffix" : { - "translations" : { - "de" : "(Kopie)", - "en" : "(copy)", - "es" : "(copia)", - "tr" : "(kopya)" - }, - "used-in" : [ "src/app/main/data/dashboard.cljs", "src/app/main/data/dashboard.cljs" ] - }, - "dashboard.create-new-team" : { - "translations" : { - "ca" : "+ Crear un nou equip", - "de" : "+ Neues Team erstellen", - "en" : "+ Create new team", - "es" : "+ Crear nuevo equipo", - "fr" : "+ Créer nouvelle équipe", - "tr" : "Yeni takım oluştur", - "zh_cn" : "+ 创建新团队" - }, - "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs" ] - }, - "dashboard.default-team-name" : { - "translations" : { - "ca" : "El teu Penpot", - "de" : "Ihr Penpot", - "en" : "Your Penpot", - "es" : "Tu Penpot", - "fr" : "Votre Penpot", - "tr" : "Penpot'un", - "zh_cn" : "你的Penpot" - }, - "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs" ] - }, - "dashboard.delete-team" : { - "translations" : { - "ca" : "Suprimir equip", - "de" : "Team löschen", - "en" : "Delete team", - "es" : "Eliminar equipo", - "fr" : "Supprimer l’équipe", - "tr" : "Takımı sil", - "zh_cn" : "删除团队" - }, - "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs" ] - }, - "dashboard.draft-title" : { - "translations" : { - "ca" : "Esborrany", - "de" : "Entwurf", - "en" : "Draft", - "es" : "Borrador", - "fr" : "Brouillon", - "ru" : "Черновик", - "tr" : "Taslak", - "zh_cn" : "草稿" - }, - "unused" : true - }, - "dashboard.duplicate" : { - "translations" : { - "de" : "Duplizieren", - "en" : "Duplicate", - "es" : "Duplicar", - "tr" : "Kopyasını oluştur" - }, - "used-in" : [ "src/app/main/ui/dashboard/project_menu.cljs", "src/app/main/ui/dashboard/file_menu.cljs" ] - }, - "dashboard.duplicate-multi" : { - "translations" : { - "en" : "Duplicate %s files", - "es" : "Duplicar %s archivos", - "tr" : "%s dosyanın kopyasını oluştur" - }, - "used-in" : [ "src/app/main/ui/dashboard/file_menu.cljs" ] - }, - "dashboard.empty-files" : { - "translations" : { - "ca" : "Encara no hi ha cap arxiu aquí", - "de" : "Sie haben hier noch keine Dateien", - "en" : "You still have no files here", - "es" : "Todavía no hay ningún archivo aquí", - "fr" : "Vous n’avez encore aucun fichier ici", - "ru" : "Файлов пока нет", - "tr" : "Burada hiç dosyan yok", - "zh_cn" : "暂无文档" - }, - "used-in" : [ "src/app/main/ui/dashboard/grid.cljs" ] - }, - "dashboard.invite-profile" : { - "translations" : { - "ca" : "Convidar a l'equip", - "de" : "Zum Team einladen", - "en" : "Invite to team", - "es" : "Invitar al equipo", - "fr" : "Inviter dans l’équipe", - "tr" : "Takıma davet et", - "zh_cn" : "邀请加入团队" - }, - "used-in" : [ "src/app/main/ui/dashboard/team.cljs" ] - }, - "dashboard.leave-team" : { - "translations" : { - "ca" : "Abandonar l'equip", - "de" : "Team verlassen", - "en" : "Leave team", - "es" : "Abandonar equipo", - "fr" : "Quitter l’équipe", - "tr" : "Takımdan ayrıl", - "zh_cn" : "退出团队" - }, - "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs", "src/app/main/ui/dashboard/sidebar.cljs" ] - }, - "dashboard.libraries-title" : { - "translations" : { - "ca" : "Biblioteques Compartides", - "de" : "Gemeinsam genutzte Bibliotheken", - "en" : "Shared Libraries", - "es" : "Bibliotecas Compartidas", - "fr" : "Bibliothèques Partagées", - "ru" : "", - "tr" : "Paylaşılan Kitaplıklar", - "zh_cn" : "共享库" - }, - "used-in" : [ "src/app/main/ui/dashboard/libraries.cljs" ] - }, - "dashboard.loading-files" : { - "translations" : { - "ca" : "carregan els teus fitxers", - "de" : "laden Ihrer Dateien …", - "en" : "loading your files …", - "es" : "cargando tus ficheros …", - "fr" : "chargement de vos fichiers…", - "tr" : "dosyalarınız yükleniyor …", - "zh_cn" : "正在加载文档…" - }, - "used-in" : [ "src/app/main/ui/dashboard/grid.cljs" ] - }, - "dashboard.move-to" : { - "translations" : { - "de" : "Verschieben nach", - "en" : "Move to", - "es" : "Mover a", - "tr" : "Şuraya taşı" - }, - "used-in" : [ "src/app/main/ui/dashboard/project_menu.cljs", "src/app/main/ui/dashboard/file_menu.cljs" ] - }, - "dashboard.move-to-multi" : { - "translations" : { - "en" : "Move %s files to", - "es" : "Mover %s archivos a", - "tr" : "%s dosyayı şuraya taşı" - }, - "used-in" : [ "src/app/main/ui/dashboard/file_menu.cljs" ] - }, - "dashboard.move-to-other-team" : { - "translations" : { - "de" : "Zu anderem Team verschieben", - "en" : "Move to other team", - "es" : "Mover a otro equipo", - "tr" : "Başkta takıma taşı" - }, - "used-in" : [ "src/app/main/ui/dashboard/file_menu.cljs" ] - }, - "dashboard.new-file" : { - "translations" : { - "ca" : "+ Nou Arxiu", - "de" : "+ Neue Datei", - "en" : "+ New File", - "es" : "+ Nuevo Archivo", - "fr" : "+ Nouveau fichier", - "ru" : "+ Новый файл", - "tr" : "Yeni Dosya", - "zh_cn" : "+ 新文档" - }, - "used-in" : [ "src/app/main/ui/dashboard/projects.cljs", "src/app/main/ui/dashboard/files.cljs" ] - }, - "dashboard.new-project" : { - "translations" : { - "ca" : "+ Nou projecte", - "de" : "+ Neues Projekt", - "en" : "+ New project", - "es" : "+ Nuevo proyecto", - "fr" : "+ Nouveau projet", - "ru" : "+ Новый проект", - "tr" : "Yeni Proje", - "zh_cn" : "+ 新项目" - }, - "used-in" : [ "src/app/main/ui/dashboard/projects.cljs" ] - }, - "dashboard.no-matches-for" : { - "translations" : { - "ca" : "No s'ha trobat cap coincidència amb “%s“", - "de" : "Keine Übereinstimmungen für “%s“ gefunden", - "en" : "No matches found for “%s“", - "es" : "No se encuentra “%s“", - "fr" : "Aucune correspondance pour « %s »", - "ru" : "Совпадений для “%s“ не найдено", - "tr" : "%s için hiç sonuç bulunamadı", - "zh_cn" : "没有找到“%s”的匹配项" - }, - "used-in" : [ "src/app/main/ui/dashboard/search.cljs" ] - }, - "dashboard.no-projects-placeholder" : { - "translations" : { - "ca" : "Els projectes fixats apareixeran aquí", - "de" : "Angeheftete Projekte werden hier angezeigt", - "en" : "Pinned projects will appear here", - "es" : "Los proyectos fijados aparecerán aquí", - "fr" : "Les projets épinglés apparaîtront ici", - "tr" : "Sabitlenmiş projeler burada görünür", - "zh_cn" : "被钉住的项目会显示在这儿" - }, - "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs" ] - }, - "dashboard.notifications.email-changed-successfully" : { - "translations" : { - "ca" : "La teva adreça de correu s'ha actualizat", - "de" : "Ihre E-Mail-Adresse wurde erfolgreich aktualisiert", - "en" : "Your email address has been updated successfully", - "es" : "Tu dirección de correo ha sido actualizada", - "fr" : "Votre adresse e‑mail a été mise à jour avec succès", - "ru" : "Ваш email адрес успешно обновлен", - "tr" : "E-posta adresiniz başarıyla güncellendi", - "zh_cn" : "已经成功更新你的电子邮件" - }, - "used-in" : [ "src/app/main/ui/auth/verify_token.cljs" ] - }, - "dashboard.notifications.email-verified-successfully" : { - "translations" : { - "ca" : "La teva adreça de correu ha sigut verificada", - "de" : "Ihre E-Mail-Adresse wurde erfolgreich verifiziert", - "en" : "Your email address has been verified successfully", - "es" : "Tu dirección de correo ha sido verificada", - "fr" : "Votre adresse e‑mail a été vérifiée avec succès", - "ru" : "Ваш email адрес успешно подтвержден", - "tr" : "E-posta adresin başarıyla doğrulandı", - "zh_cn" : "已经成功验证你的电子邮件" - }, - "used-in" : [ "src/app/main/ui/auth/verify_token.cljs" ] - }, - "dashboard.notifications.password-saved" : { - "translations" : { - "ca" : "La contrasenya s'ha desat correctament", - "de" : "Passwort wurde erfolgreich gespeichert!", - "en" : "Password saved successfully!", - "es" : "¡Contraseña guardada!", - "fr" : "Mot de passe enregistré avec succès !", - "ru" : "Пароль успешно сохранен!", - "tr" : "Parola kaydedildi", - "zh_cn" : "已经成功保存密码!" - }, - "used-in" : [ "src/app/main/ui/settings/password.cljs" ] - }, - "dashboard.num-of-members" : { - "translations" : { - "ca" : "%s membres", - "de" : "%s Mitglieder", - "en" : "%s members", - "es" : "%s integrantes", - "fr" : "%s membres", - "tr" : "%s üye", - "zh_cn" : "成员%s人" - }, - "used-in" : [ "src/app/main/ui/dashboard/team.cljs" ] - }, - "dashboard.open-in-new-tab" : { - "translations" : { - "de" : "Datei in neuem Tab öffnen", - "en" : "Open file in a new tab", - "es" : "Abrir en una pestaña nueva", - "tr" : "Dosyayı yeni sekmede aç" - }, - "used-in" : [ "src/app/main/ui/dashboard/file_menu.cljs" ] - }, - "dashboard.password-change" : { - "translations" : { - "ca" : "Canvia la contrasenya", - "de" : "Passwort ändern", - "en" : "Change password", - "es" : "Cambiar contraseña", - "fr" : "Changer le mot de passe", - "ru" : "Изменить пароль", - "tr" : "Parola değiştir", - "zh_cn" : "修改密码" - }, - "used-in" : [ "src/app/main/ui/settings/password.cljs" ] - }, - "dashboard.pin-unpin" : { - "translations" : { - "de" : "Anheften/Lösen", - "en" : "Pin/Unpin", - "es" : "Fijar/Desfijar", - "tr" : "Sabitle/Sabitleme" - }, - "used-in" : [ "src/app/main/ui/dashboard/project_menu.cljs" ] - }, - "dashboard.projects-title" : { - "translations" : { - "ca" : "Projectes", - "de" : "Projekte", - "en" : "Projects", - "es" : "Proyectos", - "fr" : "Projets", - "ru" : "Проекты", - "tr" : "Projeler", - "zh_cn" : "项目" - }, - "used-in" : [ "src/app/main/ui/dashboard/projects.cljs" ] - }, - "dashboard.promote-to-owner" : { - "translations" : { - "ca" : "Promoure a propietari", - "de" : "Zum Eigentümer befördern", - "en" : "Promote to owner", - "es" : "Promover a dueño", - "fr" : "Promouvoir propriétaire", - "tr" : "Sahibi olarak belirle", - "zh_cn" : "晋级为所有者" - }, - "used-in" : [ "src/app/main/ui/dashboard/team.cljs" ] - }, - "dashboard.remove-account" : { - "translations" : { - "ca" : "Vols esborrar el teu compte?", - "de" : "Möchten Sie Ihr Konto entfernen?", - "en" : "Want to remove your account?", - "es" : "¿Quieres borrar tu cuenta?", - "fr" : "Vous souhaitez supprimer votre compte ?", - "ru" : "Хотите удалить свой аккаунт?", - "tr" : "Hesabını silmek istediğinden emin misin?", - "zh_cn" : "希望注销您的账号?" - }, - "used-in" : [ "src/app/main/ui/settings/profile.cljs" ] - }, - "dashboard.remove-shared" : { - "translations" : { - "ca" : "Elimina com Biblioteca Compartida", - "de" : "Als gemeinsam genutzte Bibliothek entfernen", - "en" : "Remove as Shared Library", - "es" : "Eliminar como Biblioteca Compartida", - "fr" : "Retirer en tant que Bibliothèque Partagée", - "ru" : "", - "tr" : "Paylaşılan Kitaplık olarak sil", - "zh_cn" : "不再作为共享库" - }, - "used-in" : [ "src/app/main/ui/workspace/header.cljs", "src/app/main/ui/dashboard/file_menu.cljs" ] - }, - "dashboard.search-placeholder" : { - "translations" : { - "ca" : "Cerca…", - "de" : "Suchen…", - "en" : "Search…", - "es" : "Buscar…", - "fr" : "Rechercher…", - "ru" : "Поиск ", - "tr" : "Ara…", - "zh_cn" : "搜索…" - }, - "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs" ] - }, - "dashboard.searching-for" : { - "translations" : { - "ca" : "S'está cercant “%s“…", - "de" : "Suche nach “%s“…", - "en" : "Searching for “%s“…", - "es" : "Buscando “%s“…", - "fr" : "Recherche de « %s »…", - "ru" : "Ищу “%s“…", - "tr" : "%s aranıyor", - "zh_cn" : "正在搜索“%s”" - }, - "used-in" : [ "src/app/main/ui/dashboard/search.cljs" ] - }, - "dashboard.select-ui-language" : { - "translations" : { - "ca" : "Selecciona la llengua de la interfície", - "de" : "Sprache der Benutzeroberfläche auswählen", - "en" : "Select UI language", - "es" : "Cambiar el idioma de la interfaz", - "fr" : "Sélectionnez la langue de l’interface", - "ru" : "Выберите язык интерфейса", - "tr" : "Arayüz dilini seç", - "zh_cn" : "选择界面语言" - }, - "used-in" : [ "src/app/main/ui/settings/options.cljs" ] - }, - "dashboard.select-ui-theme" : { - "translations" : { - "ca" : "Selecciona un tema", - "de" : "Theme auswählen", - "en" : "Select theme", - "es" : "Selecciona un tema", - "fr" : "Sélectionnez un thème", - "ru" : "Выберите тему", - "tr" : "Tema seç", - "zh_cn" : "选择界面主题" - }, - "used-in" : [ "src/app/main/ui/settings/options.cljs" ] - }, - "dashboard.show-all-files" : { - "translations" : { - "ca" : "Veure tots els fitxers", - "de" : "Alle Dateien anzeigen", - "en" : "Show all files", - "es" : "Ver todos los ficheros", - "fr" : "Voir tous les fichiers", - "tr" : "Tüm dosyaları göster", - "zh_cn" : "显示全部文档" - }, - "used-in" : [ "src/app/main/ui/dashboard/grid.cljs" ] - }, - "dashboard.success-delete-file" : { - "translations" : { - "de" : "Ihre Datei wurde erfolgreich gelöscht", - "en" : "Your file has been deleted successfully", - "es" : "Tu archivo ha sido borrado con éxito", - "tr" : "Dosyan başarıyla silindi" - }, - "used-in" : [ "src/app/main/ui/dashboard/file_menu.cljs" ] - }, - "dashboard.success-delete-project" : { - "translations" : { - "de" : "Ihr Projekt wurde erfolgreich gelöscht", - "en" : "Your project has been deleted successfully", - "es" : "Tu proyecto ha sido borrado con éxito", - "tr" : "Projen başarıyla silindi" - }, - "used-in" : [ "src/app/main/ui/dashboard/project_menu.cljs" ] - }, - "dashboard.success-duplicate-file" : { - "translations" : { - "de" : "Ihre Datei wurde erfolgreich dupliziert", - "en" : "Your file has been duplicated successfully", - "es" : "Tu archivo ha sido duplicado con éxito", - "tr" : "Dosyanın kopyası başarıyla oluşturuldu" - }, - "used-in" : [ "src/app/main/ui/dashboard/file_menu.cljs" ] - }, - "dashboard.success-duplicate-project" : { - "translations" : { - "de" : "Ihr Projekt wurde erfolgreich dupliziert", - "en" : "Your project has been duplicated successfully", - "es" : "Tu proyecto ha sido duplicado con éxito", - "tr" : "Projenin kopyası başarıyla oluşturuldu" - }, - "used-in" : [ "src/app/main/ui/dashboard/project_menu.cljs" ] - }, - "dashboard.success-move-file" : { - "translations" : { - "de" : "Ihre Datei wurde erfolgreich verschoben", - "en" : "Your file has been moved successfully", - "es" : "Tu archivo ha sido movido con éxito", - "tr" : "Dosyan başarıyla taşındı" - }, - "used-in" : [ "src/app/main/ui/dashboard/grid.cljs", "src/app/main/ui/dashboard/sidebar.cljs", "src/app/main/ui/dashboard/file_menu.cljs" ] - }, - "dashboard.success-move-files" : { - "translations" : { - "en" : "Your files has been moved successfully", - "es" : "Tus archivos han sido movidos con éxito", - "tr" : "Dosyaların başarıyla taşındı" - }, - "used-in" : [ "src/app/main/ui/dashboard/file_menu.cljs" ] - }, - "dashboard.success-move-project" : { - "translations" : { - "de" : "Ihr Projekt wurde erfolgreich verschoben", - "en" : "Your project has been moved successfully", - "es" : "Tu proyecto ha sido movido con éxito", - "tr" : "Projen başarıyla taşındı" - }, - "used-in" : [ "src/app/main/ui/dashboard/project_menu.cljs" ] - }, - "dashboard.switch-team" : { - "translations" : { - "ca" : "Cambiar d'equip", - "de" : "Team wechseln", - "en" : "Switch team", - "es" : "Cambiar equipo", - "fr" : "Changer d’équipe", - "tr" : "Takım değiştir", - "zh_cn" : "切换团队" - }, - "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs" ] - }, - "dashboard.team-info" : { - "translations" : { - "ca" : "Informació de l'equip", - "de" : "Teaminformationen", - "en" : "Team info", - "es" : "Información del equipo", - "fr" : "Information de l’équipe", - "tr" : "Takım bilgisi", - "zh_cn" : "团队信息" - }, - "used-in" : [ "src/app/main/ui/dashboard/team.cljs" ] - }, - "dashboard.team-members" : { - "translations" : { - "ca" : "Membres de l'equip", - "de" : "Teammitglieder", - "en" : "Team members", - "es" : "Integrantes del equipo", - "fr" : "Membres de l’équipe", - "tr" : "Takım üyeleri", - "zh_cn" : "团队成员" - }, - "used-in" : [ "src/app/main/ui/dashboard/team.cljs" ] - }, - "dashboard.team-projects" : { - "translations" : { - "ca" : "Projectes de l'equip", - "de" : "Teamprojekte", - "en" : "Team projects", - "es" : "Proyectos del equipo", - "fr" : "Projets de l’équipe", - "tr" : "Takım projeleri", - "zh_cn" : "团队项目" - }, - "used-in" : [ "src/app/main/ui/dashboard/team.cljs" ] - }, - "dashboard.theme-change" : { - "translations" : { - "ca" : "Tema de l'interfície", - "de" : "UI-Theme", - "en" : "UI theme", - "es" : "Tema visual", - "fr" : "Thème de l’interface", - "ru" : "Тема интерфейса пользователя", - "tr" : "Önyüz teması", - "zh_cn" : "界面主题" - }, - "used-in" : [ "src/app/main/ui/settings/options.cljs" ] - }, - "dashboard.title-search" : { - "translations" : { - "ca" : "Membres de l'equip", - "de" : "Suchergebnisse", - "en" : "Search results", - "es" : "Resultados de búsqueda", - "fr" : "Résultats de recherche", - "ru" : "Результаты поиска", - "tr" : "Arama sonuçları", - "zh_cn" : "搜索结果" - }, - "used-in" : [ "src/app/main/ui/dashboard/search.cljs" ] - }, - "dashboard.type-something" : { - "translations" : { - "ca" : "Escriu per cercar resultats", - "de" : "Zum Suchen etwas eingeben", - "en" : "Type to search results", - "es" : "Escribe algo para buscar", - "fr" : "Écrivez pour rechercher", - "ru" : "Введите для поиска", - "tr" : "Aramak için yazın", - "zh_cn" : "输入关键词进行搜索" - }, - "used-in" : [ "src/app/main/ui/dashboard/search.cljs" ] - }, - "dashboard.update-settings" : { - "translations" : { - "ca" : "Actualitzar opcions", - "de" : "Einstellungen aktualisieren", - "en" : "Update settings", - "es" : "Actualizar opciones", - "fr" : "Mettre à jour les paramètres", - "ru" : "Обновить настройки", - "tr" : "Ayarları güncelle", - "zh_cn" : "保存设置" - }, - "used-in" : [ "src/app/main/ui/settings/profile.cljs", "src/app/main/ui/settings/password.cljs", "src/app/main/ui/settings/options.cljs" ] - }, - "dashboard.your-account-title" : { - "translations" : { - "ca" : "El teu compte", - "de" : "Ihr Konto", - "en" : "Your account", - "es" : "Tu cuenta", - "fr" : "Votre compte", - "tr" : "Hesabın", - "zh_cn" : "你的账号" - }, - "used-in" : [ "src/app/main/ui/settings.cljs" ] - }, - "dashboard.your-email" : { - "translations" : { - "ca" : "Correu electrónic", - "de" : "E-Mail", - "en" : "Email", - "es" : "Correo", - "fr" : "E‑mail", - "ru" : "Email", - "tr" : "E-posta", - "zh_cn" : "电子邮件" - }, - "used-in" : [ "src/app/main/ui/settings/profile.cljs" ] - }, - "dashboard.your-name" : { - "translations" : { - "ca" : "El teu nom", - "de" : "Ihr Name", - "en" : "Your name", - "es" : "Tu nombre", - "fr" : "Votre nom complet", - "ru" : "Ваше имя", - "tr" : "Adın", - "zh_cn" : "你的姓名" - }, - "used-in" : [ "src/app/main/ui/settings/profile.cljs" ] - }, - "dashboard.your-penpot" : { - "translations" : { - "ca" : "El teu Penpot", - "de" : "Ihr Penpot", - "en" : "Your Penpot", - "es" : "Tu Penpot", - "fr" : "Votre Penpot", - "tr" : "Penpot'un", - "zh_cn" : "你的Penpot" - }, - "used-in" : [ "src/app/main/ui/dashboard/search.cljs", "src/app/main/ui/dashboard/team.cljs", "src/app/main/ui/dashboard/team.cljs", "src/app/main/ui/dashboard/libraries.cljs", "src/app/main/ui/dashboard/projects.cljs", "src/app/main/ui/dashboard/sidebar.cljs", "src/app/main/ui/dashboard/file_menu.cljs" ] - }, - "ds.confirm-cancel" : { - "translations" : { - "ca" : "Cancel·lar", - "de" : "Abbrechen", - "en" : "Cancel", - "es" : "Cancelar", - "fr" : "Annuler", - "ru" : "Отмена", - "tr" : "Vazgeç", - "zh_cn" : "取消" - }, - "used-in" : [ "src/app/main/ui/confirm.cljs" ] - }, - "ds.confirm-ok" : { - "translations" : { - "ca" : "Ok", - "de" : "Ok", - "en" : "Ok", - "es" : "Ok", - "fr" : "Ok", - "ru" : "Ok", - "tr" : "Tamam", - "zh_cn" : "OK" - }, - "used-in" : [ "src/app/main/ui/confirm.cljs" ] - }, - "ds.confirm-title" : { - "translations" : { - "ca" : "Estàs segur?", - "de" : "Sind Sie sicher?", - "en" : "Are you sure?", - "es" : "¿Seguro?", - "fr" : "Êtes‑vous sûr ?", - "ru" : "Вы уверены?", - "tr" : "Emin misin?", - "zh_cn" : "你确定?" - }, - "used-in" : [ "src/app/main/ui/confirm.cljs", "src/app/main/ui/confirm.cljs" ] - }, - "ds.updated-at" : { - "translations" : { - "ca" : "Actualitzat: %s", - "de" : "Aktualisiert: %s", - "en" : "Updated: %s", - "es" : "Actualizado: %s", - "fr" : "Mise à jour : %s", - "ru" : "Обновлено: %s", - "tr" : "Güncellendi: %s", - "zh_cn" : "更新了:%s" - }, - "used-in" : [ "src/app/main/ui/dashboard/grid.cljs" ] - }, - "errors.clipboard-not-implemented" : { - "translations" : { - "ca" : "El teu navegador no pot realitzar aquesta operació", - "de" : "Ihr Browser kann diese Funktion nicht ausführen", - "en" : "Your browser cannot do this operation", - "es" : "Tu navegador no puede realizar esta operación", - "fr" : "Votre navigateur ne peut pas effectuer cette opération", - "ru" : "", - "tr" : "Tarayıcın bu işlemi gerçekleştiremiyor", - "zh_cn" : "你的浏览器不支持该操作" - }, - "used-in" : [ "src/app/main/data/workspace.cljs" ] - }, - "errors.email-already-exists" : { - "translations" : { - "ca" : "El correu ja està en ús", - "de" : "E-Mail-Adresse wird bereits verwendet", - "en" : "Email already used", - "es" : "Este correo ya está en uso", - "fr" : "Adresse e‑mail déjà utilisée", - "ru" : "Такой email уже используется", - "tr" : "E-posta zaten kullanımda", - "zh_cn" : "电子邮件已被占用" - }, - "used-in" : [ "src/app/main/ui/auth/verify_token.cljs", "src/app/main/ui/settings/change_email.cljs" ] - }, - "errors.email-already-validated" : { - "translations" : { - "ca" : "El correu ja està validat", - "de" : "E-Mail-Adresse wurde bereits validiert.", - "en" : "Email already validated.", - "es" : "Este correo ya está validado.", - "fr" : "Adresse e‑mail déjà validée.", - "ru" : "Электронная почта уже подтверждена.", - "tr" : "E-posta zaten doğrulandı", - "zh_cn" : "电子邮件已经验证通过" - }, - "used-in" : [ "src/app/main/ui/auth/verify_token.cljs" ] - }, - "errors.email-has-permanent-bounces" : { - "translations" : { - "ca" : "El correu «%s» té molts informes de rebot permanents", - "de" : "Die E-Mail-Adresse «%s» hat viele permanente Unzustellbarkeitsberichte.", - "en" : "The email «%s» has many permanent bounce reports.", - "es" : "El email «%s» tiene varios reportes de rebote permanente.", - "tr" : "«%s» adresi için çok fazla geri dönme raporu var.", - "zh_cn" : "电子邮件“%s”收到了非常多的永久退信报告" - }, - "used-in" : [ "src/app/main/ui/auth/register.cljs", "src/app/main/ui/auth/recovery_request.cljs", "src/app/main/ui/settings/change_email.cljs", "src/app/main/ui/dashboard/team.cljs" ] - }, - "errors.email-invalid-confirmation" : { - "translations" : { - "ca" : "El correu de confirmació ha de coincidir", - "de" : "Bestätigungs-E-Mail muss übereinstimmen", - "en" : "Confirmation email must match", - "es" : "El correo de confirmación debe coincidir", - "fr" : "L’adresse e‑mail de confirmation doit correspondre", - "ru" : "Email для подтверждения должен совпадать", - "tr" : "Doğrulama e-postası eşleşmiyor", - "zh_cn" : "确认电子邮件必须保持一致" - }, - "used-in" : [ "src/app/main/ui/settings/change_email.cljs" ] - }, - "errors.generic" : { - "translations" : { - "ca" : "Alguna cosa ha anat malament", - "de" : "Etwas ist schief gelaufen.", - "en" : "Something wrong has happened.", - "es" : "Ha ocurrido algún error.", - "fr" : "Un problème s’est produit.", - "ru" : "Что-то пошло не так.", - "tr" : "Bir şeyler ters gitti.", - "zh_cn" : "发生了某种错误。" - }, - "used-in" : [ "src/app/main/ui/auth/verify_token.cljs", "src/app/main/ui/settings/feedback.cljs", "src/app/main/ui/dashboard/team.cljs" ] - }, - "errors.google-auth-not-enabled" : { - "translations" : { - "ca" : "L'autenticació amb google ha estat desactivada a aquest servidor", - "de" : "Die Authentifizierung mit Google ist im Backend deaktiviert", - "en" : "Authentication with google disabled on backend", - "es" : "Autenticación con google esta dehabilitada en el servidor", - "tr" : "Google ile oturum açma devre dışı bırakıldı", - "zh_cn" : "后端禁用了Google授权" - }, - "used-in" : [ "src/app/main/ui/auth/login.cljs" ] - }, - "errors.ldap-disabled" : { - "translations" : { - "de" : "Die LDAP-Authentifizierung ist deaktiviert.", - "en" : "LDAP authentication is disabled.", - "es" : "La autheticacion via LDAP esta deshabilitada.", - "tr" : "LDAP ile oturum açma devre dışı bırakıldı.", - "zh_cn" : "仅用了LDAP授权。" - }, - "used-in" : [ "src/app/main/ui/auth/login.cljs" ] - }, - "errors.media-format-unsupported" : { - "translations" : { - "ca" : "El format d'imatge no està suportat (deu ser svg, jpg o png),", - "de" : "Das Bildformat wird nicht unterstützt (es muss ein SVG, JPG oder PNG sein).", - "en" : "The image format is not supported (must be svg, jpg or png).", - "es" : "No se reconoce el formato de imagen (debe ser svg, jpg o png).", - "fr" : "Le format d’image n’est pas supporté (doit être svg, jpg ou png).", - "ru" : "Формат изображения не поддерживается (должен быть svg, jpg или png).", - "tr" : "Görsel formatı desteklenmiyor (svg, jpg veya png olmalı).", - "zh_cn" : "不支持该图片格式(只能是svg、jpg或png)。" - }, - "unused" : true - }, - "errors.media-too-large" : { - "translations" : { - "ca" : "La imatge es massa gran (ha de tenir menys de 5 mb).", - "de" : "Das Bild ist zu groß, um eingefügt zu werden (es muss unter 5MB sein).", - "en" : "The image is too large to be inserted (must be under 5mb).", - "es" : "La imagen es demasiado grande (debe tener menos de 5mb).", - "fr" : "L’image est trop grande (doit être inférieure à 5 Mo).", - "ru" : "Изображение слишком большое для вставки (должно быть меньше 5mb).", - "tr" : "Bu görsel eklemek için çok büyük (5MB altında olmalı)", - "zh_cn" : "图片尺寸过大,故无法插入(不能超过5MB)。" - }, - "used-in" : [ "src/app/main/data/workspace/persistence.cljs" ] - }, - "errors.media-type-mismatch" : { - "translations" : { - "ca" : "Sembla que el contingut de la imatge no coincideix amb l'extensió del arxiu", - "de" : "Es scheint, dass der Bildinhalt nicht mit der Dateierweiterung übereinstimmt.", - "en" : "Seems that the contents of the image does not match the file extension.", - "es" : "Parece que el contenido de la imagen no coincide con la extensión del archivo.", - "fr" : "Il semble que le contenu de l’image ne correspond pas à l’extension de fichier.", - "ru" : "", - "tr" : "Dosya içeriği, uzantısı ile eşleşmiyor gibi görünüyor.", - "zh_cn" : "图片内容好像与文档扩展名不匹配。" - }, - "used-in" : [ "src/app/main/data/workspace/persistence.cljs", "src/app/main/data/media.cljs" ] - }, - "errors.media-type-not-allowed" : { - "translations" : { - "ca" : "La imatge no sembla pas vàlida", - "de" : "Es scheint, dass dies kein gültiges Bild ist.", - "en" : "Seems that this is not a valid image.", - "es" : "Parece que no es una imagen válida.", - "fr" : "L’image ne semble pas être valide.", - "ru" : "", - "tr" : "Geçerli bir görsel gibi görünmüyor.", - "zh_cn" : "该图片好像不可用。" - }, - "used-in" : [ "src/app/main/data/workspace/persistence.cljs", "src/app/main/data/workspace/persistence.cljs", "src/app/main/data/workspace/persistence.cljs", "src/app/main/data/workspace/persistence.cljs", "src/app/main/data/media.cljs" ] - }, - "errors.member-is-muted" : { - "translations" : { - "ca" : "El perfil que estàs invitant té els emails mutejats (per informes de spam o rebots alts", - "de" : "In dem von Ihnen eingeladenen Profil sind E-Mails stummgeschaltet (Spam-Berichte oder hohe Unzustellbarkeitsberichte).", - "en" : "The profile you inviting has emails muted (spam reports or high bounces).", - "es" : "El perfil que esta invitando tiene los emails silenciados (por reportes de spam o alto indice de rebote).", - "tr" : "Davet ettiğiniz profilin e-posta adresine ait çok fazla geri dönme raporu var veya spam olarak bildirilmiş.", - "zh_cn" : "你邀请的人设置了邮件免打扰(报告垃圾邮件或者多次退信)。" - }, - "used-in" : [ "src/app/main/ui/dashboard/team.cljs" ] - }, - "errors.network" : { - "translations" : { - "ca" : "Impossible connectar amb el servidor principal", - "de" : "Es kann keine Verbindung zum Server hergestellt werden.", - "en" : "Unable to connect to backend server.", - "es" : "Ha sido imposible conectar con el servidor principal.", - "fr" : "Impossible de se connecter au serveur principal.", - "ru" : "Невозможно подключиться к серверу.", - "tr" : "Sunucuya bağlanılamıyor", - "zh_cn" : "无法连接到后端服务器。" - }, - "unused" : true - }, - "errors.password-invalid-confirmation" : { - "translations" : { - "ca" : "La contrasenya de confirmació ha de coincidir", - "de" : "Bestätigungspasswort muss übereinstimmen", - "en" : "Confirmation password must match", - "es" : "La contraseña de confirmación debe coincidir", - "fr" : "Le mot de passe de confirmation doit correspondre", - "ru" : "Пароль для подтверждения должен совпадать", - "tr" : "Parolalar eşleşmedi", - "zh_cn" : "确认密码必须保持一致。" - }, - "used-in" : [ "src/app/main/ui/settings/password.cljs" ] - }, - "errors.password-too-short" : { - "translations" : { - "ca" : "La contrasenya ha de tenir 8 com a mínim 8 caràcters", - "de" : "Das Passwort sollte mindestens 8 Zeichen lang sein", - "en" : "Password should at least be 8 characters", - "es" : "La contraseña debe tener 8 caracteres como mínimo", - "fr" : "Le mot de passe doit contenir au moins 8 caractères", - "ru" : "Пароль должен быть минимум 8 символов", - "tr" : "Parola en az 8 karakterden oluşmalı", - "zh_cn" : "密码最少需要8位字符。" - }, - "used-in" : [ "src/app/main/ui/settings/password.cljs" ] - }, - "errors.profile-is-muted" : { - "translations" : { - "ca" : "El teu perfil te els emails mutejats (per informes de spam o rebots alts).", - "de" : "Ihr Profil hat stummgeschaltete E-Mails (Spam-Berichte oder hohe Unzustellbarkeitsberichte).", - "en" : "Your profile has emails muted (spam reports or high bounces).", - "es" : "Tu perfil tiene los emails silenciados (por reportes de spam o alto indice de rebote).", - "zh_cn" : "你设置了邮件免打扰(报告垃圾邮件或者多次退信)。" - }, - "used-in" : [ "src/app/main/ui/auth/recovery_request.cljs", "src/app/main/ui/settings/change_email.cljs", "src/app/main/ui/dashboard/team.cljs" ] - }, - "errors.registration-disabled" : { - "translations" : { - "ca" : "El registre està desactivat actualment", - "de" : "Die Registrierung ist derzeit deaktiviert.", - "en" : "The registration is currently disabled.", - "es" : "El registro está actualmente desactivado.", - "fr" : "L’enregistrement est actuellement désactivé.", - "ru" : "Регистрация сейчас отключена.", - "zh_cn" : "当前禁止注册。" - }, - "used-in" : [ "src/app/main/ui/auth/register.cljs" ] - }, - "errors.terms-privacy-agreement-invalid" : { - "translations" : { - "en" : "You must accept our terms of service and privacy policy.", - "es" : "Debes aceptar nuestros términos de servicio y política de privacidad." - }, - "unused" : true - }, - "errors.unexpected-error" : { - "translations" : { - "ca" : "S'ha produït un error inesperat.", - "de" : "Ein unerwarteter Fehler ist aufgetreten.", - "en" : "An unexpected error occurred.", - "es" : "Ha ocurrido un error inesperado.", - "fr" : "Une erreur inattendue s’est produite", - "ru" : "Произошла ошибка.", - "zh_cn" : "发生了意料之外的错误。" - }, - "used-in" : [ "src/app/main/data/media.cljs", "src/app/main/ui/workspace/sidebar/options/menus/exports.cljs", "src/app/main/ui/handoff/exports.cljs" ] - }, - "errors.unexpected-token" : { - "translations" : { - "ca" : "Token desconegut", - "de" : "Unbekannter Token", - "en" : "Unknown token", - "es" : "Token desconocido", - "zh_cn" : "未知的TOKEN。" - }, - "used-in" : [ "src/app/main/ui/auth/verify_token.cljs" ] - }, - "errors.wrong-credentials" : { - "translations" : { - "ca" : "El nom d'usuari o la contrasenya sembla incorrecte", - "de" : "Der Benutzername oder das Passwort ist falsch.", - "en" : "Username or password seems to be wrong.", - "es" : "El nombre o la contraseña parece incorrecto.", - "fr" : "Le nom d’utilisateur ou le mot de passe semble être faux.", - "ru" : "Неверное имя пользователя или пароль.", - "zh_cn" : "用户名或密码错误。" - }, - "used-in" : [ "src/app/main/ui/auth/login.cljs" ] - }, - "errors.wrong-old-password" : { - "translations" : { - "ca" : "La contrasenya anterior no és correcte", - "de" : "Altes Passwort ist falsch", - "en" : "Old password is incorrect", - "es" : "La contraseña anterior no es correcta", - "fr" : "L’ancien mot de passe est incorrect", - "ru" : "Старый пароль неверный", - "zh_cn" : "旧密码不正确" - }, - "used-in" : [ "src/app/main/ui/settings/password.cljs" ] - }, - "feedback.chat-start" : { - "translations" : { - "ca" : "Uneix-te al xat.", - "de" : "Dem Chat beitreten", - "en" : "Join the chat", - "es" : "Unirse al chat", - "fr" : "Rejoindre le chat", - "zh_cn" : "加入聊天" - }, - "used-in" : [ "src/app/main/ui/settings/feedback.cljs" ] - }, - "feedback.chat-subtitle" : { - "translations" : { - "ca" : "Et ve de gust parlar? Xateja amb nosaltres a Gitter", - "de" : "Möchten Sie sprechen? Chatten Sie mit uns bei Gitter", - "en" : "Feeling like talking? Chat with us at Gitter", - "es" : "¿Deseas conversar? Entra al nuestro chat de la comunidad en Gitter", - "fr" : "Envie de parler? Discutez avec nous sur Gitter", - "zh_cn" : "想说两句?来Gitter和我们聊聊" - }, - "used-in" : [ "src/app/main/ui/settings/feedback.cljs" ] - }, - "feedback.description" : { - "translations" : { - "ca" : "Descripció", - "de" : "Beschreibung", - "en" : "Description", - "es" : "Descripción", - "fr" : "Description", - "zh_cn" : "描述" - }, - "used-in" : [ "src/app/main/ui/settings/feedback.cljs" ] - }, - "feedback.discussions-go-to" : { - "translations" : { - "ca" : "", - "de" : "Zur Diskussion", - "en" : "Go to discussions", - "es" : "Ir a las discusiones", - "fr" : "Aller aux discussions", - "zh_cn" : "前往讨论" - }, - "used-in" : [ "src/app/main/ui/settings/feedback.cljs" ] - }, - "feedback.discussions-subtitle1" : { - "translations" : { - "ca" : "Uneix-te al fòrum colaboratiu de Penpot.", - "de" : "Treten Sie dem kollaborativen Kommunikationsforum des Penpot-Teams bei.", - "en" : "Join Penpot team collaborative communication forum.", - "es" : "Entra al foro colaborativo de Penpot", - "fr" : "Rejoignez le forum de communication collaborative de l'équipe Penpot.", - "zh_cn" : "加入Penpot团队协作交流论坛。" - }, - "used-in" : [ "src/app/main/ui/settings/feedback.cljs" ] - }, - "feedback.discussions-subtitle2" : { - "translations" : { - "ca" : "Pots fer i respondre preguntes, tenir converses obertes i seguir les decisións que afecten al projecte", - "de" : "Sie können Fragen stellen und beantworten, offene Gespräche führen und Entscheidungen verfolgen, die das Projekt betreffen.", - "en" : "You can ask and answer questions, have open-ended conversations, and follow along on decisions affecting the project.", - "es" : "", - "fr" : "Vous pouvez poser des questions et y répondre, avoir des conversations ouvertes et suivre les décisions affectant le projet.", - "zh_cn" : "你可以提问、回答问题,来一场开放的对话,并对影响项目的决策保持关注。" - }, - "used-in" : [ "src/app/main/ui/settings/feedback.cljs" ] - }, - "feedback.discussions-title" : { - "translations" : { - "ca" : "", - "de" : "Teambesprechungen", - "en" : "Team discussions", - "es" : "", - "fr" : "Discussions en équipe", - "zh_cn" : "团队讨论" - }, - "used-in" : [ "src/app/main/ui/settings/feedback.cljs" ] - }, - "feedback.subject" : { - "translations" : { - "ca" : "Tema", - "de" : "Betreff", - "en" : "Subject", - "es" : "Asunto", - "fr" : "Sujet", - "zh_cn" : "话题" - }, - "used-in" : [ "src/app/main/ui/settings/feedback.cljs" ] - }, - "feedback.subtitle" : { - "translations" : { - "ca" : "Si us plau descriu la raó del teu correu, especificant si es una incidència, una idea o un dubte. Un membre del nostre equip respondrà tan aviat como pugui.", - "de" : "Bitte beschreiben Sie den Grund Ihrer E-Mail und geben Sie an, ob es sich um ein Problem, eine Idee oder einem Bedenken handelt. Ein Mitglied unseres Teams wird Ihnen so schnell wie möglich antworten.", - "en" : "Please describe the reason of your email, specifying if is an issue, an idea or a doubt. A member of our team will respond as soon as possible.", - "es" : "", - "fr" : "Veuillez décrire la raison de votre e-mail, en précisant s'il s'agit d'un problème, d'une idée ou d'un doute. Un membre de notre équipe vous répondra dans les plus brefs délais.", - "zh_cn" : "请说明你发邮件的原因,详细说明这是一个问题反馈、一个点子还是一个疑问。我们会尽快回复。" - }, - "used-in" : [ "src/app/main/ui/settings/feedback.cljs" ] - }, - "feedback.title" : { - "translations" : { - "ca" : "Correu electrònic", - "de" : "E-Mail", - "en" : "Email", - "es" : "Correo electrónico", - "fr" : "Email", - "ru" : "Email", - "zh_cn" : "电子邮件" - }, - "used-in" : [ "src/app/main/ui/settings/feedback.cljs" ] - }, - "generic.error" : { - "translations" : { - "ca" : "S'ha produït un error", - "de" : "Ein Fehler ist aufgetreten", - "en" : "An error has occurred", - "es" : "Ha ocurrido un error", - "fr" : "Une erreur s’est produite", - "ru" : "Произошла ошибка", - "zh_cn" : "发生了一个错误" - }, - "used-in" : [ "src/app/main/ui/settings/password.cljs" ] - }, - "handoff.attributes.blur" : { - "translations" : { - "de" : "Weichzeichnen", - "en" : "Blur", - "es" : "Desenfocado", - "fr" : "Flou", - "zh_cn" : "模糊" - }, - "used-in" : [ "src/app/main/ui/handoff/attributes/blur.cljs" ] - }, - "handoff.attributes.blur.value" : { - "translations" : { - "de" : "Wert", - "en" : "Value", - "es" : "Valor", - "fr" : "Valeur", - "zh_cn" : "值" - }, - "used-in" : [ "src/app/main/ui/handoff/attributes/blur.cljs" ] - }, - "handoff.attributes.color.hex" : { - "translations" : { - "de" : "HEX", - "en" : "HEX", - "es" : "HEX", - "fr" : "HEX", - "zh_cn" : "HEX" - }, - "used-in" : [ "src/app/main/ui/handoff/attributes/common.cljs" ] - }, - "handoff.attributes.color.hsla" : { - "translations" : { - "de" : "HSLA", - "en" : "HSLA", - "es" : "HSLA", - "fr" : "HSLA", - "zh_cn" : "HSLA" - }, - "used-in" : [ "src/app/main/ui/handoff/attributes/common.cljs" ] - }, - "handoff.attributes.color.rgba" : { - "translations" : { - "de" : "RGBA", - "en" : "RGBA", - "es" : "RGBA", - "fr" : "RGBA", - "zh_cn" : "RGBA" - }, - "used-in" : [ "src/app/main/ui/handoff/attributes/common.cljs" ] - }, - "handoff.attributes.fill" : { - "translations" : { - "de" : "Fläche", - "en" : "Fill", - "es" : "Relleno", - "fr" : "Remplir", - "zh_cn" : "填充" - }, - "used-in" : [ "src/app/main/ui/handoff/attributes/fill.cljs" ] - }, - "handoff.attributes.image.download" : { - "translations" : { - "de" : "Originalbild herunterladen", - "en" : "Download source image", - "es" : "Descargar imagen original", - "fr" : "Télécharger l’image source", - "zh_cn" : "下载原图" - }, - "used-in" : [ "src/app/main/ui/handoff/attributes/image.cljs" ] - }, - "handoff.attributes.image.height" : { - "translations" : { - "de" : "Höhe", - "en" : "Height", - "es" : "Altura", - "fr" : "Hauteur", - "zh_cn" : "高" - }, - "used-in" : [ "src/app/main/ui/handoff/attributes/image.cljs" ] - }, - "handoff.attributes.image.width" : { - "translations" : { - "de" : "Breite", - "en" : "Width", - "es" : "Ancho", - "fr" : "Largeur", - "zh_cn" : "宽" - }, - "used-in" : [ "src/app/main/ui/handoff/attributes/image.cljs" ] - }, - "handoff.attributes.layout" : { - "translations" : { - "de" : "Layout", - "en" : "Layout", - "es" : "Estructura", - "fr" : "Mise en page", - "zh_cn" : "布局" - }, - "used-in" : [ "src/app/main/ui/handoff/attributes/layout.cljs" ] - }, - "handoff.attributes.layout.height" : { - "translations" : { - "de" : "Höhe", - "en" : "Height", - "es" : "Altura", - "fr" : "Hauteur", - "zh_cn" : "高" - }, - "used-in" : [ "src/app/main/ui/handoff/attributes/layout.cljs" ] - }, - "handoff.attributes.layout.left" : { - "translations" : { - "de" : "Links", - "en" : "Left", - "es" : "Izquierda", - "fr" : "Gauche", - "zh_cn" : "左" - }, - "used-in" : [ "src/app/main/ui/handoff/attributes/layout.cljs" ] - }, - "handoff.attributes.layout.radius" : { - "translations" : { - "de" : "Radius", - "en" : "Radius", - "es" : "Derecha", - "fr" : "Rayon", - "zh_cn" : "圆角" - }, - "used-in" : [ "src/app/main/ui/handoff/attributes/layout.cljs", "src/app/main/ui/handoff/attributes/layout.cljs" ] - }, - "handoff.attributes.layout.rotation" : { - "translations" : { - "de" : "Drehung", - "en" : "Rotation", - "es" : "Rotación", - "fr" : "Rotation", - "zh_cn" : "旋转" - }, - "used-in" : [ "src/app/main/ui/handoff/attributes/layout.cljs" ] - }, - "handoff.attributes.layout.top" : { - "translations" : { - "de" : "Oben", - "en" : "Top", - "es" : "Arriba", - "fr" : "Haut", - "zh_cn" : "顶" - }, - "used-in" : [ "src/app/main/ui/handoff/attributes/layout.cljs" ] - }, - "handoff.attributes.layout.width" : { - "translations" : { - "de" : "Breite", - "en" : "Width", - "es" : "Ancho", - "fr" : "Largeur", - "zh_cn" : "宽" - }, - "used-in" : [ "src/app/main/ui/handoff/attributes/layout.cljs" ] - }, - "handoff.attributes.shadow" : { - "translations" : { - "de" : "Schatten", - "en" : "Shadow", - "es" : "Sombra", - "fr" : "Ombre", - "zh_cn" : "阴影" - }, - "used-in" : [ "src/app/main/ui/handoff/attributes/shadow.cljs" ] - }, - "handoff.attributes.shadow.shorthand.blur" : { - "translations" : { - "de" : "B", - "en" : "B", - "es" : "B", - "fr" : "B", - "zh_cn" : "B" - }, - "used-in" : [ "src/app/main/ui/handoff/attributes/shadow.cljs" ] - }, - "handoff.attributes.shadow.shorthand.offset-x" : { - "translations" : { - "de" : "X", - "en" : "X", - "es" : "X", - "fr" : "X", - "zh_cn" : "X" - }, - "used-in" : [ "src/app/main/ui/handoff/attributes/shadow.cljs" ] - }, - "handoff.attributes.shadow.shorthand.offset-y" : { - "translations" : { - "de" : "Y", - "en" : "Y", - "es" : "Y", - "fr" : "Y", - "zh_cn" : "Y" - }, - "used-in" : [ "src/app/main/ui/handoff/attributes/shadow.cljs" ] - }, - "handoff.attributes.shadow.shorthand.spread" : { - "translations" : { - "de" : "S", - "en" : "S", - "es" : "S", - "fr" : "S", - "zh_cn" : "S" - }, - "used-in" : [ "src/app/main/ui/handoff/attributes/shadow.cljs" ] - }, - "handoff.attributes.stroke" : { - "translations" : { - "de" : "Rahmen", - "en" : "Stroke", - "es" : "Borde", - "fr" : "Contour", - "zh_cn" : "边框" - }, - "used-in" : [ "src/app/main/ui/handoff/attributes/stroke.cljs" ] - }, - "handoff.attributes.stroke.alignment.center" : { - "permanent" : true, - "translations" : { - "de" : "Zentriert", - "en" : "Center", - "es" : "Centro", - "fr" : "Centre", - "ru" : "Центр", - "zh_cn" : "居中" - }, - "used-in" : [ ] - }, - "handoff.attributes.stroke.alignment.inner" : { - "permanent" : true, - "translations" : { - "de" : "Innen", - "en" : "Inside", - "es" : "Interior", - "fr" : "Intérieur", - "ru" : "Внутрь", - "zh_cn" : "内部" - }, - "used-in" : [ ] - }, - "handoff.attributes.stroke.alignment.outer" : { - "permanent" : true, - "translations" : { - "de" : "Außen", - "en" : "Outside", - "es" : "Exterior", - "fr" : "Extérieur", - "ru" : "Наружу", - "zh_cn" : "外部" - }, - "used-in" : [ ] - }, - "handoff.attributes.stroke.style.dotted" : { - "translations" : { - "de" : "Gepunktet", - "en" : "Dotted", - "es" : "Punteado", - "fr" : "Pointillé", - "zh_cn" : "虚线" - }, - "unused" : true - }, - "handoff.attributes.stroke.style.mixed" : { - "translations" : { - "de" : "Mehrere", - "en" : "Mixed", - "es" : "Mixto", - "fr" : "Mixte", - "zh_cn" : "混合" - }, - "unused" : true - }, - "handoff.attributes.stroke.style.none" : { - "translations" : { - "de" : "Keine", - "en" : "None", - "es" : "Ninguno", - "fr" : "Aucun", - "zh_cn" : "无" - }, - "unused" : true - }, - "handoff.attributes.stroke.style.solid" : { - "translations" : { - "de" : "Solid", - "en" : "Solid", - "es" : "Sólido", - "fr" : "Solide", - "zh_cn" : "实线" - }, - "unused" : true - }, - "handoff.attributes.stroke.width" : { - "translations" : { - "de" : "Breite", - "en" : "Width", - "es" : "Ancho", - "fr" : "Épaisseur", - "zh_cn" : "宽" - }, - "used-in" : [ "src/app/main/ui/handoff/attributes/stroke.cljs" ] - }, - "handoff.attributes.typography" : { - "translations" : { - "de" : "Typografie", - "en" : "Typography", - "es" : "Tipografía", - "fr" : "Typographie", - "zh_cn" : "排版" - }, - "used-in" : [ "src/app/main/ui/handoff/attributes/text.cljs" ] - }, - "handoff.attributes.typography.font-family" : { - "translations" : { - "de" : "Schriftart", - "en" : "Font Family", - "es" : "Familia tipográfica", - "fr" : "Police de caractères", - "zh_cn" : "字体" - }, - "used-in" : [ "src/app/main/ui/handoff/attributes/text.cljs" ] - }, - "handoff.attributes.typography.font-size" : { - "translations" : { - "de" : "Schriftgröße", - "en" : "Font Size", - "es" : "Tamaño de fuente", - "fr" : "Taille de police", - "zh_cn" : "字号" - }, - "used-in" : [ "src/app/main/ui/handoff/attributes/text.cljs" ] - }, - "handoff.attributes.typography.font-style" : { - "translations" : { - "de" : "Schriftstil", - "en" : "Font Style", - "es" : "Estilo de fuente", - "fr" : "Style de police", - "zh_cn" : "文字风格" - }, - "used-in" : [ "src/app/main/ui/handoff/attributes/text.cljs" ] - }, - "handoff.attributes.typography.letter-spacing" : { - "translations" : { - "de" : "Zeichenabstand", - "en" : "Letter Spacing", - "es" : "Espaciado de letras", - "fr" : "Interlettrage", - "zh_cn" : "字距" - }, - "used-in" : [ "src/app/main/ui/handoff/attributes/text.cljs" ] - }, - "handoff.attributes.typography.line-height" : { - "translations" : { - "de" : "Zeilenabstand", - "en" : "Line Height", - "es" : "Interlineado", - "fr" : "Interlignage", - "zh_cn" : "行高" - }, - "used-in" : [ "src/app/main/ui/handoff/attributes/text.cljs" ] - }, - "handoff.attributes.typography.text-decoration" : { - "translations" : { - "de" : "Textdekoration", - "en" : "Text Decoration", - "es" : "Decoración de texto", - "fr" : "Décoration de texte", - "zh_cn" : "文字装饰" - }, - "used-in" : [ "src/app/main/ui/handoff/attributes/text.cljs" ] - }, - "handoff.attributes.typography.text-decoration.none" : { - "translations" : { - "de" : "Keine", - "en" : "None", - "es" : "Ninguna", - "fr" : "Aucune", - "zh_cn" : "无" - }, - "unused" : true - }, - "handoff.attributes.typography.text-decoration.strikethrough" : { - "translations" : { - "de" : "Durchgestrichen", - "en" : "Strikethrough", - "es" : "Tachar", - "fr" : "Barré", - "zh_cn" : "删除线" - }, - "unused" : true - }, - "handoff.attributes.typography.text-decoration.underline" : { - "translations" : { - "de" : "Unterstrichen", - "en" : "Underline", - "es" : "Subrayar", - "fr" : "Soulignage", - "zh_cn" : "下划线" - }, - "unused" : true - }, - "handoff.attributes.typography.text-transform" : { - "translations" : { - "de" : "Texttransformation", - "en" : "Text Transform", - "es" : "Transformación de texto", - "fr" : "Transformation de texte", - "zh_cn" : "文本变换" - }, - "used-in" : [ "src/app/main/ui/handoff/attributes/text.cljs" ] - }, - "handoff.attributes.typography.text-transform.lowercase" : { - "translations" : { - "de" : "Kleinbuchstaben", - "en" : "Lower Case", - "es" : "Minúsculas", - "fr" : "Minuscule", - "zh_cn" : "小写" - }, - "unused" : true - }, - "handoff.attributes.typography.text-transform.none" : { - "translations" : { - "de" : "Keine", - "en" : "None", - "es" : "Ninguna", - "fr" : "Aucune", - "zh_cn" : "无" - }, - "unused" : true - }, - "handoff.attributes.typography.text-transform.titlecase" : { - "translations" : { - "de" : "Kapitälchen", - "en" : "Title Case", - "es" : "Primera en mayúscula", - "fr" : "Premières Lettres en Capitales", - "zh_cn" : "首字母大写" - }, - "unused" : true - }, - "handoff.attributes.typography.text-transform.uppercase" : { - "translations" : { - "de" : "Großbuchstaben", - "en" : "Upper Case", - "es" : "Mayúsculas", - "fr" : "Capitales", - "zh_cn" : "大写" - }, - "unused" : true - }, - "handoff.tabs.code" : { - "translations" : { - "de" : "Code", - "en" : "Code", - "es" : "Código", - "fr" : "Code", - "zh_cn" : "码" - }, - "used-in" : [ "src/app/main/ui/handoff/right_sidebar.cljs" ] - }, - "handoff.tabs.code.selected.circle" : { - "translations" : { - "de" : "Kreis", - "en" : "Circle", - "es" : "Círculo", - "fr" : "Cercle", - "zh_cn" : "圆" - }, - "unused" : true - }, - "handoff.tabs.code.selected.curve" : { - "translations" : { - "de" : "Kurve", - "en" : "Curve", - "es" : "Curva", - "fr" : "Courbe", - "zh_cn" : "曲线" - }, - "unused" : true - }, - "handoff.tabs.code.selected.frame" : { - "translations" : { - "de" : "Zeichenfläche", - "en" : "Artboard", - "es" : "Mesa de trabajo", - "fr" : "Plan de travail", - "zh_cn" : "画板" - }, - "unused" : true - }, - "handoff.tabs.code.selected.group" : { - "translations" : { - "de" : "Gruppe", - "en" : "Group", - "es" : "Grupo", - "fr" : "Groupe", - "zh_cn" : "编组" - }, - "unused" : true - }, - "handoff.tabs.code.selected.image" : { - "translations" : { - "de" : "Bild", - "en" : "Image", - "es" : "Imagen", - "fr" : "Image", - "zh_cn" : "图片" - }, - "unused" : true - }, - "handoff.tabs.code.selected.multiple" : { - "translations" : { - "de" : "%s Ausgewählt(e)", - "en" : "%s Selected", - "es" : "%s Seleccionado", - "fr" : "%s Sélectionné", - "zh_cn" : "已选中%s项" - }, - "used-in" : [ "src/app/main/ui/handoff/right_sidebar.cljs" ] - }, - "handoff.tabs.code.selected.path" : { - "translations" : { - "de" : "Pfad", - "en" : "Path", - "es" : "Trazado", - "fr" : "Chemin", - "zh_cn" : "路径" - }, - "unused" : true - }, - "handoff.tabs.code.selected.rect" : { - "translations" : { - "de" : "Rechteck", - "en" : "Rectangle", - "es" : "Rectángulo", - "fr" : "Rectangle", - "zh_cn" : "矩形" - }, - "unused" : true - }, - "handoff.tabs.code.selected.svg-raw" : { - "translations" : { - "de" : "SVG", - "en" : "SVG", - "es" : "SVG", - "fr" : "SVG", - "zh_cn" : "SVG" - }, - "unused" : true - }, - "handoff.tabs.code.selected.text" : { - "translations" : { - "de" : "Text", - "en" : "Text", - "es" : "Texto", - "fr" : "Texte", - "zh_cn" : "文本" - }, - "unused" : true - }, - "handoff.tabs.info" : { - "translations" : { - "de" : "Info", - "en" : "Info", - "es" : "Información", - "fr" : "Information", - "zh_cn" : "信息" - }, - "used-in" : [ "src/app/main/ui/handoff/right_sidebar.cljs" ] - }, - "history.alert-message" : { - "translations" : { - "de" : "Sie sehen Version %s", - "en" : "You are seeing version %s", - "es" : "Estás viendo la versión %s", - "fr" : "Vous voyez la version %s", - "ru" : "Ваша версия %s", - "zh_cn" : "你正在查看%s版本" - }, - "unused" : true - }, - "labels.accept" : { - "translations" : { - "ca" : "Acceptar", - "de" : "Akzeptieren", - "en" : "Accept", - "es" : "Aceptar", - "fr" : "Accepter", - "ru" : "Принять", - "zh_cn" : "接受" - }, - "unused" : true - }, - "labels.admin" : { - "translations" : { - "de" : "Admin", - "en" : "Admin", - "es" : "Administración", - "fr" : "Administration", - "zh_cn" : "管理员" - }, - "used-in" : [ "src/app/main/ui/dashboard/team.cljs", "src/app/main/ui/dashboard/team.cljs", "src/app/main/ui/dashboard/team.cljs" ] - }, - "labels.all" : { - "translations" : { - "de" : "Alles", - "en" : "All", - "es" : "Todo", - "fr" : "Tous", - "zh_cn" : "全部" - }, - "used-in" : [ "src/app/main/ui/workspace/comments.cljs" ] - }, - "labels.bad-gateway.desc-message" : { - "translations" : { - "de" : "Sie müssen ein wenig warten und es erneut versuchen. Wir führen eine kurze Wartung an unseren Servern durch.", - "en" : "Looks like you need to wait a bit and retry; we are performing small maintenance of our servers.", - "es" : "Parece que necesitas esperar un poco y volverlo a intentar; estamos realizando operaciones de mantenimiento en nuestros servidores.", - "fr" : "Il semble que vous deviez attendre un peu et réessayer ; nous effectuons une petite maintenance de nos serveurs.", - "zh_cn" : "请过会儿再来试试,我们正在对服务器进行一些简单维护。" - }, - "used-in" : [ "src/app/main/ui/static.cljs" ] - }, - "labels.bad-gateway.main-message" : { - "translations" : { - "de" : "Bad Gateway", - "en" : "Bad Gateway", - "es" : "Bad Gateway", - "fr" : "Bad Gateway", - "zh_cn" : "网关错误" - }, - "used-in" : [ "src/app/main/ui/static.cljs" ] - }, - "labels.cancel" : { - "translations" : { - "de" : "Abbrechen", - "en" : "Cancel", - "es" : "Cancelar", - "fr" : "Annuler", - "ru" : "Отмена", - "zh_cn" : "取消" - }, - "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs" ] - }, - "labels.centered" : { - "translations" : { - "de" : "Zentriert", - "en" : "Center", - "es" : "Centrado", - "fr" : "Centré", - "zh_cn" : "中心" - }, - "unused" : true - }, - "labels.comments" : { - "translations" : { - "de" : "Kommentare", - "en" : "Comments", - "es" : "Comentarios", - "fr" : "Commentaires", - "zh_cn" : "评论" - }, - "used-in" : [ "src/app/main/ui/dashboard/comments.cljs" ] - }, - "labels.confirm-password" : { - "translations" : { - "de" : "Passwort bestätigen", - "en" : "Confirm password", - "es" : "Confirmar contraseña", - "fr" : "Confirmer le mot de passe", - "ru" : "Подтвердите пароль", - "zh_cn" : "确认密码" - }, - "used-in" : [ "src/app/main/ui/settings/password.cljs" ] - }, - "labels.content" : { - "translations" : { - "de" : "Inhalt", - "en" : "Content", - "es" : "Contenido", - "fr" : "Contenu", - "zh_cn" : "内容" - }, - "unused" : true - }, - "labels.create-team" : { - "translations" : { - "de" : "Neues Team erstellen", - "en" : "Create new team", - "es" : "Crea un nuevo equipo", - "fr" : "Créer nouvelle équipe", - "zh_cn" : "创建新团队" - }, - "used-in" : [ "src/app/main/ui/dashboard/team_form.cljs", "src/app/main/ui/dashboard/team_form.cljs" ] - }, - "labels.create-team.placeholder" : { - "translations" : { - "en" : "Enter new team name" - }, - "used-in" : [ "src/app/main/ui/dashboard/team_form.cljs" ] - }, - "labels.dashboard" : { - "translations" : { - "de" : "Dashboard", - "en" : "Dashboard", - "es" : "Panel", - "fr" : "Tableau de bord", - "zh_cn" : "面板" - }, - "used-in" : [ "src/app/main/ui/settings/sidebar.cljs" ] - }, - "labels.delete" : { - "translations" : { - "de" : "Löschen", - "en" : "Delete", - "es" : "Borrar", - "fr" : "Supprimer", - "ru" : "Удалить", - "zh_cn" : "删除" - }, - "used-in" : [ "src/app/main/ui/dashboard/project_menu.cljs", "src/app/main/ui/dashboard/file_menu.cljs" ] - }, - "labels.delete-comment" : { - "translations" : { - "de" : "Kommentar löschen", - "en" : "Delete comment", - "es" : "Eliminar comentario", - "fr" : "Supprimer le commentaire", - "zh_cn" : "删除该评论" - }, - "used-in" : [ "src/app/main/ui/comments.cljs" ] - }, - "labels.delete-comment-thread" : { - "translations" : { - "de" : "Thread löschen", - "en" : "Delete thread", - "es" : "Eliminar hilo", - "fr" : "Supprimer le fil", - "zh_cn" : "删除该讨论串" - }, - "used-in" : [ "src/app/main/ui/comments.cljs" ] - }, - "labels.delete-multi-files" : { - "translations" : { - "en" : "Delete %s files", - "es" : "Borrar %s archivos" - }, - "used-in" : [ "src/app/main/ui/dashboard/file_menu.cljs" ] - }, - "labels.drafts" : { - "translations" : { - "de" : "Entwürfe", - "en" : "Drafts", - "es" : "Borradores", - "fr" : "Brouillons", - "ru" : "Черновики", - "zh_cn" : "草稿" - }, - "used-in" : [ "src/app/main/ui/dashboard/projects.cljs", "src/app/main/ui/dashboard/sidebar.cljs", "src/app/main/ui/dashboard/files.cljs", "src/app/main/ui/dashboard/files.cljs", "src/app/main/ui/dashboard/file_menu.cljs" ] - }, - "labels.edit" : { - "translations" : { - "de" : "Bearbeiten", - "en" : "Edit", - "es" : "Editar", - "fr" : "Modifier", - "zh_cn" : "编辑" - }, - "used-in" : [ "src/app/main/ui/comments.cljs" ] - }, - "labels.editor" : { - "translations" : { - "de" : "Editor", - "en" : "Editor", - "es" : "Editor", - "fr" : "Éditeur", - "zh_cn" : "编辑者" - }, - "used-in" : [ "src/app/main/ui/dashboard/team.cljs", "src/app/main/ui/dashboard/team.cljs", "src/app/main/ui/dashboard/team.cljs" ] - }, - "labels.email" : { - "translations" : { - "de" : "E-Mail", - "en" : "Email", - "es" : "Correo electrónico", - "fr" : "Adresse e‑mail", - "ru" : "Email", - "zh_cn" : "电子邮件" - }, - "used-in" : [ "src/app/main/ui/dashboard/team.cljs", "src/app/main/ui/dashboard/team.cljs" ] - }, - "labels.feedback-disabled" : { - "translations" : { - "de" : "Feedback deaktiviert", - "en" : "Feedback disabled", - "es" : "El modulo de recepción de opiniones esta deshabilitado.", - "zh_cn" : "反馈被禁止" - }, - "used-in" : [ "src/app/main/ui/settings/feedback.cljs" ] - }, - "labels.feedback-sent" : { - "translations" : { - "de" : "Feedback gesendet", - "en" : "Feedback sent", - "es" : "Opinión enviada", - "zh_cn" : "反馈已发出" - }, - "used-in" : [ "src/app/main/ui/settings/feedback.cljs" ] - }, - "labels.give-feedback" : { - "translations" : { - "de" : "Feedback geben", - "en" : "Give feedback", - "es" : "Danos tu opinión", - "fr" : "Donnez votre avis", - "ru" : "Дать обратную связь", - "zh_cn" : "提交反馈" - }, - "used-in" : [ "src/app/main/ui/workspace/header.cljs", "src/app/main/ui/settings/sidebar.cljs", "src/app/main/ui/dashboard/sidebar.cljs" ] - }, - "labels.hide-resolved-comments" : { - "translations" : { - "de" : "Erledigte Kommentare ausblenden", - "en" : "Hide resolved comments", - "es" : "Ocultar comentarios resueltos", - "fr" : "Masquer les commentaires résolus", - "zh_cn" : "隐藏已决定的评论" - }, - "used-in" : [ "src/app/main/ui/workspace/comments.cljs", "src/app/main/ui/viewer/header.cljs" ] - }, - "labels.icons" : { - "translations" : { - "de" : "Icons", - "en" : "Icons", - "es" : "Iconos", - "fr" : "Icônes", - "ru" : "Иконки", - "zh_cn" : "图标" - }, - "unused" : true - }, - "labels.images" : { - "translations" : { - "de" : "Bilder", - "en" : "Images", - "es" : "Imágenes", - "fr" : "Images", - "ru" : "Изображения", - "zh_cn" : "图片" - }, - "unused" : true - }, - "labels.internal-error.desc-message" : { - "translations" : { - "de" : "Etwas ist schiefgegangen. Bitte versuchen Sie den Vorgang erneut und wenn das Problem weiterhin besteht, kontaktieren Sie den Support.", - "en" : "Something bad happened. Please retry the operation and if the problem persists, contact with support.", - "es" : "Ha ocurrido algo extraño. Por favor, reintenta la operación, y si el problema persiste, contacta con el servicio técnico.", - "fr" : "Un problème s’est produit. Veuillez réessayer l’opération et, si le problème persiste, contacter le service technique.", - "zh_cn" : "发生了一些不妙的事。请尝试重新操作。如果问题仍然存在,请联系我们以取得支持。" - }, - "used-in" : [ "src/app/main/ui/static.cljs" ] - }, - "labels.internal-error.main-message" : { - "translations" : { - "de" : "Interner Fehler", - "en" : "Internal Error", - "es" : "Error interno", - "fr" : "Erreur interne", - "zh_cn" : "内部错误" - }, - "used-in" : [ "src/app/main/ui/static.cljs" ] - }, - "labels.language" : { - "translations" : { - "de" : "Sprache", - "en" : "Language", - "es" : "Idioma", - "fr" : "Langue", - "ru" : "Язык", - "zh_cn" : "语言" - }, - "used-in" : [ "src/app/main/ui/settings/options.cljs" ] - }, - "labels.logout" : { - "translations" : { - "de" : "Abmelden", - "en" : "Logout", - "es" : "Salir", - "fr" : "Se déconnecter", - "ru" : "Выход", - "zh_cn" : "登出" - }, - "used-in" : [ "src/app/main/ui/settings.cljs", "src/app/main/ui/dashboard/sidebar.cljs" ] - }, - "labels.members" : { - "translations" : { - "de" : "Mitglieder", - "en" : "Members", - "es" : "Integrantes", - "fr" : "Membres", - "zh_cn" : "成员" - }, - "used-in" : [ "src/app/main/ui/dashboard/team.cljs", "src/app/main/ui/dashboard/team.cljs", "src/app/main/ui/dashboard/sidebar.cljs" ] - }, - "labels.name" : { - "translations" : { - "de" : "Name", - "en" : "Name", - "es" : "Nombre", - "fr" : "Nom", - "ru" : "Имя", - "zh_cn" : "名字" - }, - "used-in" : [ "src/app/main/ui/dashboard/team.cljs" ] - }, - "labels.new-password" : { - "translations" : { - "de" : "Neues Passwort", - "en" : "New password", - "es" : "Nueva contraseña", - "fr" : "Nouveau mot de passe", - "ru" : "Новый пароль", - "zh_cn" : "新密码" - }, - "used-in" : [ "src/app/main/ui/settings/password.cljs" ] - }, - "labels.no-comments-available" : { - "translations" : { - "de" : "Sie haben keine ausstehenden Kommentarbenachrichtigungen", - "en" : "You have no pending comment notifications", - "es" : "No tienes notificaciones de comentarios pendientes", - "fr" : "Vous n’avez aucune notification de commentaire en attente", - "zh_cn" : "没有待表决的评论通知" - }, - "used-in" : [ "src/app/main/ui/workspace/comments.cljs", "src/app/main/ui/dashboard/comments.cljs" ] - }, - "labels.not-found.auth-info" : { - "translations" : { - "de" : "Sie sind angemeldet als", - "en" : "You’re signed in as", - "es" : "Estás identificado como", - "fr" : "Vous êtes connecté en tant que", - "zh_cn" : "你已登陆为" - }, - "used-in" : [ "src/app/main/ui/static.cljs" ] - }, - "labels.not-found.desc-message" : { - "translations" : { - "de" : "Möglicherweise existiert diese Seite nicht oder Sie haben keine Zugriffsrechte darauf.", - "en" : "This page might not exist or you don’t have permissions to access to it.", - "es" : "Esta página no existe o no tienes permisos para verla.", - "fr" : "Cette page n’existe pas ou vous ne disposez pas des permissions nécessaires pour y accéder.", - "zh_cn" : "可能该页面不存在,也可能你没有访问权限。" - }, - "used-in" : [ "src/app/main/ui/static.cljs" ] - }, - "labels.not-found.main-message" : { - "translations" : { - "de" : "Ups!", - "en" : "Oops!", - "es" : "¡Huy!", - "fr" : "Oups !", - "zh_cn" : "嚯!" - }, - "used-in" : [ "src/app/main/ui/static.cljs" ] - }, - "labels.num-of-files" : { - "translations" : { - "de" : [ "1 Datei", "%s Dateien" ], - "en" : [ "1 file", "%s files" ], - "es" : [ "1 archivo", "%s archivos" ], - "fr" : [ "1 fichier", "%s fichiers" ], - "zh_cn" : [ "1 个文档", "共 %s 个文档" ] - }, - "used-in" : [ "src/app/main/ui/dashboard/team.cljs" ] - }, - "labels.num-of-projects" : { - "translations" : { - "de" : [ "1 Projekt", "%s Projekte" ], - "en" : [ "1 project", "%s projects" ], - "es" : [ "1 proyecto", "%s proyectos" ], - "fr" : [ "1 projet", "%s projets" ], - "zh_cn" : [ "1 个项目", "共 %s 个项目" ] - }, - "used-in" : [ "src/app/main/ui/dashboard/team.cljs" ] - }, - "labels.old-password" : { - "translations" : { - "de" : "Altes Passwort", - "en" : "Old password", - "es" : "Contraseña anterior", - "fr" : "Ancien mot de passe", - "ru" : "Старый пароль", - "zh_cn" : "旧密码" - }, - "used-in" : [ "src/app/main/ui/settings/password.cljs" ] - }, - "labels.only-yours" : { - "translations" : { - "de" : "Nur Ihre", - "en" : "Only yours", - "es" : "Sólo los tuyos", - "fr" : "Seulement les vôtres", - "zh_cn" : "仅你的" - }, - "used-in" : [ "src/app/main/ui/workspace/comments.cljs" ] - }, - "labels.owner" : { - "translations" : { - "de" : "Eigentümer", - "en" : "Owner", - "es" : "Dueño", - "fr" : "Propriétaire", - "zh_cn" : "所有者" - }, - "used-in" : [ "src/app/main/ui/dashboard/team.cljs", "src/app/main/ui/dashboard/team.cljs" ] - }, - "labels.password" : { - "translations" : { - "de" : "Passwort", - "en" : "Password", - "es" : "Contraseña", - "fr" : "Mot de passe", - "ru" : "Пароль", - "zh_cn" : "密码" - }, - "used-in" : [ "src/app/main/ui/settings/sidebar.cljs", "src/app/main/ui/dashboard/sidebar.cljs" ] - }, - "labels.permissions" : { - "translations" : { - "de" : "Berechtigungen", - "en" : "Permissions", - "es" : "Permisos", - "fr" : "Permissions", - "zh_cn" : "许可" - }, - "used-in" : [ "src/app/main/ui/dashboard/team.cljs" ] - }, - "labels.profile" : { - "translations" : { - "de" : "Profil", - "en" : "Profile", - "es" : "Perfil", - "fr" : "Profil", - "ru" : "Профиль", - "zh_cn" : "个人资料" - }, - "used-in" : [ "src/app/main/ui/settings/sidebar.cljs", "src/app/main/ui/dashboard/sidebar.cljs" ] - }, - "labels.projects" : { - "translations" : { - "de" : "Projekte", - "en" : "Projects", - "es" : "Proyectos", - "fr" : "Projets", - "ru" : "Проекты", - "zh_cn" : "项目" - }, - "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs" ] - }, - "labels.recent" : { - "translations" : { - "ca" : "Recent", - "de" : "Zuletzt", - "en" : "Recent", - "es" : "Reciente", - "fr" : "Récent", - "ru" : "Недавние", - "zh_cn" : "最近" - }, - "unused" : true - }, - "labels.release-notes" : { - "translations" : { - "de" : "Versionshinweise", - "en" : "Release notes", - "es" : "Notas de versión", - "fr" : "Notes de version", - "ru" : "примечания к выпуску", - "zh_cn" : "發行說明" - }, - "used-in" : [ "src/app/main/ui/settings/sidebar.cljs" ] - }, - "labels.remove" : { - "translations" : { - "de" : "Entfernen", - "en" : "Remove", - "es" : "Quitar", - "fr" : "Retirer", - "ru" : "", - "zh_cn" : "移除" - }, - "used-in" : [ "src/app/main/ui/workspace/libraries.cljs", "src/app/main/ui/dashboard/team.cljs" ] - }, - "labels.rename" : { - "translations" : { - "de" : "Umbenennen", - "en" : "Rename", - "es" : "Renombrar", - "fr" : "Renommer", - "zh_cn" : "重命名" - }, - "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs", "src/app/main/ui/dashboard/project_menu.cljs", "src/app/main/ui/dashboard/file_menu.cljs" ] - }, - "labels.rename-team" : { - "translations" : { - "de" : "Team umbenennen", - "en" : "Rename team", - "es" : "Renomba el equipo", - "zh_cn" : "重命名团队" - }, - "used-in" : [ "src/app/main/ui/dashboard/team_form.cljs" ] - }, - "labels.retry" : { - "translations" : { - "de" : "Wiederholen", - "en" : "Retry", - "es" : "Reintentar", - "fr" : "Réessayer", - "zh_cn" : "重试" - }, - "used-in" : [ "src/app/main/ui/static.cljs", "src/app/main/ui/static.cljs", "src/app/main/ui/static.cljs" ] - }, - "labels.role" : { - "translations" : { - "de" : "Rolle", - "en" : "Role", - "es" : "Cargo", - "fr" : "Rôle", - "zh_cn" : "角色" - }, - "used-in" : [ "src/app/main/ui/dashboard/team.cljs" ] - }, - "labels.save" : { - "translations" : { - "ca" : "Desa", - "de" : "Speichern", - "en" : "Save", - "es" : "Guardar", - "fr" : "Enregistrer", - "ru" : "Сохранить", - "zh_cn" : "保存" - }, - "unused" : true - }, - "labels.send" : { - "translations" : { - "de" : "Senden", - "en" : "Send", - "es" : "Enviar", - "zh_cn" : "发送" - }, - "used-in" : [ "src/app/main/ui/settings/feedback.cljs" ] - }, - "labels.sending" : { - "translations" : { - "de" : "Senden…", - "en" : "Sending...", - "es" : "Enviando...", - "zh_cn" : "正在发送…" - }, - "used-in" : [ "src/app/main/ui/settings/feedback.cljs" ] - }, - "labels.service-unavailable.desc-message" : { - "translations" : { - "de" : "Wir befinden uns in der planmäßigen Wartung unserer Systeme.", - "en" : "We are in programmed maintenance of our systems.", - "es" : "Estamos en una operación de mantenimiento programado de nuestros sistemas.", - "fr" : "Nous sommes en maintenance planifiée de nos systèmes.", - "zh_cn" : "我们正在进行系统的程序维护。" - }, - "used-in" : [ "src/app/main/ui/static.cljs" ] - }, - "labels.service-unavailable.main-message" : { - "translations" : { - "de" : "Service nicht verfügbar", - "en" : "Service Unavailable", - "es" : "El servicio no está disponible", - "fr" : "Service non disponible", - "zh_cn" : "服务不可用" - }, - "used-in" : [ "src/app/main/ui/static.cljs" ] - }, - "labels.settings" : { - "translations" : { - "de" : "Einstellungen", - "en" : "Settings", - "es" : "Configuración", - "fr" : "Configuration", - "ru" : "Параметры", - "zh_cn" : "设置" - }, - "used-in" : [ "src/app/main/ui/settings/sidebar.cljs", "src/app/main/ui/dashboard/team.cljs", "src/app/main/ui/dashboard/team.cljs", "src/app/main/ui/dashboard/sidebar.cljs" ] - }, - "labels.shared-libraries" : { - "translations" : { - "de" : "Gemeinsam genutzte Bibliotheken", - "en" : "Shared Libraries", - "es" : "Bibliotecas Compartidas", - "fr" : "Bibliothèques Partagées", - "ru" : "", - "zh_cn" : "共享库" - }, - "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs" ] - }, - "labels.show-all-comments" : { - "translations" : { - "de" : "Alle Kommentare anzeigen", - "en" : "Show all comments", - "es" : "Mostrar todos los comentarios", - "fr" : "Afficher tous les commentaires", - "zh_cn" : "显示所有评论" - }, - "used-in" : [ "src/app/main/ui/workspace/comments.cljs", "src/app/main/ui/viewer/header.cljs" ] - }, - "labels.show-your-comments" : { - "translations" : { - "de" : "Nur eigene Kommentare anzeigen", - "en" : "Show only yours comments", - "es" : "Mostrar sólo tus comentarios", - "fr" : "Afficher uniquement vos commentaires", - "zh_cn" : "只显示你的评论" - }, - "used-in" : [ "src/app/main/ui/workspace/comments.cljs", "src/app/main/ui/viewer/header.cljs" ] - }, - "labels.sign-out" : { - "translations" : { - "de" : "Abmelden", - "en" : "Sign out", - "es" : "Salir", - "fr" : "Se déconnecter", - "ru" : "Выход", - "zh_cn" : "登出" - }, - "used-in" : [ "src/app/main/ui/static.cljs" ] - }, - "labels.update" : { - "translations" : { - "de" : "Aktualisieren", - "en" : "Update", - "es" : "Actualizar", - "fr" : "Actualiser", - "ru" : "Обновить", - "zh_cn" : "更新" - }, - "used-in" : [ "src/app/main/ui/settings/profile.cljs" ] - }, - "labels.update-team" : { - "translations" : { - "de" : "Team aktualisieren", - "en" : "Update team", - "es" : "Actualiza el equipo", - "zh_cn" : "更新团队" - }, - "used-in" : [ "src/app/main/ui/dashboard/team_form.cljs" ] - }, - "labels.viewer" : { - "translations" : { - "de" : "Zuschauer", - "en" : "Viewer", - "es" : "Visualizador", - "fr" : "Spectateur", - "zh_cn" : "查看者" - }, - "used-in" : [ "src/app/main/ui/dashboard/team.cljs" ] - }, - "labels.write-new-comment" : { - "translations" : { - "de" : "Neuen Kommentar schreiben", - "en" : "Write new comment", - "es" : "Escribir un nuevo comentario", - "fr" : "Écrire un nouveau commentaire", - "zh_cn" : "写一条新评论" - }, - "used-in" : [ "src/app/main/ui/comments.cljs" ] - }, - "media.loading" : { - "translations" : { - "de" : "Bild laden…", - "en" : "Loading image…", - "es" : "Cargando imagen…", - "fr" : "Chargement de l’image…", - "ru" : "Загружаю изображение…", - "zh_cn" : "正在加载图片…" - }, - "used-in" : [ "src/app/main/data/workspace/persistence.cljs", "src/app/main/data/workspace/persistence.cljs", "src/app/main/data/media.cljs" ] - }, - "modals.add-shared-confirm.accept" : { - "translations" : { - "de" : "Hinzufügen als gemeinsam genutzte Bibliothek", - "en" : "Add as Shared Library", - "es" : "Añadir como Biblioteca Compartida", - "fr" : "Ajouter comme Bibliothèque Partagée", - "ru" : "", - "zh_cn" : "添加为共享库" - }, - "used-in" : [ "src/app/main/ui/workspace/header.cljs", "src/app/main/ui/dashboard/file_menu.cljs" ] - }, - "modals.add-shared-confirm.hint" : { - "translations" : { - "de" : "Sobald es als gemeinsam genutzte Bibliothek hinzugefügt wurde, können die Assets dieser Dateibibliothek von den anderen Dateien verwendet werden.", - "en" : "Once added as Shared Library, the assets of this file library will be available to be used among the rest of your files.", - "es" : "Una vez añadido como Biblioteca Compartida, los recursos de este archivo estarán disponibles para ser usado por el resto de tus archivos.", - "fr" : "Une fois ajoutées en tant que Bibliothèque Partagée, les ressources de cette bibliothèque de fichiers seront disponibles pour être utilisées parmi le reste de vos fichiers.", - "ru" : "", - "zh_cn" : "一旦添加为共享库,此文档库中的素材就可被用于你的其他文档中。" - }, - "used-in" : [ "src/app/main/ui/workspace/header.cljs", "src/app/main/ui/dashboard/file_menu.cljs" ] - }, - "modals.add-shared-confirm.message" : { - "translations" : { - "de" : "Hinzufügen von “%s” als gemeinsam genutzte Bibliothek", - "en" : "Add “%s” as Shared Library", - "es" : "Añadir “%s” como Biblioteca Compartida", - "fr" : "Ajouter « %s » comme Bibliothèque Partagée", - "ru" : "", - "zh_cn" : "将“%s”添加为共享库" - }, - "used-in" : [ "src/app/main/ui/workspace/header.cljs", "src/app/main/ui/dashboard/file_menu.cljs" ] - }, - "modals.change-email.confirm-email" : { - "translations" : { - "de" : "Neue E-Mail-Adresse verifizieren", - "en" : "Verify new email", - "es" : "Verificar el nuevo correo", - "fr" : "Vérifier la nouvelle adresse e‑mail", - "ru" : "Подтвердить новый email адрес", - "zh_cn" : "验证新的邮件" - }, - "used-in" : [ "src/app/main/ui/settings/change_email.cljs" ] - }, - "modals.change-email.info" : { - "translations" : { - "de" : "Wir senden Ihnen eine Nachricht an Ihre aktuelle E-Mail-Adresse “%s”, um Ihre Identität zu überprüfen.", - "en" : "We'll send you an email to your current email “%s” to verify your identity.", - "es" : "Enviaremos un mensaje a tu correo actual “%s” para verificar tu identidad.", - "fr" : "Nous enverrons un e‑mail à votre adresse actuelle « %s » pour vérifier votre identité.", - "ru" : "Мы отправим письмо для подтверждения подлиности на текущий email адрес “%s”.", - "zh_cn" : "我们会发送一封信的邮件到当前的电子邮件“%s”,以验证你的身份。" - }, - "used-in" : [ "src/app/main/ui/settings/change_email.cljs" ] - }, - "modals.change-email.new-email" : { - "translations" : { - "de" : "Neue E-Mail-Adresse", - "en" : "New email", - "es" : "Nuevo correo", - "fr" : "Nouvel e‑mail", - "ru" : "Новый email адрес", - "zh_cn" : "新电子邮件" - }, - "used-in" : [ "src/app/main/ui/settings/change_email.cljs" ] - }, - "modals.change-email.submit" : { - "translations" : { - "de" : "E-Mail-Adresse ändern", - "en" : "Change email", - "es" : "Cambiar correo", - "fr" : "Changer adresse e‑mail", - "ru" : "Сменить email адрес", - "zh_cn" : "修改电子邮件" - }, - "used-in" : [ "src/app/main/ui/settings/change_email.cljs" ] - }, - "modals.change-email.title" : { - "translations" : { - "de" : "Ihre E-Mail-Adresse ändern", - "en" : "Change your email", - "es" : "Cambiar tu correo", - "fr" : "Changez votre adresse e‑mail", - "ru" : "Сменить email адрес", - "zh_cn" : "修改你的电子邮件" - }, - "used-in" : [ "src/app/main/ui/settings/change_email.cljs" ] - }, - "modals.delete-account.cancel" : { - "translations" : { - "de" : "Abbrechen und mein Konto behalten", - "en" : "Cancel and keep my account", - "es" : "Cancelar y mantener mi cuenta", - "fr" : "Annuler et conserver mon compte", - "ru" : "Отменить и сохранить мой аккаунт", - "zh_cn" : "取消操作并保留我的账号" - }, - "used-in" : [ "src/app/main/ui/settings/delete_account.cljs" ] - }, - "modals.delete-account.confirm" : { - "translations" : { - "de" : "Ja, mein Konto löschen", - "en" : "Yes, delete my account", - "es" : "Si, borrar mi cuenta", - "fr" : "Oui, supprimer mon compte", - "ru" : "Да, удалить мой аккаунт", - "zh_cn" : "是的,删除我的账号" - }, - "used-in" : [ "src/app/main/ui/settings/delete_account.cljs" ] - }, - "modals.delete-account.info" : { - "translations" : { - "de" : "Wenn Sie Ihr Konto löschen, verlieren Sie alle Ihre aktuellen Projekte und Archive.", - "en" : "By removing your account you’ll lose all your current projects and archives.", - "es" : "Si borras tu cuenta perderás todos tus proyectos y archivos.", - "fr" : "En supprimant votre compte, vous perdrez tous vos projets et archives actuelles.", - "ru" : "Удалив аккаунт Вы потеряете все прокты и архивы.", - "zh_cn" : "删除账号后,你会失去所有项目和存档。" - }, - "used-in" : [ "src/app/main/ui/settings/delete_account.cljs" ] - }, - "modals.delete-account.title" : { - "translations" : { - "de" : "Sind Sie sicher, dass Sie Ihr Konto löschen möchten?", - "en" : "Are you sure you want to delete your account?", - "es" : "¿Seguro que quieres borrar tu cuenta?", - "fr" : "Êtes‑vous sûr de vouloir supprimer votre compte ?", - "ru" : "Вы уверены, что хотите удалить аккаунт?", - "zh_cn" : "你确定想要删除你的账号?" - }, - "used-in" : [ "src/app/main/ui/settings/delete_account.cljs" ] - }, - "modals.delete-comment-thread.accept" : { - "translations" : { - "de" : "Konversation löschen", - "en" : "Delete conversation", - "es" : "Eliminar conversación", - "fr" : "Supprimer la conversation", - "zh_cn" : "删除对话" - }, - "used-in" : [ "src/app/main/ui/comments.cljs" ] - }, - "modals.delete-comment-thread.message" : { - "translations" : { - "de" : "Sind Sie sicher, dass Sie diese Konversation löschen möchten? Alle Kommentare in diesem Thread werden gelöscht.", - "en" : "Are you sure you want to delete this conversation? All comments in this thread will be deleted.", - "es" : "¿Seguro que quieres eliminar esta conversación? Todos los comentarios en este hilo serán eliminados.", - "fr" : "Êtes‑vous sûr de vouloir supprimer cette conversation ? Tous les commentaires de ce fil seront supprimés.", - "zh_cn" : "你确定想要删除这个对话?该讨论串里的所有评论都会被一同删除。" - }, - "used-in" : [ "src/app/main/ui/comments.cljs" ] - }, - "modals.delete-comment-thread.title" : { - "translations" : { - "de" : "Konversation löschen", - "en" : "Delete conversation", - "es" : "Eliminar conversación", - "fr" : "Supprimer une conversation", - "zh_cn" : "删除对话" - }, - "used-in" : [ "src/app/main/ui/comments.cljs" ] - }, - "modals.delete-file-confirm.accept" : { - "translations" : { - "de" : "Datei löschen", - "en" : "Delete file", - "es" : "Eliminar archivo", - "fr" : "Supprimer le fichier", - "zh_cn" : "删除文档" - }, - "used-in" : [ "src/app/main/ui/dashboard/file_menu.cljs" ] - }, - "modals.delete-file-confirm.message" : { - "translations" : { - "de" : "Sind Sie sicher, dass Sie diese Datei löschen wollen?", - "en" : "Are you sure you want to delete this file?", - "es" : "¿Seguro que quieres eliminar este archivo?", - "fr" : "Êtes‑vous sûr de vouloir supprimer ce fichier ?", - "zh_cn" : "你确定想要删除这个文档?" - }, - "used-in" : [ "src/app/main/ui/dashboard/file_menu.cljs" ] - }, - "modals.delete-file-confirm.title" : { - "translations" : { - "de" : "Datei löschen", - "en" : "Deleting file", - "es" : "Eliminando archivo", - "fr" : "Supprimer un fichier", - "zh_cn" : "正在删除文档" - }, - "used-in" : [ "src/app/main/ui/dashboard/file_menu.cljs" ] - }, - "modals.delete-file-multi-confirm.accept" : { - "translations" : { - "en" : "Delete files", - "es" : "Eliminar archivos" - }, - "used-in" : [ "src/app/main/ui/dashboard/file_menu.cljs" ] - }, - "modals.delete-file-multi-confirm.message" : { - "translations" : { - "en" : "Are you sure you want to delete %s files?", - "es" : "¿Seguro que quieres eliminar %s archivos?" - }, - "used-in" : [ "src/app/main/ui/dashboard/file_menu.cljs" ] - }, - "modals.delete-file-multi-confirm.title" : { - "translations" : { - "en" : "Deleting %s files", - "es" : "Eliminando %s archivos" - }, - "used-in" : [ "src/app/main/ui/dashboard/file_menu.cljs" ] - }, - "modals.delete-page.body" : { - "translations" : { - "de" : "Sind Sie sicher, dass Sie diese Seite löschen wollen?", - "en" : "Are you sure you want to delete this page?", - "es" : "¿Seguro que quieres borrar esta página?", - "fr" : "Êtes‑vous sûr de vouloir supprimer cette page ?", - "zh_cn" : "你确定想要删除这个页面?" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/sitemap.cljs" ] - }, - "modals.delete-page.title" : { - "translations" : { - "de" : "Seite löschen", - "en" : "Delete page", - "es" : "Borrar página", - "fr" : "Supprimer une page", - "zh_cn" : "删除页面" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/sitemap.cljs" ] - }, - "modals.delete-project-confirm.accept" : { - "translations" : { - "de" : "Projekt löschen", - "en" : "Delete project", - "es" : "Eliminar proyecto", - "fr" : "Supprimer le projet", - "zh_cn" : "删除项目" - }, - "used-in" : [ "src/app/main/ui/dashboard/project_menu.cljs" ] - }, - "modals.delete-project-confirm.message" : { - "translations" : { - "de" : "Sind Sie sicher, dass Sie dieses Projekt löschen möchten?", - "en" : "Are you sure you want to delete this project?", - "es" : "¿Seguro que quieres eliminar este proyecto?", - "fr" : "Êtes‑vous sûr de vouloir supprimer ce projet ?", - "zh_cn" : "你确定想要删除这个项目?" - }, - "used-in" : [ "src/app/main/ui/dashboard/project_menu.cljs" ] - }, - "modals.delete-project-confirm.title" : { - "translations" : { - "de" : "Projekt löschen", - "en" : "Delete project", - "es" : "Eliminar proyecto", - "fr" : "Supprimer un projet", - "zh_cn" : "删除项目" - }, - "used-in" : [ "src/app/main/ui/dashboard/project_menu.cljs" ] - }, - "modals.delete-team-confirm.accept" : { - "translations" : { - "de" : "Team löschen", - "en" : "Delete team", - "es" : "Eliminar equipo", - "fr" : "Supprimer l’équipe", - "zh_cn" : "删除团队" - }, - "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs" ] - }, - "modals.delete-team-confirm.message" : { - "translations" : { - "de" : "Sind Sie sicher, dass Sie dieses Team löschen möchten? Alle Projekte und Dateien, die mit dem Team verbunden sind, werden dauerhaft gelöscht.", - "en" : "Are you sure you want to delete this team? All projects and files associated with team will be permanently deleted.", - "es" : "¿Seguro que quieres eliminar este equipo? Todos los proyectos y archivos asociados con el equipo serán eliminados permamentemente.", - "fr" : "Êtes‑vous sûr de vouloir supprimer cette équipe ? Tous les projets et fichiers associés à l’équipe seront définitivement supprimés.", - "zh_cn" : "你确定想要删除这个团队?与该团队关联的所有项目和文档都会被永久删除。" - }, - "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs" ] - }, - "modals.delete-team-confirm.title" : { - "translations" : { - "de" : "Team löschen", - "en" : "Deleting team", - "es" : "Eliminando equipo", - "fr" : "Suppression d’une équipe", - "zh_cn" : "正在删除团队" - }, - "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs" ] - }, - "modals.delete-team-member-confirm.accept" : { - "translations" : { - "de" : "Mitglied löschen", - "en" : "Delete member", - "es" : "Eliminando miembro", - "fr" : "Supprimer le membre", - "zh_cn" : "删除成员" - }, - "used-in" : [ "src/app/main/ui/dashboard/team.cljs" ] - }, - "modals.delete-team-member-confirm.message" : { - "translations" : { - "de" : "Sind Sie sicher, dass Sie dieses Mitglied aus dem Team löschen möchten?", - "en" : "Are you sure you want to delete this member from the team?", - "es" : "¿Seguro que quieres eliminar este integrante del equipo?", - "fr" : "Êtes‑vous sûr de vouloir supprimer ce membre de l’équipe ?", - "zh_cn" : "你确定想要从团队中删除这个成员?" - }, - "used-in" : [ "src/app/main/ui/dashboard/team.cljs" ] - }, - "modals.delete-team-member-confirm.title" : { - "translations" : { - "de" : "Teammitglied löschen", - "en" : "Delete team member", - "es" : "Eliminar integrante del equipo", - "fr" : "Supprimer un membre d’équipe", - "zh_cn" : "删除团队成员" - }, - "used-in" : [ "src/app/main/ui/dashboard/team.cljs" ] - }, - "modals.invite-member-confirm.accept" : { - "translations" : { - "de" : "Einladung senden", - "en" : "Send invitation", - "es" : "Enviar invitacion", - "fr" : "Envoyer l'invitation" - }, - "used-in" : [ "src/app/main/ui/dashboard/team.cljs" ] - }, - "modals.invite-member.title" : { - "translations" : { - "de" : "Einladen, dem Team beizutreten", - "en" : "Invite to join the team", - "es" : "Invitar a unirse al equipo", - "fr" : "Inviter à rejoindre l’équipe", - "zh_cn" : "邀请加入团队" - }, - "used-in" : [ "src/app/main/ui/dashboard/team.cljs" ] - }, - "modals.leave-and-reassign.hint1" : { - "translations" : { - "de" : "Sie sind der Eigentümer von %s.", - "en" : "You are %s owner.", - "es" : "Eres %s dueño.", - "fr" : "Vous êtes le propriétaire de %s.", - "zh_cn" : "你是%s的所有者。" - }, - "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs" ] - }, - "modals.leave-and-reassign.hint2" : { - "translations" : { - "de" : "Befördern Sie ein anderes Mitglied zum Eigentümer, bevor Sie das Team verlassen", - "en" : "Select other member to promote before leave", - "es" : "Promociona otro miembro a dueño antes de abandonar el equipo", - "fr" : "Sélectionnez un autre membre à promouvoir avant de quitter l’équipe", - "zh_cn" : "请在退出前,从其他成员中选择一位晋升。" - }, - "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs" ] - }, - "modals.leave-and-reassign.promote-and-leave" : { - "translations" : { - "de" : "Befördern und verlassen", - "en" : "Promote and leave", - "es" : "Promocionar y abandonar", - "fr" : "Promouvoir et quitter", - "zh_cn" : "晋升并退出" - }, - "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs" ] - }, - "modals.leave-and-reassign.select-memeber-to-promote" : { - "translations" : { - "de" : "Wählen Sie ein Mitglied aus, das befördert werden soll", - "en" : "Select a member to promote", - "es" : "Selecciona un miembro a promocionar", - "fr" : "Sélectionnez un membre à promouvoir", - "zh_cn" : "选择一位成员晋升" - }, - "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs" ] - }, - "modals.leave-and-reassign.title" : { - "translations" : { - "de" : "Wählen Sie ein Mitglied aus, das befördert werden soll", - "en" : "Select a member to promote", - "es" : "Selecciona un miembro a promocionar", - "fr" : "Sélectionnez un membre à promouvoir", - "zh_cn" : "选择一位成员晋升" - }, - "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs" ] - }, - "modals.leave-confirm.accept" : { - "translations" : { - "de" : "Team verlassen", - "en" : "Leave team", - "es" : "Abandonar el equipo", - "fr" : "Quitter l’équipe", - "zh_cn" : "退出团队" - }, - "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs" ] - }, - "modals.leave-confirm.message" : { - "translations" : { - "de" : "Sind Sie sicher, dass Sie dieses Team verlassen wollen?", - "en" : "Are you sure you want to leave this team?", - "es" : "¿Seguro que quieres abandonar este equipo?", - "fr" : "Êtes‑vous sûr de vouloir quitter cette équipe ?", - "zh_cn" : "选择一位成员晋升" - }, - "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs" ] - }, - "modals.leave-confirm.title" : { - "translations" : { - "de" : "Team verlassen", - "en" : "Leaving team", - "es" : "Abandonando el equipo", - "fr" : "Quitter l’équipe", - "zh_cn" : "正在退出团队" - }, - "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs" ] - }, - "modals.promote-owner-confirm.accept" : { - "translations" : { - "de" : "Befördern", - "en" : "Promote", - "es" : "Promocionar", - "fr" : "Promouvoir", - "zh_cn" : "晋升" - }, - "used-in" : [ "src/app/main/ui/dashboard/team.cljs" ] - }, - "modals.promote-owner-confirm.message" : { - "translations" : { - "de" : "Sind Sie sicher, dass Sie diesen Benutzer zum Eigentümer befördern wollen?", - "en" : "Are you sure you want to promote this user to owner?", - "es" : "¿Seguro que quieres promocionar este usuario a dueño?", - "fr" : "Êtes‑vous sûr de vouloir promouvoir cette personne propriétaire ?", - "zh_cn" : "你确定想要晋升该用户为所有者?" - }, - "used-in" : [ "src/app/main/ui/dashboard/team.cljs" ] - }, - "modals.promote-owner-confirm.title" : { - "translations" : { - "de" : "Zum Eigentümer befördern", - "en" : "Promote to owner", - "es" : "Promocionar a dueño", - "fr" : "Promouvoir propriétaire", - "zh_cn" : "晋升为所有者" - }, - "used-in" : [ "src/app/main/ui/dashboard/team.cljs" ] - }, - "modals.remove-shared-confirm.accept" : { - "translations" : { - "de" : "Als gemeinsam genutzte Bibliothek entfernen", - "en" : "Remove as Shared Library", - "es" : "Eliminar como Biblioteca Compartida", - "fr" : "Supprimer en tant que Bibliothèque Partagée", - "ru" : "", - "zh_cn" : "不再作为共享库" - }, - "used-in" : [ "src/app/main/ui/workspace/header.cljs", "src/app/main/ui/dashboard/file_menu.cljs" ] - }, - "modals.remove-shared-confirm.hint" : { - "translations" : { - "de" : "Nach dem Entfernen als gemeinsam genutzte Bibliothek steht die Bibliothek dieser Datei nicht mehr für den Rest Ihrer Dateien zur Verfügung.", - "en" : "Once removed as Shared Library, the File Library of this file will stop being available to be used among the rest of your files.", - "es" : "Una vez eliminado como Biblioteca Compartida, la Biblioteca de este archivo dejará de estar disponible para ser usada por el resto de tus archivos.", - "fr" : "Une fois supprimée en tant que Bibliothèque Partagée, la Bibliothèque de ce fichier ne pourra plus être utilisée par le reste de vos fichiers.", - "ru" : "", - "zh_cn" : "一旦不再作为共享库,该文档库就不能继续用于你的其他文档中。" - }, - "used-in" : [ "src/app/main/ui/workspace/header.cljs", "src/app/main/ui/dashboard/file_menu.cljs" ] - }, - "modals.remove-shared-confirm.message" : { - "translations" : { - "de" : "Entfernen Sie “%s” als gemeinsam genutzte Bibliothek", - "en" : "Remove “%s” as Shared Library", - "es" : "Añadir “%s” como Biblioteca Compartida", - "fr" : "Retirer « %s » en tant que Bibliothèque Partagée", - "ru" : "", - "zh_cn" : "不再将“%s”作为共享库" - }, - "used-in" : [ "src/app/main/ui/workspace/header.cljs", "src/app/main/ui/dashboard/file_menu.cljs" ] - }, - "modals.update-remote-component.accept" : { - "translations" : { - "de" : "Komponente aktualisieren", - "en" : "Update component", - "es" : "Actualizar componente", - "fr" : "Actualiser le composant", - "ru" : "", - "zh_cn" : "更新组件" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/component.cljs", "src/app/main/ui/workspace/context_menu.cljs" ] - }, - "modals.update-remote-component.cancel" : { - "translations" : { - "de" : "Abbrechen", - "en" : "Cancel", - "es" : "Cancelar", - "fr" : "Annuler", - "ru" : "", - "zh_cn" : "取消" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/component.cljs", "src/app/main/ui/workspace/context_menu.cljs" ] - }, - "modals.update-remote-component.hint" : { - "translations" : { - "de" : "Sie sind dabei, eine Komponente in einer gemeinsam genutzten Bibliothek zu aktualisieren. Dies kann sich auf andere Dateien auswirken, die es verwenden.", - "en" : "You are about to update a component in a shared library. This may affect other files that use it.", - "es" : "Vas a actualizar un componente en una librería compartida. Esto puede afectar a otros archivos que la usen.", - "fr" : "Vous êtes sur le point de mettre à jour le composant d’une Bibliothèque Partagée. Cela peut affecter d’autres fichiers qui l’utilisent.", - "ru" : "", - "zh_cn" : "你即将更新共享库中的一个组件。这可能会对使用该组件的其他文档产生影响。" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/component.cljs", "src/app/main/ui/workspace/context_menu.cljs" ] - }, - "modals.update-remote-component.message" : { - "translations" : { - "de" : "Aktualisieren einer Komponente in einer gemeinsam genutzten Bibliothek", - "en" : "Update a component in a shared library", - "es" : "Actualizar un componente en librería", - "fr" : "Actualiser le composant d’une bibliothèque", - "ru" : "", - "zh_cn" : "更新共享库中的一个组件" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/component.cljs", "src/app/main/ui/workspace/context_menu.cljs" ] - }, - "notifications.invitation-email-sent" : { - "translations" : { - "de" : "Einladung erfolgreich gesendet", - "en" : "Invitation sent successfully", - "es" : "Invitación enviada con éxito", - "fr" : "E‑mail d'invitation envoyé!" - }, - "used-in" : [ "src/app/main/ui/dashboard/team.cljs" ] - }, - "notifications.profile-deletion-not-allowed" : { - "translations" : { - "de" : "Sie können Ihr Profil nicht löschen. Weisen Sie Ihre Teams neu zu, bevor Sie fortfahren.", - "en" : "You can't delete you profile. Reassign your teams before proceed.", - "es" : "No puedes borrar tu perfil. Reasigna tus equipos antes de seguir.", - "fr" : "Vous ne pouvez pas supprimer votre profil. Réassignez vos équipes avant de continuer.", - "ru" : "Вы не можете удалить профиль. Сначала смените команду.", - "zh_cn" : "你无法删除你的个人资料。请先转让你的团队。" - }, - "used-in" : [ "src/app/main/ui/settings/delete_account.cljs" ] - }, - "notifications.profile-saved" : { - "translations" : { - "de" : "Profil erfolgreich gespeichert!", - "en" : "Profile saved successfully!", - "es" : "Perfil guardado correctamente!", - "fr" : "Profil enregistré avec succès !", - "ru" : "Профиль успешно сохранен!", - "zh_cn" : "个人资料保存成功!" - }, - "used-in" : [ "src/app/main/ui/settings/profile.cljs", "src/app/main/ui/settings/options.cljs" ] - }, - "notifications.validation-email-sent" : { - "translations" : { - "de" : "Verifizierungs-E-Mail an %s gesendet. Prüfen Sie Ihren Posteingang!", - "en" : "Verification email sent to %s. Check your email!", - "es" : "Verificación de email enviada a %s. Comprueba tu correo.", - "fr" : "E‑mail de vérification envoyé à %s. Vérifiez votre e‑mail !", - "zh_cn" : "验证邮件已发至%s。请检查电子邮箱。" - }, - "used-in" : [ "src/app/main/ui/settings/change_email.cljs" ] - }, - "profile.recovery.go-to-login" : { - "translations" : { - "de" : "Zur Anmeldung", - "en" : "Go to login", - "es" : null, - "fr" : "Aller à la page de connexion", - "ru" : null, - "zh_cn" : "去登录" - }, - "used-in" : [ "src/app/main/ui/auth/recovery.cljs" ] - }, - "settings.multiple" : { - "translations" : { - "de" : "Mehrere", - "en" : "Mixed", - "es" : "Varios", - "fr" : "Divers", - "ru" : "Смешаный", - "zh_cn" : "混合" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs", "src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs", "src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs", "src/app/main/ui/workspace/sidebar/options/menus/layer.cljs", "src/app/main/ui/workspace/sidebar/options/menus/typography.cljs", "src/app/main/ui/workspace/sidebar/options/menus/typography.cljs", "src/app/main/ui/workspace/sidebar/options/menus/typography.cljs", "src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs", "src/app/main/ui/workspace/sidebar/options/menus/blur.cljs" ] - }, - "title.dashboard.files" : { - "translations" : { - "en" : "%s - Penpot" - }, - "used-in" : [ "src/app/main/ui/dashboard/files.cljs" ] - }, - "title.dashboard.projects" : { - "translations" : { - "ca" : "Projectes - %s - Penpot", - "de" : "Projekte - %s - Penpot", - "en" : "Projects - %s - Penpot", - "es" : "Proyectos - %s - Penpot", - "fr" : "Projets - %s - Penpot", - "ru" : "Проекты - %s - Penpot", - "zh_cn" : "项目 - %s - Penpot" - }, - "used-in" : [ "src/app/main/ui/dashboard/projects.cljs" ] - }, - "title.dashboard.search" : { - "translations" : { - "ca" : "Cerca - %s - Penpot", - "de" : "Suchen - %s - Penpot", - "en" : "Search - %s - Penpot", - "es" : "Buscar - %s - Penpot", - "fr" : "Rechercher - %s - Penpot", - "ru" : "Поиск - %s - Penpot", - "zh_cn" : "搜索 - %s - Penpot" - }, - "used-in" : [ "src/app/main/ui/dashboard/search.cljs" ] - }, - "title.dashboard.shared-libraries" : { - "translations" : { - "de" : "Gemeinsam genutzte Bibliotheken - %s - Penpot", - "en" : "Shared Libraries - %s - Penpot", - "es" : "Bibliotecas Compartidas - %s - Penpot", - "fr" : "Bibliothèques Partagées - %s - Penpot", - "ru" : "", - "zh_cn" : "共享库 - %s - Penpot" - }, - "used-in" : [ "src/app/main/ui/dashboard/libraries.cljs" ] - }, - "title.default" : { - "translations" : { - "en" : "Penpot - Design Freedom for Teams", - "es" : "Penpot - Diseño Libre para Equipos" - }, - "used-in" : [ "src/app/main/ui/auth/verify_token.cljs", "src/app/main/ui/auth.cljs" ] - }, - "title.settings.feedback" : { - "translations" : { - "de" : "Feedback geben - Penpot", - "en" : "Give feedback - Penpot", - "es" : "Danos tu opinión - Penpot", - "fr" : "Donnez votre avis - Penpot", - "ru" : "Дать обратную связь - Penpot", - "zh_cn" : "提交反馈 - Penpot" - }, - "used-in" : [ "src/app/main/ui/settings/feedback.cljs" ] - }, - "title.settings.options" : { - "translations" : { - "de" : "Einstellungen - Penpot", - "en" : "Settings - Penpot", - "es" : "Configuración - Penpot", - "fr" : "Configuration - Penpot", - "ru" : "Параметры - Penpot", - "zh_cn" : "设置 - Penpot" - }, - "used-in" : [ "src/app/main/ui/settings/options.cljs" ] - }, - "title.settings.password" : { - "translations" : { - "de" : "Passwort - Penpot", - "en" : "Password - Penpot", - "es" : "Contraseña - Penpot", - "fr" : "Mot de passe - Penpot", - "ru" : "Пароль - Penpot", - "zh_cn" : "密码 - Penpot" - }, - "used-in" : [ "src/app/main/ui/settings/password.cljs" ] - }, - "title.settings.profile" : { - "translations" : { - "de" : "Profil - Penpot", - "en" : "Profile - Penpot", - "es" : "Perfil - Penpot", - "fr" : "Profil - Penpot", - "ru" : "Профиль - Penpot", - "zh_cn" : "个人资料 - Penpot" - }, - "used-in" : [ "src/app/main/ui/settings/profile.cljs" ] - }, - "title.team-members" : { - "translations" : { - "de" : "Mitglieder - %s - Penpot", - "en" : "Members - %s - Penpot", - "es" : "Integrantes - %s - Penpot", - "fr" : "Membres - %s - Penpot", - "zh_cn" : "成员 - %s - Penpot" - }, - "used-in" : [ "src/app/main/ui/dashboard/team.cljs" ] - }, - "title.team-settings" : { - "translations" : { - "de" : "Einstellungen - %s - Penpot", - "en" : "Settings - %s - Penpot", - "es" : "Configuración - %s - Penpot", - "fr" : "Configuration - %s - Penpot", - "ru" : "Параметры - %s - Penpot", - "zh_cn" : "设置 - %s - Penpot" - }, - "used-in" : [ "src/app/main/ui/dashboard/team.cljs" ] - }, - "title.viewer" : { - "translations" : { - "de" : "%s - Ansichtsmodus - Penpot", - "en" : "%s - View mode - Penpot", - "es" : "%s - Modo de visualización - Penpot", - "fr" : "%s - Mode spectateur - Penpot", - "ru" : "%s - Режим просмотра - Penpot", - "zh_cn" : "%s - 预览模式)- Penpot" - }, - "used-in" : [ "src/app/main/ui/handoff.cljs", "src/app/main/ui/viewer.cljs" ] - }, - "title.workspace" : { - "translations" : { - "en" : "%s - Penpot" - }, - "used-in" : [ "src/app/main/ui/workspace.cljs" ] - }, - "viewer.empty-state" : { - "translations" : { - "de" : "Keine Zeichenflächen auf der Seite gefunden.", - "en" : "No frames found on the page.", - "es" : "No se ha encontrado ningún tablero.", - "fr" : "Aucun cadre trouvé sur la page.", - "ru" : "На странице не найдено ни одного кадра", - "zh_cn" : "该页面上未找到任何画框。" - }, - "used-in" : [ "src/app/main/ui/handoff.cljs", "src/app/main/ui/viewer.cljs" ] - }, - "viewer.frame-not-found" : { - "translations" : { - "de" : "Keine Zeichenfläche gefunden.", - "en" : "Frame not found.", - "es" : "No se encuentra el tablero.", - "fr" : "Cadre introuvable.", - "ru" : "Кадры не найдены.", - "zh_cn" : "画框未找到。" - }, - "used-in" : [ "src/app/main/ui/handoff.cljs", "src/app/main/ui/viewer.cljs" ] - }, - "viewer.header.dont-show-interactions" : { - "translations" : { - "de" : "Interaktionen nicht anzeigen", - "en" : "Don't show interactions", - "es" : "No mostrar interacciones", - "fr" : "Ne pas afficher les interactions", - "ru" : "Не показывать взаимодействия", - "zh_cn" : "不显示交互" - }, - "used-in" : [ "src/app/main/ui/viewer/header.cljs" ] - }, - "viewer.header.edit-page" : { - "translations" : { - "de" : "Seite bearbeiten", - "en" : "Edit page", - "es" : "Editar página", - "fr" : "Modifier la page", - "ru" : "Редактировать страницу", - "zh_cn" : "编辑页面" - }, - "used-in" : [ "src/app/main/ui/viewer/header.cljs" ] - }, - "viewer.header.fullscreen" : { - "translations" : { - "de" : "Vollbildmodus", - "en" : "Full Screen", - "es" : "Pantalla completa", - "fr" : "Plein écran", - "ru" : "Полный экран", - "zh_cn" : "全屏" - }, - "used-in" : [ "src/app/main/ui/viewer/header.cljs" ] - }, - "viewer.header.share.copy-link" : { - "translations" : { - "de" : "Link kopieren", - "en" : "Copy link", - "es" : "Copiar enlace", - "fr" : "Copier le lien", - "ru" : "Копировать ссылку", - "zh_cn" : "复制链接" - }, - "used-in" : [ "src/app/main/ui/viewer/header.cljs" ] - }, - "viewer.header.share.create-link" : { - "translations" : { - "de" : "Link erstellen", - "en" : "Create link", - "es" : "Crear enlace", - "fr" : "Créer le lien", - "ru" : "Создать ссылку", - "zh_cn" : "创建链接" - }, - "used-in" : [ "src/app/main/ui/viewer/header.cljs" ] - }, - "viewer.header.share.placeholder" : { - "translations" : { - "de" : "Der Link fürs Teilen wird hier angezeigt", - "en" : "Share link will appear here", - "es" : "El enlace para compartir aparecerá aquí", - "fr" : "Le lien de partage apparaîtra ici", - "ru" : "Здесь будет ссылка для обмена", - "zh_cn" : "分享链接将会显示在这里" - }, - "used-in" : [ "src/app/main/ui/viewer/header.cljs" ] - }, - "viewer.header.share.remove-link" : { - "translations" : { - "de" : "Link entfernen", - "en" : "Remove link", - "es" : "Eliminar enlace", - "fr" : "Supprimer le lien", - "ru" : "Удалить ссылку", - "zh_cn" : "移除链接" - }, - "used-in" : [ "src/app/main/ui/viewer/header.cljs" ] - }, - "viewer.header.share.subtitle" : { - "translations" : { - "de" : "Jeder mit dem Link hat Zugriff", - "en" : "Anyone with the link will have access", - "es" : "Cualquiera con el enlace podrá acceder", - "fr" : "Toute personne disposant du lien aura accès", - "ru" : "Любой, у кого есть ссылка будет иметь доступ", - "zh_cn" : "任何人都可以通过本链接访问" - }, - "used-in" : [ "src/app/main/ui/viewer/header.cljs" ] - }, - "viewer.header.share.title" : { - "translations" : { - "de" : "Link teilen", - "en" : "Share link", - "es" : "Enlace", - "fr" : "Lien de partage", - "ru" : "Поделиться ссылкой", - "zh_cn" : "分享链接" - }, - "used-in" : [ "src/app/main/ui/viewer/header.cljs", "src/app/main/ui/viewer/header.cljs", "src/app/main/ui/viewer/header.cljs" ] - }, - "viewer.header.show-interactions" : { - "translations" : { - "de" : "Interaktionen anzeigen", - "en" : "Show interactions", - "es" : "Mostrar interacciones", - "fr" : "Afficher les interactions", - "ru" : "Показывать взаимодействия", - "zh_cn" : "显示交互" - }, - "used-in" : [ "src/app/main/ui/viewer/header.cljs" ] - }, - "viewer.header.show-interactions-on-click" : { - "translations" : { - "de" : "Interaktionen beim Klicken anzeigen", - "en" : "Show interactions on click", - "es" : "Mostrar interacciones al hacer click", - "fr" : "Afficher les interactions au clic", - "ru" : "Показывать взаимодействия по клику", - "zh_cn" : "点击时显示交互" - }, - "used-in" : [ "src/app/main/ui/viewer/header.cljs" ] - }, - "viewer.header.sitemap" : { - "translations" : { - "de" : "Sitemap", - "en" : "Sitemap", - "es" : "Mapa del sitio", - "fr" : "Plan du site", - "ru" : "План сайта", - "zh_cn" : "站点地图" - }, - "used-in" : [ "src/app/main/ui/viewer/header.cljs" ] - }, - "workspace.align.hcenter" : { - "translations" : { - "de" : "Zentrieren (horizontal)", - "en" : "Align horizontal center", - "es" : "Alinear al centro", - "fr" : "Aligner horizontalement au centre", - "ru" : "Выровнять по горизонтали", - "zh_cn" : "水平居中对齐" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/align.cljs" ] - }, - "workspace.align.hdistribute" : { - "translations" : { - "de" : "Horizontal verteilen", - "en" : "Distribute horizontal spacing", - "es" : "Distribuir espacio horizontal", - "fr" : "Répartir l’espacement horizontal", - "ru" : "Распределить горизонтальное пространство", - "zh_cn" : "水平均匀分布" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/align.cljs" ] - }, - "workspace.align.hleft" : { - "translations" : { - "de" : "Linksbündig ausrichten", - "en" : "Align left", - "es" : "Alinear a la izquierda", - "fr" : "Aligner à gauche", - "ru" : "Выровнять по левому краю", - "zh_cn" : "靠左对齐" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/align.cljs" ] - }, - "workspace.align.hright" : { - "translations" : { - "de" : "Rechtsbündig ausrichten", - "en" : "Align right", - "es" : "Alinear a la derecha", - "fr" : "Aligner à droite", - "ru" : "Выровнять по правому краю", - "zh_cn" : "靠右对齐" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/align.cljs" ] - }, - "workspace.align.vbottom" : { - "translations" : { - "de" : "Unten ausrichten", - "en" : "Align bottom", - "es" : "Alinear abajo", - "fr" : "Aligner en bas", - "ru" : "Выровнять по нижнему краю", - "zh_cn" : "底部对齐" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/align.cljs" ] - }, - "workspace.align.vcenter" : { - "translations" : { - "de" : "Mittig ausrichten (vertikal)", - "en" : "Align vertical center", - "es" : "Alinear al centro", - "fr" : "Aligner verticalement au centre", - "ru" : "Выровнять по вертикали", - "zh_cn" : "垂直居中对齐" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/align.cljs" ] - }, - "workspace.align.vdistribute" : { - "translations" : { - "de" : "Vertikal verteilen", - "en" : "Distribute vertical spacing", - "es" : "Distribuir espacio vertical", - "fr" : "Répartir l’espacement vertical", - "ru" : "Распределить вертикальное пространство", - "zh_cn" : "垂直均匀分布" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/align.cljs" ] - }, - "workspace.align.vtop" : { - "translations" : { - "de" : "Oben ausrichten", - "en" : "Align top", - "es" : "Alinear arriba", - "fr" : "Aligner en haut", - "ru" : "Выровнять по верхнему краю", - "zh_cn" : "顶部对齐" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/align.cljs" ] - }, - "workspace.assets.assets" : { - "translations" : { - "de" : "Assets", - "en" : "Assets", - "es" : "Recursos", - "fr" : "Ressources", - "ru" : "", - "zh_cn" : "素材" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs" ] - }, - "workspace.assets.box-filter-all" : { - "translations" : { - "de" : "Alle Assets", - "en" : "All assets", - "es" : "Todos", - "fr" : "Toutes", - "ru" : "", - "zh_cn" : "所有素材" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs" ] - }, - "workspace.assets.box-filter-graphics" : { - "translations" : { - "de" : "Grafiken", - "en" : "Graphics", - "es" : "Gráficos", - "fr" : "Graphiques", - "ru" : "", - "zh_cn" : "图形" - }, - "unused" : true - }, - "workspace.assets.colors" : { - "translations" : { - "de" : "Farben", - "en" : "Colors", - "es" : "Colores", - "fr" : "Couleurs", - "ru" : "", - "zh_cn" : "颜色" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs", "src/app/main/ui/workspace/sidebar/assets.cljs" ] - }, - "workspace.assets.components" : { - "translations" : { - "de" : "Komponente", - "en" : "Components", - "es" : "Componentes", - "fr" : "Composants", - "ru" : "", - "zh_cn" : "组件" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs", "src/app/main/ui/workspace/sidebar/assets.cljs" ] - }, - "workspace.assets.delete" : { - "translations" : { - "de" : "Löschen", - "en" : "Delete", - "es" : "Borrar", - "fr" : "Supprimer", - "ru" : "", - "zh_cn" : "删除" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/sitemap.cljs", "src/app/main/ui/workspace/sidebar/assets.cljs", "src/app/main/ui/workspace/sidebar/assets.cljs", "src/app/main/ui/workspace/sidebar/assets.cljs", "src/app/main/ui/workspace/sidebar/assets.cljs" ] - }, - "workspace.assets.duplicate" : { - "translations" : { - "de" : "Duplizieren", - "en" : "Duplicate", - "es" : "Duplicar", - "fr" : "Dupliquer", - "ru" : "", - "zh_cn" : "创建副本" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/sitemap.cljs", "src/app/main/ui/workspace/sidebar/assets.cljs" ] - }, - "workspace.assets.edit" : { - "translations" : { - "de" : "Bearbeiten", - "en" : "Edit", - "es" : "Editar", - "fr" : "Modifier", - "ru" : "", - "zh_cn" : "编辑" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs", "src/app/main/ui/workspace/sidebar/assets.cljs" ] - }, - "workspace.assets.file-library" : { - "translations" : { - "de" : "Dateibibliothek", - "en" : "File library", - "es" : "Biblioteca del archivo", - "fr" : "Bibliothèque du fichier", - "ru" : "", - "zh_cn" : "文档库" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs" ] - }, - "workspace.assets.graphics" : { - "translations" : { - "de" : "Grafiken", - "en" : "Graphics", - "es" : "Gráficos", - "fr" : "Graphiques", - "ru" : "", - "zh_cn" : "图形" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs", "src/app/main/ui/workspace/sidebar/assets.cljs" ] - }, - "workspace.assets.libraries" : { - "translations" : { - "de" : "Bibliotheken", - "en" : "Libraries", - "es" : "Bibliotecas", - "fr" : "Bibliothèques", - "ru" : "", - "zh_cn" : "库" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs" ] - }, - "workspace.assets.not-found" : { - "translations" : { - "de" : "Keine Assets gefunden", - "en" : "No assets found", - "es" : "No se encontraron recursos", - "fr" : "Aucune ressource trouvée", - "ru" : "", - "zh_cn" : "未找到素材" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs" ] - }, - "workspace.assets.rename" : { - "translations" : { - "de" : "Umbenennen", - "en" : "Rename", - "es" : "Renombrar", - "fr" : "Renommer", - "ru" : "", - "zh_cn" : "重命名" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/sitemap.cljs", "src/app/main/ui/workspace/sidebar/assets.cljs", "src/app/main/ui/workspace/sidebar/assets.cljs", "src/app/main/ui/workspace/sidebar/assets.cljs", "src/app/main/ui/workspace/sidebar/assets.cljs" ] - }, - "workspace.assets.search" : { - "translations" : { - "de" : "Assets suchen", - "en" : "Search assets", - "es" : "Buscar recursos", - "fr" : "Chercher des ressources", - "ru" : "", - "zh_cn" : "搜索素材" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs" ] - }, - "workspace.assets.shared" : { - "translations" : { - "de" : "GETEILT", - "en" : "SHARED", - "es" : "COMPARTIDA", - "fr" : "PARTAGÉ", - "ru" : "", - "zh_cn" : "共享的" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs" ] - }, - "workspace.assets.typography" : { - "translations" : { - "de" : "Typografien", - "en" : "Typographies", - "es" : "Tipografías", - "fr" : "Typographies", - "zh_cn" : "排版" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs", "src/app/main/ui/workspace/sidebar/assets.cljs" ] - }, - "workspace.assets.typography.font-id" : { - "translations" : { - "de" : "Schriftart", - "en" : "Font", - "es" : "Fuente", - "fr" : "Police", - "zh_cn" : "字体" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/typography.cljs" ] - }, - "workspace.assets.typography.font-size" : { - "translations" : { - "de" : "Größe", - "en" : "Size", - "es" : "Tamaño", - "fr" : "Taille", - "zh_cn" : "尺寸" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/typography.cljs" ] - }, - "workspace.assets.typography.font-variant-id" : { - "translations" : { - "de" : "Variante", - "en" : "Variant", - "es" : "Variante", - "fr" : "Variante", - "zh_cn" : "变体" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/typography.cljs" ] - }, - "workspace.assets.typography.go-to-edit" : { - "translations" : { - "de" : "Wechseln Sie zur Stilbibliotheksdatei, um sie zu bearbeiten", - "en" : "Go to style library file to edit", - "es" : "Ir al archivo de la biblioteca del estilo para editar", - "fr" : "Accéder au fichier de bibliothèque de styles à modifier", - "zh_cn" : "前往样式库文件进行编辑" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/typography.cljs" ] - }, - "workspace.assets.typography.letter-spacing" : { - "translations" : { - "de" : "Zeichenabstand", - "en" : "Letter Spacing", - "es" : "Interletrado", - "fr" : "Interlettrage", - "zh_cn" : "字距" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/typography.cljs" ] - }, - "workspace.assets.typography.line-height" : { - "translations" : { - "de" : "Zeilenabstand", - "en" : "Line Height", - "es" : "Interlineado", - "fr" : "Interlignage", - "zh_cn" : "行高" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/typography.cljs" ] - }, - "workspace.assets.typography.sample" : { - "translations" : { - "de" : "Ag", - "en" : "Ag", - "es" : "Ag", - "fr" : "Ag", - "zh_cn" : "Ag" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/typography.cljs", "src/app/main/ui/handoff/attributes/text.cljs", "src/app/main/ui/handoff/attributes/text.cljs" ] - }, - "workspace.assets.typography.text-transform" : { - "translations" : { - "de" : "Texttransformation", - "en" : "Text Transform", - "es" : "Transformar texto", - "fr" : "Transformer le texte", - "zh_cn" : "文本变换" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/typography.cljs" ] - }, - "workspace.gradients.linear" : { - "translations" : { - "de" : "Linearer Farbverlauf", - "en" : "Linear gradient", - "es" : "Degradado lineal", - "fr" : "Dégradé linéaire", - "zh_cn" : "线性渐变" - }, - "used-in" : [ "src/app/main/data/workspace/libraries.cljs", "src/app/main/ui/components/color_bullet.cljs" ] - }, - "workspace.gradients.radial" : { - "translations" : { - "de" : "Radialer Farbverlauf", - "en" : "Radial gradient", - "es" : "Degradado radial", - "fr" : "Dégradé radial", - "zh_cn" : "放射渐变" - }, - "used-in" : [ "src/app/main/data/workspace/libraries.cljs", "src/app/main/ui/components/color_bullet.cljs" ] - }, - "workspace.header.menu.disable-dynamic-alignment" : { - "translations" : { - "de" : "Dynamische Ausrichtung deaktivieren", - "en" : "Disable dynamic alignment", - "es" : "Desactivar alineamiento dinámico", - "fr" : "Désactiver l’alignement dynamique", - "ru" : "Отключить активное выравнивание", - "zh_cn" : "禁用动态对齐" - }, - "used-in" : [ "src/app/main/ui/workspace/header.cljs" ] - }, - "workspace.header.menu.disable-snap-grid" : { - "translations" : { - "de" : "Am Raster ausrichten deaktivieren", - "en" : "Disable snap to grid", - "es" : "Desactivar alinear a la rejilla", - "fr" : "Désactiver l’alignement sur la grille", - "ru" : "Отключить привязку к сетке", - "zh_cn" : "禁用吸附到网格" - }, - "used-in" : [ "src/app/main/ui/workspace/header.cljs" ] - }, - "workspace.header.menu.enable-dynamic-alignment" : { - "translations" : { - "de" : "Dynamische Ausrichtung aktivieren", - "en" : "Enable dynamic aligment", - "es" : "Activar alineamiento dinámico", - "fr" : "Activer l’alignement dynamique", - "ru" : "Включить активное выравнивание", - "zh_cn" : "启用动态对齐" - }, - "used-in" : [ "src/app/main/ui/workspace/header.cljs" ] - }, - "workspace.header.menu.enable-snap-grid" : { - "translations" : { - "de" : "Am Raster ausrichten", - "en" : "Snap to grid", - "es" : "Alinear a la rejilla", - "fr" : "Aligner sur la grille", - "ru" : "Привяка к сетке", - "zh_cn" : "吸附到网格" - }, - "used-in" : [ "src/app/main/ui/workspace/header.cljs" ] - }, - "workspace.header.menu.hide-assets" : { - "translations" : { - "de" : "Assets ausblenden", - "en" : "Hide assets", - "es" : "Ocultar recursos", - "fr" : "Masquer les ressources", - "ru" : "", - "zh_cn" : "隐藏素材" - }, - "used-in" : [ "src/app/main/ui/workspace/header.cljs" ] - }, - "workspace.header.menu.hide-grid" : { - "translations" : { - "de" : "Raster ausblenden", - "en" : "Hide grids", - "es" : "Ocultar rejillas", - "fr" : "Masquer la grille", - "ru" : "Спрятать сетку", - "zh_cn" : "隐藏网格" - }, - "used-in" : [ "src/app/main/ui/workspace/header.cljs" ] - }, - "workspace.header.menu.hide-layers" : { - "translations" : { - "de" : "Ebenen ausblenden", - "en" : "Hide layers", - "es" : "Ocultar capas", - "fr" : "Masquer les calques", - "ru" : "Спрятать слои", - "zh_cn" : "隐藏图层" - }, - "used-in" : [ "src/app/main/ui/workspace/header.cljs" ] - }, - "workspace.header.menu.hide-palette" : { - "translations" : { - "de" : "Farbpalette ausblenden", - "en" : "Hide color palette", - "es" : "Ocultar paleta de colores", - "fr" : "Masquer la palette de couleurs", - "ru" : "Спрятать палитру цветов", - "zh_cn" : "隐藏调色盘" - }, - "used-in" : [ "src/app/main/ui/workspace/header.cljs" ] - }, - "workspace.header.menu.hide-rules" : { - "translations" : { - "de" : "Lineale ausblenden", - "en" : "Hide rules", - "es" : "Ocultar reglas", - "fr" : "Masquer les règles", - "ru" : "Спрятать линейки", - "zh_cn" : "隐藏标尺" - }, - "used-in" : [ "src/app/main/ui/workspace/header.cljs" ] - }, - "workspace.header.menu.select-all" : { - "translations" : { - "de" : "Alles auswählen", - "en" : "Select all", - "es" : "Seleccionar todo", - "fr" : "Tout sélectionner", - "ru" : "", - "zh_cn" : "全选" - }, - "used-in" : [ "src/app/main/ui/workspace/header.cljs" ] - }, - "workspace.header.menu.show-assets" : { - "translations" : { - "de" : "Assets einblenden", - "en" : "Show assets", - "es" : "Mostrar recursos", - "fr" : "Montrer les ressources", - "ru" : "", - "zh_cn" : "显示素材" - }, - "used-in" : [ "src/app/main/ui/workspace/header.cljs" ] - }, - "workspace.header.menu.show-grid" : { - "translations" : { - "de" : "Raster einblenden", - "en" : "Show grid", - "es" : "Mostrar rejilla", - "fr" : "Montrer la grille", - "ru" : "Показать сетку", - "zh_cn" : "显示网格" - }, - "used-in" : [ "src/app/main/ui/workspace/header.cljs" ] - }, - "workspace.header.menu.show-layers" : { - "translations" : { - "de" : "Ebenen einblenden", - "en" : "Show layers", - "es" : "Mostrar capas", - "fr" : "Montrer les calques", - "ru" : "Показать слои", - "zh_cn" : "显示图层" - }, - "used-in" : [ "src/app/main/ui/workspace/header.cljs" ] - }, - "workspace.header.menu.show-palette" : { - "translations" : { - "de" : "Farbpalette einblenden", - "en" : "Show color palette", - "es" : "Mostrar paleta de colores", - "fr" : "Montrer la palette de couleurs", - "ru" : "Показать палитру цветов", - "zh_cn" : "显示调色盘" - }, - "used-in" : [ "src/app/main/ui/workspace/header.cljs" ] - }, - "workspace.header.menu.show-rules" : { - "translations" : { - "de" : "Lineale einblenden", - "en" : "Show rules", - "es" : "Mostrar reglas", - "fr" : "Montrer les règles", - "ru" : "Показать линейки", - "zh_cn" : "显示标尺" - }, - "used-in" : [ "src/app/main/ui/workspace/header.cljs" ] - }, - "workspace.header.save-error" : { - "translations" : { - "de" : "Fehler beim Speichern", - "en" : "Error on saving", - "es" : "Error al guardar", - "fr" : "Erreur d’enregistrement", - "zh_cn" : "保存时发生错误" - }, - "used-in" : [ "src/app/main/ui/workspace/header.cljs" ] - }, - "workspace.header.saved" : { - "translations" : { - "de" : "Gespeichert", - "en" : "Saved", - "es" : "Guardado", - "fr" : "Enregistré", - "zh_cn" : "已保存" - }, - "used-in" : [ "src/app/main/ui/workspace/header.cljs" ] - }, - "workspace.header.saving" : { - "translations" : { - "de" : "Speichern", - "en" : "Saving", - "es" : "Guardando", - "fr" : "Enregistrement", - "zh_cn" : "正在保存" - }, - "used-in" : [ "src/app/main/ui/workspace/header.cljs" ] - }, - "workspace.header.unsaved" : { - "translations" : { - "de" : "Ungespeicherte Änderungen", - "en" : "Unsaved changes", - "es" : "Cambios sin guardar", - "fr" : "Modifications non sauvegardées", - "zh_cn" : "未保存的修改" - }, - "used-in" : [ "src/app/main/ui/workspace/header.cljs" ] - }, - "workspace.header.viewer" : { - "translations" : { - "de" : "Ansichtsmodus (%s)", - "en" : "View mode (%s)", - "es" : "Modo de visualización (%s)", - "fr" : "Mode spectateur (%s)", - "ru" : "Режим просмотра (%s)", - "zh_cn" : "预览模式(%s)" - }, - "used-in" : [ "src/app/main/ui/workspace/header.cljs" ] - }, - "workspace.libraries.add" : { - "translations" : { - "de" : "Hinzufügen", - "en" : "Add", - "es" : "Añadir", - "fr" : "Ajouter", - "ru" : "", - "zh_cn" : "添加" - }, - "used-in" : [ "src/app/main/ui/workspace/libraries.cljs" ] - }, - "workspace.libraries.colors" : { - "translations" : { - "de" : "%s Farben", - "en" : "%s colors", - "es" : "%s colors", - "fr" : "%s couleurs", - "ru" : "", - "zh_cn" : "%s种颜色" - }, - "used-in" : [ "src/app/main/ui/workspace/libraries.cljs" ] - }, - "workspace.libraries.colors.big-thumbnails" : { - "translations" : { - "de" : "Große Miniaturen", - "en" : "Big thumbnails", - "es" : "Miniaturas grandes", - "fr" : "Grandes vignettes", - "zh_cn" : "大缩略图" - }, - "used-in" : [ "src/app/main/ui/workspace/colorpalette.cljs" ] - }, - "workspace.libraries.colors.file-library" : { - "translations" : { - "de" : "Dateibibliothek", - "en" : "File library", - "es" : "Biblioteca del archivo", - "fr" : "Bibliothèque du fichier", - "zh_cn" : "文档库" - }, - "used-in" : [ "src/app/main/ui/workspace/colorpicker/libraries.cljs", "src/app/main/ui/workspace/colorpalette.cljs" ] - }, - "workspace.libraries.colors.recent-colors" : { - "translations" : { - "de" : "Aktuelle Farben", - "en" : "Recent colors", - "es" : "Colores recientes", - "fr" : "Couleurs récentes", - "zh_cn" : "最近使用的颜色" - }, - "used-in" : [ "src/app/main/ui/workspace/colorpicker/libraries.cljs", "src/app/main/ui/workspace/colorpalette.cljs" ] - }, - "workspace.libraries.colors.save-color" : { - "translations" : { - "de" : "Farbstil speichern", - "en" : "Save color style", - "es" : "Guardar estilo de color", - "fr" : "Enregistrer le style de couleur", - "zh_cn" : "保存颜色风格" - }, - "used-in" : [ "src/app/main/ui/workspace/colorpicker.cljs" ] - }, - "workspace.libraries.colors.small-thumbnails" : { - "translations" : { - "de" : "Kleine Miniaturen", - "en" : "Small thumbnails", - "es" : "Miniaturas pequeñas", - "fr" : "Petites vignettes", - "zh_cn" : "小缩略图" - }, - "used-in" : [ "src/app/main/ui/workspace/colorpalette.cljs" ] - }, - "workspace.libraries.components" : { - "translations" : { - "de" : "%s Komponenten", - "en" : "%s components", - "es" : "%s componentes", - "fr" : "%s composants", - "ru" : "", - "zh_cn" : "%s个组件" - }, - "used-in" : [ "src/app/main/ui/workspace/libraries.cljs" ] - }, - "workspace.libraries.file-library" : { - "translations" : { - "de" : "Dateibibliothek", - "en" : "File library", - "es" : "Biblioteca de este archivo", - "fr" : "Bibliothèque du fichier", - "ru" : "", - "zh_cn" : "文档库" - }, - "used-in" : [ "src/app/main/ui/workspace/libraries.cljs" ] - }, - "workspace.libraries.graphics" : { - "translations" : { - "de" : "%s Grafik(en)", - "en" : "%s graphics", - "es" : "%s gráficos", - "fr" : "%s graphiques", - "ru" : "", - "zh_cn" : "%s个图形" - }, - "used-in" : [ "src/app/main/ui/workspace/libraries.cljs" ] - }, - "workspace.libraries.in-this-file" : { - "translations" : { - "de" : "BIBLIOTHEKEN IN DIESER DATEI", - "en" : "LIBRARIES IN THIS FILE", - "es" : "BIBLIOTECAS EN ESTE ARCHIVO", - "fr" : "BIBLIOTHÈQUES DANS CE FICHIER", - "ru" : "", - "zh_cn" : "本文档中的库" - }, - "used-in" : [ "src/app/main/ui/workspace/libraries.cljs" ] - }, - "workspace.libraries.libraries" : { - "translations" : { - "de" : "BIBLIOTHEKEN", - "en" : "LIBRARIES", - "es" : "BIBLIOTECAS", - "fr" : "BIBLIOTHÈQUES", - "ru" : "", - "zh_cn" : "库" - }, - "used-in" : [ "src/app/main/ui/workspace/libraries.cljs" ] - }, - "workspace.libraries.library" : { - "translations" : { - "de" : "BIBLIOTHEK", - "en" : "LIBRARY", - "es" : "BIBLIOTECA", - "fr" : "BIBLIOTHÈQUE", - "ru" : "", - "zh_cn" : "库" - }, - "used-in" : [ "src/app/main/ui/workspace/libraries.cljs" ] - }, - "workspace.libraries.no-libraries-need-sync" : { - "translations" : { - "de" : "Es gibt keine gemeinsam genutzte Bibliotheken, die aktualisiert werden müssen", - "en" : "There are no Shared Libraries that need update", - "es" : "No hay bibliotecas que necesiten ser actualizadas", - "fr" : "Aucune Bibliothèque Partagée n’a besoin d’être mise à jour", - "ru" : "", - "zh_cn" : "没有需要更新的共享库" - }, - "used-in" : [ "src/app/main/ui/workspace/libraries.cljs" ] - }, - "workspace.libraries.no-matches-for" : { - "translations" : { - "de" : "Keine Übereinstimmungen für “%s“ gefunden", - "en" : "No matches found for “%s“", - "es" : "No se encuentra “%s“", - "fr" : "Aucune correspondance pour « %s »", - "ru" : "Совпадений для “%s“ не найдено", - "zh_cn" : "没有找到“%s”的匹配项" - }, - "used-in" : [ "src/app/main/ui/workspace/libraries.cljs" ] - }, - "workspace.libraries.no-shared-libraries-available" : { - "translations" : { - "de" : "Es sind keine gemeinsam genutzte Bibliotheken verfügbar", - "en" : "There are no Shared Libraries available", - "es" : "No hay bibliotecas compartidas disponibles", - "fr" : "Aucune Bibliothèque Partagée disponible", - "ru" : "", - "zh_cn" : "没有可用的共享库" - }, - "used-in" : [ "src/app/main/ui/workspace/libraries.cljs" ] - }, - "workspace.libraries.search-shared-libraries" : { - "translations" : { - "de" : "Suche nach gemeinsam genutzten Bibliotheken", - "en" : "Search shared libraries", - "es" : "Buscar bibliotecas compartidas", - "fr" : "Rechercher des Bibliothèques Partagées", - "ru" : "", - "zh_cn" : "搜索共享库" - }, - "used-in" : [ "src/app/main/ui/workspace/libraries.cljs" ] - }, - "workspace.libraries.shared-libraries" : { - "translations" : { - "de" : "GEMEINSAM GENUTZTE BIBLIOTHEKEN", - "en" : "SHARED LIBRARIES", - "es" : "BIBLIOTECAS COMPARTIDAS", - "fr" : "BIBLIOTHÈQUES PARTAGÉES", - "ru" : "", - "zh_cn" : "共享库" - }, - "used-in" : [ "src/app/main/ui/workspace/libraries.cljs" ] - }, - "workspace.libraries.text.multiple-typography" : { - "translations" : { - "de" : "Mehrere Typografien", - "en" : "Multiple typographies", - "es" : "Varias tipografías", - "fr" : "Multiple typographies", - "zh_cn" : "复合排版" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/text.cljs" ] - }, - "workspace.libraries.text.multiple-typography-tooltip" : { - "translations" : { - "de" : "Verknüpfung aller Typografien aufheben", - "en" : "Unlink all typographies", - "es" : "Desvincular todas las tipografías", - "fr" : "Dissocier toutes les typographies", - "zh_cn" : "断开所有排版的链接" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/text.cljs" ] - }, - "workspace.libraries.typography" : { - "translations" : { - "de" : "%s Typografien", - "en" : "%s typographies", - "es" : "%s tipografías", - "fr" : "%s typographies", - "zh_cn" : "%s个排版" - }, - "used-in" : [ "src/app/main/ui/workspace/libraries.cljs" ] - }, - "workspace.libraries.update" : { - "translations" : { - "de" : "Aktualisieren", - "en" : "Update", - "es" : "Actualizar", - "fr" : "Actualiser", - "ru" : "", - "zh_cn" : "更新" - }, - "used-in" : [ "src/app/main/ui/workspace/libraries.cljs" ] - }, - "workspace.libraries.updates" : { - "translations" : { - "de" : "AKTUALISIERUNG", - "en" : "UPDATES", - "es" : "ACTUALIZACIONES", - "fr" : "MISES À JOUR", - "ru" : "", - "zh_cn" : "更新" - }, - "used-in" : [ "src/app/main/ui/workspace/libraries.cljs" ] - }, - "workspace.library.all" : { - "translations" : { - "de" : "Alle Bibliotheken", - "en" : "All libraries", - "es" : "Todas", - "fr" : "Toutes les bibliothèques", - "ru" : "Все библиотеки", - "zh_cn" : "所有库" - }, - "unused" : true - }, - "workspace.library.libraries" : { - "translations" : { - "de" : "Bibliotheken", - "en" : "Libraries", - "es" : "Bibliotecas", - "fr" : "Bibliothèques", - "ru" : "Библиотеки", - "zh_cn" : "库" - }, - "unused" : true - }, - "workspace.library.own" : { - "translations" : { - "de" : "Meine Bibliotheken", - "en" : "My libraries", - "es" : "Mis bibliotecas", - "fr" : "Mes bibliothèques", - "ru" : "Мои библиотеки", - "zh_cn" : "我的库" - }, - "unused" : true - }, - "workspace.library.store" : { - "translations" : { - "de" : "Gespeicherte Bibliotheken", - "en" : "Store libraries", - "es" : "Predefinidas", - "fr" : "Prédéfinies", - "ru" : "Сохраненные библиотеки", - "zh_cn" : "来自商店的库" - }, - "unused" : true - }, - "workspace.options.blur-options.background-blur" : { - "translations" : { - "de" : "Hintergrund", - "en" : "Background", - "es" : "Fondo", - "fr" : "Fond", - "zh_cn" : "背景" - }, - "unused" : true - }, - "workspace.options.blur-options.layer-blur" : { - "translations" : { - "de" : "Ebene", - "en" : "Layer", - "es" : "Capa", - "fr" : "Calque", - "zh_cn" : "图层" - }, - "unused" : true - }, - "workspace.options.blur-options.title" : { - "translations" : { - "de" : "Weichzeichnen", - "en" : "Blur", - "es" : "Desenfoque", - "fr" : "Flou", - "zh_cn" : "模糊" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/blur.cljs" ] - }, - "workspace.options.blur-options.title.group" : { - "translations" : { - "de" : "Gruppe weichzeichnen", - "en" : "Group blur", - "es" : "Desenfoque del grupo", - "fr" : "Flou de groupe", - "zh_cn" : "编组模糊" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/blur.cljs" ] - }, - "workspace.options.blur-options.title.multiple" : { - "translations" : { - "de" : "Auswahl weichzeichnen", - "en" : "Selection blur", - "es" : "Desenfoque de la selección", - "fr" : "Flou de sélection", - "zh_cn" : "选项模糊" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/blur.cljs" ] - }, - "workspace.options.canvas-background" : { - "translations" : { - "de" : "Hintergrundfarbe", - "en" : "Canvas background", - "es" : "Color de fondo", - "fr" : "Couleur de fond du canvas", - "ru" : "Фон холста", - "zh_cn" : "画布背景" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/page.cljs" ] - }, - "workspace.options.component" : { - "translations" : { - "de" : "Komponente", - "en" : "Component", - "es" : "Componente", - "fr" : "Composant", - "zh_cn" : "组件" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/component.cljs" ] - }, - "workspace.options.design" : { - "translations" : { - "de" : "Design", - "en" : "Design", - "es" : "Diseño", - "fr" : "Conception", - "ru" : "Дизайн", - "zh_cn" : "设计" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options.cljs" ] - }, - "workspace.options.export" : { - "translations" : { - "de" : "Exportieren", - "en" : "Export", - "es" : "Exportar", - "fr" : "Export", - "ru" : "Экспорт", - "zh_cn" : "导出" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/exports.cljs", "src/app/main/ui/handoff/exports.cljs" ] - }, - "workspace.options.export-object" : { - "translations" : { - "de" : "Form exportieren", - "en" : "Export shape", - "es" : "Exportar forma", - "fr" : "Exporter la forme", - "ru" : "Экспорт фигуры", - "zh_cn" : "导出形状" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/exports.cljs", "src/app/main/ui/handoff/exports.cljs" ] - }, - "workspace.options.export.suffix" : { - "translations" : { - "de" : "Suffix", - "en" : "Suffix", - "es" : "Sufijo", - "fr" : "Suffixe", - "zh_cn" : "后缀" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/exports.cljs" ] - }, - "workspace.options.exporting-object" : { - "translations" : { - "de" : "Exportiere…", - "en" : "Exporting…", - "es" : "Exportando", - "fr" : "Export en cours…", - "ru" : "Экспортирую…", - "zh_cn" : "正在导出…" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/exports.cljs", "src/app/main/ui/handoff/exports.cljs" ] - }, - "workspace.options.fill" : { - "translations" : { - "de" : "Fläche", - "en" : "Fill", - "es" : "Relleno", - "fr" : "Remplissage", - "ru" : "Заливка", - "zh_cn" : "填充" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/fill.cljs" ] - }, - "workspace.options.grid.auto" : { - "translations" : { - "de" : "Automatisch", - "en" : "Auto", - "es" : "Automático", - "fr" : "Automatique", - "ru" : "Авто", - "zh_cn" : "自动" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs" ] - }, - "workspace.options.grid.column" : { - "translations" : { - "de" : "Spalten", - "en" : "Columns", - "es" : "Columnas", - "fr" : "Colonnes", - "ru" : "Колонки", - "zh_cn" : "列" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs" ] - }, - "workspace.options.grid.params.columns" : { - "translations" : { - "de" : "Spalten", - "en" : "Columns", - "es" : "Columnas", - "fr" : "Colonnes", - "ru" : "Колонки", - "zh_cn" : "列" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs" ] - }, - "workspace.options.grid.params.gutter" : { - "translations" : { - "de" : "Zwischenräume", - "en" : "Gutter", - "es" : "Espaciado", - "fr" : "Gouttière", - "ru" : "Желоб", - "zh_cn" : "栅格间隔" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs" ] - }, - "workspace.options.grid.params.height" : { - "translations" : { - "de" : "Höhe", - "en" : "Height", - "es" : "Altura", - "fr" : "Hauteur", - "ru" : "Высота", - "zh_cn" : "高" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs" ] - }, - "workspace.options.grid.params.margin" : { - "translations" : { - "de" : "Rand", - "en" : "Margin", - "es" : "Margen", - "fr" : "Marge", - "ru" : "Поле", - "zh_cn" : "外边距" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs" ] - }, - "workspace.options.grid.params.rows" : { - "translations" : { - "de" : "Zeilen", - "en" : "Rows", - "es" : "Filas", - "fr" : "Lignes", - "ru" : "Строки", - "zh_cn" : "行" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs" ] - }, - "workspace.options.grid.params.set-default" : { - "translations" : { - "de" : "Als Standard festlegen", - "en" : "Set as default", - "es" : "Establecer valor por defecto", - "fr" : "Définir par défaut", - "ru" : "Установить по умолчанию", - "zh_cn" : "设为默认" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs" ] - }, - "workspace.options.grid.params.size" : { - "translations" : { - "de" : "Größe", - "en" : "Size", - "es" : "Tamaño", - "fr" : "Taille", - "ru" : "Размер", - "zh_cn" : "尺寸" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs" ] - }, - "workspace.options.grid.params.type" : { - "translations" : { - "de" : "Art", - "en" : "Type", - "es" : "Tipo", - "fr" : "Type", - "ru" : "Тип", - "zh_cn" : "类型" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs" ] - }, - "workspace.options.grid.params.type.bottom" : { - "translations" : { - "de" : "Unten", - "en" : "Bottom", - "es" : "Abajo", - "fr" : "Bas", - "ru" : "Низ", - "zh_cn" : "底" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs" ] - }, - "workspace.options.grid.params.type.center" : { - "translations" : { - "de" : "Zentriert", - "en" : "Center", - "es" : "Centro", - "fr" : "Centre", - "ru" : "Центр", - "zh_cn" : "居中" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs" ] - }, - "workspace.options.grid.params.type.left" : { - "translations" : { - "de" : "Links", - "en" : "Left", - "es" : "Izquierda", - "fr" : "Gauche", - "ru" : "Левый", - "zh_cn" : "左" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs" ] - }, - "workspace.options.grid.params.type.right" : { - "translations" : { - "de" : "Rechts", - "en" : "Right", - "es" : "Derecha", - "fr" : "Droite", - "ru" : "Правый", - "zh_cn" : "右" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs" ] - }, - "workspace.options.grid.params.type.stretch" : { - "translations" : { - "de" : "Gestreckt", - "en" : "Stretch", - "es" : "Estirar", - "fr" : "Étirer", - "ru" : "Растягивать", - "zh_cn" : "拉伸" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs" ] - }, - "workspace.options.grid.params.type.top" : { - "translations" : { - "de" : "Oben", - "en" : "Top", - "es" : "Arriba", - "fr" : "Haut", - "ru" : "Верх", - "zh_cn" : "顶" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs" ] - }, - "workspace.options.grid.params.use-default" : { - "translations" : { - "de" : "Standardwerte verwenden", - "en" : "Use default", - "es" : "Usar valor por defecto", - "fr" : "Utiliser la valeur par défaut", - "ru" : "Использовать значение по умолчанию", - "zh_cn" : "使用默认" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs" ] - }, - "workspace.options.grid.params.width" : { - "translations" : { - "de" : "Breite", - "en" : "Width", - "es" : "Ancho", - "fr" : "Largeur", - "ru" : "Ширина", - "zh_cn" : "宽" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs" ] - }, - "workspace.options.grid.row" : { - "translations" : { - "de" : "Zeile", - "en" : "Rows", - "es" : "Filas", - "fr" : "Lignes", - "ru" : "Строки", - "zh_cn" : "行" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs" ] - }, - "workspace.options.grid.square" : { - "translations" : { - "de" : "Quadrat", - "en" : "Square", - "es" : "Cuadros", - "fr" : "Carré", - "ru" : "Квадрат", - "zh_cn" : "正方形" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs" ] - }, - "workspace.options.grid.title" : { - "translations" : { - "de" : "Raster & Layouts", - "en" : "Grid & Layouts", - "es" : "Rejilla & Estructuras", - "fr" : "Grille & Calques", - "ru" : "Сетка и Макеты", - "zh_cn" : "网格与布局" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs" ] - }, - "workspace.options.group-fill" : { - "translations" : { - "de" : "Gruppe füllen", - "en" : "Group fill", - "es" : "Relleno de grupo", - "fr" : "Remplissage de groupe", - "ru" : "Заливка для группы", - "zh_cn" : "编组填充" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/fill.cljs" ] - }, - "workspace.options.group-stroke" : { - "translations" : { - "de" : "Gruppe einrahmen", - "en" : "Group stroke", - "es" : "Borde de grupo", - "fr" : "Contour de groupe", - "ru" : "Обводка для группы", - "zh_cn" : "编组边框" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs" ] - }, - "workspace.options.layer-options.blend-mode.color" : { - "translations" : { - "de" : "Farbe", - "en" : "Color", - "es" : "Color" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/layer.cljs" ] - }, - "workspace.options.layer-options.blend-mode.color-burn" : { - "translations" : { - "de" : "Farbig nachbelichten", - "en" : "Color burn", - "es" : "Color más oscuro" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/layer.cljs" ] - }, - "workspace.options.layer-options.blend-mode.color-dodge" : { - "translations" : { - "de" : "Farbig abwedeln", - "en" : "Color dodge", - "es" : "Color más suave" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/layer.cljs" ] - }, - "workspace.options.layer-options.blend-mode.darken" : { - "translations" : { - "de" : "Abdunkeln", - "en" : "Darken", - "es" : "Oscurecer" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/layer.cljs" ] - }, - "workspace.options.layer-options.blend-mode.difference" : { - "translations" : { - "de" : "Differenz", - "en" : "Difference", - "es" : "Diferencia" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/layer.cljs" ] - }, - "workspace.options.layer-options.blend-mode.exclusion" : { - "translations" : { - "de" : "Ausschluss", - "en" : "Exclusion", - "es" : "Exclusión" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/layer.cljs" ] - }, - "workspace.options.layer-options.blend-mode.hard-light" : { - "translations" : { - "de" : "Hartes Licht", - "en" : "Hard light", - "es" : "Luz fuerte" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/layer.cljs" ] - }, - "workspace.options.layer-options.blend-mode.hue" : { - "translations" : { - "de" : "Farbton", - "en" : "Hue", - "es" : "Tono" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/layer.cljs" ] - }, - "workspace.options.layer-options.blend-mode.lighten" : { - "translations" : { - "de" : "Aufhellen", - "en" : "Lighten", - "es" : "Aclarar" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/layer.cljs" ] - }, - "workspace.options.layer-options.blend-mode.luminosity" : { - "translations" : { - "de" : "Luminanz", - "en" : "Luminosity", - "es" : "Luminosidad" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/layer.cljs" ] - }, - "workspace.options.layer-options.blend-mode.multiply" : { - "translations" : { - "de" : "Multiplizieren", - "en" : "Multiply", - "es" : "Multiplicar" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/layer.cljs" ] - }, - "workspace.options.layer-options.blend-mode.normal" : { - "translations" : { - "de" : "Normal", - "en" : "Normal", - "es" : "Normal" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/layer.cljs" ] - }, - "workspace.options.layer-options.blend-mode.overlay" : { - "translations" : { - "de" : "Ineinanderkopieren", - "en" : "Overlay", - "es" : "Superponer" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/layer.cljs" ] - }, - "workspace.options.layer-options.blend-mode.saturation" : { - "translations" : { - "de" : "Sättigung", - "en" : "Saturation", - "es" : "Saturación" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/layer.cljs" ] - }, - "workspace.options.layer-options.blend-mode.screen" : { - "translations" : { - "de" : "Negativ multiplizieren", - "en" : "Screen", - "es" : "Trama" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/layer.cljs" ] - }, - "workspace.options.layer-options.blend-mode.soft-light" : { - "translations" : { - "de" : "Weiches Licht", - "en" : "Soft light", - "es" : "Luz suave" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/layer.cljs" ] - }, - "workspace.options.layer-options.title" : { - "translations" : { - "de" : "Ebene", - "en" : "Layer", - "es" : "Capa" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/layer.cljs" ] - }, - "workspace.options.layer-options.title.group" : { - "translations" : { - "de" : "Ebenen gruppieren", - "en" : "Group layers", - "es" : "Capas de grupo" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/layer.cljs" ] - }, - "workspace.options.layer-options.title.multiple" : { - "translations" : { - "de" : "Ausgewählte Ebenen", - "en" : "Selected layers", - "es" : "Capas seleccionadas" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/layer.cljs" ] - }, - "workspace.options.navigate-to" : { - "translations" : { - "de" : "Navigiere zu", - "en" : "Navigate to", - "es" : "Navegar a", - "fr" : "Naviguer vers", - "ru" : "Перейти к", - "zh_cn" : "导航到" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs" ] - }, - "workspace.options.none" : { - "translations" : { - "de" : "Keine", - "en" : "None", - "es" : "Ninguno", - "fr" : "Aucun", - "ru" : "Не задано", - "zh_cn" : "无" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs" ] - }, - "workspace.options.position" : { - "translations" : { - "de" : "Position", - "en" : "Position", - "es" : "Posición", - "fr" : "Position", - "ru" : "Позиция", - "zh_cn" : "位置" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs", "src/app/main/ui/workspace/sidebar/options/menus/measures.cljs" ] - }, - "workspace.options.prototype" : { - "translations" : { - "de" : "Prototyp", - "en" : "Prototype", - "es" : "Prototipo", - "fr" : "Prototype", - "ru" : "Прототип", - "zh_cn" : "原型" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options.cljs" ] - }, - "workspace.options.radius" : { - "translations" : { - "de" : "Radius", - "en" : "Radius", - "es" : "Radio", - "fr" : "Rayon", - "ru" : "Радиус", - "zh_cn" : "圆角" - }, - "unused" : true - }, - "workspace.options.radius.all-corners" : { - "translations" : { - "de" : "Alle Ecken", - "en" : "All corners", - "es" : "Todas las esquinas", - "zh_cn" : "所有角" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/measures.cljs" ] - }, - "workspace.options.radius.single-corners" : { - "translations" : { - "de" : "Einzelne Ecken", - "en" : "Single corners", - "es" : "Esquinas individuales", - "zh_cn" : "单个角" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/measures.cljs" ] - }, - "workspace.options.rotation" : { - "translations" : { - "de" : "Drehung", - "en" : "Rotation", - "es" : "Rotación", - "fr" : "Rotation", - "ru" : "Вращение", - "zh_cn" : "旋转" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/measures.cljs" ] - }, - "workspace.options.select-a-shape" : { - "translations" : { - "de" : "Wählen Sie eine Form, Zeichenfläche oder Gruppe aus, um eine Verbindung zu einer anderen Zeichenfläche herzustellen.", - "en" : "Select a shape, artboard or group to drag a connection to other artboard.", - "es" : "Selecciona una figura, tablero o grupo para arrastrar una conexión a otro tablero.", - "fr" : "Sélectionnez une forme, un plan de travail ou un groupe pour faire glisser une connexion vers un autre plan de travail.", - "ru" : "Выберите фигуру, рабочую область или группу чтобы перенести связь на другую рабочую область.", - "zh_cn" : "选择一个形状、画板或编组,拖至另一个画板,以创建关联。" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs" ] - }, - "workspace.options.select-artboard" : { - "translations" : { - "de" : "Wählen Sie eine Zeichenfläche aus", - "en" : "Select artboard", - "es" : "Selecciona un tablero", - "fr" : "Sélectionner un plan de travail", - "ru" : "Выберите рабочую область", - "zh_cn" : "选择画板" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs" ] - }, - "workspace.options.selection-fill" : { - "translations" : { - "de" : "Auswahl füllen", - "en" : "Selection fill", - "es" : "Relleno de selección", - "fr" : "Remplissage de sélection", - "ru" : "Заливка выбранного", - "zh_cn" : "选项填充" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/fill.cljs" ] - }, - "workspace.options.selection-stroke" : { - "translations" : { - "de" : "Auswahl einrahmen", - "en" : "Selection stroke", - "es" : "Borde de selección", - "fr" : "Contour de sélection", - "ru" : "Обводка выбранного", - "zh_cn" : "选项边框" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs" ] - }, - "workspace.options.shadow-options.blur" : { - "translations" : { - "de" : "Weichzeichnen", - "en" : "Blur", - "es" : "Desenfoque", - "fr" : "Flou", - "zh_cn" : "模糊" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs" ] - }, - "workspace.options.shadow-options.drop-shadow" : { - "translations" : { - "de" : "Schlagschatten", - "en" : "Drop shadow", - "es" : "Sombra arrojada", - "fr" : "Ombre portée", - "zh_cn" : "外阴影" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs" ] - }, - "workspace.options.shadow-options.inner-shadow" : { - "translations" : { - "de" : "Schatten nach innen", - "en" : "Inner shadow", - "es" : "Sombra interior", - "fr" : "Ombre intérieure", - "zh_cn" : "内阴影" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs" ] - }, - "workspace.options.shadow-options.offsetx" : { - "translations" : { - "de" : "X", - "en" : "X", - "es" : "X", - "fr" : "X", - "zh_cn" : "X" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs" ] - }, - "workspace.options.shadow-options.offsety" : { - "translations" : { - "de" : "Y", - "en" : "Y", - "es" : "Y", - "fr" : "Y", - "zh_cn" : "Y" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs" ] - }, - "workspace.options.shadow-options.spread" : { - "translations" : { - "de" : "Streuung", - "en" : "Spread", - "es" : "Difusión", - "fr" : "Diffusion", - "zh_cn" : "展开" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs" ] - }, - "workspace.options.shadow-options.title" : { - "translations" : { - "de" : "Schatten", - "en" : "Shadow", - "es" : "Sombra", - "fr" : "Ombre", - "zh_cn" : "阴影" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs" ] - }, - "workspace.options.shadow-options.title.group" : { - "translations" : { - "de" : "Gruppe schattieren", - "en" : "Group shadow", - "es" : "Sombra del grupo", - "fr" : "Ombre de groupe", - "zh_cn" : "编组阴影" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs" ] - }, - "workspace.options.shadow-options.title.multiple" : { - "translations" : { - "de" : "Auswahl schattieren", - "en" : "Selection shadows", - "es" : "Sombras de la seleccíón", - "fr" : "Ombres de la sélection", - "zh_cn" : "选项阴影" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs" ] - }, - "workspace.options.size" : { - "translations" : { - "de" : "Größe", - "en" : "Size", - "es" : "Tamaño", - "fr" : "Taille", - "ru" : "Размер", - "zh_cn" : "尺寸" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs", "src/app/main/ui/workspace/sidebar/options/menus/measures.cljs" ] - }, - "workspace.options.size-presets" : { - "translations" : { - "de" : "Größenvoreinstellungen", - "en" : "Size presets", - "es" : "Tamaños predefinidos", - "fr" : "Tailles prédéfinies", - "ru" : "Предустановки для размеров", - "zh_cn" : "尺寸预设" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs" ] - }, - "workspace.options.stroke" : { - "translations" : { - "de" : "Rahmen", - "en" : "Stroke", - "es" : "Borde", - "fr" : "Bordure", - "ru" : "Обводка", - "zh_cn" : "边框" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs" ] - }, - "workspace.options.stroke.center" : { - "translations" : { - "de" : "Zentriert", - "en" : "Center", - "es" : "Centro", - "fr" : "Centre", - "ru" : "Центр", - "zh_cn" : "居中" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs" ] - }, - "workspace.options.stroke.dashed" : { - "translations" : { - "de" : "Gestrichelt", - "en" : "Dashed", - "es" : "Rayado", - "fr" : "Tirets", - "ru" : "Пунктирный", - "zh_cn" : "长虚线" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs" ] - }, - "workspace.options.stroke.dotted" : { - "translations" : { - "de" : "Gepunktet", - "en" : "Dotted", - "es" : "Punteado", - "fr" : "Pointillé", - "ru" : "Точечный", - "zh_cn" : "虚线" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs" ] - }, - "workspace.options.stroke.inner" : { - "translations" : { - "de" : "Innen", - "en" : "Inside", - "es" : "Interior", - "fr" : "Intérieur", - "ru" : "Внутрь", - "zh_cn" : "内部" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs" ] - }, - "workspace.options.stroke.mixed" : { - "translations" : { - "de" : "Mehrere", - "en" : "Mixed", - "es" : "Mezclado", - "fr" : "Mixte", - "ru" : "Смешаный", - "zh_cn" : "混合" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs" ] - }, - "workspace.options.stroke.outer" : { - "translations" : { - "de" : "Außen", - "en" : "Outside", - "es" : "Exterior", - "fr" : "Extérieur", - "ru" : "Наружу", - "zh_cn" : "外部" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs" ] - }, - "workspace.options.stroke.solid" : { - "translations" : { - "de" : "Solid", - "en" : "Solid", - "es" : "Sólido", - "fr" : "Solide", - "ru" : "Сплошной", - "zh_cn" : "实线" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs" ] - }, - "workspace.options.text-options.align-bottom" : { - "translations" : { - "de" : "Unten ausrichten", - "en" : "Align bottom", - "es" : "Alinear abajo", - "fr" : "Aligner en bas", - "ru" : "Выровнять низ", - "zh_cn" : "底部对齐" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/text.cljs" ] - }, - "workspace.options.text-options.align-center" : { - "translations" : { - "de" : "Zentrieren", - "en" : "Align center", - "es" : "Aliniear al centro", - "fr" : "Aligner au centre", - "ru" : "Выравнивание по центру", - "zh_cn" : "居中对齐" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/text.cljs" ] - }, - "workspace.options.text-options.align-justify" : { - "translations" : { - "de" : "Ausrichtung in der Breite", - "en" : "Justify", - "es" : "Justificar", - "fr" : "Justifier", - "ru" : "Выравнивание по ширине", - "zh_cn" : "整理" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/text.cljs" ] - }, - "workspace.options.text-options.align-left" : { - "translations" : { - "de" : "Linksbündig ausrichten", - "en" : "Align left", - "es" : "Alinear a la izquierda", - "fr" : "Aligner à gauche", - "ru" : "Выравнивание по левому краю", - "zh_cn" : "靠左对齐" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/text.cljs" ] - }, - "workspace.options.text-options.align-middle" : { - "translations" : { - "de" : "An Mitte ausrichten", - "en" : "Align middle", - "es" : "Alinear al centro", - "fr" : "Aligner verticalement au milieu", - "ru" : "Выравнивание по центру", - "zh_cn" : "中间对齐" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/text.cljs" ] - }, - "workspace.options.text-options.align-right" : { - "translations" : { - "de" : "Rechtsbündig ausrichten", - "en" : "Align right", - "es" : "Alinear a la derecha", - "fr" : "Aligner à droite", - "ru" : "Выравнивание по правому краю", - "zh_cn" : "靠右对齐" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/text.cljs" ] - }, - "workspace.options.text-options.align-top" : { - "translations" : { - "de" : "Oben ausrichten", - "en" : "Align top", - "es" : "Alinear arriba", - "fr" : "Aligner en haut", - "ru" : "Выравнивание по верхнему краю", - "zh_cn" : "顶部对齐" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/text.cljs" ] - }, - "workspace.options.text-options.decoration" : { - "translations" : { - "de" : "Textdekoration", - "en" : "Decoration", - "es" : "Decoración", - "fr" : "Décoration", - "ru" : "Оформление", - "zh_cn" : "装饰" - }, - "unused" : true - }, - "workspace.options.text-options.direction-ltr" : { - "translations" : { - "en" : "LTR", - "es" : "LTR" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/text.cljs" ] - }, - "workspace.options.text-options.direction-rtl" : { - "translations" : { - "en" : "RTL", - "es" : "RTL" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/text.cljs" ] - }, - "workspace.options.text-options.google" : { - "translations" : { - "de" : "Google", - "en" : "Google", - "es" : "Google", - "zh_cn" : "谷歌" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/typography.cljs" ] - }, - "workspace.options.text-options.grow-auto-height" : { - "translations" : { - "de" : "Automatische Höhe", - "en" : "Auto height", - "es" : "Alto automático", - "fr" : "Hauteur automatique", - "zh_cn" : "自动高度" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/text.cljs" ] - }, - "workspace.options.text-options.grow-auto-width" : { - "translations" : { - "de" : "Automatische Breite", - "en" : "Auto width", - "es" : "Ancho automático", - "fr" : "Largeur automatique", - "zh_cn" : "自动宽度" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/text.cljs" ] - }, - "workspace.options.text-options.grow-fixed" : { - "translations" : { - "de" : "Feste Größe", - "en" : "Fixed", - "es" : "Fijo", - "fr" : "Fixe", - "zh_cn" : "固定" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/text.cljs" ] - }, - "workspace.options.text-options.letter-spacing" : { - "translations" : { - "de" : "Zeichenabstand", - "en" : "Letter Spacing", - "es" : "Espaciado entre letras", - "fr" : "Interlettrage", - "ru" : "Межсимвольный интервал", - "zh_cn" : "字距" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/typography.cljs" ] - }, - "workspace.options.text-options.line-height" : { - "translations" : { - "de" : "Zeilenabstand", - "en" : "Line height", - "es" : "Altura de línea", - "fr" : "Interlignage", - "ru" : "Высота строки", - "zh_cn" : "行高" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/typography.cljs" ] - }, - "workspace.options.text-options.lowercase" : { - "translations" : { - "de" : "Kleinbuchstaben", - "en" : "Lowercase", - "es" : "Minúsculas", - "fr" : "Minuscule", - "ru" : "Нижний регистр", - "zh_cn" : "小写" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/typography.cljs" ] - }, - "workspace.options.text-options.none" : { - "translations" : { - "de" : "Keine", - "en" : "None", - "es" : "Nada", - "fr" : "Aucune", - "ru" : "Не задано", - "zh_cn" : "无" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/text.cljs", "src/app/main/ui/workspace/sidebar/options/menus/typography.cljs" ] - }, - "workspace.options.text-options.preset" : { - "translations" : { - "de" : "Vordefiniert", - "en" : "Preset", - "es" : "Predefinidos", - "zh_cn" : "预设" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/typography.cljs" ] - }, - "workspace.options.text-options.strikethrough" : { - "translations" : { - "de" : "Durchgestrichen", - "en" : "Strikethrough", - "es" : "Tachado", - "fr" : "Barré", - "ru" : "Перечеркнутый", - "zh_cn" : "删除线" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/text.cljs" ] - }, - "workspace.options.text-options.text-case" : { - "translations" : { - "de" : "Schriftauszeichnung", - "en" : "Case", - "es" : "Mayús/minús", - "fr" : "Casse", - "ru" : "Регистр", - "zh_cn" : "大小写模式" - }, - "unused" : true - }, - "workspace.options.text-options.title" : { - "translations" : { - "de" : "Text", - "en" : "Text", - "es" : "Texto", - "fr" : "Texte", - "ru" : "Текст", - "zh_cn" : "文本" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/text.cljs" ] - }, - "workspace.options.text-options.title-group" : { - "translations" : { - "de" : "Gruppe Text", - "en" : "Group text", - "es" : "Texto de grupo", - "fr" : "Texte de groupe", - "ru" : "Текст группы", - "zh_cn" : "编组文本" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/text.cljs" ] - }, - "workspace.options.text-options.title-selection" : { - "translations" : { - "de" : "Ausgewählter Text", - "en" : "Selection text", - "es" : "Texto de selección", - "fr" : "Texte de la sélection", - "ru" : "Выбранный текст", - "zh_cn" : "选项文本" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/text.cljs" ] - }, - "workspace.options.text-options.titlecase" : { - "translations" : { - "de" : "Kapitälchen", - "en" : "Title Case", - "es" : "Título", - "fr" : "Premières Lettres en Capitales", - "ru" : "Каждое слово с заглавной буквы", - "zh_cn" : "首字母大写" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/typography.cljs" ] - }, - "workspace.options.text-options.underline" : { - "translations" : { - "de" : "Unterstrichen", - "en" : "Underline", - "es" : "Subrayado", - "fr" : "Soulignage", - "ru" : "Подчеркнутый", - "zh_cn" : "下划线" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/text.cljs" ] - }, - "workspace.options.text-options.uppercase" : { - "translations" : { - "de" : "Großbuchstaben", - "en" : "Uppercase", - "es" : "Mayúsculas", - "fr" : "Majuscule", - "ru" : "Верхний регистр", - "zh_cn" : "大写" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/typography.cljs" ] - }, - "workspace.options.text-options.vertical-align" : { - "translations" : { - "de" : "Vertikal ausrichten", - "en" : "Vertical align", - "es" : "Alineación vertical", - "fr" : "Alignement vertical", - "ru" : "Вертикальное выравнивание", - "zh_cn" : "垂直对齐" - }, - "unused" : true - }, - "workspace.options.use-play-button" : { - "translations" : { - "de" : "Verwenden Sie die Wiedergabetaste in der Kopfzeile, um die Prototypansicht zu wechseln.", - "en" : "Use the play button at the header to run the prototype view.", - "es" : "Usa el botón de play de la cabecera para arrancar la vista de prototipo.", - "fr" : "Utilisez le bouton de lecture dans l’en‑tête pour exécuter la vue du prototype.", - "ru" : "Используй кнопку запуск в заголовке чтобы перейти на экран прототипа.", - "zh_cn" : "点击页面顶端的播放按钮预览原型。" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs" ] - }, - "workspace.shape.menu.back" : { - "translations" : { - "de" : "In den Hintergrund", - "en" : "Send to back", - "es" : "Enviar al fondo", - "fr" : "Envoyer au fond", - "zh_cn" : "移至底层" - }, - "used-in" : [ "src/app/main/ui/workspace/context_menu.cljs" ] - }, - "workspace.shape.menu.backward" : { - "translations" : { - "de" : "Eins nach hinten", - "en" : "Send backward", - "es" : "Enviar atrás", - "fr" : "Éloigner", - "zh_cn" : "向下移动一层" - }, - "used-in" : [ "src/app/main/ui/workspace/context_menu.cljs" ] - }, - "workspace.shape.menu.copy" : { - "translations" : { - "de" : "Kopieren", - "en" : "Copy", - "es" : "Copiar", - "fr" : "Copier", - "zh_cn" : "复制" - }, - "used-in" : [ "src/app/main/ui/workspace/context_menu.cljs" ] - }, - "workspace.shape.menu.create-component" : { - "translations" : { - "de" : "Komponente erstellen", - "en" : "Create component", - "es" : "Crear componente", - "fr" : "Créer un composant", - "zh_cn" : "创建组件" - }, - "used-in" : [ "src/app/main/ui/workspace/context_menu.cljs" ] - }, - "workspace.shape.menu.cut" : { - "translations" : { - "de" : "Ausschneiden", - "en" : "Cut", - "es" : "Cortar", - "fr" : "Couper", - "zh_cn" : "剪切" - }, - "used-in" : [ "src/app/main/ui/workspace/context_menu.cljs" ] - }, - "workspace.shape.menu.delete" : { - "translations" : { - "de" : "Löschen", - "en" : "Delete", - "es" : "Eliminar", - "fr" : "Supprimer", - "zh_cn" : "删除" - }, - "used-in" : [ "src/app/main/ui/workspace/context_menu.cljs" ] - }, - "workspace.shape.menu.detach-instance" : { - "translations" : { - "de" : "Instanz trennen", - "en" : "Detach instance", - "es" : "Desacoplar instancia", - "fr" : "Détacher l’instance", - "zh_cn" : "解绑实例" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/component.cljs", "src/app/main/ui/workspace/sidebar/options/menus/component.cljs", "src/app/main/ui/workspace/context_menu.cljs", "src/app/main/ui/workspace/context_menu.cljs" ] - }, - "workspace.shape.menu.duplicate" : { - "translations" : { - "de" : "Duplizieren", - "en" : "Duplicate", - "es" : "Duplicar", - "fr" : "Dupliquer", - "zh_cn" : "创建副本" - }, - "used-in" : [ "src/app/main/ui/workspace/context_menu.cljs" ] - }, - "workspace.shape.menu.edit" : { - "translations" : { - "de" : "Bearbeiten", - "en" : "Edit", - "es" : "Editar", - "zh_cn" : "编辑" - }, - "used-in" : [ "src/app/main/ui/workspace/context_menu.cljs" ] - }, - "workspace.shape.menu.flip-horizontal" : { - "translations" : { - "de" : "Horizontal spiegeln", - "en" : "Flip horizontal", - "es" : "Voltear horizontal", - "zh_cn" : "水平翻转" - }, - "used-in" : [ "src/app/main/ui/workspace/context_menu.cljs" ] - }, - "workspace.shape.menu.flip-vertical" : { - "translations" : { - "de" : "Vertikal spiegeln", - "en" : "Flip vertical", - "es" : "Voltear vertical", - "zh_cn" : "垂直翻转" - }, - "used-in" : [ "src/app/main/ui/workspace/context_menu.cljs" ] - }, - "workspace.shape.menu.forward" : { - "translations" : { - "de" : "Eins nach vorne", - "en" : "Bring forward", - "es" : "Mover hacia delante", - "fr" : "Avancer", - "zh_cn" : "向上移动一层" - }, - "used-in" : [ "src/app/main/ui/workspace/context_menu.cljs" ] - }, - "workspace.shape.menu.front" : { - "translations" : { - "de" : "In den Vordergrund", - "en" : "Bring to front", - "es" : "Mover al frente", - "fr" : "Amener au premier plan", - "zh_cn" : "移至顶层" - }, - "used-in" : [ "src/app/main/ui/workspace/context_menu.cljs" ] - }, - "workspace.shape.menu.go-main" : { - "translations" : { - "de" : "Zur Hauptkomponentendatei wechseln", - "en" : "Go to main component file", - "es" : "Ir al archivo del componente principal", - "fr" : "Aller au fichier du composant principal", - "zh_cn" : "前往主组件文档" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/component.cljs", "src/app/main/ui/workspace/context_menu.cljs" ] - }, - "workspace.shape.menu.group" : { - "translations" : { - "de" : "Gruppieren", - "en" : "Group", - "es" : "Grupo", - "fr" : "Groupe", - "zh_cn" : "编组" - }, - "used-in" : [ "src/app/main/ui/workspace/context_menu.cljs" ] - }, - "workspace.shape.menu.hide" : { - "translations" : { - "de" : "Ausblenden", - "en" : "Hide", - "es" : "Ocultar", - "fr" : "Masquer", - "zh_cn" : "隐藏" - }, - "used-in" : [ "src/app/main/ui/workspace/context_menu.cljs" ] - }, - "workspace.shape.menu.lock" : { - "translations" : { - "de" : "Sperren", - "en" : "Lock", - "es" : "Bloquear", - "fr" : "Bloquer", - "zh_cn" : "锁定" - }, - "used-in" : [ "src/app/main/ui/workspace/context_menu.cljs" ] - }, - "workspace.shape.menu.mask" : { - "translations" : { - "de" : "Maskieren", - "en" : "Mask", - "es" : "Máscara", - "fr" : "Masque", - "zh_cn" : "蒙板" - }, - "used-in" : [ "src/app/main/ui/workspace/context_menu.cljs", "src/app/main/ui/workspace/context_menu.cljs" ] - }, - "workspace.shape.menu.paste" : { - "translations" : { - "de" : "Einfügen", - "en" : "Paste", - "es" : "Pegar", - "fr" : "Coller", - "zh_cn" : "粘贴" - }, - "used-in" : [ "src/app/main/ui/workspace/context_menu.cljs", "src/app/main/ui/workspace/context_menu.cljs" ] - }, - "workspace.shape.menu.reset-overrides" : { - "translations" : { - "de" : "Änderungen zurücksetzen", - "en" : "Reset overrides", - "es" : "Deshacer modificaciones", - "fr" : "Annuler les modifications", - "zh_cn" : "还原自定义选项" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/component.cljs", "src/app/main/ui/workspace/sidebar/options/menus/component.cljs", "src/app/main/ui/workspace/context_menu.cljs", "src/app/main/ui/workspace/context_menu.cljs" ] - }, - "workspace.shape.menu.show" : { - "translations" : { - "de" : "Anzeigen", - "en" : "Show", - "es" : "Mostrar", - "fr" : "Montrer", - "zh_cn" : "显示" - }, - "used-in" : [ "src/app/main/ui/workspace/context_menu.cljs" ] - }, - "workspace.shape.menu.show-main" : { - "translations" : { - "de" : "Hauptkomponente anzeigen", - "en" : "Show main component", - "es" : "Ver componente principal", - "fr" : "Afficher le composant principal", - "zh_cn" : "显示主组件" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/component.cljs", "src/app/main/ui/workspace/context_menu.cljs" ] - }, - "workspace.shape.menu.ungroup" : { - "translations" : { - "de" : "Gruppierung aufheben", - "en" : "Ungroup", - "es" : "Desagrupar", - "fr" : "Dégrouper", - "zh_cn" : "取消编组" - }, - "used-in" : [ "src/app/main/ui/workspace/context_menu.cljs" ] - }, - "workspace.shape.menu.unlock" : { - "translations" : { - "de" : "Entsperren", - "en" : "Unlock", - "es" : "Desbloquear", - "fr" : "Débloquer", - "zh_cn" : "取消锁定" - }, - "used-in" : [ "src/app/main/ui/workspace/context_menu.cljs" ] - }, - "workspace.shape.menu.unmask" : { - "translations" : { - "de" : "Maske entfernen", - "en" : "Unmask", - "es" : "Quitar máscara", - "fr" : "Supprimer le masque", - "zh_cn" : "取消蒙版" - }, - "used-in" : [ "src/app/main/ui/workspace/context_menu.cljs" ] - }, - "workspace.shape.menu.update-main" : { - "translations" : { - "de" : "Hauptkomponente aktualisieren", - "en" : "Update main component", - "es" : "Actualizar componente principal", - "fr" : "Actualiser le composant principal", - "zh_cn" : "更新主组件" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/component.cljs", "src/app/main/ui/workspace/sidebar/options/menus/component.cljs", "src/app/main/ui/workspace/context_menu.cljs", "src/app/main/ui/workspace/context_menu.cljs" ] - }, - "workspace.sidebar.history" : { - "translations" : { - "de" : "Verlauf (%s)", - "en" : "History (%s)", - "es" : "Historial (%s)", - "fr" : "Historique (%s)", - "zh_cn" : "历史(%s)" - }, - "used-in" : [ "src/app/main/ui/workspace/left_toolbar.cljs" ] - }, - "workspace.sidebar.layers" : { - "translations" : { - "de" : "Ebenen (%s)", - "en" : "Layers (%s)", - "es" : "Capas (%s)", - "fr" : "Calques (%s)", - "zh_cn" : "图层(%s)" - }, - "used-in" : [ "src/app/main/ui/workspace/left_toolbar.cljs" ] - }, - "workspace.sidebar.options.svg-attrs.title" : { - "translations" : { - "de" : "Importierte SVG-Attribute", - "en" : "Imported SVG Attributes", - "es" : "Atributos del SVG Importado" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/menus/svg_attrs.cljs", "src/app/main/ui/handoff/attributes/svg.cljs" ] - }, - "workspace.sidebar.sitemap" : { - "translations" : { - "de" : "Seiten", - "en" : "Pages", - "es" : "Páginas", - "fr" : "Pages", - "ru" : "Страницы", - "zh_cn" : "页面" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/sitemap.cljs" ] - }, - "workspace.sitemap" : { - "translations" : { - "de" : "Sitemap", - "en" : "Sitemap", - "es" : "Mapa del sitio", - "fr" : "Plan du site", - "ru" : "Карта сайта", - "zh_cn" : "站点地图" - }, - "used-in" : [ "src/app/main/ui/workspace/header.cljs" ] - }, - "workspace.toolbar.assets" : { - "translations" : { - "de" : "Assets (%s)", - "en" : "Assets (%s)", - "es" : "Recursos (%s)", - "fr" : "Ressources (%s)", - "ru" : "", - "zh_cn" : "素材(%s)" - }, - "used-in" : [ "src/app/main/ui/workspace/left_toolbar.cljs" ] - }, - "workspace.toolbar.color-palette" : { - "translations" : { - "de" : "Farbpalette (%s)", - "en" : "Color Palette (%s)", - "es" : "Paleta de colores (%s)", - "fr" : "Palette de couleurs (%s)", - "ru" : "Палитра цветов (%s)", - "zh_cn" : "调色盘(%s)" - }, - "used-in" : [ "src/app/main/ui/workspace/left_toolbar.cljs" ] - }, - "workspace.toolbar.comments" : { - "translations" : { - "de" : "Kommentare (%s)", - "en" : "Comments (%s)", - "es" : "Comentarios (%s)", - "fr" : "Commentaires (%s)", - "zh_cn" : "评论(%s)" - }, - "used-in" : [ "src/app/main/ui/workspace/left_toolbar.cljs" ] - }, - "workspace.toolbar.curve" : { - "translations" : { - "de" : "Stift (%s)", - "en" : "Curve (%s)", - "es" : "Curva (%s)", - "fr" : "Courbe (%s)", - "ru" : "Кривая (%s)", - "zh_cn" : "曲线(%s)" - }, - "used-in" : [ "src/app/main/ui/workspace/left_toolbar.cljs" ] - }, - "workspace.toolbar.ellipse" : { - "translations" : { - "de" : "Ellipse (E)", - "en" : "Ellipse (E)", - "es" : "Elipse (E)", - "fr" : "Ellipse (E)", - "ru" : "", - "zh_cn" : "椭圆(E)" - }, - "used-in" : [ "src/app/main/ui/workspace/left_toolbar.cljs" ] - }, - "workspace.toolbar.frame" : { - "translations" : { - "de" : "Zeichenfläche (A)", - "en" : "Artboard (A)", - "es" : "Tablero (A)", - "fr" : "Plan de travail (A)", - "ru" : "Рабочая область (A)", - "zh_cn" : "画板(A)" - }, - "used-in" : [ "src/app/main/ui/workspace/left_toolbar.cljs" ] - }, - "workspace.toolbar.image" : { - "translations" : { - "de" : "Bild (K)", - "en" : "Image (K)", - "es" : "Imagen (K)", - "fr" : "Image (K)", - "ru" : "Изображение (K)", - "zh_cn" : "图片(K)" - }, - "used-in" : [ "src/app/main/ui/workspace/left_toolbar.cljs" ] - }, - "workspace.toolbar.move" : { - "translations" : { - "de" : "Verschieben", - "en" : "Move", - "es" : "Mover", - "fr" : "Déplacer", - "ru" : "Вытеснить", - "zh_cn" : "移动" - }, - "used-in" : [ "src/app/main/ui/workspace/left_toolbar.cljs" ] - }, - "workspace.toolbar.path" : { - "translations" : { - "de" : "Pfad (P)", - "en" : "Path (P)", - "es" : "Ruta (P)", - "fr" : "Chemin (P)", - "ru" : "Линия (P)", - "zh_cn" : "路径(P)" - }, - "used-in" : [ "src/app/main/ui/workspace/left_toolbar.cljs" ] - }, - "workspace.toolbar.rect" : { - "translations" : { - "de" : "Rechteck (R)", - "en" : "Rectangle (R)", - "es" : "Rectángulo (R)", - "fr" : "Rectangle (R)", - "ru" : "Прямоугольник (R)", - "zh_cn" : "矩形(R)" - }, - "used-in" : [ "src/app/main/ui/workspace/left_toolbar.cljs" ] - }, - "workspace.toolbar.text" : { - "translations" : { - "de" : "Text (T)", - "en" : "Text (T)", - "es" : "Texto (T)", - "fr" : "Texte (T)", - "ru" : "Текст (T)", - "zh_cn" : "文本(T)" - }, - "used-in" : [ "src/app/main/ui/workspace/left_toolbar.cljs" ] - }, - "workspace.undo.empty" : { - "translations" : { - "de" : "Es gibt bisher keine Änderungen im Verlauf", - "en" : "There are no history changes so far", - "es" : "Todavía no hay cambios en el histórico", - "fr" : "Il n’y a aucun changement dans l’historique pour l’instant", - "zh_cn" : "目前没有历史修改" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/history.cljs" ] - }, - "workspace.undo.entry.delete" : { - "translations" : { - "de" : "%s gelöscht", - "en" : "Deleted %s", - "es" : "%s eliminado", - "fr" : "Supprimé %s", - "zh_cn" : "%s已删除" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/history.cljs" ] - }, - "workspace.undo.entry.modify" : { - "translations" : { - "de" : "%s verändert", - "en" : "Modified %s", - "es" : "%s modificado", - "fr" : "Modifié %s", - "zh_cn" : "%s已修改" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/history.cljs" ] - }, - "workspace.undo.entry.move" : { - "translations" : { - "de" : "Verschobene Objekte", - "en" : "Moved objects", - "es" : "Objetos movidos", - "fr" : "Objets déplacés", - "zh_cn" : "对象已移动" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/history.cljs" ] - }, - "workspace.undo.entry.multiple.circle" : { - "translations" : { - "de" : "Kreise", - "en" : "circles", - "es" : "círculos", - "fr" : "cercles", - "zh_cn" : "圆" - }, - "unused" : true - }, - "workspace.undo.entry.multiple.color" : { - "translations" : { - "de" : "Farben", - "en" : "color assets", - "es" : "colores", - "fr" : "couleurs", - "zh_cn" : "颜色素材" - }, - "unused" : true - }, - "workspace.undo.entry.multiple.component" : { - "translations" : { - "de" : "Komponenten", - "en" : "components", - "es" : "componentes", - "fr" : "composants", - "zh_cn" : "组件" - }, - "unused" : true - }, - "workspace.undo.entry.multiple.curve" : { - "translations" : { - "de" : "Kurven", - "en" : "curves", - "es" : "curvas", - "fr" : "courbes", - "zh_cn" : "曲线" - }, - "unused" : true - }, - "workspace.undo.entry.multiple.frame" : { - "translations" : { - "de" : "Zeichenfläche", - "en" : "artboard", - "es" : "mesa de trabajo", - "fr" : "plan de travail", - "zh_cn" : "画板" - }, - "unused" : true - }, - "workspace.undo.entry.multiple.group" : { - "translations" : { - "de" : "Gruppen", - "en" : "groups", - "es" : "grupos", - "fr" : "groupes", - "zh_cn" : "编组" - }, - "unused" : true - }, - "workspace.undo.entry.multiple.media" : { - "translations" : { - "de" : "Grafiken", - "en" : "graphic assets", - "es" : "gráficos", - "fr" : "graphiques", - "zh_cn" : "图形素材" - }, - "unused" : true - }, - "workspace.undo.entry.multiple.multiple" : { - "translations" : { - "de" : "Objekte", - "en" : "objects", - "es" : "objetos", - "fr" : "objets", - "zh_cn" : "对象" - }, - "unused" : true - }, - "workspace.undo.entry.multiple.page" : { - "translations" : { - "de" : "Seiten", - "en" : "pages", - "es" : "páginas", - "fr" : "pages", - "zh_cn" : "页面" - }, - "unused" : true - }, - "workspace.undo.entry.multiple.path" : { - "translations" : { - "de" : "Pfade", - "en" : "paths", - "es" : "trazos", - "fr" : "chemins", - "zh_cn" : "路径" - }, - "unused" : true - }, - "workspace.undo.entry.multiple.rect" : { - "translations" : { - "de" : "Rechtecke", - "en" : "rectangles", - "es" : "rectángulos", - "fr" : "rectangles", - "zh_cn" : "矩形" - }, - "unused" : true - }, - "workspace.undo.entry.multiple.shape" : { - "translations" : { - "de" : "Formen", - "en" : "shapes", - "es" : "formas", - "fr" : "formes", - "zh_cn" : "形状" - }, - "unused" : true - }, - "workspace.undo.entry.multiple.text" : { - "translations" : { - "de" : "Texte", - "en" : "texts", - "es" : "textos", - "fr" : "textes", - "zh_cn" : "文本" - }, - "unused" : true - }, - "workspace.undo.entry.multiple.typography" : { - "translations" : { - "de" : "Typografie", - "en" : "typography assets", - "es" : "tipografías", - "fr" : "typographie", - "zh_cn" : "排版素材" - }, - "unused" : true - }, - "workspace.undo.entry.new" : { - "translations" : { - "de" : "Neu: %s", - "en" : "New %s", - "es" : "Nuevo %s", - "fr" : "Nouveau %s", - "zh_cn" : "新建%s" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/history.cljs" ] - }, - "workspace.undo.entry.single.circle" : { - "translations" : { - "de" : "Kreis", - "en" : "circle", - "es" : "círculo", - "fr" : "cercle", - "zh_cn" : "圆" - }, - "unused" : true - }, - "workspace.undo.entry.single.color" : { - "translations" : { - "de" : "Farben", - "en" : "color asset", - "es" : "color", - "fr" : "couleur", - "zh_cn" : "颜色素材" - }, - "unused" : true - }, - "workspace.undo.entry.single.component" : { - "translations" : { - "de" : "Komponente", - "en" : "component", - "es" : "componente", - "fr" : "composant", - "zh_cn" : "组件" - }, - "unused" : true - }, - "workspace.undo.entry.single.curve" : { - "translations" : { - "de" : "Kurve", - "en" : "curve", - "es" : "curva", - "fr" : "courbe", - "zh_cn" : "曲线" - }, - "unused" : true - }, - "workspace.undo.entry.single.frame" : { - "translations" : { - "de" : "Zeichenfläche", - "en" : "artboard", - "es" : "mesa de trabajo", - "fr" : "plan de travail", - "zh_cn" : "画板" - }, - "unused" : true - }, - "workspace.undo.entry.single.group" : { - "translations" : { - "de" : "Gruppe", - "en" : "group", - "es" : "grupo", - "fr" : "groupe", - "zh_cn" : "编组" - }, - "unused" : true - }, - "workspace.undo.entry.single.image" : { - "translations" : { - "de" : "Bild", - "en" : "image", - "es" : "imagen", - "fr" : "image", - "zh_cn" : "图片" - }, - "unused" : true - }, - "workspace.undo.entry.single.media" : { - "translations" : { - "de" : "Grafik", - "en" : "graphic asset", - "es" : "gráfico", - "fr" : "graphique", - "zh_cn" : "图形素材" - }, - "unused" : true - }, - "workspace.undo.entry.single.multiple" : { - "translations" : { - "de" : "Objekt", - "en" : "object", - "es" : "objeto", - "fr" : "objet", - "zh_cn" : "对象" - }, - "unused" : true - }, - "workspace.undo.entry.single.page" : { - "translations" : { - "de" : "Seite", - "en" : "page", - "es" : "página", - "fr" : "page", - "zh_cn" : "页面" - }, - "unused" : true - }, - "workspace.undo.entry.single.path" : { - "translations" : { - "de" : "Pfad", - "en" : "path", - "es" : "trazo", - "fr" : "chemin", - "zh_cn" : "路径" - }, - "unused" : true - }, - "workspace.undo.entry.single.rect" : { - "translations" : { - "de" : "Rechteck", - "en" : "rectangle", - "es" : "rectángulo", - "fr" : "rectangle", - "zh_cn" : "矩形" - }, - "unused" : true - }, - "workspace.undo.entry.single.shape" : { - "translations" : { - "de" : "Form", - "en" : "shape", - "es" : "forma", - "fr" : "forme", - "zh_cn" : "形状" - }, - "unused" : true - }, - "workspace.undo.entry.single.text" : { - "translations" : { - "de" : "Text", - "en" : "text", - "es" : "texto", - "fr" : "texte", - "zh_cn" : "文本" - }, - "unused" : true - }, - "workspace.undo.entry.single.typography" : { - "translations" : { - "de" : "Typografie", - "en" : "typography asset", - "es" : "tipografía", - "fr" : "typographie", - "zh_cn" : "排版素材" - }, - "unused" : true - }, - "workspace.undo.entry.unknown" : { - "translations" : { - "de" : "Vorgang über %s", - "en" : "Operation over %s", - "es" : "Operación sobre %s", - "fr" : "Opération sur %s", - "zh_cn" : "操作覆盖%s" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/history.cljs" ] - }, - "workspace.undo.title" : { - "translations" : { - "de" : "Verlauf", - "en" : "History", - "es" : "Histórico", - "fr" : "Historique", - "zh_cn" : "历史" - }, - "used-in" : [ "src/app/main/ui/workspace/sidebar/history.cljs" ] - }, - "workspace.updates.dismiss" : { - "translations" : { - "de" : "Ignorieren", - "en" : "Dismiss", - "es" : "Ignorar", - "fr" : "Ignorer", - "ru" : "", - "zh_cn" : "忽略" - }, - "used-in" : [ "src/app/main/data/workspace/libraries.cljs" ] - }, - "workspace.updates.there-are-updates" : { - "translations" : { - "de" : "Es gibt Updates in gemeinsam genutzten Bibliotheken", - "en" : "There are updates in shared libraries", - "es" : "Hay actualizaciones en librerías compartidas", - "fr" : "Il y a des mises à jour dans les Bibliothèques Partagées", - "ru" : "", - "zh_cn" : "共享库有更新" - }, - "used-in" : [ "src/app/main/data/workspace/libraries.cljs" ] - }, - "workspace.updates.update" : { - "translations" : { - "de" : "Aktualisieren", - "en" : "Update", - "es" : "Actualizar", - "fr" : "Actualiser", - "ru" : "", - "zh_cn" : "更新" - }, - "used-in" : [ "src/app/main/data/workspace/libraries.cljs" ] - }, - "workspace.viewport.click-to-close-path" : { - "translations" : { - "de" : "Klicken Sie, um den Pfad zu schließen", - "en" : "Click to close the path", - "es" : "Pulsar para cerrar la ruta", - "fr" : "Cliquez pour fermer le chemin", - "ru" : "Кликни чтобы закончить фигуру", - "zh_cn" : "单击以闭合路径" - }, - "unused" : true - } -} diff --git a/frontend/resources/styles/common/framework.scss b/frontend/resources/styles/common/framework.scss index ce0ee94d9a..629cee8d1a 100644 --- a/frontend/resources/styles/common/framework.scss +++ b/frontend/resources/styles/common/framework.scss @@ -1129,22 +1129,25 @@ input[type=range]:focus::-ms-fill-upper { } &.fixed { - position: fixed; - top: 0; - left: 0px; - width: 100%; + border-radius: $br-small; + box-shadow: 0px 4px 4px rgba(0,0,0,0.2); height: 48px; + max-width: 1000px; + min-width: 500px; + position: fixed; + padding-left: 16px; + top: 16px; + right: 16px; z-index: 13; display: flex; - justify-content: center; align-items: center; .wrapper { display: flex; justify-content: center; align-items: center; - max-width: 60%; + padding-right: 64px; .icon { margin-right: $medium; diff --git a/frontend/resources/styles/main-default.scss b/frontend/resources/styles/main-default.scss index 4ad1f8eac8..0c9ee57b14 100644 --- a/frontend/resources/styles/main-default.scss +++ b/frontend/resources/styles/main-default.scss @@ -2,10 +2,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. // -// This Source Code Form is "Incompatible With Secondary Licenses", as -// defined by the Mozilla Public License, v. 2.0. -// -// Copyright (c) 2020 UXBOX Labs SL +// Copyright (c) UXBOX Labs SL //################################################# diff --git a/frontend/resources/styles/main/layouts/login.scss b/frontend/resources/styles/main/layouts/login.scss index 03de4ceaac..13be87951c 100644 --- a/frontend/resources/styles/main/layouts/login.scss +++ b/frontend/resources/styles/main/layouts/login.scss @@ -57,7 +57,7 @@ .form-container { width: 412px; - .btn-ocean { + .auth-buttons { margin-top: $x-big; } diff --git a/frontend/resources/styles/main/layouts/main-layout.scss b/frontend/resources/styles/main/layouts/main-layout.scss index b7045cd39a..cb4b1c7b2c 100644 --- a/frontend/resources/styles/main/layouts/main-layout.scss +++ b/frontend/resources/styles/main/layouts/main-layout.scss @@ -2,10 +2,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. // -// This Source Code Form is "Incompatible With Secondary Licenses", as -// defined by the Mozilla Public License, v. 2.0. -// -// Copyright (c) 2020 UXBOX Labs SL +// Copyright (c) UXBOX Labs SL .main-content { display: flex; diff --git a/frontend/resources/styles/main/layouts/viewer.scss b/frontend/resources/styles/main/layouts/viewer.scss index ea6bf998aa..8f120296fc 100644 --- a/frontend/resources/styles/main/layouts/viewer.scss +++ b/frontend/resources/styles/main/layouts/viewer.scss @@ -1,6 +1,6 @@ .viewer-layout { display: grid; - grid-template-rows: 40px auto; + grid-template-rows: 48px auto; grid-template-columns: 1fr; user-select: none; diff --git a/frontend/resources/styles/main/partials/activity-bar.scss b/frontend/resources/styles/main/partials/activity-bar.scss index 5948b738c1..b1fb0dc947 100644 --- a/frontend/resources/styles/main/partials/activity-bar.scss +++ b/frontend/resources/styles/main/partials/activity-bar.scss @@ -2,10 +2,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. // -// This Source Code Form is "Incompatible With Secondary Licenses", as -// defined by the Mozilla Public License, v. 2.0. -// -// Copyright (c) 2020 UXBOX Labs SL +// Copyright (c) UXBOX Labs SL .activity-bar { background-color: $color-gray-50; diff --git a/frontend/resources/styles/main/partials/color-bullet.scss b/frontend/resources/styles/main/partials/color-bullet.scss index a8041e58b0..eb4882a510 100644 --- a/frontend/resources/styles/main/partials/color-bullet.scss +++ b/frontend/resources/styles/main/partials/color-bullet.scss @@ -2,10 +2,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. // -// This Source Code Form is "Incompatible With Secondary Licenses", as -// defined by the Mozilla Public License, v. 2.0. -// -// Copyright (c) 2020 UXBOX Labs SL +// Copyright (c) UXBOX Labs SL .color-cell { .color-bullet { @@ -77,7 +74,7 @@ ul.palette-menu .color-bullet { background-size: 8px; } -.asset-group .group-list-item .color-bullet { +.asset-section .asset-list-item .color-bullet { border: 1px solid $color-gray-20; border-radius: 10px; height: 20px; diff --git a/frontend/resources/styles/main/partials/color-palette.scss b/frontend/resources/styles/main/partials/color-palette.scss index 6bff34acba..5cb587d059 100644 --- a/frontend/resources/styles/main/partials/color-palette.scss +++ b/frontend/resources/styles/main/partials/color-palette.scss @@ -2,10 +2,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. // -// This Source Code Form is "Incompatible With Secondary Licenses", as -// defined by the Mozilla Public License, v. 2.0. -// -// Copyright (c) 2020 UXBOX Labs SL +// Copyright (c) UXBOX Labs SL .color-palette { @include animation(0,.5s,fadeInUp); diff --git a/frontend/resources/styles/main/partials/colorpicker.scss b/frontend/resources/styles/main/partials/colorpicker.scss index 115ee933e0..410437ae6e 100644 --- a/frontend/resources/styles/main/partials/colorpicker.scss +++ b/frontend/resources/styles/main/partials/colorpicker.scss @@ -342,8 +342,8 @@ .btn-primary { height: 1.5rem; - padding: 0 2.5rem; font-size: $fs12; + width: 100%; } } @@ -498,7 +498,7 @@ height: 20px; margin: 5px 0 0 0; padding: 0 $x-small; - width: 64px; + width: 84px; font-size: $fs13; &:focus { diff --git a/frontend/resources/styles/main/partials/context-menu.scss b/frontend/resources/styles/main/partials/context-menu.scss index 3696e8dc1c..d052cd3498 100644 --- a/frontend/resources/styles/main/partials/context-menu.scss +++ b/frontend/resources/styles/main/partials/context-menu.scss @@ -2,10 +2,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. // -// This Source Code Form is "Incompatible With Secondary Licenses", as -// defined by the Mozilla Public License, v. 2.0. -// -// Copyright (c) 2020 UXBOX Labs SL +// Copyright (c) UXBOX Labs SL .context-menu { position: relative; diff --git a/frontend/resources/styles/main/partials/dashboard-grid.scss b/frontend/resources/styles/main/partials/dashboard-grid.scss index d761718325..a491f9c7e4 100644 --- a/frontend/resources/styles/main/partials/dashboard-grid.scss +++ b/frontend/resources/styles/main/partials/dashboard-grid.scss @@ -2,10 +2,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. // -// This Source Code Form is "Incompatible With Secondary Licenses", as -// defined by the Mozilla Public License, v. 2.0. -// -// Copyright (c) 2020 UXBOX Labs SL +// Copyright (c) UXBOX Labs SL .dashboard-grid { font-size: $fs14; @@ -36,7 +33,10 @@ position: relative; text-align: center; width: 18%; - box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.1); + + &:hover { + box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.1); + } .grid-item-th { text-align: initial; diff --git a/frontend/resources/styles/main/partials/dashboard-header.scss b/frontend/resources/styles/main/partials/dashboard-header.scss index cb95e1d8fd..c3af04cb22 100644 --- a/frontend/resources/styles/main/partials/dashboard-header.scss +++ b/frontend/resources/styles/main/partials/dashboard-header.scss @@ -2,10 +2,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. // -// This Source Code Form is "Incompatible With Secondary Licenses", as -// defined by the Mozilla Public License, v. 2.0. -// -// Copyright (c) 2020 UXBOX Labs SL +// Copyright (c) UXBOX Labs SL .dashboard-header { align-items: center; @@ -80,6 +77,7 @@ display: flex; flex-shrink: 0; font-size: $fs18; + font-weight: 600; z-index: 10; } diff --git a/frontend/resources/styles/main/partials/dashboard-settings.scss b/frontend/resources/styles/main/partials/dashboard-settings.scss index c273e3b711..7a81add248 100644 --- a/frontend/resources/styles/main/partials/dashboard-settings.scss +++ b/frontend/resources/styles/main/partials/dashboard-settings.scss @@ -1,11 +1,10 @@ +// Copyright (c) 2020 UXBOX Labs SL // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. // -// This Source Code Form is "Incompatible With Secondary Licenses", as -// defined by the Mozilla Public License, v. 2.0. -// -// Copyright (c) 2020 UXBOX Labs SL +// Copyright (c) UXBOX Labs SL + .dashboard-sidebar { &.settings { diff --git a/frontend/resources/styles/main/partials/dashboard-sidebar.scss b/frontend/resources/styles/main/partials/dashboard-sidebar.scss index 089f8c08b3..2c4a0b8d15 100644 --- a/frontend/resources/styles/main/partials/dashboard-sidebar.scss +++ b/frontend/resources/styles/main/partials/dashboard-sidebar.scss @@ -2,10 +2,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. // -// This Source Code Form is "Incompatible With Secondary Licenses", as -// defined by the Mozilla Public License, v. 2.0. -// -// Copyright (c) 2020 UXBOX Labs SL +// Copyright (c) UXBOX Labs SL .dashboard-sidebar { background-color: $color-white; @@ -170,7 +167,6 @@ padding: $size-2; svg { - border-radius: 3px; fill: $color-black; margin-right: 8px; height: $size-3; diff --git a/frontend/resources/styles/main/partials/dashboard-team.scss b/frontend/resources/styles/main/partials/dashboard-team.scss index 6c0f4fa019..6688ffebe4 100644 --- a/frontend/resources/styles/main/partials/dashboard-team.scss +++ b/frontend/resources/styles/main/partials/dashboard-team.scss @@ -24,7 +24,8 @@ } .custom-select { - width: 103px + width: 103px; + overflow: hidden; } .action-buttons { diff --git a/frontend/resources/styles/main/partials/dashboard.scss b/frontend/resources/styles/main/partials/dashboard.scss index 2a86079f01..f8423ce48c 100644 --- a/frontend/resources/styles/main/partials/dashboard.scss +++ b/frontend/resources/styles/main/partials/dashboard.scss @@ -2,10 +2,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. // -// This Source Code Form is "Incompatible With Secondary Licenses", as -// defined by the Mozilla Public License, v. 2.0. -// -// Copyright (c) 2020 UXBOX Labs SL +// Copyright (c) UXBOX Labs SL .dashboard-container { background-color: $color-dashboard; @@ -45,7 +42,7 @@ cursor: pointer; font-size: 15px; line-height: 1rem; - font-weight: unset; + font-weight: 500; color: $color-black; margin-right: $medium; } diff --git a/frontend/resources/styles/main/partials/handoff.scss b/frontend/resources/styles/main/partials/handoff.scss index 60739b8c49..fab38c7af4 100644 --- a/frontend/resources/styles/main/partials/handoff.scss +++ b/frontend/resources/styles/main/partials/handoff.scss @@ -2,10 +2,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. // -// This Source Code Form is "Incompatible With Secondary Licenses", as -// defined by the Mozilla Public License, v. 2.0. -// -// Copyright (c) 2020 UXBOX Labs SL +// Copyright (c) UXBOX Labs SL .handoff-svg-wrapper { width: 100%; diff --git a/frontend/resources/styles/main/partials/modal.scss b/frontend/resources/styles/main/partials/modal.scss index fee4b62d6d..d67985d15c 100644 --- a/frontend/resources/styles/main/partials/modal.scss +++ b/frontend/resources/styles/main/partials/modal.scss @@ -134,6 +134,11 @@ margin-bottom: 0px; } } + + } + + .btn-disabled { + opacity: 0.5; } } diff --git a/frontend/resources/styles/main/partials/sidebar-assets.scss b/frontend/resources/styles/main/partials/sidebar-assets.scss index b2915448ee..bcae65e665 100644 --- a/frontend/resources/styles/main/partials/sidebar-assets.scss +++ b/frontend/resources/styles/main/partials/sidebar-assets.scss @@ -118,41 +118,97 @@ cursor: pointer; } - .asset-group { + .listing-options { + background-color: $color-gray-60; + display: flex; + align-items: center; + padding: $medium $small 0 $small; + + .selected-count { + color: $color-primary; + font-size: $fs11; + } + + .listing-option-btn { + cursor: pointer; + margin-left: $small; + + &.first { + margin-left: auto; + } + + svg { + fill: $color-gray-20; + height: 16px; + width: 16px; + } + } + } + + .asset-section { background-color: $color-gray-60; - border-top: 1px solid $color-gray-50; padding: $small; font-size: $fs12; color: $color-gray-20; /* TODO: see if this is useful, or is better to leave only one scroll bar in the whole sidebar - (also see .group-list) */ + (also see .asset-list) */ // max-height: 30rem; // overflow-y: scroll; - .group-title { - display: flex; - cursor: pointer; + // First child is the listing options buttons + &:not(:nth-child(2)) { + border-top: 1px solid $color-gray-50; + } + + .asset-title { + display: flex; + cursor: pointer; & .num-assets { color: $color-gray-30; } & svg { - height: 8px; - width: 8px; - fill: $color-gray-30; - margin-right: 4px; - transform: rotate(90deg); + height: 8px; + width: 8px; + fill: $color-gray-30; + margin-right: 4px; + transform: rotate(90deg); } &.closed svg { - transform: rotate(0deg); - transition: transform 0.3s; + transform: rotate(0deg); + transition: transform 0.3s; } } - .group-button { + .group-title { + display: flex; + cursor: pointer; + margin-top: $small; + margin-bottom: $x-small; + color: $color-white; + + & svg { + height: 8px; + width: 8px; + fill: $color-white; + margin-right: 4px; + transform: rotate(90deg); + } + + &.closed svg { + transform: rotate(0deg); + transition: transform 0.3s; + } + + & .dim { + color: $color-gray-40; + } + } + + .assets-button { margin-left: auto; cursor: pointer; @@ -167,8 +223,11 @@ } } - .group-grid { - margin-top: $medium; + .asset-title + .asset-grid { + margin-top: $small; + } + + .asset-grid { display: grid; grid-template-columns: 1fr 1fr 1fr 1fr; grid-auto-rows: 6vh; @@ -192,6 +251,7 @@ .grid-cell { background-color: $color-canvas; border-radius: 4px; + border: 2px solid transparent; overflow: hidden; display: flex; align-items: center; @@ -242,29 +302,85 @@ } .grid-cell:hover { - border: 1px solid $color-primary; + border: 2px solid $color-primary; & .cell-name { display: block; } } + .grid-cell.selected { + border: 2px solid $color-primary; + } + + .asset-title + .asset-enum { + margin-top: $small; + } + + .asset-enum { + .enum-item { + display: flex; + align-items: center; + margin-bottom: $small; + cursor: pointer; + + & > svg, + & > img { + background-color: $color-canvas; + border-radius: 4px; + border: 2px solid transparent; + height: 24px; + width: 24px; + margin-right: $small; + } + + .item-name { + width: calc(100% - 24px - #{$small}); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + display: block; + + &.editing { + display: flex; + align-items: center; + + .editable-label-input { + height: 24px; + } + + .editable-label-close { + display: none; + } + } + } + } + + .enum-item:hover, + .enum-item.selected, + { + color: $color-primary; + } + } + /* TODO: see if this is useful, or is better to leave only one scroll bar in the whole sidebar - (also see .asset-group) */ - // .group-list { + (also see .asset-section) */ + // .asset-list { // max-height: 30rem; // overflow-y: scroll; // } - .group-list { + .asset-list { margin-top: $medium; } - .group-list-item { + .asset-list-item { display: flex; align-items: center; - margin-top: $small; + border: 1px solid transparent; + margin-top: $x-small; + padding: 2px; font-size: $fs12; color: $color-white; cursor: pointer; @@ -274,6 +390,10 @@ color: $color-gray-30; text-transform: uppercase; } + + &.selected { + border: 1px solid $color-primary; + } } .context-menu { diff --git a/frontend/resources/styles/main/partials/sidebar-document-history.scss b/frontend/resources/styles/main/partials/sidebar-document-history.scss index ec00f2f739..93ea710f1a 100644 --- a/frontend/resources/styles/main/partials/sidebar-document-history.scss +++ b/frontend/resources/styles/main/partials/sidebar-document-history.scss @@ -2,10 +2,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. // -// This Source Code Form is "Incompatible With Secondary Licenses", as -// defined by the Mozilla Public License, v. 2.0. -// -// Copyright (c) 2020 UXBOX Labs SL +// Copyright (c) UXBOX Labs SL .history-toolbox { display: flex; diff --git a/frontend/resources/styles/main/partials/sidebar-element-options.scss b/frontend/resources/styles/main/partials/sidebar-element-options.scss index e0770692dd..36da777ab1 100644 --- a/frontend/resources/styles/main/partials/sidebar-element-options.scss +++ b/frontend/resources/styles/main/partials/sidebar-element-options.scss @@ -865,6 +865,10 @@ &:hover { background: #1F1F1F; } + + &.selected { + border: 1px solid $color-primary; + } } .exports-options, @@ -983,7 +987,7 @@ display: flex; } -.asset-group { +.asset-section { .typography-entry { margin: 0.25rem 0; } diff --git a/frontend/resources/styles/main/partials/viewer-header.scss b/frontend/resources/styles/main/partials/viewer-header.scss index 21c1e75c75..9e8449c461 100644 --- a/frontend/resources/styles/main/partials/viewer-header.scss +++ b/frontend/resources/styles/main/partials/viewer-header.scss @@ -3,7 +3,7 @@ background-color: $color-gray-50; border-bottom: 1px solid $color-gray-60; display: flex; - height: 40px; + height: 48px; padding: 0 $medium 0 55px; position: relative; z-index: 12; @@ -23,7 +23,7 @@ left: 0; position: absolute; top: 0; - width: 40px; + width: 48px; a { height: 30px; @@ -123,7 +123,7 @@ align-items: center; height: 100%; - width: 40px; + width: 48px; svg { fill: $color-gray-20; diff --git a/frontend/resources/styles/main/partials/workspace-header.scss b/frontend/resources/styles/main/partials/workspace-header.scss index 4b220a49d9..e191a725f5 100644 --- a/frontend/resources/styles/main/partials/workspace-header.scss +++ b/frontend/resources/styles/main/partials/workspace-header.scss @@ -2,10 +2,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. // -// This Source Code Form is "Incompatible With Secondary Licenses", as -// defined by the Mozilla Public License, v. 2.0. -// -// Copyright (c) 2020 UXBOX Labs SL +// Copyright (c) UXBOX Labs SL .workspace-header { align-items: center; diff --git a/frontend/resources/styles/main/partials/workspace.scss b/frontend/resources/styles/main/partials/workspace.scss index 65965da270..d8e3b69ef7 100644 --- a/frontend/resources/styles/main/partials/workspace.scss +++ b/frontend/resources/styles/main/partials/workspace.scss @@ -305,10 +305,9 @@ } &.is-disabled { - opacity: 0.3; - - &:hover svg { - fill: initial; + cursor: initial; + svg { + fill: $color-gray-20; } } diff --git a/frontend/resources/templates/index.mustache b/frontend/resources/templates/index.mustache index dc6e7c179c..0b0dfd2790 100644 --- a/frontend/resources/templates/index.mustache +++ b/frontend/resources/templates/index.mustache @@ -15,10 +15,9 @@ - + - +