2

I have the following Clojure code with a render function which renders a html page using enlive-html. Depending on the selected language a different html template is used.

As you can see, there is a lot of code duplication and I would like to remove it.

I was thinking of writing some macros but, if I understand correctly, the language (i.e. lang parameter) is not available at macro execution time because it is provided in the request and that is at execution time and not at compilation time.

I also tried to modify enlive in order to add i18n support at some later point but my Clojure skills are not there yet.

So the questions are:

How can I remove the code duplication in the code below?

Is enlive-html the way to go or should I use another library? Is there a library similar to enlive with support for i18n?

Thanks!

See the code here:

(ns myapp.core
  (:require [net.cgrand.enlive-html :as e))

(deftemplate not-found-en "en/404.html"
  [{path :path}]
  [:#path] (e/content path))

(deftemplate not-found-fr "fr/404.html"
  [{path :path}]
  [:#path] (e/content path))


(defn getTemplate [page lang]
  (case lang
      :en (case page
                :page/not-found not-found-en)
      :fr (case page
                :page/not-found not-found-fr)))

(defn render [lang [page params]]
  (apply (getTemplate page lang) params))
amalloy
  • 89,153
  • 8
  • 140
  • 205
vidi
  • 2,056
  • 16
  • 34
  • What exactly do you mean, re: "language is not available at macro execution time"? – Charles Duffy Jan 11 '17 at 22:06
  • ...if your macros are generating your `deftemplate`s, you only need to run them at compile-time to do that execution. – Charles Duffy Jan 11 '17 at 22:07
  • 3
    This belongs on http://codereview.stackexchange.com/ because it already works. – tar Jan 11 '17 at 22:11
  • @CharlesDuffy the choice of words was bad. I added an explanation in the body – vidi Jan 11 '17 at 22:16
  • @tar it hardly works because this is not really a choice for my application since I need to support *many* languages – vidi Jan 11 '17 at 22:22
  • 1
    @vidi, ...as I said in my earlier comment, you don't need to know which `lang` will be in use for any given request at compile time; you only need to know which `lang`s *can* be used -- which ones you have translations available for. Generate all the possible expansions at compile-time and you're done, without needing any runtime-only data. – Charles Duffy Jan 11 '17 at 22:24
  • I've rolled back your question to its previous version, because your last edit was apparently intended to be an answer, and it also substantially changed the question by adding a number of additional requirements. Rather than editing that into the question itself, you can post an answer to your own question. – amalloy Jan 15 '17 at 20:20

2 Answers2

2

On the one hand, it is not too hard to write a macro that will generate the exact code you have here for an arbitrary set of languages. On the other hand, there is probably a better approach than using deftemplate - things that are defd are things you expect to refer to by name in the source code, whereas you just want this thing created and used automatically. But I'm not familiar with the enlive API so I can't say what you should do instead.

If you decide to stick with the macro instead, you could write something like:

(defmacro def-language-404s [languages]
  `(do
     ~@(for [lang languages]
         `(deftemplate ~(symbol (str "not-found-" lang)) ~(str lang "/404.html")
            [{path# :path}]
            [:#path] (e/content path#)))
     (defn get-template [page# lang#]
       (case page#
         :page/not-found (case lang#
                           ~@(for [lang languages
                                   clause [(keyword lang)
                                           (symbol (str "not-found-" lang))]]
                               clause))))))

user> (macroexpand-1 '(def-language-404s [en fr]))
(do
  (deftemplate not-found-en "en/404.html"
    [{path__2275__auto__ :path}]
    [:#path] (content path__2275__auto__))
  (deftemplate not-found-fr "fr/404.html"
    [{path__2275__auto__ :path}]
    [:#path] (content path__2275__auto__))
  (defn get-template [page__2276__auto__ lang__2277__auto__]
    (case page__2276__auto__
      :page/not-found (case lang__2277__auto__
                        :en not-found-en
                        :fr not-found-fr))))
amalloy
  • 89,153
  • 8
  • 140
  • 205
  • Thanks for the answer. The macro would be fine, at least for now but the one in your answer doesn't work for me. It complains about the construction [:#path] See here: https://gist.github.com/anonymous/ec0f2b18269f28d5d1c333945cf386ac – vidi Jan 12 '17 at 07:11
  • Huh? `:#path` is copied verbatim from your question; my macro expands into the code you needed. I don't know what is the deal with your repl errors, but they are not related to `:#path`, and indeed they do not even mention it. Did you copy/paste wrong? Did the boot repl mess it up? Who knows, but the error message doesn't match the code. – amalloy Jan 12 '17 at 09:33
  • 1
    I didn't copy paste wrong. Both boot and lein use the same version of nrepl so I don't think it's related to boot. I tried both Clojure v1.7.0 and 1.8.0 and it behaves the same. I said above that the problem is related to the character # in :#path because if I remove the # the error is no more . I guess that because # has a meaning inside the macro it should be escaped or something but I haven't found how to do that. See this gist https://gist.github.com/anonymous/312877d8e15a0f5791ff9bab759a203f – vidi Jan 12 '17 at 19:03
  • 1
    I can confirm it does not work in lein repl either. I don't know what's wrong with the boot/lein repls (though if they both have it, I assume it's an nrepl problem). The snippet *does* work fine if you `java -jar clojure.jar` directly, or if you write it to a .clj file and require that file, which shows that this macro is correct as far as Clojure is concerned; it is your tooling that is messing it up. – amalloy Jan 12 '17 at 19:32
  • 1
    Further research suggests this is a bug in sjacket, the code used by lein repl and boot repl to parse multi-line forms. Another way to see this is to note that if you paste my code as a single line, it works fine. sjacket hasn't been updated for three years, so I don't know how anyone is going to get a fix for this into lein/boot. – amalloy Jan 12 '17 at 20:04
1

After quite a bit of Macro-Fu I got to a result that I'm happy with. With some help from a few nice stackoverflowers I wrote the following macros on top of enlive:

(ns hello-enlive
  (:require [net.cgrand.enlive-html :refer [deftemplate]]))

(defn- template-name [lang page] (symbol (str "-template-" (name page) "-" (name lang) "__")))
(defn- html-file [lang page] (str (name lang) "/" (name page) ".html"))
(defn- page-fun-name [page] (symbol (str "-page" (name page))))

(defmacro def-page [app languages [page & forms]]
  `(do
     ~@(for [lang languages]
         `(deftemplate ~(template-name lang page) ~(html-file lang page)
            ~@forms))

      (defn ~(page-fun-name page) [lang#]
         (case lang#
           ~@(for [lang languages
                   clause [(keyword lang) (template-name lang page)]]
               clause)))

      (def ^:dynamic ~app
        (assoc ~app ~page ~(page-fun-name page)))
      ))

(defmacro def-app [app-name languages pages]
  (let [app (gensym "app__")]
    `(do
       (def ~(vary-meta app merge {:dynamic true}) {})

       ~@(for [page# pages]
           `(def-page ~app ~languages ~page#))

       (defn ~app-name [lang# [page# params#]]
         (apply (apply (get ~app page#) [lang#]) params#)))))

...which are then used like this:

The html templates are stored in a tree like this

html/fr/not-found.html
html/fr/index.html
html/en/not-found.html
html/en/index.html
...

...and the rendering logic looks like this:

(def-app my-app [:en :it :fr :de]
  [ [:page/index [] ]

    ;... put your rendering here

    [:page/not-found [{path :path}]
      [:#path] (content path)]])

...and the usage look like this:

...
(render lang [:page/index {}])
(render lang [:page/not-found {:path path}])
...

The result, although it can probably be improved, I think is pretty nice, without duplication and boilerplate code.

vidi
  • 2,056
  • 16
  • 34