DEV Community

Rajasegar Chandran
Rajasegar Chandran

Posted on

Setting up i18n (Internationalization) for a Common Lisp web application

In this post we are going to take a look at how to setup i18n for a Common Lisp web application. Before diving into the article let's setup some context around the concepts and tools which we are going to use to setup our internationalization system for the application.

i18n

According to Wikipedia, internationalization and localization, often abbreviated i18n and L10n, are means of adapting computer software to different languages, regional peculiarities and technical requirements of a target locale.

Internationalization is the process of designing a software application so that it can be adapted to various languages and regions without engineering changes. Localization is the process of adapting internationalized software for a specific region or language by translating text and adding locale-specific components.

cl-locale

In Common Lisp, there are many libraries to implement i18n functionality. For our app, we are going to use a library called cl-locale. It is a simple i18n library for Common Lisp created by Eitraro Fukamachi

Bootstrapping our project

We are going to use the Caveman framework to build our app. With quicklisp we can just load the the caveman2 package and create our project.

(ql:quickload :caveman2)
(caveman2:make-project #P"~/quicklisp/local-projects/cl-i18n-demo")
Enter fullscreen mode Exit fullscreen mode

If you want to know more about building web applications in Common Lisp with Caveman, you can refer to my previous post

Installing cl-locale in your app

With Caveman, you can add your dependencies in the ASDF system definition file called cl-i18n-demo.asd which is created by Cavement and it is located in the root of our project folder.
Just add cl-locale as our dependency as the very last item under :depends-on

(defsystem "cl-i18n-demo"
  :version "0.1.0"
  :author "Rajasegar Chandran"
  :license ""
  :depends-on ("clack"
               "lack"
               "caveman2"
               "envy"
               "cl-ppcre"
               "uiop"

               ;; for @route annotation
               "cl-syntax-annot"

               ;; HTML Template
               "djula"

               ;; for DB
               "datafly"
               "sxql"

           ;; i18n
           "cl-locale")
  :components ((:module "src"
                :components
                ((:file "main" :depends-on ("config" "view" "db"))
                 (:file "web" :depends-on ("view"))
                 (:file "view" :depends-on ("config"))
                 (:file "db" :depends-on ("config"))
                 (:file "config"))))
  :description ""
  :in-order-to ((test-op (test-op "cl-i18n-demo-test"))))
Enter fullscreen mode Exit fullscreen mode

And now you can load our project using quicklisp and it automatically installs the project dependencies.

(ql:quickload :cl-i18-demo)
Enter fullscreen mode Exit fullscreen mode

Djula i18n

Since we are going to bootstrap our web application using Caveman which uses Djula as the underlying template engine for rendering HTML, we are going to take advantage of the inbuilt i18n capabilities present in Djula.

Djula offers a standard syntax for rendering translated strings in the HTML templates. The easiest way to translate a string or variable is to enclose it between {_ and _}

<p>{_ var _}</p>
<span>{_ "hello" _}</span>
Enter fullscreen mode Exit fullscreen mode

It also offers us a tag trans (which is also a filter) to render the translated strings in your HTML markup within Djula templates.

<p>{% trans var %}</p>
<span>{% trans "hello" %}</span>
Enter fullscreen mode Exit fullscreen mode

With the filter syntax, you can use something like this:

<p>{{ var | trans }}</p>
<span>{{ "my string" |  trans }}</span>
Enter fullscreen mode Exit fullscreen mode

I think the first version using {_ and _} as the delimiters for translated strings is the most simple, easy and terse way of putting translated content in your templates. I personally prefer to use this syntax.

Locale files

Now, it's time to create our locale files for the translations. We are going to put all our locale files inside a folder named i18n

cd src
mkdir i18n
cd i18n
touch en.lisp
touch fr.lisp
Enter fullscreen mode Exit fullscreen mode

English locale file

Now we will start adding keys and values for the translations, this locale file is a simple lisp file containing an Hash table.

(("Calendar" . "Calendar")
 ("Library" . "Library")
 ("Bio" . "I am :name")
 ("importing" . "Importing... :progress%")
 ("estimated-minutes-remaining" .  "Estimated  :minutes minutes remaining"))
Enter fullscreen mode Exit fullscreen mode

You can also add placeholders inside the values with a colon preceding the value name, in our example, :name, :progress and :minutes are placeholders for the respective values.

French locale file

This is how the locale file for the French language for the same set of translation keys and values like in English.

(("Calendar" . "Calendrier")
 ("Library" . "Bibliotheque")
 ("Bio" . "Je suis :name")
 ("importing" . "Importation ... :progress%")
 ("estimated-minutes-remaining" .  "Durée estimée  :minutes minutes restantes"))
Enter fullscreen mode Exit fullscreen mode

Setup i18n

Now, we are going to tell Caveman, how to make use of cl-locale as the i18n engine for our application. Just add these lines in our src/web.lisp file, just after the ;; Application section.

We are instructing Caveman to use the locale syntax and where to find the locale files for the translations and creating a i18n dictionary with the locale keys like en for English and fr for French. And finally we set the newly created dictionary as the current one.

You can also create more locale keys for regional languages like en-US, en-UK and so on.

(cl-locale:enable-locale-syntax)

(cl-locale:define-dictionary i18n-dictionary
  (:en (merge-pathnames #P"src/i18n/en.lisp" *application-root*))
  (:fr (merge-pathnames #P"src/i18n/fr.lisp" *application-root*)))


(setf (cl-locale:current-dictionary) :i18n-dictionary)

Enter fullscreen mode Exit fullscreen mode

Routing

Now we add our routes for our application. Our demo app will contain just two routes one with the root url / and one specifically for the French language with url /fr.

And for each route, will setup the the current language with Djula and connect the translation backend of Djula with Caveman and cl-locale with the appropriate language. This is the crucial step required to automatically fetch the translations from the respective locale files to the Djula templates so that Djula have context about the keys and values.

;;
;; Routing rules

(defroute "/" ()
    (let ((djula:*current-language* :en)
                (djula:*translation-backend* :locale))
  (render #P"index.html" )))

(defroute "/fr" ()
    (let ((djula:*current-language* :fr)
                (djula:*translation-backend* :locale))
  (render #P"french.html" )))

Enter fullscreen mode Exit fullscreen mode

Rendering translations in the HTML

And as I mentioned above, we can use the {_ and _} syntax to render translations. If there is a placeholder in the translation we need to supply the value for the same along with the key name in the locale file.

So our template file will have markup something like below:

<h2>Calendar: {_ "Calendar" _}</h2>
    <h3>Library: {_ "Library" _}</h3>
    <h3>Bio: {_ "Bio" :name "Rajasegar" _}</h3>
    <p>Progress: {_ "importing" :progress 56 _}</p>
    <p>Time: {_ "estimated-minutes-remaining" :minutes 30 _}</p>
Enter fullscreen mode Exit fullscreen mode

Once you did all of the above, then you can start our server using the code below:

(cl-i18n-demo:start :port 3000)
Enter fullscreen mode Exit fullscreen mode

This is how the English language page looks like

Image description

This is how the French language page looks like

Image description

Source code

The source code for this post is located in Github. You can refer the code for any missing details in the post.

Hope you enjoyed the post and learned how to setup i18n for your Common Lisp web applications with Caveman, Djula and cl-locale. Please let me know in the comments section for any feedback or queries.

References

Latest comments (0)