Repository: CSCfi/rems
Pull request #1651 (2019-09-20 10:01:26)

Print styles for application page
With these styles, the application page prints nicely in firefox & chrome. Since the changes are small, I decided to go with the existing page and CSS. An alternative would have been a fresh print.css, or a separate printable application page with less styling and no interactions.

The main caveat is that large (>1 page) textareas don't get printed nicely. Luckily we don't use textarea for readonly applicaitons, so this problem affects only drafts.

For #1643.

# Definition of Done / Review checklist

## Reviewability
- [x] link to issue
- ~note if PR is on top of other PR~
- ~note if related change in rems-deploy repo~
- [x] consider adding screenshots for ease of review

## API
- ~API is documented and shows up in Swagger UI~
- ~API is backwards compatible or completely new~
- ~Events are backwards compatible~

## Documentation
- [x] update changelog if necessary
- ~add or update docstrings for namespaces and functions~
- ~components are added to guide page~
- ~documentation _at least_ for config options (i.e. docs folder)~
- ~ADR for major architectural decisions or experiments~

## Different installations
- ~new configuration options added to rems-deploy repository~
- ~instance specific translations (i.e. LBR kielivara)~

## Testing
- ~complex logic is unit tested~
- [ ] valuable features are integration / browser / acceptance tested automatically

## Accessibility
- ~all icons have the aria-label attribute~
- ~all fields have a label~
- ~errors are linked to fields with aria-describedby~
- ~contrast is checked~
- ~conscious decision about where to move focus after an action~

## Follow-up
- ~new tasks are created for pending or remaining tasks~
- ~no critical TODOs left to implement~



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fc2d729a4..6a3e59990 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -78,6 +78,7 @@ Changes since v2.4
 ### Fixes
 - Entitlement API
 - Search on the catalogue and admin pages did not support multiple search terms (#1541)
+- Printing application pages now works (except for drafts) (#1643)
 
 ## v2.5 "Maarintie" 2019-07-18
 
diff --git a/example-theme/extra-translations/en.edn b/example-theme/extra-translations/en.edn
index 762a98a8e..ec7c24640 100644
--- a/example-theme/extra-translations/en.edn
+++ b/example-theme/extra-translations/en.edn
@@ -1,5 +1,3 @@
 {:t
  {:administration {:catalogue-items "Catalogue"}
-  ;; the extra %1 and the :unused-key are for rems.test-locales
-  :create-license {:license-text "Text %1"}
-  :unused-key "Unused"}}
+  :create-license {:license-text "Text"}}}
diff --git a/resources/translations/en.edn b/resources/translations/en.edn
index 6616d29f2..d22981591 100644
--- a/resources/translations/en.edn
+++ b/resources/translations/en.edn
@@ -217,43 +217,43 @@
                     :title "Title"}
   :dropdown {:no-results "No results"
              :placeholder "Select..."}
-  :email {:application-approved {:subject-to-applicant "Your application %3 has been approved"
-                                 :message-to-applicant "Dear %1,\n\nYour application %3 has been approved.\n\nYou can view the application and the decision at %6"
-                                 :subject-to-handler "(%3) Application has been approved"
-                                 :message-to-handler "Dear %1,\n\n%2 has approved the application %3 from %4.\n\nYou can view the application and the decision at %6"}
-          :application-rejected {:subject-to-applicant "Your application %3 has been rejected"
-                                 :message-to-applicant "Dear %1,\n\nYour application %3 has been rejected.\n\nYou can view the application and the decision at %6"
-                                 :subject-to-handler "(%3) Application has been rejected"
-                                 :message-to-handler "Dear %1,\n\n%2 has rejected the application %3 from %4.\n\nYou can view the application and the decision at %6"}
-          :application-closed {:subject-to-applicant "Your application %3 has been closed"
-                               :message-to-applicant "Dear %1,\n\nYour application %3 has been closed.\n\nYou can view the application at %6"
-                               :subject-to-handler "(%3) Application has been closed"
-                               :message-to-handler "Dear %1,\n\n%2 has closed the application %3 from %4.\n\nYou can view the application at %6"}
-          :application-returned {:subject-to-applicant "Your application %3 needs to be amended"
-                                 :message-to-applicant "Dear %1,\n\nYour application %3 has been returned for your consideration. Please, amend according to requests and resubmit.\n\nYou can review the application at %6"
-                                 :subject-to-handler "(%3) Application has been returned to applicant"
-                                 :message-to-handler "Dear %1,\n\n%2 has returned the application %3 to the applicant %4 for modifications.\n\nYou can view the application at %6"}
-          :application-licenses-added {:subject "Your application's %3 terms of use have changed"
-                                       :message "Dear %1,\n\n%2 has requested your approval for changed terms of use to application %3.\n\nYou can review the application and approve the changed the terms of use at %6"}
-          :application-resubmitted {:subject "(%3) Application has been resubmitted"
-                                    :message "Dear %1,\n\nApplication %3 has been resubmitted by %4.\n\nYou can review the application at %6"}
-          :application-submitted {:subject "(%3) A new application has been submitted"
-                                  :message "Dear %1,\n\n%4 has submitted a new application %3 to access resource(s) %5.\n\nYou can review the application at %6"}
-          :comment-requested {:subject "(%3) Review request"
-                              :message "Dear %1,\n\n%2 has requested your review on application %3, submitted by %4.\n\nYou can review the application at %6"}
-          :commented {:subject "(%3) Application has been reviewed"
-                      :message "Dear %1,\n\n%2 has reviewed the application %3, submitted by %4.\n\nYou can review the application at %6"}
-          :decided {:subject "(%3) Application decision notification"
-                    :message "Dear %1,\n\n%2 has filed a decision on application %3, submitted by %4.\n\nYou can review the application at %6"}
-          :decision-requested {:subject "(%3) Decision request"
-                               :message "Dear %1,\n\n%2 has requested your decision on application %3, submitted by %4.\n\nYou can review application at %6"}
+  :email {:application-approved {:subject-to-applicant "Your application has been approved (%3)"
+                                 :message-to-applicant "Dear %1,\n\nYour application %3 has been approved.\n\nYou can view the application and the decision: %6"
+                                 :subject-to-handler "Application approved (%3)"
+                                 :message-to-handler "Dear %1,\n\n%2 has approved the application %3 from %4.\n\nYou can view the application and the decision: %6"}
+          :application-rejected {:subject-to-applicant "Your application has been rejected (%3)"
+                                 :message-to-applicant "Dear %1,\n\nYour application %3 has been rejected.\n\nYou can view the application and the decision: %6"
+                                 :subject-to-handler "Application rejected (%3)"
+                                 :message-to-handler "Dear %1,\n\n%2 has rejected the application %3 from %4.\n\nYou can view the application and the decision: %6"}
+          :application-closed {:subject-to-applicant "Your application has been closed (%3)"
+                               :message-to-applicant "Dear %1,\n\nYour application %3 has been closed.\n\nYou can view the application: %6"
+                               :subject-to-handler "Application closed (%3)"
+                               :message-to-handler "Dear %1,\n\n%2 has closed the application %3 from %4.\n\nYou can view the application: %6"}
+          :application-returned {:subject-to-applicant "Your application has been returned for modifications (%3)"
+                                 :message-to-applicant "Dear %1,\n\nYour application %3 has been returned to you for modifications.\n\nYou can modify and resubmit the application: %6"
+                                 :subject-to-handler "Application has been returned for modifications (%3)"
+                                 :message-to-handler "Dear %1,\n\n%2 has returned the application %3 to the applicant %4 for modifications.\n\nYou can view the application: %6"}
+          :application-licenses-added {:subject "New terms of use waiting for approval (%3)"
+                                       :message "Dear %1,\n\n%2 has requested your acceptance for new terms of use for application %3.\n\nYou can view the application and accept the terms of use: %6"}
+          :application-resubmitted {:subject "Application has been resubmitted (%3)"
+                                    :message "Dear %1,\n\nApplication %3 has been resubmitted by %4.\n\nYou can view the application: %6"}
+          :application-submitted {:subject "A new application has been submitted (%3)"
+                                  :message "Dear %1,\n\n%4 has submitted a new application %3 to access resource(s) %5.\n\nYou can view the application: %6"}
+          :comment-requested {:subject "Review request (%3)"
+                              :message "Dear %1,\n\n%2 has requested your review on application %3 submitted by %4.\n\nYou can review the application: %6"}
+          :commented {:subject "Application has been reviewed (%3)"
+                      :message "Dear %1,\n\n%2 has reviewed the application %3 submitted by %4.\n\nYou can view the application and the review: %6"}
+          :decided {:subject "Decision notification (%3)"
+                    :message "Dear %1,\n\n%2 has sent a decision on application %3 submitted by %4.\n\nYou can view the application and the decision: %6"}
+          :decision-requested {:subject "Decision requested (%3)"
+                               :message "Dear %1,\n\n%2 has requested your decision on application %3 submitted by %4.\n\nYou can view application: %6"}
           :footer "\n\nPlease do not reply to this automatically generated message."
-          :member-added {:subject "Added as a member of an application %3"
-                         :message "Dear %1,\n\nYou've been added as a member of application %3, submitted by %4.\n\nYou can view application and accept the terms of use at %6"}
-          :member-invited {:subject "Invitation to participate in application %3"
-                           :message "Dear %1,\n\nYou have been invited to participate in application %3, by %2.\n\nYou can view the application and accept the terms of use at %4"}
-          :remarked {:subject "(%3) Application has been commented"
-                     :message "Dear %1,\n\n%2 has commented on the application %3, submitted by %4.\n\nYou can review the application at %6"}}
+          :member-added {:subject "Added as a member of an application (%3)"
+                         :message "Dear %1,\n\nYou've been added as a member of application %3.\n\nView application: %6"}
+          :member-invited {:subject "Invitation to participate in an application"
+                           :message "Dear %1,\n\nYou have been invited to participate in an application submitted by %2. The title of the application is %3.\n\nYou can view the application and accept the terms of use: %4"}
+          :remarked {:subject "Application has been commented on (%3)"
+                     :message "Dear %1,\n\n%2 has commented on the application %3 submitted by %4.\n\nYou can view the application and the comment: %6"}}
   :footer "CSC – IT Center for Science"
   :form {:accepted-licenses "Accepted terms of use"
          :actions "Actions"
diff --git a/resources/translations/fi.edn b/resources/translations/fi.edn
index 8352e0b41..907e0bbd8 100644
--- a/resources/translations/fi.edn
+++ b/resources/translations/fi.edn
@@ -215,43 +215,43 @@
                     :title "Työvuon nimi"}
   :dropdown {:no-results "Ei tuloksia"
              :placeholder "Valitse..."}
