Frontend Guide #

This guide intends to explain the essential details of the frontend application.

Icons & Assets #

The icons used on the frontend application are loaded using svgsprite (properly handled by the gulp watch task). All icons should be in SVG format located in resources/images/icons. The gulp task will generate the sprite and the embed it into the index.html file.

Then, you can reference the icon from the sprite using the app.builtins.icons/icon-xref macro:

(ns some.namespace
(:require-macros [app.main.ui.icons :refer [icon-xref]]))

(icon-xref :arrow)

For performance reasons, all used icons are statically defined in the src/app/main/ui/icons.cljs file.

Logging, Tracing & Debugging #

Logging framework #

To trace and debug the execution of the code, one method is to enable the log traces that currently are in the code using the Logging framework. You can edit a module and set a lower log level, to see more traces in console. Search for this kind of line and change to :info or :debug:

(ns some.ns
(:require [app.util.logging :as log]))

(log/set-level! :info)

Or you can change it live with the debug utility (see below):

debug.set_logging("namespace", "level")

Temporary traces #

Of course, you have the traditional way of inserting temporary traces inside the code to output data to the devtools console. There are several ways of doing this.

Use clojurescript helper prn #

This helper automatically formats the clojure and js data structures as plain EDN for visual inspection and to know the exact type of the data.

(prn "message" expression)

prn example

Use pprint function #

We have set up a wrapper over fipp pprint function, that gives a human-readable formatting to the data, useful for easy understanding of larger data structures.

The wrapper allows to easily specify level, length and width parameters, with reasonable defaults, to control the depth level of objects to print, the number of attributes to show and the display width.

(:require [app.common.pprint :refer [pprint]])

;; On the code
(pprint shape {:level 2
:length 21
:width 30})

pprint example

Use the js native functions #

The clj->js function converts the clojure data structure into a javacript object, interactively inspectable in the devtools.console.

(js/console.log "message" (clj->js expression))

clj->js example

Breakpoints #

You can insert standard javascript debugger breakpoints in the code, with this function:


The Clojurescript environment generates source maps to trace your code step by step and inspect variable values. You may also insert breakpoints from the sources tab, like when you debug javascript code.

One way of locating a source file is to output a trace with (js/console.log) and then clicking in the source link that shows in the console at the right of the trace.

Access to clojure from js console #

The penpot namespace of the main application is exported, so that is accessible from javascript console in Chrome developer tools. Object names and data types are converted to javascript style. For example you can emit the event to reset zoom level by typing this at the console (there is autocompletion for help):

Debug utility #

We have defined, at src/debug.cljs, a debug namespace with many functions easily accesible from devtools console.

Change log level #

You can change the log level of one namespace without reloading the page:

debug.set_logging("namespace", "level")

Dump state and objects #

There are some functions to inspect the global state or parts of it:

// print the whole global state

// print the latest events in the global stream

// print a key of the global state
debug.get_state(":workspace-data :pages 0")

// print the objects list of the current page

// print a single object by name

// print the currently selected objects

// print all objects in the current page and local library components.
// Objects are displayed as a tree in the same order of the
// layers tree, and also links to components are shown.

// This last one has two optional flags. The first one displays the
// object ids, and the second one the {touched} state.
debug.dump_tree(true, true)

And a bunch of other utilities (see the file for more).

Workspace visual debug #

Debugging a problem in the viewport algorithms for grouping and rotating is difficult. We have set a visual debug mode that displays some annotations on screen, to help understanding what's happening. This is also in the debug namespace.

To activate it, open the javascript console and type:


Current options are bounding-boxes, group, events and rotation-handler.

You can also activate or deactivate all visual aids with


Translations (I18N) #

How it works #

All the translation strings of this application are stored in standard gettext files in frontend/translations/*.po.

They have a self explanatory format that looks like this:

#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs
msgid "auth.create-demo-account"
msgstr "Create demo account"

The files are automatically bundled into the index.html file on compile time (in development and production). The bundled content is a simplified version of this data structure to avoid loading unnecesary data. The development environment has a watch process that detect changes on that file and recompiles the index.html.

There are no hot reload for translations strings, you just need to refresh the browser tab to refresh the translations in the running the application.

Finally, when you have finished adding texts, execute the following command inside the devenv, to reformat the file before commiting the file into the repository:

# cd <repo>/frontend
yarn run validate-translations

At Penpot core team we maintain manually the english and spanish .po files. All the others are managed in

When a new language is available in weblate, to enable it in the application you need to add it in two places:

frontend/src/app/util/i18n.cljs (supported-locales)
frontend/gulpfile.js (const langs)

How to use it #

You need to use the app.util.i18n/tr function for lookup translation strings:

(require [app.util.i18n :as i18n :refer [tr]])

(tr "auth.create-demo-account")
;; => "Create demo account"

If you want to insert a variable into a translated text, use %s as placeholder, and then pass the variable value to the (tr ...) call.:

#: src/app/main/ui/settings/change_email.cljs
msgid "notifications.validation-email-sent"
msgstr "Verification email sent to %s. Check your email!"
(require [app.util.i18n :as i18n :refer [tr]])

(tr "notifications.validation-email-sent" email)
;; => "Verification email sent to Check your email!"

If you have defined plurals for some translation resource, then you need to pass an additional parameter marked as counter in order to allow the system know when to show the plural:

#: src/app/main/ui/dashboard/team.cljs
msgid "labels.num-of-projects"
msgid_plural "labels.num-of-projects"
msgstr[0] "1 project"
msgstr[1] "%s projects"
(require [app.util.i18n :as i18n :refer [tr]])

(tr "labels.num-of-projects" (i18n/c 10))
;; => "10 projects"

(tr "labels.num-of-projects" (i18n/c 1))
;; => "1 project"

Tests #

Frontend tests have to be compiled first, and then run with node.

npx shadow-cljs compile tests && node target/tests.js

Or run the watch (that automatically runs the test):

npx shadow-cljs watch tests