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")
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"))))
And now you can load our project using quicklisp and it automatically installs the project dependencies.
(ql:quickload :cl-i18-demo)
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>
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>
With the filter syntax, you can use something like this:
<p>{{ var | trans }}</p>
<span>{{ "my string" | trans }}</span>
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
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"))
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"))
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)
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" )))
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>
Once you did all of the above, then you can start our server using the code below:
(cl-i18n-demo:start :port 3000)
This is how the English language page looks like
This is how the French language page looks like
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.
Top comments (0)