-  :email {:application-approved {:subject-to-applicant "Hakemuksesi %3 on hyväksytty"
-                                 :message-to-applicant "Hyvä %1,\n\nHakemuksesi %3 on hyväksytty.\n\nVoit tarkastella hakemusta ja päätöstä osoitteessa %6"
-                                 :subject-to-handler "(%3) Hakemus on hyväksytty"
-                                 :message-to-handler "Hyvä %1,\n\n%2 on hyväksynyt käyttäjän %4 hakemuksen %3.\n\nVoit tarkastella hakemusta ja päätöstä osoitteessa %6"}
-          :application-rejected {:subject-to-applicant "Hakemuksesi %3 on hylätty"
-                                 :message-to-applicant "Hyvä %1,\n\nHakemuksesi %3 on hylätty.\n\nVoit tarkastella hakemusta ja päätöstä osoitteessa %6"
-                                 :subject-to-handler "(%3) Hakemus on hylätty"
-                                 :message-to-handler "Hyvä %1,\n\n%2 on hylännyt käyttäjän %4 hakemuksen %3.\n\nVoit tarkastella hakemusta ja päätöstä osoitteessa %6"}
-          :application-closed {:subject-to-applicant "Hakemuksesi %3 on suljettu"
+  :email {:application-approved {:subject-to-applicant "Hakemuksesi on hyväksytty (%3)"
+                                 :message-to-applicant "Hyvä %1,\n\nHakemuksesi %3 on hyväksytty.\n\nVoit tarkastella hakemusta ja päätöstä: %6"
+                                 :subject-to-handler "Hakemus hyväksytty (%3)"
+                                 :message-to-handler "Hyvä %1,\n\n%2 on hyväksynyt käyttäjän %4 hakemuksen %3.\n\nVoit tarkastella hakemusta ja päätöstä: %6"}
+          :application-rejected {:subject-to-applicant "Hakemuksesi on hylätty (%3)"
+                                 :message-to-applicant "Hyvä %1,\n\nHakemuksesi %3 on hylätty.\n\nVoit tarkastella hakemusta ja päätöstä: %6"
+                                 :subject-to-handler "Hakemus hylätty (%3)"
+                                 :message-to-handler "Hyvä %1,\n\n%2 on hylännyt käyttäjän %4 hakemuksen %3.\n\nVoit tarkastella hakemusta ja päätöstä: %6"}
+          :application-closed {:subject-to-applicant "Hakemuksesi on suljettu (%3)"
                                :message-to-applicant "Hyvä %1,\n\nHakemuksesi %3 on suljettu.\n\nVoit tarkastella hakemusta: %6"
-                               :subject-to-handler "(%3) Hakemus on suljettu"
+                               :subject-to-handler "Hakemus suljettu (%3)"
                                :message-to-handler "Hyvä %1,\n\n%2 on sulkenut käyttäjän %4 hakemuksen %3.\n\nVoit tarkastella hakemusta: %6"}
-          :application-returned {:subject-to-applicant "Hakemuksesi %3 vaatii täydentämistä"
-                                 :message-to-applicant "Hyvä %1,\n\nHakemuksesi %3 on palautettu muokattavaksesi. Ole hyvä ja täydennä hakemusta kommenttien mukaan.\n\nVoit tarkastella hakemusta osoitteessa %6"
-                                 :subject-to-handler "(%3) Hakemus on palautettu"
-                                 :message-to-handler "Hyvä %1,\n\n%2 on palauttanut käyttäjän %4 hakemuksen %3.\n\nVoit tarkastella hakemusta osoitteessa %6"}
-          :application-licenses-added {:subject "Hakemuksen %3 käyttöehdot ovat muuttuneet"
-                                       :message "Hyvä %1,\n\n%2 pyytää hyväksyntääsi muuttuneille käyttöehdoille hakemukseen %3.\n\nVoit tarkastella hakemusta ja hyväksyä muuttuneet käyttöehdot osoitteessa %6"}
-          :application-resubmitted {:subject "(%3) Hakemus lähetetty uudelleen"
-                                    :message "Dear %1,\n\nKäyttäjä %4 on lähettänyt uudelleen hakemuksen %3.\n\nVoit tarkastella hakemusta osoitteessa %6"}
-          :application-submitted {:subject "(%3) Uusi hakemus"
-                                  :message "Hyvä %1,\n\n%4 on lähettänyt käyttöoikeushakemuksen %3 resurss(e)ille %5.\n\nVoit tarkastella hakemusta osoitteessa %6"}
-          :comment-requested {:subject "(%3) Katselmointipyyntö"
-                              :message "Hyvä %1,\n\n%2 on pyytänyt katselmointiasi käyttäjän %4 lähettämälle hakemukselle %3.\n\nVoit tarkastella hakemusta osoitteessa %6"}
-          :commented {:subject "(%3) Hakemus katselmoitu"
-                      :message "Hyvä %1,\n\n%2 on katselmoinut käyttäjän %4 lähettämän hakemuksen %3.\n\nVoit tarkastella hakemusta osoitteessa %6"}
-          :decided {:subject "(%3) Päätös lähetetty"
-                    :message "Hyvä %1,\n\n%2 on antanut päätöksen käyttäjän %4 hakemuksesta %3.\n\nVoit tarkastella hakemusta osoitteessa %6"}
-          :decision-requested {:subject "(%3) Päätöspyyntö"
-                               :message "Hyvä %1,\n\n%2 on pyytänyt sinulta päätöstä käyttäjän %4 hakemuksesta %3.\n\nVoit tarkastella hakemusta osoitteessa %6"}
+          :application-returned {:subject-to-applicant "Hakemuksesi on palautettu (%3)"
+                                 :message-to-applicant "Hyvä %1,\n\nHakemuksesi %3 on palautettu muokattavaksi.\n\nVoit muokata hakemusta ja lähettää sen uudelleen: %6"
+                                 :subject-to-handler "Hakemus palautettu (%3)"
+                                 :message-to-handler "Hyvä %1,\n\n%2 on palauttanut käyttäjän %4 hakemuksen %3.\n\nVoit tarkastella hakemusta: %6"}
+          :application-licenses-added {:subject "Hakemukseen lisätty käyttöehtoja (%3)"
+                                       :message "Hyvä %1,\n\n%2 pyytää hyväksyntääsi uusille käyttöehdoille hakemukseen %3.\n\nVoit tarkastella hakemusta ja hyväksyä käyttöehdot: %6"}
+          :application-resubmitted {:subject "Hakemus lähetetty uudelleen (%3)"
+                                    :message "Dear %1,\n\nKäyttäjä %4 on lähettänyt uudelleen hakemuksen %3.\n\nVoit tarkastella hakemusta: %6"}
+          :application-submitted {:subject "Uusi hakemus (%3)"
+                                  :message "Hyvä %1,\n\n%4 on lähettänyt käyttöoikeushakemuksen %3 resurss(e)ille %5.\n\nVoit tarkastella hakemusta: %6"}
+          :comment-requested {:subject "Katselmointipyyntö (%3)"
+                              :message "Hyvä %1,\n\n%2 on pyytänyt katselmointiasi käyttäjän %4 lähettämälle hakemukselle %3.\n\nVoit katselmoida hakemuksen: %6"}
+          :commented {:subject "Hakemus katselmoitu (%3)"
+                      :message "Hyvä %1,\n\n%2 on katselmoinut käyttäjän %4 lähettämän hakemuksen %3.\n\nVoit tarkastella hakemusta ja lukea kommentit: %6"}
+          :decided {:subject "Päätös lähetetty (%3)"
+                    :message "Hyvä %1,\n\n%2 on lähettänyt päätöksen käyttäjän %4 hakemuksesta %3.\n\nVoit tarkastella hakemusta ja päätöstä: %6"}
+          :decision-requested {:subject "Päätöspyyntö (%3)"
+                               :message "Hyvä %1,\n\n%2 on pyytänyt sinulta päätöstä käyttäjän %4 hakemuksesta %3.\n\nVoit tarkastella hakemusta: %6"}
           :footer "\n\nTämä on automaattinen viesti. Älä vastaa."
-          :member-added {:subject "Lisätty hakemuksen %3 jäseneksi"
-                         :message "Hyvä %1,\n\nSinut on lisätty käyttäjän %4 jättämän hakemuksen %3 jäseneksi.\n\nVoit tarkastella hakemusta ja hyväksyä käyttöehdot osoitteessa %6"}
-          :member-invited {:subject "Kutsu osallistumaan hakemukseen %3"
-                           :message "Hyvä %1,\n\nSinut on kutsuttu osallistumaan käyttäjän %2 jättämään käyttöoikeushakemukseen %3.\n\nVoit tarkastella hakemusta ja hyväksyä käyttöehdot osoitteessa %4"}
-          :remarked {:subject "(%3) Hakemusta kommentoitu"
-                     :message "Hyvä %1,\n\n%2 on kommentoinut käyttäjän %4 lähettämää hakemusta %3.\n\nVoit tarkastella hakemusta osoitteessa %6"}}
+          :member-added {:subject "Lisätty hakemuksen jäseneksi (%3)"
+                         :message "Hyvä %1,\n\nSinut on lisätty hakemuksen %3 jäseneksi.\n\nVoit tarkastella hakemusta: %6"}
+          :member-invited {:subject "Kutsu osallistumaan hakemukseen"
+                           :message "Hyvä %1,\n\nSinut on kutsuttu osallistumaan käyttäjän %2 jättämään käyttöoikeushakemukseen. Hakemuksen otsikko on %3.\n\nVoit tarkastella hakemusta ja hyväksyä käyttöehdot: %4"}
+          :remarked {:subject "Hakemusta kommentoitu (%3)"
+                     :message "Hyvä %1,\n\n%2 on kommentoinut käyttäjän %4 lähettämää hakemusta %3.\n\nVoit tarkastella hakemusta ja lukea kommentin: %6"}}
   :footer "CSC – Tieteen tietotekniikan keskus"
   :form {:accepted-licenses "Käyttöehdot hyväksytty"
          :actions "Toiminnot"
diff --git a/src/clj/rems/css/styles.clj b/src/clj/rems/css/styles.clj
index 92da952dc..fcc68462c 100644
--- a/src/clj/rems/css/styles.clj
+++ b/src/clj/rems/css/styles.clj
@@ -714,10 +714,6 @@
    [:#main-content.page-application {:max-width (u/px (int (* 1.5 (:magnitude content-width))))}]
    [:#float-actions {:position :sticky
                      :top "100px"}]
-   [:.reload-indicator {:position :fixed
-                        :bottom "15px"
-                        :right "15px"
-                        :z-index 1000}] ; over re-frisk devtool
 
    ;; application list
    [:.rems-table
@@ -758,6 +754,32 @@
 
    (generate-phase-styles)
    [(s/descendant :.document :h3) {:margin-top (u/rem 4)}]
+
+   ;; print styling
+   (stylesheet/at-media
+    {:print true}
+    ;; workaround for firefox only printing one page of flex elements
+    ;; https://github.com/twbs/bootstrap/issues/23489
+    ;; https://bugzilla.mozilla.org/show_bug.cgi?id=939897
+    [:.row {:display :block}]
+    [:#app {:display :block}]
+    [(s/> :#app :div) {:display :block}]
+    [:#main-content {:display :block}]
+    [:body {:display :block}]
+
+    ;; hide some unnecessary elements
+    ;; TODO: consider a hide-print class?
+    [:.fixed-top {:display :none}]
+    [:#actions {:display :none}]
+    [:.commands {:display :none}]
+    [:#member-action-forms {:display :none}]
+    [:#resource-action-forms {:display :none}]
+    [:.flash-message {:display :none}]
+
+    ;; open "show more" drawers
+    [".collapse:not(.show)" {:display :block}]
+    [:.collapse-toggle.collapse {:display :none}])
+
    ;; These must be last as the parsing fails when the first non-standard element is met
    (generate-form-placeholder-styles)))
 
diff --git a/src/clj/rems/locales.clj b/src/clj/rems/locales.clj
index c435180ff..3e420047a 100644
--- a/src/clj/rems/locales.clj
+++ b/src/clj/rems/locales.clj
@@ -1,75 +1,42 @@
 (ns rems.locales
   {:ns-tracker/resource-deps ["translations/en.edn" "translations/fi.edn"]}
   (:require [clojure.java.io :as io]
-            [clojure.set :as set]
             [clojure.string :as str]
-            [clojure.tools.logging :as log]
             [mount.core :refer [defstate]]
-            [rems.common-util :refer [deep-merge recursive-keys]]
+            [rems.common-util :refer [deep-merge]]
             [rems.config :refer [env]])
   (:import (java.io FileNotFoundException)))
 
-(defn- file-or-resource
-  "Given a path (a String or File), resolve it to either an existing file or existing resource.
-  Returns a File or URL object, or nil if file nor resource can be found.
-  Prefers files over resources."
-  [path]
-  (let [file (io/file path)
-        resource (io/resource (str file))]
-    (cond
-      (and file (.exists file)) file
-      resource resource
-      :else nil)))
-
-(defn- translations-from-file [file]
-  (if-let [chosen (file-or-resource file)]
-    (read-string (slurp chosen))
-    (throw (FileNotFoundException.
-            (str "translations could not be found in file or resource \"" file "\"")))))
+(defn- translations-from-file [filename dir]
+  (let [file (when dir
+               (io/file dir filename))
+        resource-path (str dir filename)
+        resource (io/resource resource-path)
+        file-contents (cond
+                        (and file (.exists file)) file
+                        resource resource
+                        :else (throw (FileNotFoundException.
+                                      (if file
+                                        (str "translations could not be found in " file " file or " resource-path " resource")
+                                        (str "translations could not be found in " resource-path " resource and " :translations-directory " was not set")))))]
+    (read-string (slurp file-contents))))
 
 (defn- extra-translations-path [theme-path]
-  (let [theme-dir (.getParentFile (io/file theme-path))]
-    (io/file theme-dir "extra-translations")))
-
-(defn extract-format-parameters [string]
-  (set
-   (when (string? string)
-     (re-seq #"%\d+" string))))
-
-(defn- unused-translation-keys [translations extras]
-  (let [keys (set (recursive-keys translations))
-        extra-keys (set (recursive-keys extras))]
-    (seq (set/difference extra-keys keys))))
-
-(defn- nonmatching-format-parameters [translations extras]
-  (seq
-   (for [k (recursive-keys extras)
-         :let [params (extract-format-parameters (get-in translations k))
-               extra-params (extract-format-parameters (get-in extras k))]
-         :when (not= params extra-params)]
-     {:key k
-      :translations params
-      :extra-translations extra-params})))
+  (let [path (str/join "/" (butlast (str/split theme-path #"/"))) ;;Theme-path is of form /foo/bar/theme.edn
+        translations-path (str path "/extra-translations/")]
+    translations-path))
 
 (defn- load-translation [language translations-directory theme-path]
-  (let [filename (str (name language) ".edn")
-        translations (translations-from-file (io/file translations-directory filename))
-        extra-path (when theme-path (extra-translations-path theme-path))]
-    (if (and extra-path (.exists extra-path))
-      (let [extra-file (io/file extra-path filename)
-            extra-translations (translations-from-file extra-file)]
-        (log/info "Loaded extra translations from" (str extra-file))
-        (when-let [unused (unused-translation-keys translations extra-translations)]
-          (log/warn "Unused translation keys defined in" (str extra-file) ":" unused))
-        (when-let [errors (nonmatching-format-parameters translations extra-translations)]
-          (log/warn "Nonmatching format parameters in" (str extra-file) ":" (pr-str errors)))
-        (deep-merge {language translations} {language extra-translations}))
-      {language translations})))
+  (let [filename (str (name language) ".edn")]
+    (if (and theme-path (.exists (io/file (extra-translations-path theme-path))))
+      (deep-merge {language (translations-from-file filename translations-directory)}
+                  {language (translations-from-file filename (extra-translations-path theme-path))})
+      {language (translations-from-file filename translations-directory)})))
 
 (defn load-translations [{:keys [languages translations-directory theme-path]}]
   (if translations-directory
     (->> languages
-         (mapv #(load-translation % translations-directory theme-path))
+         (map #(load-translation % translations-directory theme-path))
          (apply merge))
     (throw (RuntimeException. ":translations-directory was not set in config"))))
 
diff --git a/src/clj/rems/poller/email.clj b/src/clj/rems/poller/email.clj
index 5db899f30..e60c8122c 100644
--- a/src/clj/rems/poller/email.clj
+++ b/src/clj/rems/poller/email.clj
@@ -123,7 +123,7 @@
                                 :t.email.application-closed/message-to-handler)))
 
 (defmethod event-to-emails-impl :application.event/returned [event application]
-  (concat (emails-to-recipients [(:application/applicant application)]
+  (concat (emails-to-recipients (applicant-and-members application)
                                 event application
                                 :t.email.application-returned/subject-to-applicant
                                 :t.email.application-returned/message-to-applicant)
@@ -191,11 +191,7 @@
   (with-language (:default-language env)
     (fn []
       [{:to (:email (:application/member event))
-        :subject (text-format :t.email.member-invited/subject
-                              (:name (:application/member event))
-                              (user-for-email (:application/applicant application))
-                              (format-application-for-email application)
-                              (invitation-link (:invitation/token event)))
+        :subject (text-format :t.email.member-invited/subject)
         :body (str
                (text-format :t.email.member-invited/message
                             (:name (:application/member event))
diff --git a/src/cljc/rems/common_util.cljc b/src/cljc/rems/common_util.cljc
index 370a0abf8..881cbee89 100644
--- a/src/cljc/rems/common_util.cljc
+++ b/src/cljc/rems/common_util.cljc
@@ -125,15 +125,3 @@
     (is (= [{:a 1 :b 2 :c 2}]
            (deep-merge '({:a 1 :b 1})
                        '({:b 2 :c 2}))))))
-
-(defn recursive-keys [m]
-  (mapcat (fn [[k v]]
-            (if (map? v)
-              (map (partial cons k) (recursive-keys v))
-              [(list k)]))
-          m))
-
-(deftest test-recursive-keys
-  (is (= [[:a] [:b]] (recursive-keys {:a [1] :b "foo"})))
-  (is (= [[:a :b] [:a :c] [:a :d :e] [:a :d :f]]
-         (recursive-keys {:a {:b 1 :c nil :d {:e "foo" :f [3]}}}))))
diff --git a/src/cljs/rems/actions/add_licenses.cljs b/src/cljs/rems/actions/add_licenses.cljs
index 6a7e9582c..edf5e97a7 100644
--- a/src/cljs/rems/actions/add_licenses.cljs
+++ b/src/cljs/rems/actions/add_licenses.cljs
@@ -10,9 +10,7 @@
 (rf/reg-fx
  ::fetch-licenses
  (fn [on-success]
-   (fetch "/api/licenses"
-          {:handler on-success
-           :error-handler (flash-message/default-error-handler :top "Fetch licenses")})))
+   (fetch "/api/licenses" {:handler on-success})))
 
 (rf/reg-event-fx
  ::open-form
diff --git a/src/cljs/rems/actions/add_member.cljs b/src/cljs/rems/actions/add_member.cljs
index 194139ca8..c83f92207 100644
--- a/src/cljs/rems/actions/add_member.cljs
+++ b/src/cljs/rems/actions/add_member.cljs
@@ -12,9 +12,7 @@
 (rf/reg-fx
  ::fetch-potential-members
  (fn [on-success]
-   (fetch "/api/applications/members"
-          {:handler on-success
-           :error-handler (flash-message/default-error-handler :top "Fetch potential members")})))
+   (fetch "/api/applications/members" {:handler on-success})))
 
 (rf/reg-event-fx
  ::open-form
diff --git a/src/cljs/rems/actions/change_resources.cljs b/src/cljs/rems/actions/change_resources.cljs
index 6f1c28641..69f8cf771 100644
--- a/src/cljs/rems/actions/change_resources.cljs
+++ b/src/cljs/rems/actions/change_resources.cljs
@@ -5,7 +5,7 @@
             [rems.flash-message :as flash-message]
             [rems.spinner :as spinner]
             [rems.text :refer [text get-localized-title]]
-            [rems.util :refer [post!]]))
+            [rems.util :refer [fetch post!]]))
 
 (rf/reg-event-fx
  ::open-form
diff --git a/src/cljs/rems/actions/request_decision.cljs b/src/cljs/rems/actions/request_decision.cljs
index 77d59e7d6..c84d2db65 100644
--- a/src/cljs/rems/actions/request_decision.cljs
+++ b/src/cljs/rems/actions/request_decision.cljs
@@ -10,9 +10,7 @@
 (rf/reg-fx
  ::fetch-potential-deciders
  (fn [on-success]
-   (fetch "/api/applications/deciders"
-          {:handler on-success
-           :error-handler (flash-message/default-error-handler :top "Fetch potential deciders")})))
+   (fetch "/api/applications/deciders" {:handler on-success})))
 
 (rf/reg-event-fx
  ::open-form
diff --git a/src/cljs/rems/actions/request_review.cljs b/src/cljs/rems/actions/request_review.cljs
index 582c100b4..2a8eb3e42 100644
--- a/src/cljs/rems/actions/request_review.cljs
+++ b/src/cljs/rems/actions/request_review.cljs
@@ -11,9 +11,7 @@
 (rf/reg-fx
  ::fetch-potential-reviewers
  (fn [on-success]
-   (fetch "/api/applications/commenters"
-          {:handler on-success
-           :error-handler (flash-message/default-error-handler :top "Fetch potential reviewers")})))
+   (fetch "/api/applications/commenters" {:handler on-success})))
 
 (rf/reg-event-fx
  ::open-form
diff --git a/src/cljs/rems/administration/catalogue_item.cljs b/src/cljs/rems/administration/catalogue_item.cljs
index 81150c970..0efd1e173 100644
--- a/src/cljs/rems/administration/catalogue_item.cljs
+++ b/src/cljs/rems/administration/catalogue_item.cljs
@@ -19,8 +19,7 @@
 
 (defn- fetch-catalogue-item [catalogue-item-id]
   (fetch (str "/api/catalogue-items/" catalogue-item-id)
-         {:handler #(rf/dispatch [::fetch-catalogue-item-result %])
-          :error-handler (flash-message/default-error-handler :top "Fetch catalogue items")}))
+         {:handler #(rf/dispatch [::fetch-catalogue-item-result %])}))
 
 (rf/reg-fx ::fetch-catalogue-item (fn [[catalogue-item-id]] (fetch-catalogue-item catalogue-item-id)))
 
diff --git a/src/cljs/rems/administration/catalogue_items.cljs b/src/cljs/rems/administration/catalogue_items.cljs
index e1f2ef978..e069262fb 100644
--- a/src/cljs/rems/administration/catalogue_items.cljs
+++ b/src/cljs/rems/administration/catalogue_items.cljs
@@ -21,7 +21,7 @@
  ::fetch-catalogue
  (fn [{:keys [db]}]
    (let [description (text :t.administration/catalogue-items)]
-     (fetch "/api/catalogue-items"
+     (fetch "/api/catalogue-items/"
             {:url-params {:expand :names
                           :disabled true
                           :expired (::display-old? db)
diff --git a/src/cljs/rems/administration/create_catalogue_item.cljs b/src/cljs/rems/administration/create_catalogue_item.cljs
index 42c848137..167b76413 100644
--- a/src/cljs/rems/administration/create_catalogue_item.cljs
+++ b/src/cljs/rems/administration/create_catalogue_item.cljs
@@ -113,9 +113,7 @@
 (rf/reg-event-fx ::edit-catalogue-item edit-catalogue-item!)
 
 (defn- fetch-workflows []
-  (fetch "/api/workflows"
-         {:handler #(rf/dispatch [::fetch-workflows-result %])
-          :error-handler (flash-message/default-error-handler :top "Fetch workflows")}))
+  (fetch "/api/workflows" {:handler #(rf/dispatch [::fetch-workflows-result %])}))
 
 (rf/reg-fx ::fetch-workflows fetch-workflows)
 
@@ -128,9 +126,7 @@
 (rf/reg-sub ::workflows (fn [db _] (::workflows db)))
 
 (defn- fetch-resources []
-  (fetch "/api/resources"
-         {:handler #(rf/dispatch [::fetch-resources-result %])
-          :error-handler (flash-message/default-error-handler :top "Fetch resources")}))
+  (fetch "/api/resources" {:handler #(rf/dispatch [::fetch-resources-result %])}))
 
 (rf/reg-fx ::fetch-resources fetch-resources)
 
@@ -144,9 +140,7 @@
 
 
 (defn- fetch-forms []
-  (fetch "/api/forms"
-         {:handler #(rf/dispatch [::fetch-forms-result %])
-          :error-handler (flash-message/default-error-handler :top "Fetch forms")}))
+  (fetch "/api/forms" {:handler #(rf/dispatch [::fetch-forms-result %])}))
 
 (rf/reg-fx ::fetch-forms fetch-forms)
 
@@ -161,8 +155,7 @@
 
 (defn- fetch-catalogue-item [id]
   (fetch (str "/api/catalogue-items/" id)
-         {:handler #(rf/dispatch [::fetch-catalogue-item-result %])
-          :error-handler (flash-message/default-error-handler :top "Fetch catalogue item")}))
+         {:handler #(rf/dispatch [::fetch-catalogue-item-result %])}))
 
 (rf/reg-fx
  ::fetch-catalogue-item
diff --git a/src/cljs/rems/administration/create_form.cljs b/src/cljs/rems/administration/create_form.cljs
index 2c6f177ce..7ac388fda 100644
--- a/src/cljs/rems/administration/create_form.cljs
+++ b/src/cljs/rems/administration/create_form.cljs
@@ -28,8 +28,7 @@
  (fn [{:keys [db]} [_ form-id]]
    (when form-id
      (fetch (str "/api/forms/" form-id)
-            {:handler #(rf/dispatch [::fetch-form-result %])
-             :error-handler (flash-message/default-error-handler :top "Fetch form")})
+            {:handler #(rf/dispatch [::fetch-form-result %])})
      {:db (assoc db ::loading-form? true)})))
 
 (rf/reg-event-db
@@ -147,12 +146,9 @@
 (defn- validate-field [field id languages]
   {id (merge (validate-text-field field :field/type)
              (validate-localized-text-field field :field/title languages)
-             (when (supports-placeholder? field)
-               (validate-optional-localized-field field :field/placeholder languages))
-             (when (supports-max-length? field)
-               (validate-max-length (:field/max-length field)))
-             (when (supports-options? field)
-               (validate-options (:field/options field) languages)))})
+             (validate-optional-localized-field field :field/placeholder languages)
+             (validate-max-length (:field/max-length field))
+             (validate-options (:field/options field) languages))})
 
 (defn- nil-if-empty [m]
   (when-not (empty? m)
@@ -342,38 +338,6 @@
    "/#/administration/forms"
    (text :t.administration/cancel)])
 
-(defn- format-validation-link [target content]
-  [:li [:a {:href "#" :on-click (in-page-anchor-link target)}
-        content]])
-
-(defn- format-field-validation [field field-errors]
-  (let [field-id (:field/id field)]
-    [:li (text-format :t.create-form/field-n (inc field-id))
-     (into [:ul]
-           (concat
-            (for [[lang error] (:field/title field-errors)]
-              (format-validation-link (str "fields-" field-id "-title-" (name lang))
-                                      (text-format error (str (text :t.create-form/field-title)
-                                                              " (" (.toUpperCase (name lang)) ")"))))
-            (for [[lang error] (:field/placeholder field-errors)]
-              (format-validation-link (str "fields-" field-id "-placeholder-" (name lang))
-                                      (text-format error (str (text :t.create-form/placeholder)
-                                                              " (" (.toUpperCase (name lang)) ")"))))
-            (when (:field/max-length field-errors)
-              [(format-validation-link (str "fields-" field-id "-max-length")
-                                       (str (text :t.create-form/maxlength) ": " (text (:field/max-length field-errors))))])
-            (for [[option-id option-errors] (into (sorted-map) (:field/options field-errors))]
-              [:li (text-format :t.create-form/option-n (inc option-id))
-               [:ul
-                (when (:key option-errors)
-                  (format-validation-link (str "fields-" field-id "-options-" option-id "-key")
-                                          (text-format (:key option-errors) (text :t.create-form/option-key))))
-                (into [:<>]
-                      (for [[lang error] (:label option-errors)]
-                        (format-validation-link (str "fields-" field-id "-options-" option-id "-label-" (name lang))
-                                                (text-format error (str (text :t.create-form/option-label)
-                                                                        " (" (.toUpperCase (name lang)) ")")))))]])))]))
-
 (defn- format-validation-errors [form-errors form]
   ;; TODO: deduplicate with field definitions
   (into [:ul
@@ -389,7 +353,51 @@
 
         (for [[field-id field-errors] (into (sorted-map) (:form/fields form-errors))]
           (let [field (get-in form [:form/fields field-id])]
-            [format-field-validation field field-errors]))))
+            [:li (text-format :t.create-form/field-n (inc field-id))
+             [:ul
+
+              (when (:field/title field-errors)
+                (into [:<>]
+                      (for [[lang error] (:field/title field-errors)]
+                        [:li [:a {:href "#"
+                                  :on-click (in-page-anchor-link (str "fields-" field-id "-title-" (name lang)))}
+                              (text-format error (str (text :t.create-form/field-title)
+                                                      " (" (.toUpperCase (name lang)) ")"))]])))
+
+              (when (supports-placeholder? field)
+                (when (:field/placeholder field-errors)
+                  (into [:<>]
+                        (for [[lang error] (:field/placeholder field-errors)]
+                          [:li [:a {:href "#"
+                                    :on-click (in-page-anchor-link (str "fields-" field-id "-placeholder-" (name lang)))}
+                                (text-format error (str (text :t.create-form/placeholder)
+                                                        " (" (.toUpperCase (name lang)) ")"))]]))))
+
+              (when (supports-max-length? field)
+                (when (:field/max-length field-errors)
+                  [:li [:a {:href "#"
+                            :on-click (in-page-anchor-link (str "fields-" field-id "-max-length"))}
+                        (text :t.create-form/maxlength) ": " (text (:field/max-length field-errors))]]))
+
+              (when (supports-options? field)
+                (when (:field/options field-errors)
+                  (into [:<>]
+                        (for [[option-id option-errors] (into (sorted-map) (:field/options field-errors))]
+                          [:li (text-format :t.create-form/option-n (inc option-id))
+                           [:ul
+
+                            (when (:key option-errors)
+                              [:li [:a {:href "#"
+                                        :on-click (in-page-anchor-link (str "fields-" field-id "-options-" option-id "-key"))}
+                                    (text-format (:key option-errors) (text :t.create-form/option-key))]])
+
+                            (when (:label option-errors)
+                              (into [:<>]
+                                    (for [[lang error] (:label option-errors)]
+                                      [:li [:a {:href "#"
+                                                :on-click (in-page-anchor-link (str "fields-" field-id "-options-" option-id "-label-" (name lang)))}
+                                            (text-format error (str (text :t.create-form/option-label)
+                                                                    " (" (.toUpperCase (name lang)) ")"))]])))]]))))]]))))
 
 (defn- validation-errors-summary []
   (let [form @(rf/subscribe [::form])
@@ -440,7 +448,7 @@
      [flash-message/component :top]
      (if loading-form?
        [:div [spinner/big]]
-       [:div.container-fluid
+       [:div.container-fluid.editor-content
         [validation-errors-summary]
         [:div.row
          [:div.col-lg
diff --git a/src/cljs/rems/administration/create_license.cljs b/src/cljs/rems/administration/create_license.cljs
index e150dac72..e1334c32a 100644
--- a/src/cljs/rems/administration/create_license.cljs
+++ b/src/cljs/rems/administration/create_license.cljs
@@ -72,8 +72,7 @@
   (post! "/api/licenses/add_attachment"
          {:body form-data
           :handler (fn [response]
-                     (rf/dispatch [::attachment-saved language (:id response)]))
-          :error-handler (flash-message/default-error-handler :top "Save attachment")}))
+                     (rf/dispatch [::attachment-saved language (:id response)]))}))
 
 (rf/reg-event-db
  ::attachment-saved
@@ -83,8 +82,7 @@
 (defn- remove-attachment [attachment-id]
   (post! "/api/licenses/remove_attachment"
          {:url-params {:attachment-id attachment-id}
-          :body {}
-          :error-handler (flash-message/default-error-handler :top "Remove attachment")}))
+          :body {}}))
 
 (rf/reg-event-fx
  ::save-attachment
diff --git a/src/cljs/rems/administration/create_resource.cljs b/src/cljs/rems/administration/create_resource.cljs
index 3380aea69..5b30dec80 100644
--- a/src/cljs/rems/administration/create_resource.cljs
+++ b/src/cljs/rems/administration/create_resource.cljs
@@ -59,8 +59,7 @@
 (defn- fetch-licenses []
   (fetch "/api/licenses"
          {:url-params {:active true}
-          :handler #(rf/dispatch [::fetch-licenses-result %])
-          :error-handler (flash-message/default-error-handler :top "Fetch licenses")}))
+          :handler #(rf/dispatch [::fetch-licenses-result %])}))
 
 (rf/reg-fx ::fetch-licenses (fn [_] (fetch-licenses)))
 
diff --git a/src/cljs/rems/administration/create_workflow.cljs b/src/cljs/rems/administration/create_workflow.cljs
index 9c092ffd5..be73009da 100644
--- a/src/cljs/rems/administration/create_workflow.cljs
+++ b/src/cljs/rems/administration/create_workflow.cljs
@@ -39,8 +39,7 @@
  (fn [workflow-id]
    (when workflow-id
      (fetch (str "/api/workflows/" workflow-id)
-            {:handler #(rf/dispatch [::fetch-workflow-result %])
-             :error-handler (flash-message/default-error-handler :top "Fetch workflow")}))))
+            {:handler #(rf/dispatch [::fetch-workflow-result %])}))))
 
 (rf/reg-event-db
  ::fetch-workflow-result
@@ -109,9 +108,7 @@
 (rf/reg-event-db ::set-handlers (fn [db [_ handlers]] (assoc-in db [::form :handlers] (sort-by :userid handlers))))
 
 (defn- fetch-actors []
-  (fetch "/api/workflows/actors"
-         {:handler #(rf/dispatch [::fetch-actors-result %])
-          :error-handler (flash-message/default-error-handler :top "Fetch actors")}))
+  (fetch "/api/workflows/actors" {:handler #(rf/dispatch [::fetch-actors-result %])}))
 
 (rf/reg-fx ::fetch-actors fetch-actors)
 
diff --git a/src/cljs/rems/administration/form.cljs b/src/cljs/rems/administration/form.cljs
index c286e0014..3420e9a96 100644
--- a/src/cljs/rems/administration/form.cljs
+++ b/src/cljs/rems/administration/form.cljs
@@ -20,8 +20,7 @@
 
 (defn- fetch-form [form-id]
   (fetch (str "/api/forms/" form-id)
-         {:handler #(rf/dispatch [::fetch-form-result %])
-          :error-handler (flash-message/default-error-handler :top "Fetch form")}))
+         {:handler #(rf/dispatch [::fetch-form-result %])}))
 
 (rf/reg-fx ::fetch-form (fn [[form-id]] (fetch-form form-id)))
 
diff --git a/src/cljs/rems/administration/forms.cljs b/src/cljs/rems/administration/forms.cljs
index 41b91e040..c6bac0896 100644
--- a/src/cljs/rems/administration/forms.cljs
+++ b/src/cljs/rems/administration/forms.cljs
@@ -21,7 +21,7 @@
  ::fetch-forms
  (fn [db]
    (let [description (text :t.administration/forms)]
-     (fetch "/api/forms"
+     (fetch "/api/forms/"
             {:url-params {:disabled true
                           :expired (::display-old? db)
                           :archived (::display-old? db)}
diff --git a/src/cljs/rems/administration/license.cljs b/src/cljs/rems/administration/license.cljs
index 408ac2e67..024818601 100644
--- a/src/cljs/rems/administration/license.cljs
+++ b/src/cljs/rems/administration/license.cljs
@@ -19,8 +19,7 @@
 
 (defn- fetch-license [license-id]
   (fetch (str "/api/licenses/" license-id)
-         {:handler #(rf/dispatch [::fetch-license-result %])
-          :error-handler (flash-message/default-error-handler :top "Fetch license")}))
+         {:handler #(rf/dispatch [::fetch-license-result %])}))
 
 (rf/reg-fx ::fetch-license (fn [[license-id]] (fetch-license license-id)))
 
diff --git a/src/cljs/rems/administration/licenses.cljs b/src/cljs/rems/administration/licenses.cljs
index c73605b72..041cd19ec 100644
--- a/src/cljs/rems/administration/licenses.cljs
+++ b/src/cljs/rems/administration/licenses.cljs
@@ -20,7 +20,7 @@
  ::fetch-licenses
  (fn [db]
    (let [description (text :t.administration/licenses)]
-     (fetch "/api/licenses"
+     (fetch "/api/licenses/"
             {:url-params {:disabled true
                           :expired (::display-old? db)
                           :archived (::display-old? db)}
diff --git a/src/cljs/rems/administration/resource.cljs b/src/cljs/rems/administration/resource.cljs
index 1c1cbd9dc..2345f87b6 100644
--- a/src/cljs/rems/administration/resource.cljs
+++ b/src/cljs/rems/administration/resource.cljs
@@ -20,8 +20,7 @@
 
 (defn- fetch-resource [resource-id]
   (fetch (str "/api/resources/" resource-id)
-         {:handler #(rf/dispatch [::fetch-resource-result %])
-          :error-handler (flash-message/default-error-handler :top "Fetch resource")}))
+         {:handler #(rf/dispatch [::fetch-resource-result %])}))
 
 (rf/reg-fx ::fetch-resource (fn [[resource-id]] (fetch-resource resource-id)))
 
diff --git a/src/cljs/rems/administration/workflow.cljs b/src/cljs/rems/administration/workflow.cljs
index 3959fc4b7..0244fb391 100644
--- a/src/cljs/rems/administration/workflow.cljs
+++ b/src/cljs/rems/administration/workflow.cljs
@@ -21,8 +21,7 @@
 
 (defn- fetch-workflow [workflow-id]
   (fetch (str "/api/workflows/" workflow-id)
-         {:handler #(rf/dispatch [::fetch-workflow-result %])
-          :error-handler (flash-message/default-error-handler :top "Fetch workflow")}))
+         {:handler #(rf/dispatch [::fetch-workflow-result %])}))
 
 (rf/reg-fx ::fetch-workflow (fn [[workflow-id]] (fetch-workflow workflow-id)))
 
diff --git a/src/cljs/rems/administration/workflows.cljs b/src/cljs/rems/administration/workflows.cljs
index 6db74bd39..694dae84c 100644
--- a/src/cljs/rems/administration/workflows.cljs
+++ b/src/cljs/rems/administration/workflows.cljs
@@ -21,12 +21,11 @@
  ::fetch-workflows
  (fn [db]
    (let [description (text :t.administration/workflows)]
-     (fetch "/api/workflows"
-            {:url-params {:disabled true
-                          :archived (::display-old? db)
-                          :expired (::display-old? db)}
-             :handler #(rf/dispatch [::fetch-workflows-result %])
-             :error-handler (flash-message/default-error-handler :top description)}))
+     (fetch "/api/workflows/" {:url-params {:disabled true
+                                            :archived (::display-old? db)
+                                            :expired (::display-old? db)}
+                               :handler #(rf/dispatch [::fetch-workflows-result %])
+                               :error-handler (flash-message/default-error-handler :top description)}))
    (assoc db ::loading? true)))
 
 (rf/reg-event-db
diff --git a/src/cljs/rems/application.cljs b/src/cljs/rems/application.cljs
index 8044266d6..9a6aad09f 100644
--- a/src/cljs/rems/application.cljs
+++ b/src/cljs/rems/application.cljs
@@ -24,7 +24,6 @@
             [rems.catalogue-util :refer [urn-catalogue-item-link]]
             [rems.collapsible :as collapsible]
             [rems.common-util :refer [index-by]]
-            [rems.fetcher :as fetcher]
             [rems.fields :as fields]
             [rems.flash-message :as flash-message]
             [rems.guide-utils :refer [lipsum lipsum-short lipsum-paragraphs]]
@@ -37,7 +36,7 @@
 ;;;; Helpers
 
 (defn reload! [application-id]
-  (rf/dispatch [::fetch-application application-id]))
+  (rf/dispatch [:rems.application/reload-application-page application-id]))
 
 (defn- in-processing? [application]
   (not (contains? #{:application.state/approved
@@ -85,42 +84,49 @@
 
 ;;;; State
 
-(rf/reg-sub ::application-id (fn [db _] (::application-id db)))
-(rf/reg-sub ::application (fn [db [_ k]] (get-in db [::application (or k :data)])))
+(rf/reg-sub ::application (fn [db _] (::application db)))
 (rf/reg-sub ::edit-application (fn [db _] (::edit-application db)))
 
 (rf/reg-event-fx
  ::enter-application-page
  (fn [{:keys [db]} [_ id]]
-   {:db (-> db
-            (assoc ::application-id id)
-            (dissoc ::application ::edit-application ::attachment-success))
+   {:db (dissoc db ::application ::edit-application ::attachment-success)
     :dispatch [::fetch-application id]}))
 
 (rf/reg-event-fx
  ::fetch-application
- (fn [{:keys [db]} [_ id]]
+ (fn [_ [_ id]]
    (fetch (str "/api/applications/" id)
-          {:handler #(rf/dispatch [::fetch-application-result %])
-           :error-handler (comp #(rf/dispatch [::fetch-application-result nil])
-                                (flash-message/default-error-handler :top (text :t.applications/application)))})
-   {:db (update db ::application fetcher/started)}))
-
-(defn- initialize-edit-application [db]
-  (let [application (get-in db [::application :data])
-        field-values (->> (get-in application [:application/form :form/fields])
-                          (map (juxt :field/id :field/value))
-                          (into {}))]
-    (assoc db ::edit-application {:field-values field-values
-                                  :show-diff {}
-                                  :validation-errors nil})))
+          {:handler #(rf/dispatch [::fetch-application-result %])})
+   {}))
 
 (rf/reg-event-db
  ::fetch-application-result
  (fn [db [_ application]]
-   (let [initial-fetch? (not (:initialized? (::application db)))]
-     (cond-> (update db ::application fetcher/finished application)
-       initial-fetch? (initialize-edit-application)))))
+   (assoc db
+          ::application application
+          ::edit-application {:field-values (->> (get-in application [:application/form :form/fields])
+                                                 (map (juxt :field/id :field/value))
+                                                 (into {}))
+                              :show-diff {}
+                              :validation-errors nil})))
+
+(rf/reg-event-fx
+ ::reload-application-page
+ (fn [{:keys [db]} [_ id]]
+   {::reload-application id}))
+
+(rf/reg-fx
+ ::reload-application
+ (fn [id]
+   (fetch (str "/api/applications/" id)
+          {:handler #(rf/dispatch [::reload-application-result %])})))
+
+(rf/reg-event-db
+ ::reload-application-result
+ (fn [db [_ application]]
+   (assoc db
+          ::application application)))
 
 (rf/reg-event-db
  ::set-validation-errors
@@ -138,13 +144,14 @@
           :handler (flash-message/default-success-handler
                     :actions
                     description
-                    #(rf/dispatch [::fetch-application application-id]))
+                    (fn [_]
+                      (rf/dispatch [::fetch-application application-id])))
           :error-handler (flash-message/default-error-handler :actions description)}))
 
 (rf/reg-event-fx
  ::save-application
  (fn [{:keys [db]} [_ description]]
-   (let [application (:data (::application db))
+   (let [application (::application db)
          edit-application (::edit-application db)]
      (save-application! description
                         (:application/id application)
@@ -182,7 +189,7 @@
 (rf/reg-event-fx
  ::submit-application
  (fn [{:keys [db]} [_ description]]
-   (let [application (:data (::application db))
+   (let [application (::application db)
          edit-application (::edit-application db)]
      (submit-application! application
                           description
@@ -194,7 +201,7 @@
 (rf/reg-event-fx
  ::copy-as-new-application
  (fn [{:keys [db]} _]
-   (let [application-id (get-in db [::application :data :application/id])
+   (let [application-id (get-in db [::application :application/id])
          description (text :t.form/copy-as-new)]
      (post! "/api/applications/copy-as-new"
             {:params {:application-id application-id}
@@ -208,7 +215,7 @@
    {}))
 
 (defn- save-attachment [{:keys [db]} [_ field-id file description]]
-  (let [application-id (get-in db [::application :data :application/id])]
+  (let [application-id (get-in db [::application :application/id])]
     (post! "/api/applications/add-attachment"
            {:url-params {:application-id application-id
                          :field-id field-id}
@@ -683,7 +690,9 @@
                 [change-resources-form application can-comment? (partial reload! application-id)]]]}]))
 
 (defn- render-application [{:keys [application edit-application attachment-success config userid]}]
-  [:<>
+  [:div.container-fluid.editor-content
+   [document-title (str (text :t.applications/application) " " (format-application-id config application))]
+   [flash-message/component :top]
    [disabled-items-warning application]
    (text :t.applications/intro)
    [:div.row
@@ -702,32 +711,21 @@
 
 (defn application-page []
   (let [config @(rf/subscribe [:rems.config/config])
-        application-id @(rf/subscribe [::application-id])
         application @(rf/subscribe [::application])
-        loading? @(rf/subscribe [::application :loading?])
-        reloading? @(rf/subscribe [::application :reloading?])
         edit-application @(rf/subscribe [::edit-application])
         attachment-success @(rf/subscribe [::attachment-success])
-        userid (get-in @(rf/subscribe [:identity]) [:user :eppn])]
-    [:div.container-fluid
-     [document-title (if application
-                       (str (text :t.applications/application) " " (format-application-id config application))
-                       (text :t.applications/application))]
-     ^{:key application-id} ; re-render to clear flash messages when navigating to another application
-     [flash-message/component :top]
-     (when loading?
-       [spinner/big])
-     (when application
-       [render-application {:application application
-                            :edit-application edit-application
-                            :attachment-success attachment-success
-                            :config config
-                            :userid userid}])
-     ;; Located after the application to avoid re-rendering the application
-     ;; when this element is added or removed from virtual DOM.
-     (when reloading?
-       [:div.reload-indicator
-        [spinner/small]])]))
+        userid (get-in @(rf/subscribe [:identity]) [:user :eppn])
+        loading? (not application)]
+    (if loading?
+      [:div
+       [document-title (text :t.applications/application)]
+       [spinner/big]]
+      [render-application {:application application
+                           :edit-application edit-application
+                           :attachment-success attachment-success
+                           :config config
+                           :userid userid}])))
+
 
 ;;;; Guide
 
diff --git a/src/cljs/rems/catalogue.cljs b/src/cljs/rems/catalogue.cljs
index cb3a13cdf..c23a29101 100644
--- a/src/cljs/rems/catalogue.cljs
+++ b/src/cljs/rems/catalogue.cljs
@@ -31,10 +31,8 @@
 (rf/reg-event-fx
  ::fetch-catalogue
  (fn [{:keys [db]} _]
-   ;; TODO: better error handler, don't show spinner if request has failed
    (fetch "/api/catalogue"
-          {:handler #(rf/dispatch [::fetch-catalogue-result %])
-           :error-handler (flash-message/default-error-handler :top "Fetch catalogue")})
+          {:handler #(rf/dispatch [::fetch-catalogue-result %])})
    {:db (assoc db ::loading-catalogue? true)}))
 
 (rf/reg-event-db
@@ -62,10 +60,8 @@
 (rf/reg-event-fx
  ::fetch-drafts
  (fn [{:keys [db]} _]
-   ;; TODO: better error handler, don't show spinner if request has failed
    (fetch "/api/my-applications"
-          {:handler #(rf/dispatch [::fetch-drafts-result %])
-           :error-handler (flash-message/default-error-handler :top "Fetch drafts")})
+          {:handler #(rf/dispatch [::fetch-drafts-result %])})
    {:db (assoc db ::loading-drafts? true)}))
 
 (rf/reg-event-db
diff --git a/src/cljs/rems/config.cljs b/src/cljs/rems/config.cljs
index de6f7a8a9..983744427 100644
--- a/src/cljs/rems/config.cljs
+++ b/src/cljs/rems/config.cljs
@@ -1,15 +1,13 @@
 (ns rems.config
   (:require [re-frame.core :as rf]
-            [rems.flash-message :as flash-message]
             [rems.util :refer [fetch]]))
 
 (rf/reg-event-db
  ::loaded-config
  (fn [db [_ config]]
-   (assoc db
-          :config config
-          :default-language (get config :default-language)
-          :languages (get config :languages))))
+   (assoc db :config config
+             :default-language (get config :default-language)
+             :languages (get config :languages))))
 
 (rf/reg-sub
  ::config
@@ -17,9 +15,7 @@
    (:config db)))
 
 (defn fetch-config! []
-  (fetch "/api/config"
-         {:handler #(rf/dispatch [::loaded-config %])
-          :error-handler (flash-message/default-error-handler :top "Fetch config")}))
+  (fetch "/api/config" {:handler #(rf/dispatch [::loaded-config %])}))
 
 (defn dev-environment? []
   (let [config @(rf/subscribe [::config])]
diff --git a/src/cljs/rems/fetcher.cljs b/src/cljs/rems/fetcher.cljs
deleted file mode 100644
index 6fffe520b..000000000
--- a/src/cljs/rems/fetcher.cljs
+++ /dev/null
@@ -1,27 +0,0 @@
-(ns rems.fetcher
-  "Helper functions for tracking the state of a data fetch request.
-
-  The state map will contain the following keys:
-    :data - the data which was fetched (nil if error or not fetched)
-    :fetching? - whether a fetch is in progress
-    :initialized? - whether at least one fetch has finished
-    :loading? - whether the first fetch is in progress
-    :reloading? - whether a subsequent fetch is in progress")
-
-(defn- update-derived-state [state]
-  (let [{:keys [initialized? fetching?]} state]
-    (assoc state
-           :loading? (and fetching? (not initialized?))
-           :reloading? (and fetching? initialized?))))
-
-(defn started [state]
-  (-> state
-      (assoc :fetching? true)
-      update-derived-state))
-
-(defn finished [state data]
-  (-> state
-      (assoc :data data
-             :initialized? true
-             :fetching? false)
-      update-derived-state))
diff --git a/src/cljs/rems/flash_message.cljs b/src/cljs/rems/flash_message.cljs
index 9d747f207..4eaa97589 100644
--- a/src/cljs/rems/flash_message.cljs
+++ b/src/cljs/rems/flash_message.cljs
@@ -112,8 +112,7 @@
         (show-default-success! location description)
         (when on-success
           (on-success response)))
-      (show-default-error! location description (format-response-error response)))
-    response))
+      (show-default-error! location description (format-response-error response)))))
 
 (defn status-update-handler [location description on-success]
   (fn [response]
@@ -122,10 +121,8 @@
         (show-default-success! location description)
         (when on-success
           (on-success response)))
-      (show-default-error! location description (status-flags/format-update-failure response)))
-    response))
+      (show-default-error! location description (status-flags/format-update-failure response)))))
 
 (defn default-error-handler [location description]
   (fn [response]
-    (show-default-error! location description (format-response-error response))
-    response))
+    (show-default-error! location description (format-response-error response))))
diff --git a/src/cljs/rems/spa.cljs b/src/cljs/rems/spa.cljs
index 79616d943..4dfc76fcd 100644
--- a/src/cljs/rems/spa.cljs
+++ b/src/cljs/rems/spa.cljs
@@ -423,14 +423,10 @@
 ;; Initialize app
 
 (defn fetch-translations! []
-  (fetch "/api/translations"
-         {:handler #(rf/dispatch [:loaded-translations %])
-          :error-handler (flash-message/default-error-handler :top "Fetch translations")}))
+  (fetch "/api/translations" {:handler #(rf/dispatch [:loaded-translations %])}))
 
 (defn fetch-theme! []
-  (fetch "/api/theme"
-         {:handler #(rf/dispatch [:loaded-theme %])
-          :error-handler (flash-message/default-error-handler :top "Fetch theme")}))
+  (fetch "/api/theme" {:handler #(rf/dispatch [:loaded-theme %])}))
 
 (defn mount-components []
   (rf/clear-subscription-cache!)


Test patch:
diff --git a/test/clj/rems/application/test_model.clj b/test/clj/rems/application/test_model.clj
index de9384dfa..e1c5dd4ab 100644
--- a/test/clj/rems/application/test_model.clj
+++ b/test/clj/rems/application/test_model.clj
@@ -848,8 +848,7 @@
                                                                  :application/events events
                                                                  :application/members #{}
                                                                  :application/past-members #{{:userid "member"}}})]
-                                (is (= expected-application (apply-events events))))))))))
-                  )))))))))
+                                (is (= expected-application (apply-events events)))))))))))))))))))
 
 (deftest test-calculate-permissions
   ;; TODO: is this what we want? wouldn't it be useful to be able to write more than one comment?
diff --git a/test/clj/rems/poller/test_email.clj b/test/clj/rems/poller/test_email.clj
index fe8ad6309..1106cc5fe 100644
--- a/test/clj/rems/poller/test_email.clj
+++ b/test/clj/rems/poller/test_email.clj
@@ -135,14 +135,14 @@
   (let [mails (emails created-events submit-event)]
     (is (= #{"assistant" "handler"} (email-recipients mails)))
     (is (= {:to-user "assistant"
-            :subject "(2001/3, \"Application title\") A new application has been submitted"
-            :body "Dear assistant,\n\nAlice Applicant has submitted a new application 2001/3, \"Application title\" to access resource(s) en title 11, en title 21.\n\nYou can review the application at http://example.com/#/application/7\n\nPlease do not reply to this automatically generated message."}
+            :subject "A new application has been submitted (2001/3, \"Application title\")"
+            :body "Dear assistant,\n\nAlice Applicant has submitted a new application 2001/3, \"Application title\" to access resource(s) en title 11, en title 21.\n\nYou can view the application: http://example.com/#/application/7\n\nPlease do not reply to this automatically generated message."}
            (email-to "assistant" mails)))))
 
 (deftest test-member-invited
-  (is (= [{:to "somebody@example.com"
-           :subject "Invitation to participate in application 2001/3, \"Application title\""
-           :body "Dear Some Body,\n\nYou have been invited to participate in application 2001/3, \"Application title\", by Alice Applicant.\n\nYou can view the application and accept the terms of use at http://example.com/accept-invitation?token=abc\n\nPlease do not reply to this automatically generated message."}]
+  (is (= [{:to "somebody@example.com",
+           :subject "Invitation to participate in an application",
+           :body "Dear Some Body,\n\nYou have been invited to participate in an application submitted by Alice Applicant. The title of the application is 2001/3, \"Application title\".\n\nYou can view the application and accept the terms of use: http://example.com/accept-invitation?token=abc\n\nPlease do not reply to this automatically generated message."}]
          (emails base-events
                  {:application/id 7
                   :event/type :application.event/member-invited
@@ -161,8 +161,8 @@
       (let [mails (emails base-events request)]
         (is (= #{"commenter1" "commenter2"} (email-recipients mails)))
         (is (= {:to-user "commenter1"
-                :subject "(2001/3, \"Application title\") Review request"
-                :body "Dear commenter1,\n\nHannah Handler has requested your review on application 2001/3, \"Application title\", submitted by Alice Applicant.\n\nYou can review the application at http://example.com/#/application/7\n\nPlease do not reply to this automatically generated message."}
+                :subject "Review request (2001/3, \"Application title\")"
+                :body "Dear commenter1,\n\nHannah Handler has requested your review on application 2001/3, \"Application title\" submitted by Alice Applicant.\n\nYou can review the application: http://example.com/#/application/7\n\nPlease do not reply to this automatically generated message."}
                (email-to "commenter1" mails)))))
     (testing "commented"
       (let [mails (emails requested-events {:application/id 7
@@ -172,8 +172,8 @@
                                             :application/comment "this is a comment"})]
         (is (= #{"assistant" "handler"} (email-recipients mails)))
         (is (= {:to-user "assistant"
-                :subject "(2001/3, \"Application title\") Application has been reviewed"
-                :body "Dear assistant,\n\ncommenter2 has reviewed the application 2001/3, \"Application title\", submitted by Alice Applicant.\n\nYou can review the application at http://example.com/#/application/7\n\nPlease do not reply to this automatically generated message."}
+                :subject "Application has been reviewed (2001/3, \"Application title\")"
+                :body "Dear assistant,\n\ncommenter2 has reviewed the application 2001/3, \"Application title\" submitted by Alice Applicant.\n\nYou can view the application and the review: http://example.com/#/application/7\n\nPlease do not reply to this automatically generated message."}
                (email-to "assistant" mails)))))))
 
 (deftest test-remarked
@@ -183,8 +183,8 @@
                                    :application/comment "remark!"})]
     (is (= #{"assistant" "handler"} (email-recipients mails)))
     (is (= {:to-user "assistant"
-            :subject "(2001/3, \"Application title\") Application has been commented"
-            :body "Dear assistant,\n\nremarker has commented on the application 2001/3, \"Application title\", submitted by Alice Applicant.\n\nYou can review the application at http://example.com/#/application/7\n\nPlease do not reply to this automatically generated message."}
+            :subject "Application has been commented on (2001/3, \"Application title\")"
+            :body "Dear assistant,\n\nremarker has commented on the application 2001/3, \"Application title\" submitted by Alice Applicant.\n\nYou can view the application and the comment: http://example.com/#/application/7\n\nPlease do not reply to this automatically generated message."}
            (email-to "assistant" mails)))))
 
 (deftest test-members-licenses-approved-closed
@@ -198,8 +198,8 @@
         member-events (conj base-events add-member join)]
     (testing "member-added"
       (is (= [{:to-user "member",
-               :subject "Added as a member of an application 2001/3, \"Application title\"",
-               :body "Dear member,\n\nYou've been added as a member of application 2001/3, \"Application title\", submitted by Alice Applicant.\n\nYou can view application and accept the terms of use at http://example.com/#/application/7\n\nPlease do not reply to this automatically generated message."}]
+               :subject "Added as a member of an application (2001/3, \"Application title\")",
+               :body "Dear member,\n\nYou've been added as a member of application 2001/3, \"Application title\".\n\nView application: http://example.com/#/application/7\n\nPlease do not reply to this automatically generated message."}]
              (emails base-events add-member))))
     (testing "licenses-added"
       (let [mails (emails member-events {:application/id 7
@@ -208,8 +208,8 @@
                                          :application/licenses [{:license/id 1234}]})]
         (is (= #{"applicant" "member" "somebody"} (email-recipients mails)))
         (is (= {:to-user "applicant"
-                :subject "Your application's 2001/3, \"Application title\" terms of use have changed"
-                :body "Dear Alice Applicant,\n\nHannah Handler has requested your approval for changed terms of use to application 2001/3, \"Application title\".\n\nYou can review the application and approve the changed the terms of use at http://example.com/#/application/7\n\nPlease do not reply to this automatically generated message."}
+                :subject "New terms of use waiting for approval (2001/3, \"Application title\")"
+                :body "Dear Alice Applicant,\n\nHannah Handler has requested your acceptance for new terms of use for application 2001/3, \"Application title\".\n\nYou can view the application and accept the terms of use: http://example.com/#/application/7\n\nPlease do not reply to this automatically generated message."}
                (email-to "applicant" mails)))))
     (testing "approved"
       (let [mails (emails member-events {:application/id 7
@@ -217,16 +217,16 @@
                                          :event/actor "handler"})]
         (is (= #{"applicant" "member" "somebody" "assistant"} (email-recipients mails)))
         (is (= {:to-user "applicant"
-                :subject "Your application 2001/3, \"Application title\" has been approved"
-                :body "Dear Alice Applicant,\n\nYour application 2001/3, \"Application title\" has been approved.\n\nYou can view the application and the decision at http://example.com/#/application/7\n\nPlease do not reply to this automatically generated message."}
+                :subject "Your application has been approved (2001/3, \"Application title\")"
+                :body "Dear Alice Applicant,\n\nYour application 2001/3, \"Application title\" has been approved.\n\nYou can view the application and the decision: http://example.com/#/application/7\n\nPlease do not reply to this automatically generated message."}
                (email-to "applicant" mails)))
         (is (= {:to-user "member"
-                :subject "Your application 2001/3, \"Application title\" has been approved"
-                :body "Dear member,\n\nYour application 2001/3, \"Application title\" has been approved.\n\nYou can view the application and the decision at http://example.com/#/application/7\n\nPlease do not reply to this automatically generated message."}
+                :subject "Your application has been approved (2001/3, \"Application title\")"
+                :body "Dear member,\n\nYour application 2001/3, \"Application title\" has been approved.\n\nYou can view the application and the decision: http://example.com/#/application/7\n\nPlease do not reply to this automatically generated message."}
                (email-to "member" mails)))
         (is (= {:to-user "assistant"
-                :subject "(2001/3, \"Application title\") Application has been approved"
-                :body "Dear assistant,\n\nHannah Handler has approved the application 2001/3, \"Application title\" from Alice Applicant.\n\nYou can view the application and the decision at http://example.com/#/application/7\n\nPlease do not reply to this automatically generated message."}
+                :subject "Application approved (2001/3, \"Application title\")"
+                :body "Dear assistant,\n\nHannah Handler has approved the application 2001/3, \"Application title\" from Alice Applicant.\n\nYou can view the application and the decision: http://example.com/#/application/7\n\nPlease do not reply to this automatically generated message."}
                (email-to "assistant" mails)))))
     (testing "closed"
       (let [mails (emails member-events {:application/id 7
@@ -234,12 +234,12 @@
                                          :event/actor "assistant"})]
         (is (= #{"applicant" "member" "somebody" "handler"} (email-recipients mails)))
         (is (= {:to-user "applicant"
-                :subject "Your application 2001/3, \"Application title\" has been closed"
-                :body "Dear Alice Applicant,\n\nYour application 2001/3, \"Application title\" has been closed.\n\nYou can view the application at http://example.com/#/application/7\n\nPlease do not reply to this automatically generated message."}
+                :subject "Your application has been closed (2001/3, \"Application title\")"
+                :body "Dear Alice Applicant,\n\nYour application 2001/3, \"Application title\" has been closed.\n\nYou can view the application: http://example.com/#/application/7\n\nPlease do not reply to this automatically generated message."}
                (email-to "applicant" mails)))
         (is (= {:to-user "handler"
-                :subject "(2001/3, \"Application title\") Application has been closed"
-                :body "Dear Hannah Handler,\n\nassistant has closed the application 2001/3, \"Application title\" from Alice Applicant.\n\nYou can view the application at http://example.com/#/application/7\n\nPlease do not reply to this automatically generated message."}
+                :subject "Application closed (2001/3, \"Application title\")"
+                :body "Dear Hannah Handler,\n\nassistant has closed the application 2001/3, \"Application title\" from Alice Applicant.\n\nYou can view the application: http://example.com/#/application/7\n\nPlease do not reply to this automatically generated message."}
                (email-to "handler" mails)))))))
 
 (deftest test-decisions
@@ -251,8 +251,8 @@
         requested-events (conj base-events decision-request)]
     (testing "decision-requested"
       (is (= [{:to-user "decider",
-               :subject "(2001/3, \"Application title\") Decision request",
-               :body "Dear decider,\n\nassistant has requested your decision on application 2001/3, \"Application title\", submitted by Alice Applicant.\n\nYou can review application at http://example.com/#/application/7\n\nPlease do not reply to this automatically generated message."}]
+               :subject "Decision requested (2001/3, \"Application title\")",
+               :body "Dear decider,\n\nassistant has requested your decision on application 2001/3, \"Application title\" submitted by Alice Applicant.\n\nYou can view application: http://example.com/#/application/7\n\nPlease do not reply to this automatically generated message."}]
              (emails base-events decision-request))))
     (testing "decided"
       (let [mails (emails requested-events {:application/id 7
@@ -261,17 +261,17 @@
                                             :application/decision :approved})]
         (is (= #{"assistant" "handler"} (email-recipients mails)))
         (is (= {:to-user "assistant",
-                :subject "(2001/3, \"Application title\") Application decision notification",
-                :body "Dear assistant,\n\ndecider has filed a decision on application 2001/3, \"Application title\", submitted by Alice Applicant.\n\nYou can review the application at http://example.com/#/application/7\n\nPlease do not reply to this automatically generated message."}
+                :subject "Decision notification (2001/3, \"Application title\")",
+                :body "Dear assistant,\n\ndecider has sent a decision on application 2001/3, \"Application title\" submitted by Alice Applicant.\n\nYou can view the application and the decision: http://example.com/#/application/7\n\nPlease do not reply to this automatically generated message."}
                (email-to "assistant" mails)))))))
 
 (deftest test-rejected
   (is (= [{:to-user "applicant"
-           :subject "Your application 2001/3, \"Application title\" has been rejected",
-           :body "Dear Alice Applicant,\n\nYour application 2001/3, \"Application title\" has been rejected.\n\nYou can view the application and the decision at http://example.com/#/application/7\n\nPlease do not reply to this automatically generated message."}
+           :subject "Your application has been rejected (2001/3, \"Application title\")",
+           :body "Dear Alice Applicant,\n\nYour application 2001/3, \"Application title\" has been rejected.\n\nYou can view the application and the decision: http://example.com/#/application/7\n\nPlease do not reply to this automatically generated message."}
           {:to-user "assistant"
-           :subject "(2001/3, \"Application title\") Application has been rejected",
-           :body "Dear assistant,\n\nHannah Handler has rejected the application 2001/3, \"Application title\" from Alice Applicant.\n\nYou can view the application and the decision at http://example.com/#/application/7\n\nPlease do not reply to this automatically generated message."}]
+           :subject "Application rejected (2001/3, \"Application title\")",
+           :body "Dear assistant,\n\nHannah Handler has rejected the application 2001/3, \"Application title\" from Alice Applicant.\n\nYou can view the application and the decision: http://example.com/#/application/7\n\nPlease do not reply to this automatically generated message."}]
          (emails base-events {:application/id 7
                               :event/type :application.event/rejected
                               :event/actor "handler"}))))
@@ -279,14 +279,14 @@
 (deftest test-id-field
   (with-redefs [rems.config/env (assoc rems.config/env :application-id-column :id)]
     (is (= {:to-user "assistant"
-            :subject "(7, \"Application title\") A new application has been submitted"
-            :body "Dear assistant,\n\nAlice Applicant has submitted a new application 7, \"Application title\" to access resource(s) en title 11, en title 21.\n\nYou can review the application at http://example.com/#/application/7\n\nPlease do not reply to this automatically generated message."}
+            :subject "A new application has been submitted (7, \"Application title\")"
+            :body "Dear assistant,\n\nAlice Applicant has submitted a new application 7, \"Application title\" to access resource(s) en title 11, en title 21.\n\nYou can view the application: http://example.com/#/application/7\n\nPlease do not reply to this automatically generated message."}
            (email-to "assistant" (emails created-events submit-event))))))
 
 (deftest test-title-optional
   (is (= {:to-user "assistant"
-          :subject "(2001/3) A new application has been submitted"
-          :body "Dear assistant,\n\nAlice Applicant has submitted a new application 2001/3 to access resource(s) en title 11.\n\nYou can review the application at http://example.com/#/application/7\n\nPlease do not reply to this automatically generated message."}
+          :subject "A new application has been submitted (2001/3)"
+          :body "Dear assistant,\n\nAlice Applicant has submitted a new application 2001/3 to access resource(s) en title 11.\n\nYou can view the application: http://example.com/#/application/7\n\nPlease do not reply to this automatically generated message."}
          (email-to "assistant"
                    (emails [{:application/id 7
                              :application/external-id "2001/3"
@@ -301,31 +301,26 @@
                             :event/actor "applicant"})))))
 
 (deftest test-return-resubmit
-  (let [add-member {:application/id 7
-                    :event/type :application.event/member-added
-                    :event/actor "handler"
-                    :application/member {:userid "member"}}
-        added (conj base-events add-member)
-        return {:application/id 7
+  (let [return {:application/id 7
                 :event/type :application.event/returned
                 :event/actor "handler"
                 :application/comment ["requesting changes"]}
-        returned-events (conj added return)
+        returned-events (conj base-events return)
         resubmit {:application/id 7
                   :event/type :application.event/submitted
                   :event/actor "applicant"}]
     (is (= [{:to-user "applicant"
-             :subject "Your application 2001/3, \"Application title\" needs to be amended"
-             :body "Dear Alice Applicant,\n\nYour application 2001/3, \"Application title\" has been returned for your consideration. Please, amend according to requests and resubmit.\n\nYou can review the application at http://example.com/#/application/7\n\nPlease do not reply to this automatically generated message."}
+             :subject "Your application has been returned for modifications (2001/3, \"Application title\")"
+             :body "Dear Alice Applicant,\n\nYour application 2001/3, \"Application title\" has been returned to you for modifications.\n\nYou can modify and resubmit the application: http://example.com/#/application/7\n\nPlease do not reply to this automatically generated message."}
             {:to-user "assistant"
-             :subject "(2001/3, \"Application title\") Application has been returned to applicant"
-             :body "Dear assistant,\n\nHannah Handler has returned the application 2001/3, \"Application title\" to the applicant Alice Applicant for modifications.\n\nYou can view the application at http://example.com/#/application/7\n\nPlease do not reply to this automatically generated message."}]
-           (emails added return)))
+             :subject "Application has been returned for modifications (2001/3, \"Application title\")"
+             :body "Dear assistant,\n\nHannah Handler has returned the application 2001/3, \"Application title\" to the applicant Alice Applicant for modifications.\n\nYou can view the application: http://example.com/#/application/7\n\nPlease do not reply to this automatically generated message."}]
+           (emails base-events return)))
     (let [mails (emails returned-events resubmit)]
       (is (= #{"assistant" "handler"} (email-recipients mails)))
       (is (= {:to-user "assistant"
-              :subject "(2001/3, \"Application title\") Application has been resubmitted"
-              :body "Dear assistant,\n\nApplication 2001/3, \"Application title\" has been resubmitted by Alice Applicant.\n\nYou can review the application at http://example.com/#/application/7\n\nPlease do not reply to this automatically generated message."}
+              :subject "Application has been resubmitted (2001/3, \"Application title\")"
+              :body "Dear assistant,\n\nApplication 2001/3, \"Application title\" has been resubmitted by Alice Applicant.\n\nYou can view the application: http://example.com/#/application/7\n\nPlease do not reply to this automatically generated message."}
              (email-to "assistant" mails))))))
 
 (deftest test-finnish-emails
@@ -334,6 +329,6 @@
     (let [mails (emails :fi created-events submit-event)]
       (is (= #{"assistant" "handler"} (email-recipients mails)))
       (is (= {:to-user "handler"
-              :subject "(2001/3, \"Application title\") Uusi hakemus"
-              :body "Hyvä Hannah Handler,\n\nAlice Applicant on lähettänyt käyttöoikeushakemuksen 2001/3, \"Application title\" resurss(e)ille fi title 11, fi title 21.\n\nVoit tarkastella hakemusta osoitteessa http://example.com/#/application/7\n\nTämä on automaattinen viesti. Älä vastaa."}
+              :subject "Uusi hakemus (2001/3, \"Application title\")"
+              :body "Hyvä Hannah Handler,\n\nAlice Applicant on lähettänyt käyttöoikeushakemuksen 2001/3, \"Application title\" resurss(e)ille fi title 11, fi title 21.\n\nVoit tarkastella hakemusta: http://example.com/#/application/7\n\nTämä on automaattinen viesti. Älä vastaa."}
              (email-to "handler" mails))))))
diff --git a/test/clj/rems/test_locales.clj b/test/clj/rems/test_locales.clj
index 068bd337d..15bdfebac 100644
--- a/test/clj/rems/test_locales.clj
+++ b/test/clj/rems/test_locales.clj
@@ -4,43 +4,50 @@
             [clojure.set :as set]
             [clojure.string :as str]
             [clojure.test :refer :all]
-            [clojure.tools.logging]
-            [rems.common-util :refer [recursive-keys]]
             [rems.locales :as locales]
             [rems.testing-util :refer [create-temp-dir delete-recursively]]
             [rems.util :refer [getx-in]]
             [taoensso.tempura.impl :refer [compile-dictionary]])
   (:import (java.io FileNotFoundException)))
 
-(deftest test-unused-translation-keys
-  (let [unused-translation-keys #'rems.locales/unused-translation-keys]
-    (is (= nil (unused-translation-keys {:a {:b "x" :c "x"}} {:a {:b "y"}})))
-    (is (= (set [[:x] [:a :d]])
-           (set (unused-translation-keys {:a {:b "x" :c "x"}}
-                                         {:a {:b "y" :d "y"} :x "y"}))))))
-
 (defn loc-en []
   (read-string (slurp (io/resource "translations/en.edn"))))
 
 (defn loc-fi []
   (read-string (slurp (io/resource "translations/fi.edn"))))
 
+(defn map-structure
+  "Recurse into map m and replace all leaves with true."
+  [m]
+  (let [transform (fn [v] (if (map? v) (map-structure v) true))]
+    (reduce-kv (fn [m k v] (assoc m k (transform v))) {} m)))
+
 (deftest test-all-languages-defined
-  (is (= (recursive-keys (loc-en))
-         (recursive-keys (loc-fi)))))
+  (is (= (map-structure (loc-en))
+         (map-structure (loc-fi)))))
+
+(defn extract-format-parameters [string]
+  (set (re-seq #"%\d+" string)))
 
 (deftest test-extract-format-parameters
-  (is (= #{} (locales/extract-format-parameters "hey you are 100% correct!")))
-  (is (= #{"%3" "%5" "%7"} (locales/extract-format-parameters "user %3 has made %7 alterations in %5!"))))
+  (is (= #{} (extract-format-parameters "hey you are 100% correct!")))
+  (is (= #{"%3" "%5" "%7"} (extract-format-parameters "user %3 has made %7 alterations in %5!"))))
 
 (deftest test-format-parameters-match
-  (testing "[:en vs :fi]"
-    (let [en (loc-en)
-          fi (loc-fi)]
-      (doseq [k (recursive-keys en)] ;; we check that keys match separately
-        (testing k
-          (is (= (locales/extract-format-parameters (getx-in en (vec k)))
-                 (locales/extract-format-parameters (getx-in fi (vec k))))))))))
+  (letfn [(check [left right]
+            (cond
+              (string? left)
+              (do
+                (is (string? right))
+                (is (= (extract-format-parameters left)
+                       (extract-format-parameters right))))
+              (map? left)
+              (doseq [k (keys left)]
+                (is (map? right))
+                (testing k
+                  (check (get left k) (get right k))))))]
+    (testing "[:en vs :fi]"
+      (check (loc-en) (loc-fi)))))
 
 (defn- translation-keywords-in-use []
   ;; git grep would be nice, but circleci's git grep doesn't have -o
@@ -102,30 +109,35 @@
                           (locales/load-translations {:languages [:xx]}))))
 
   (testing "missing translations is an error"
-    (is (thrown-with-msg? FileNotFoundException #"^translations could not be found in file or resource \"some-dir/xx.edn\"$"
+    (is (thrown-with-msg? FileNotFoundException #"^\Qtranslations could not be found in some-dir/xx.edn file or some-dir/xx.edn resource\E$"
                           (locales/load-translations {:translations-directory "some-dir/"
                                                       :languages [:xx]})))))
 
 (deftest override-translations-with-extra-translations
-  (let [log (atom [])
-        ;; redeffing log* is hacky, could instead set *logger-factory* to a fake logger
-        translations (with-redefs [clojure.tools.logging/log* (fn [_ _ _ msg] (swap! log conj (str msg)))]
-                       (locales/load-translations {:languages [:en]
+  (testing "extra translations override translations"
+    (let [translations (locales/load-translations {:languages [:en]
                                                    :translations-directory "translations/"
-                                                   :theme-path "./example-theme/theme.edn"}))]
-    (testing "extra translations override translations"
+                                                   :theme-path "./example-theme/theme.edn"})]
       (is (= "Catalogue" (getx-in translations [:en :t :administration :catalogue-items])))
-      (is (= "Text %1" (getx-in translations [:en :t :create-license :license-text]))))
-    (testing "extra translations don't override keys that are not defined in extras"
-      (is (= "Active" (getx-in translations [:en :t :administration :active]))))
-    (testing "warnings"
-      (is (< 0 (count @log)))
-      (testing "for unused key"
-        (is (some #(.contains % ":unused-key") @log)
-            (pr-str @log)))
-      (testing "for extra format parameters"
-        (is (some #(.contains % "%1") @log)
-            (pr-str @log))))))
+      (is (= "Text" (getx-in translations [:en :t :create-license :license-text])))))
+  (testing "extra translations don't override keys that are not defined in extras"
+    (let [translations (locales/load-translations {:languages [:en]
+                                                   :translations-directory "translations/"
+                                                   :theme-path "example-theme/theme.edn"})]
+      (is (= "Active" (getx-in translations [:en :t :administration :active])))))
+  ;; TODO: This should rather be part of validation of all custom themes, but
+  ;;   it's probably better to at least have it here than not at all.
+  (testing "extra translations don't add keys that are not defined in original"
+    (doseq [lang [:en :fi]]
+      (let [translations
+            (locales/load-translations {:languages [lang]
+                                        :translations-directory "translations/"
+                                        :theme-path "example-theme/theme.edn"})
+            translations-without-extras
+            (locales/load-translations {:languages [lang]
+                                        :translations-directory "translations/"})]
+        (is (= (map-structure translations)
+               (map-structure translations-without-extras)))))))
 
 (deftest theme-path-given-no-extra-translations
   (testing "translations work with theme-path in config and no extra-translations"
diff --git a/test/cljs/rems/administration/test_create_form.cljs b/test/cljs/rems/administration/test_create_form.cljs
index 642307653..1ac03f290 100644
--- a/test/cljs/rems/administration/test_create_form.cljs
+++ b/test/cljs/rems/administration/test_create_form.cljs
@@ -310,12 +310,6 @@
                      [:form/fields 0 :field/placeholder :fi])
              :t.form.validation/required)))
 
-    (testing "placeholder is not validated if it is not used"
-      (let [form (-> form
-                     (assoc-in [:form/fields 0 :field/type] :label)
-                     (assoc-in [:form/fields 0 :field/placeholder :fi] ""))]
-        (is (empty? (validate-form form)))))
-
     (testing "option fields"
       (let [form (-> form
                      (assoc-in [:form/fields 0 :field/type] :option)
@@ -335,12 +329,6 @@
                          [:form/fields 0 :field/options 0 :key])
                  :t.form.validation/required)))
 
-        (testing "... are not validated when options are not used"
-          (let [form (-> form
-                         (assoc-in [:form/fields 0 :field/options 0 :key] "")
-                         (assoc-in [:form/fields 0 :field/type] :texta))]
-            (is (empty? (validate-form form)))))
-
         (testing "missing option label"
           (let [empty-label (validate-form (assoc-in form [:form/fields 0 :field/options 0 :label] {:en "" :fi ""}) languages)
                 nil-label (validate-form (assoc-in form [:form/fields 0 :field/options 0 :label] nil) languages)]