<?phpif (!defined('ABSPATH'))    exit; // Exit if accessed directlyadd_action("wp_ajax_sliding-captcha", "sliding_captcha_fn");add_action("wp_ajax_nopriv_sliding-captcha", "sliding_captcha_fn");function sliding_captcha_fn() {    $Response['error'] = false;    $action = sanitize_text_field($_POST["action"]);    $qaptcha_key = sanitize_text_field($_POST["qaptcha_key"]);    if (isset($_POST['action']) && isset($_POST['qaptcha_key'])) {        if (htmlentities($action, ENT_QUOTES, 'UTF-8') == 'sliding-captcha') {            echo json_encode($Response);        } else {            $Response['error'] = true;            echo json_encode($Response);            exit;        }    } else {        $Response['error'] = true;        echo json_encode($Response);        exit;    }    die;}{"id":86,"date":"2014-04-07T17:00:10","date_gmt":"2014-04-07T15:00:10","guid":{"rendered":"https:\/\/nylnook.art\/\/en\/?p=86"},"modified":"2019-03-11T16:38:17","modified_gmt":"2019-03-11T15:38:17","slug":"docpad-i18n","status":"publish","type":"post","link":"https:\/\/nylnook.art\/en\/blog\/docpad-i18n\/","title":{"rendered":"Docpad Internationalized ?"},"content":{"rendered":"<p><em>EDIT March 2019: this website is now based on WordPress, this article is kept as an archive<\/em><\/p>\n<p><strong>EDIT : This method for translating DocPad <a href=\"https:\/\/bitbucket.org\/bgschaid\/docpad-plugin-multilanguage\">is currently developped as a plugin<\/a> by Bernhard F.W. Gschaider ! Many thanks to him !<\/strong><\/p>\n<p>In my way building this website, one the major technical point to solve was internationalization and multilingual support. I want to publish in English and French, and maybe more languages when times comes.<br \/>\nBut for now (v6.64), <a href=\"http:\/\/docpad.org\/\">Docpad<\/a>, the publishing system I choosed for this website, <a href=\"https:\/\/github.com\/bevry\/docpad\/issues\/17\">do not have any official localization abilities<\/a>.<br \/>\nSo here is a full description the solution I finally implemented today (took much more time than expected !).<\/p>\n<h3>TL; DR;<\/h3>\n<p>No, sorry, I&#8217;m afraid there is no shortways.<\/p>\n<h4>Table of content<\/h4>\n<ul>\n<li><a href=\"#thanks-first\">Thanks first<\/a><\/li>\n<li><a href=\"#how-i-like-it-to-be\">How I like it to be<\/a><\/li>\n<li><a href=\"#the-solution\">The solution I ended up with<\/a>\n<ul>\n<li><a href=\"#structure\">Structure<\/a><\/li>\n<li><a href=\"#declaration\">Declaration<\/a><\/li>\n<li><a href=\"#generate-metadata\">Generate language specific Metadatas with collections<\/a><\/li>\n<li><a href=\"#translation-strings-and-files\">Translation strings, and translation files in JSON Format<\/a>\n<ul>\n<li><a href=\"#translations-functions\">The translations functions<\/a><\/li>\n<li><a href=\"#where-are-translation-files\">Tell Docpad where are our translation files<\/a><\/li>\n<\/ul>\n<\/li>\n<li><a href=\"#localize-dates\">Localize Dates<\/a><\/li>\n<li><a href=\"#path-urls\">Path and urls<\/a>\n<ul>\n<li><a href=\"#navigation-menus\">Navigation and menus<\/a><\/li>\n<li><a href=\"#associated-files\">Collections of associated files<\/a><\/li>\n<li><a href=\"#language-switcher\">Language switcher<\/a><\/li>\n<\/ul>\n<\/li>\n<li><a href=\"#language-redirection\">Language redirection for visitors<\/a>\n<ul>\n<li><a href=\"#server-side-redirection\">Server side<\/a><\/li>\n<li><a href=\"#client-side-redirection\">Client side<\/a><\/li>\n<li><a href=\"#client-side-storage\">Client side storage of prefered language<\/a><\/li>\n<li><a href=\"#404\">The 404 case<\/a><\/li>\n<\/ul>\n<\/li>\n<li><a href=\"#limits\">Limits<\/a><\/li>\n<\/ul>\n<\/li>\n<li><a href=\"#abstracting\">Abstracting ?<\/a><\/li>\n<\/ul>\n<h3><a name=\"thanks-first\"><\/a>Thanks first<\/h3>\n<p>I&#8217;m a designer, not a real coder, and my solution is heavily inspired by 3 already existing projets.<br \/>\nWithout their code available, I wouldn&#8217;t have achieved this work: many thanks for being open !<\/p>\n<p>In chronological order :<br \/>\n&#8211; Zeno Rocha&#8217;s <a href=\"https:\/\/github.com\/zenorocha\/browser-diet\">Browser Diet<\/a> website<br \/>\n&#8211; Adam Pritchard&#8217;s <a href=\"https:\/\/bitbucket.org\/psiphon\/psiphon-circumvention-system\/src\/tip\/Website\/?at=default\">Psiphon Circumvention System<\/a> website<br \/>\n&#8211; Artem Sapegin&#8217;s <a href=\"http:\/\/blog.sapegin.me\/all\/multilingual-docpad\">Blog<\/a><\/p>\n<p>I must also thanks <a href=\"https:\/\/github.com\/balupton\">Benjamin Lupton<\/a> for his work on Docpad, the needed infrastructure work, and the initial idea behind this solution.<\/p>\n<h3><a name=\"how-i-like-it-to-be\"><\/a>How I like it to be<\/h3>\n<p>As you may have noticed, I host many, many images, they will grow, and I can&#8217;t aford to duplicate them, a major flaw IMHO in two of the previous mentionned already exiting solutions.<br \/>\nSo no duplication, or a minimum : duplicate only text, which you can&#8217;t avoid in a translation !<\/p>\n<p>My requierments were :<br \/>\n1. Minimal disk usage<br \/>\n2. KISS<br \/>\n3. translations strings available for templating (like <code>@_ 'translate-me'<\/code>) with plural support<br \/>\n4. localize dates<br \/>\n5. Transparent visitor redirection, server side prefered<br \/>\n6. SEO friendly (urls and titles in the correct language)<br \/>\n7. do not mess too much with Docpad behavior, and let important plugins do their usual job (Clean URLs, tagging, partials&#8230;).<br \/>\n8. generate every language in one go, with a simple <code>docpad run<\/code> (no parameters like <code>--lang-en<\/code>)<br \/>\n9. Allow for translation services like <a href=\"https:\/\/www.transifex.com\/\">Transifex<\/a> to be used partially or fully. So JSON format for translation files. Artem Sapegin&#8217;s solution use YAMLjs instead.<\/p>\n<h2><a name=\"the-solution\"><\/a>The solution I ended up with<\/h2>\n<p>I spend many days messing around with this code, and it&#8217;s not perfect. But it mostly do what I want so far.<br \/>\nAs already mentioned in the footer of this website, my code is available on <a href=\"https:\/\/github.com\/nylnook\/nylnook-website\">Github<\/a>, and it&#8217;s commented if you want to test an implementation or dig into it.<\/p>\n<h3><a name=\"structure\"><\/a>Structure<\/h3>\n<p>First of all, a structure. I tried to keep it as simple and near to an untranslated Docpad project as possible.<\/p>\n<p>My solution work by setting up language specific directories inside the <code>documents<\/code> folder, which automatically force Docpad in using language specific subdirectories.<br \/>\nThis mean visitors will always see the subdirectories in your URLS, like <code>example.com\/en\/<\/code> or <code>example.com\/fr\/<\/code>. Yes, even for your base language (often english).<br \/>\nIf you don&#8217;t want subdirectories, this is not for you. You may try <a href=\"http:\/\/blog.sapegin.me\/all\/multilingual-docpad\">Artem Sapegin&#8217;s solution<\/a> if you want to publish into subdomains or differents domains, or try to adapt my solution to your context with some Express.js server-side redirection.<\/p>\n<p>So, your project structure must look like this one :<br \/>\n&#8211; docpad-project<br \/>\n&#8211; src<br \/>\n&#8211; documents<br \/>\n&#8211; en<br \/>\n&#8211; blog<br \/>\n&#8211; other-category<br \/>\n&#8211; fr<br \/>\n&#8211; blog<br \/>\n&#8211; other-category<br \/>\n&#8211; any-language<br \/>\n&#8211; blog<br \/>\n&#8211; other-category<br \/>\n&#8211; scripts<br \/>\n&#8211; styles<br \/>\n&#8211; files<br \/>\n&#8211; layouts<br \/>\n&#8211; out<\/p>\n<p>With one document subdirectory for each language, even the base one. You can place a document directly in <code>documents<\/code> root, but it must not call translation functions, and it won&#8217;t be translated.<\/p>\n<p>If you want to keep things simple, a limit is that if you want to get collections of documents into sub-subdirectories like <code>blog<\/code> from your layouts, thoses sub-subdirectories must have the same name in every language . Not really a problem in my case, because <code>blog<\/code> and <code>img<\/code> can be untranslated. One way to have a workaround may be to use metadatas instead of sub-subdirectories for your categories (<code>cat: 'My category'<\/code>), and another is to use the navigation menu trick I will explain later.<\/p>\n<p>Note that the <code>file<\/code> directory remain untouched. That&#8217;s on purpose, to avoid duplication of files and blobs especially.<\/p>\n<p>Additionnaly, you can create a <code>lang<\/code> or <code>locale<\/code> directory somewhere, to put your translation files into it. In my case I choosed not to have one more subdirectory, and I put my translation file directly in the corresponding folder, for example <code>documents\/en\/en.json<\/code> and <code>documents\/fr\/fr.json<\/code><\/p>\n<p>Right now, you can start translating files into each subdirectory, and it will work. But that&#8217;s limited because Docpad still have no idea of what we do.<\/p>\n<h3><a name=\"declaration\"><\/a>Declaration<\/h3>\n<p>Second step is to tell Docpad what we expect from it to do on this front.<\/p>\n<p>So, open <code>docpad.coffee<\/code> or your docpad configuration file and start to declare :<\/p>\n<pre><code class=\"coffeescript\">templateData:\r\n\r\n        # -----------------------------\r\n        # Language Definition\r\n\r\n        # List all available languages here\r\n        # Must match with a corresponding directory in src\/documents\/\r\n        languages: ['en', 'fr']\r\n\r\n        # Define default language\r\n        default_lang: 'en'\r\n\r\n        # Define plural for each language. A list is available here :\r\n        # https:\/\/github.com\/airbnb\/polyglot.js\/blob\/master\/lib\/polyglot.js\r\n        # (don't forget to convert to coffeescript !)\r\n        plural_types:\r\n            en: (n) -&gt; (if n isnt 1 then 1 else 0)\r\n            fr: (n) -&gt; (if n &gt; 1 then 1 else 0)\r\n\r\n        # Translation file location\r\n        # Depending on your translation system, you may want to change that\r\n        # remember this path start from the docpad directory\r\n        translation_files:\r\n            en: 'src\/documents\/en\/en.json'\r\n            fr: 'src\/documents\/fr\/fr.json'\r\n\r\n        # Translations will be loaded into this object. Required.\r\n        translations: {}\r\n<\/code><\/pre>\n<p>and you can continue with your classical template datas :<\/p>\n<pre><code class=\"coffeescript\">        # -----------------------------\r\n        # Non translated definitions (untranslated names, urls ..)\r\n        site:\r\n            # The production url of our website\r\n            url: \"http:\/\/nylnook.com\"\r\n\r\n            # Here are some old site urls that you would like to redirect from\r\n            oldUrls: [\r\n                'www.nylnook.com'\r\n            ]\r\n\r\n            # The default title of our website\r\n            title: \"nylnook\"\r\n\r\n            # The website author\r\n            author: \"Camille Bissuel\"\r\n<\/code><\/pre>\n<p>Ok, we have done some declaration, but so what ? Next, use them.<\/p>\n<h3><a name=\"generate-metadata\"><\/a>Generate language specific Metadatas with collections<\/h3>\n<p>Next step is to automatically build language metadatas for every document in every language. That&#8217;s sound crazy, but this is the only reliable method to be able to easily know what is the current language in any document. With this, a simple <code>@document.lang<\/code> (with eco templating) will give us the current language.<\/p>\n<p>Thanks again to Adam Pritchard, this is almost a copy-and-paste from his work :<\/p>\n<pre><code class=\"coffeescript\">    # =================================\r\n    # Collections\r\n    collections:\r\n        # This is internationalized, and only to build automatic template data !!!\r\n        # If you try to use it in your files, you will obtain all results in every language\r\n        # In your templates, use for example\r\n        # @getCollection(\"html\").findAllLive({relativeOutDirPath: @document.lang+'\/blog'},[{date:-1}])\r\n        # instead of thoses collections\r\n\r\n        # This collection is absolutely required to make internationalization work !\r\n        # this wil add a \"lang\" metadata correponding to the same directory to each file in documents\r\n        setlangforalldocuments: (database) -&gt;\r\n            lang_dirs = ('\/'+lang+'\/' for lang in @config.templateData.languages)\r\n            lang_regex = ('^'+lang_dir for lang_dir in lang_dirs).join('|')\r\n\r\n            @getCollection('documents').createChildCollection()\r\n                .setFilter 'search', (model) -&gt;\r\n                    return false if not model.get('url')\r\n\r\n                    lang_match = model.get('url').match(lang_regex)\r\n                    return false if not lang_match\r\n\r\n                    lang = lang_match[0].replace(\/^\\\/|\\\/$\/g, '')\r\n                    model.setMetaDefaults { lang: lang }\r\n                    true\r\n<\/code><\/pre>\n<p>And you can continue with this technic to generate other metadatas, in this example a layout<\/p>\n<pre><code class=\"coffeescript\">        # Build template data for posts (only relative path and metadata change)\r\n        posts: (database) -&gt;\r\n            lang_dirs = ('\/'+lang+'\/blog' for lang in @config.templateData.languages)\r\n            lang_regex = ('^'+lang_dir for lang_dir in lang_dirs).join('|')\r\n\r\n            @getCollection('documents').createChildCollection()\r\n                .setFilter 'search', (model) -&gt;\r\n                    return false if not model.get('url')\r\n\r\n                    lang_match = model.get('url').match(lang_regex)\r\n                    return false if not lang_match\r\n\r\n                    lang = lang_match[0].replace(\/^\\\/|\\\/$\/g, '')\r\n                    model.setMetaDefaults { layout:\"post-layout\"}\r\n                    true\r\n<\/code><\/pre>\n<p><strong>This is the core of our solution<\/strong>. This is absolutelly needed to make translation work.<\/p>\n<p>But this mean you cannot use anymore in your templates nice calls like the one explained <a href=\"http:\/\/docpad.org\/docs\/begin#creating-custom-collections-via-the-configuration-file\">in the Docpad Beginner guide<\/a> :<\/p>\n<pre><code>&lt;% for posts in @getCollection(\"posts\").toJSON(): %&gt;\r\n<\/code><\/pre>\n<p>because this wil send you every post in every language.<br \/>\nBut instead you will have to use less nice ones like (note the <code>relativeOutDirPath: @document.lang+'\/blog'<\/code> parameter) :<\/p>\n<pre><code>&lt;% for posts in @getCollection(\"html\").findAllLive({relativeOutDirPath: @document.lang+'\/blog'},[{date:-1}]).toJSON(): %&gt;\r\n<\/code><\/pre>\n<p>That&#8217;s ok, but I don&#8217;t know what it does on the preformance side. For now, I didn&#8217;t noticed any difference.<\/p>\n<h3><a name=\"translation-strings-and-files\"><\/a>Translation strings, and translation files in JSON Format<\/h3>\n<p>In our templates at least, we want to be able to translate any sentence in the correct language. We will do that by replacing text with a call to a translation function.<\/p>\n<p>So HTML<\/p>\n<pre><code><\/code><\/pre>\n<p>I want to translate this sentence<\/p>\n<pre><code><\/code><\/pre>\n<p>will become (with Eco)<\/p>\n<pre><code><\/code><\/pre>\n<p>&lt;%- @_ &#8216;I want to translate this sentence&#8217; %&gt;<\/p>\n<pre><code><\/code><\/pre>\n<p>or shortly<\/p>\n<pre><code><\/code><\/pre>\n<p>&lt;%- @_ &#8216;sentence_key&#8217; %&gt;<\/p>\n<pre><code><\/code><\/pre>\n<p>You can add variables into it<\/p>\n<pre><code><\/code><\/pre>\n<p>&lt;%- @_ &#8216;The answer is&#8217;, num: 42 %&gt;<\/p>\n<pre><code><\/code><\/pre>\n<p>And even use the plural<\/p>\n<pre><code><\/code><\/pre>\n<p>&lt;%- @_ &#8216;posts-number&#8217;, num: documents.length, posts: @plural(documents.length, &#8216;post|posts&#8217;) %&gt;<\/p>\n<pre><code><\/code><\/pre>\n<p>The translation is send to JSON, so characters you must escape with a <code>\\<\/code> are the <code>\\<\/code> itself and <code>\"<\/code><\/p>\n<p>But until now there is no corresponding translation file to our calls, and our sentences will not be translated (they will appear as they are). So we must create the files we declared before.<\/p>\n<p>In my case <code>documents\/en\/en.json<\/code><\/p>\n<pre><code class=\"json\">{\r\n    \"sentence_key\" : \"I want to translate this sentence\",\r\n    \"The answer is\": \"The answer is {num}\",\r\n    \"post|posts\" : \"post|posts\",\r\n    \"posts-number\" : \"{num} {posts}\"\r\n\r\n}\r\n<\/code><\/pre>\n<p>and <code>documents\/fr\/fr.json<\/code><\/p>\n<pre><code class=\"json\">{\r\n    \"sentence_key\" : \"Je veux traduire cette phrase\",\r\n    \"The answer is\": \"La r\u00e9ponse est {num}\",\r\n    \"post|posts\" : \"article|articles\",\r\n    \"posts-number\" : \"{num} {posts}\"\r\n\r\n}\r\n<\/code><\/pre>\n<h4><a name=\"translations-functions\"><\/a>The translations functions<\/h4>\n<p>but as is Docpad will send you errors if we don&#8217;t add the translations functions in <code>docpad.coffee<\/code> templateData<\/p>\n<pre><code class=\"coffeescript\">    templateData:\r\n        # ...\r\n\r\n        # Translate the given key into the language of the current document.\r\n        # Fallback to default language if the key if not found or fallback again to unstranslated string.\r\n        # You can use simple variables: @_ 'The answer is', num: 42   \r\n        _: (key, translations_with_parameters=null) -&gt;\r\n            translations_with_parameters ?= []\r\n            if @translations[@document.lang][key]?\r\n                message = @translations[@document.lang][key]\r\n            else if @translations[@default_lang][key]?\r\n                message = @translations[@default_lang][key]\r\n            else\r\n                message = key\r\n            message.replace \/\\{([^\\}]+)\\}\/g, (translation, param) -&gt;\r\n                translations_with_parameters[param] or translation\r\n\r\n        # Plural form for translations\r\n        # Fallback to default language if the key if not found or fallback again to unstranslated string.\r\n        # Simple example : @plural(3, 'dog|dogs')\r\n        # Example in context : @_ '{num} {posts}', num: documents.length, posts: @plural(documents.length, 'post|posts')\r\n        plural: (n, key) -&gt;\r\n            if @translations[@document.lang][key]?\r\n                return ((@_ key).split '|')[@plural_types[@document.lang](n)]\r\n            else if @translations[@default_lang][key]?\r\n                return ((@_ key).split '|')[@plural_types[@default_lang](n)]\r\n            else\r\n                return ((@_ key).split '|').slice(0,1)\r\n\r\n        # ...\r\n<\/code><\/pre>\n<h4><a name=\"where-are-translation-files\"><\/a>Tell Docpad where are our translation files<\/h4>\n<p>That&#8217;s not enough&#8230; Docpad need to load our translation files before starting to render documents !<br \/>\nSo we add a dedicated event :<\/p>\n<pre><code class=\"coffeescript\">    # =================================\r\n    # DocPad Events\r\n\r\n    # Here we can define handlers for events that DocPad fires\r\n    # You can find a full listing of events on the DocPad Wiki\r\n    events:\r\n\r\n        # We add this event to load the translations from locale JSON files\r\n        renderBefore: (opts, next) -&gt;\r\n            fs = require 'fs'\r\n\r\n            for lang of opts.templateData.translation_files\r\n                langJSON = fs.readFileSync opts.templateData.translation_files[lang]\r\n                try\r\n                    opts.templateData.translations[lang] = JSON.parse(langJSON)\r\n                catch error\r\n                    console.log \"\\n\\nERROR: Language JSON fail: #{lang}: #{error}\\n\"\r\n                    throw error\r\n\r\n            next()\r\n<\/code><\/pre>\n<p>Remember we have declared their relative urls in the <code>templateData<\/code> part earlier.<\/p>\n<blockquote><p>And the magic can happen&#8230; translated strings !<\/p><\/blockquote>\n<h3><a name=\"localize-dates\"><\/a>Localize Dates<\/h3>\n<p>Next step is to localize dates. We will need the essential <a href=\"http:\/\/momentjs.com\/\">Moment.js<\/a> library to manage that with beauty.<\/p>\n<p>So, let&#8217;s install it :<\/p>\n<pre><code class=\"bash\">npm install --save moment\r\n<\/code><\/pre>\n<p>And let&#8217;s add 2 more functions in our <code>docpad.coffee templateData<\/code> :<\/p>\n<pre><code class=\"coffeescript\">    templateData:\r\n        # ...\r\n\r\n        # Returns a human readable formatted date. Require Moment.js\r\n        # Example : @date()\r\n        # Example with parameters : @date(post.date, post.lang)\r\n        date: (date, lang) -&gt;\r\n            if not date\r\n                date = @document.date\r\n            if not lang\r\n                lang = @document.lang\r\n            moment = require 'moment'\r\n            moment.lang(lang)\r\n            return moment(date).format('LL');\r\n\r\n        # Returns a computer readable formatted date. Require Moment.js\r\n        # Example : @computerDate()\r\n        # Example with parameters : @computerDate(post.date)\r\n        # Example in HTML5: &lt;time pubdate=\"&lt;%- @computerDate() %&gt;\"&gt;&lt;%- @date() %&gt;&lt;\/time&gt;\r\n        computerDate: (date) -&gt;\r\n            if not date\r\n                date = @document.date\r\n            moment = require 'moment'\r\n            return moment(date).format('YYYY-MM-DD');\r\n\r\n        # ...\r\n<\/code><\/pre>\n<p>in our templates, we will just put<\/p>\n<pre><code>&lt;time pubdate=\"&lt;%- @computerDate() %&gt;\"&gt;&lt;%- @date() %&gt;&lt;\/time&gt;\r\n<\/code><\/pre>\n<p>To get a localized date. Please adjust the <code>format<\/code> as mentioned on <a href=\"http:\/\/momentjs.com\/\">Moment.js website<\/a> to suit your needs.<\/p>\n<p>To use dates in a call for a collections, we will just add parameters<\/p>\n<pre><code>&lt;% for post in @getCollection(\"html\").findAllLive({relativeOutDirPath: @document.lang+'\/blog'},[{date:-1}]).toJSON(): %&gt;\r\n    &lt;article class=\"post\"&gt;\r\n       &lt;h2&gt;&lt;a href=\"&lt;%= post.url %&gt;\"&gt;&lt;%- post.title %&gt;&lt;\/a&gt;&lt;\/h2&gt;\r\n       &lt;time pubdate=\"&lt;%- @computerDate(post.date) %&gt;\"&gt;&lt;%- @date(post.date, post.lang) %&gt;&lt;\/time&gt;\r\n           &lt;%- post.contentRenderedWithoutLayouts %&gt;\r\n    &lt;\/article&gt;\r\n&lt;% end %&gt;\r\n<\/code><\/pre>\n<blockquote><p>Ok for dates, thank you Moment.js !<\/p><\/blockquote>\n<h3><a name=\"path-urls\"><\/a>Path and urls<\/h3>\n<p>Additionally, you may want to create link to your images, or to any file.<br \/>\nGood news is that&#8217;s quite easy : just use relative urls, and do not forget to go one parent directory earlier in your paths.<\/p>\n<p>If your document path is for example <code>\/en\/blog\/this-post.html<\/code><br \/>\nthe path to your file will be at<\/p>\n<pre><code class=\"bash\">..\/..\/img\/mypicture.jpg\r\n<\/code><\/pre>\n<p>If you absolutely need to parse url, here are 2 relatively useless functions for reference, but I finally removed them in my case, they were unused, and bad for performances.<\/p>\n<pre><code class=\"coffeescript\">    templateData:\r\n        # ...\r\n        # Allow to obtain current language from URL if @document.lang isn't available\r\n        # Using @document.lang is prefered for performance\r\n        langFromPath: (document) -&gt;\r\n            if not document\r\n                document = @document\r\n            return document.relativeDirPath.split('\/').slice(0,1)\r\n\r\n        # Get a path without current language. Maybe useful in templates to get a file path\r\n        # Example : &lt;%= @pathWithoutLang(post) %&gt;\r\n        pathWithoutLang: (document) -&gt;\r\n            if not document\r\n                document = @document\r\n            return document.relativeOutDirPath.split('\/').slice(1)\r\n<\/code><\/pre>\n<h4><a name=\"navigation-menus\"><\/a>Navigation and menus<\/h4>\n<p>The tricky part is when you want to link to a corresponding document in another language. For example, you have a navigation menu refering to<\/p>\n<pre><code class=\"xml\">documents\/en\/about.html\r\n<\/code><\/pre>\n<p>in english<\/p>\n<p>and to<\/p>\n<pre><code class=\"xml\">documents\/fr\/a-propos.html\r\n<\/code><\/pre>\n<p>in french.<\/p>\n<p>We may use the same filename in every language, and just change the lang path :<\/p>\n<pre><code class=\"xml\">documents\/&lt;%- @document.lang %&gt;\/about.html\r\n<\/code><\/pre>\n<p>But remember we want to be SEO friendly, so path name should be similar to titles names in the corresponding language.<\/p>\n<p>The simplest solution I found is to add additional &#8220;key&#8221;:&#8221;value&#8221; in our translations files.<\/p>\n<p>in en.json<\/p>\n<pre><code class=\"json\">{\r\n    \"about\": \"About\",\r\n    \"about-filename\": \"about\"\r\n}\r\n<\/code><\/pre>\n<p>in fr.json<\/p>\n<pre><code class=\"json\">{\r\n    \"about\": \"\u00c0 propos\",\r\n    \"about-filename\": \"a-propos\"\r\n}\r\n<\/code><\/pre>\n<p>So the link in our template become :<\/p>\n<pre><code>&lt;a href=\"\/&lt;%- @document.lang %&gt;\/&lt;%= @_ 'about-filename' %&gt;\"&gt;&lt;%= @_ 'about' %&gt;&lt;\/a&gt;\r\n<\/code><\/pre>\n<p>If your navigation is quite short (6 entries in my case), which is a good idea anyway, this is ok.<br \/>\nBut you will have to find another solution if you have a lot. See <a href=\"https:\/\/bitbucket.org\/psiphon\/psiphon-circumvention-system\/src\/tip\/Website\/?at=default\">Adam Pritchard&#8217;s solution<\/a> maybe.<\/p>\n<h4><a name=\"associated-files\"><\/a>Collections of associated files<\/h4>\n<p>As a side note to path management, the <a href=\"https:\/\/github.com\/docpad\/docpad-plugin-associatedfiles\">associatedfiles Docpad plugin<\/a> will not work anymore with this internationalization solution.<\/p>\n<p>Replace it simply with this kind of templating (<code>@getFilesAtPath<\/code> is a Docpad function) :<\/p>\n<pre><code>&lt;% for file in @getFilesAtPath('..\/..\/img\/gallery').toJSON(): %&gt;\r\n\r\n&lt;% end %&gt;\r\n<\/code><\/pre>\n<h4><a name=\"language-switcher\"><\/a>Language switcher<\/h4>\n<p>That&#8217;s not the strongest part of my code, but in my case I wanted it very simple, so here it is, in a template :<\/p>\n<pre><code><\/code><\/pre>\n<nav id=\"language-selector\"><a class=\"current-lang\" href=\"#\">&lt;%- @document.lang %&gt;<\/a> &lt;% for lang in @languages : %&gt; &lt;% if lang isnt @document.lang : %&gt; <a lang=\"\" href=\"&quot;\/&lt;%-\">&#8221; &gt;&lt;%- lang %&gt;<\/a> &lt;% end %&gt; &lt;% end %&gt;<\/nav>\n<pre><code><\/code><\/pre>\n<p>&nbsp;<\/p>\n<pre><code><\/code><\/pre>\n<p>And no functions at all.<\/p>\n<p>Yes, that mean there is no way for a visitor to switch from a blog post in a language to the same blog post in another language. A visitor will always have to go back to home page&#8230;<\/p>\n<p>But are you constantly switching language when you visit a website ?<br \/>\nI simply suppose the behavior is :<br \/>\n1. land on the website in a language (correctly detected from visitor browser preferences if possible)<br \/>\n2. switch language if it doesn&#8217;t suit<br \/>\n3. visit and never change the language again<\/p>\n<p>So I don&#8217;t need complex functions and a lot of metadatas everywhere just to handle a marginal case.<\/p>\n<p>And if a correspondance is absolutely needed somewhere, use the menu trick mentionned before.<\/p>\n<h3><a name=\"language-redirection\"><\/a>Language redirection for visitors<\/h3>\n<h4><a name=\"server-side-redirection\"><\/a>Server side<\/h4>\n<p>So, we want visitors to land in the best language according to their preference.<br \/>\nIf possible, we want to do that server side to avoid the &#8220;no-script&#8221; user case.<\/p>\n<p>Good news once again, if we have a dynamic server, it&#8217;s possible ! Furthermore, it&#8217;s my case.<br \/>\nThe main idea is to use the <code>req.headers[\"accept-language\"]<\/code> Express.js function to detect our vistor HTTP header.<\/p>\n<p>To have a robust and simple code, we will use the <a href=\"https:\/\/github.com\/federomero\/negotiator\">Negociator library<\/a>.<\/p>\n<p>Installation :<\/p>\n<pre><code class=\"bash\">npm install --save negociator\r\n<\/code><\/pre>\n<p>and in the event section of <code>docpad.coffee<\/code> we will add a second part to the classic <code>serverExtend<\/code> event :<\/p>\n<pre><code class=\"coffeescript\">    events:\r\n\r\n        #...\r\n\r\n\r\n        # Server Extend\r\n        # Used to add our own custom routes to the server before the docpad routes are added\r\n        serverExtend: (opts) -&gt;\r\n            # Extract the server from the options\r\n            {server} = opts\r\n            docpad = @docpad\r\n\r\n            # As we are now running in an event,\r\n            # ensure we are using the latest copy of the docpad configuraiton\r\n            # and fetch our urls from it\r\n            latestConfig = docpad.getConfig()\r\n            oldUrls = latestConfig.templateData.site.oldUrls or []\r\n            newUrl = latestConfig.templateData.site.url\r\n\r\n\r\n            # Redirect any requests accessing one of our sites oldUrls to the new site url\r\n            server.use (req,res,next) -&gt;\r\n                if req.headers.host in oldUrls\r\n                    res.redirect(newUrl+req.url, 301)\r\n                else\r\n                    next()\r\n\r\n\r\n            # We add this event to redirect visitor reaching root domain to their prefered language\r\n            # at the corresponding subdomain.\r\n            # We use the \"Accept-Language\" header for that. Require the \"negociator\" npm package\r\n            # If negociator fail to find any solution, default language is used\r\n\r\n            # \/!\\ This will not work if the server is static !\r\n            # If static, use an index.html file with client side javascript detection to replace this.\r\n            # Example script in \/src\/documents\/404.hml.eco\r\n            server.use (req,res,next) -&gt;\r\n                if req.path == \"\/\"\r\n                    Negotiator = require('negotiator')\r\n                    negotiator = new Negotiator(req)\r\n                    available_languages = latestConfig.templateData.languages\r\n                    if (!!negotiator.language(available_languages))\r\n                        #console.log('redirect to \/' + negotiator.language(availableLanguages))\r\n                        res.redirect(negotiator.language(available_languages))\r\n                    else\r\n                        #console.log('redirect to default language')\r\n                        res.redirect(latestConfig.templateData.default_lang)\r\n                else\r\n                    next()\r\n<\/code><\/pre>\n<p>So, if a visitor hit the root domain, he is redirected to the best language we can find for him, or to the default one.<\/p>\n<h4><a name=\"client-side-redirection\"><\/a>Client side<\/h4>\n<p>If you can&#8217;t do server-side redirection because your server is static or you render static files, client side redirection is required.<br \/>\nCreate an <code>index.html<\/code> file at the <code>documents<\/code> root, with this script (by Adam Pritchard) :<\/p>\n<pre><code class=\"javascript\">&lt;script&gt;\r\n    var defaultLanguage, supportedLanguages, language, redirectLanguage;\r\n\r\n    defaultLanguage = 'en';\r\n    supportedLanguages = &lt;%- JSON.stringify @languages %&gt;;\r\n    redirectLanguage = defaultLanguage;\r\n\r\n    try {\r\n        language = window.navigator.userLanguage || window.navigator.language || defaultLanguage;\r\n\r\n    \/\/ Some languages we support look like 'en' and some like 'en-US'. Some languages\r\n    \/\/ set in navigator.language look like 'en' and some like 'en-US'. So we'll\r\n    \/\/ check both combinations.\r\n\r\n        if (supportedLanguages.indexOf(language) &gt;= 0) {\r\n            redirectLanguage = language;\r\n        }\r\n        else if (supportedLanguages.indexOf(language.substr(0, 2)) &gt;= 0) {\r\n            redirectLanguage = language.substr(0, 2);\r\n        }\r\n    }\r\n    catch (e) {\r\n    \/\/ Really old Internet Explorer seems to throw an exception on the above code.\r\n    \/\/ We'll just catch and pass.\r\n    }\r\n\r\n    \/\/ Redirect to a language-specific index page\r\n    window.location.href = '\/' + redirectLanguage + '\/index.html';\r\n&lt;\/script&gt;\r\n<\/code><\/pre>\n<p>and a fallback link to default language for the &#8220;no script&#8221; visitor. Remember that you can&#8217;t add any translation function in this file, this will result in an &#8216;undefined&#8217; error.<\/p>\n<h4><a name=\"client-side-storage\"><\/a>Client side storage of prefered language<\/h4>\n<p>Not done yet&#8230; Plan to use the <a href=\"http:\/\/brian.io\/lawnchair\/plugins\/\">lawnchair<\/a> library.<\/p>\n<h4><a name=\"404\"><\/a>The 404 case<\/h4>\n<p>If a visitor go to a missing url, what happen ? How to redirect him to a page in his language ?<\/p>\n<p>We can&#8217;t rely on the urls he will use, nor on any metadata. So we are back again with language redirection, server side or client side.<br \/>\nUnfortunatelly, a <a href=\"https:\/\/github.com\/bevry\/docpad\/issues\/779\">Docpad bug<\/a> prevent us to do any server side 404 redirection, because setting <code>middleware404: false<\/code> in <code>docpad.coffee<\/code> results in a long node.js Type error.<\/p>\n<p>So we use a client side redirection :<br \/>\nput a 404.html file at the <code>documents<\/code> root with this script :<\/p>\n<pre><code class=\"javascript\">&lt;script&gt;\r\n    var defaultLanguage, supportedLanguages, language, redirectLanguage;\r\n\r\n    defaultLanguage = 'en';\r\n    supportedLanguages = &lt;%- JSON.stringify @languages %&gt;;\r\n    redirectLanguage = defaultLanguage;\r\n\r\n    try {\r\n        language = window.navigator.userLanguage || window.navigator.language || defaultLanguage;\r\n\r\n    \/\/ Some languages we support look like 'en' and some like 'en-US'. Some languages\r\n    \/\/ set in navigator.language look like 'en' and some like 'en-US'. So we'll\r\n    \/\/ check both combinations.\r\n\r\n        if (supportedLanguages.indexOf(language) &gt;= 0) {\r\n            redirectLanguage = language;\r\n        }\r\n        else if (supportedLanguages.indexOf(language.substr(0, 2)) &gt;= 0) {\r\n            redirectLanguage = language.substr(0, 2);\r\n        }\r\n    }\r\n    catch (e) {\r\n    \/\/ Really old Internet Explorer seems to throw an exception on the above code.\r\n    \/\/ We'll just catch and pass.\r\n    }\r\n\r\n    \/\/ Redirect to a language-specific index page\r\n    window.location.href = '\/' + redirectLanguage + '\/404.html';\r\n&lt;\/script&gt;\r\n<\/code><\/pre>\n<p>and a fallback link to default language for the &#8220;no script&#8221; visitor. Remember that you can&#8217;t add any translation function in this file, this will result in an &#8216;undefined&#8217; error.<\/p>\n<p>Plus create a translated 404 files in each document language : they can be translated<br \/>\nSo the visitor will see the error in the detected language : arguably it&#8217;s better than &#8220;english only&#8221;.<\/p>\n<p>Working examples :<br \/>\n&#8211; <a href=\"https:\/\/github.com\/nylnook\/nylnook-website\/blob\/master\/src\/documents\/404.html.eco\">documents\/404.html<\/a><br \/>\n&#8211; <a href=\"https:\/\/github.com\/nylnook\/nylnook-website\/blob\/master\/src\/documents\/en\/404.html.md\">documents\/en\/404.html<\/a><br \/>\n&#8211; <a href=\"https:\/\/github.com\/nylnook\/nylnook-website\/blob\/master\/src\/documents\/fr\/404.html.md\">documents\/fr\/404.html<\/a><\/p>\n<h3><a name=\"limits\"><\/a>Limits<\/h3>\n<p>The main one : this a subdirectory only solution.<\/p>\n<p>Additionally to the 404 bug, two Docpad plugins at least break with this internationalization solution : <a href=\"https:\/\/github.com\/docpad\/docpad-plugin-associatedfiles\">associatedfiles<\/a> and <a href=\"https:\/\/github.com\/brockfanning\/docpad-plugin-moment\">docpad-plugin-moment<\/a>.<\/p>\n<p>And I didn&#8217;t test right-to-left or non-latin languages yet. So this code probably miss some declarations and functions to make them work. Adam Pritchard&#8217;s <a href=\"https:\/\/bitbucket.org\/psiphon\/psiphon-circumvention-system\/src\/tip\/Website\/?at=default\">Psiphon Circumvention System<\/a> website contain some, if you want to dig into that.<\/p>\n<h2><a name=\"abstracting\"><\/a>Abstracting ?<\/h2>\n<p>With some abstration to manage the subdomain and other domains cases, all this may go into a Docpad plugin to simplify installation&#8230; But I&#8217;m afraid I&#8217;m not the good guy for that&#8230; Remember : I&#8217;m a designer, not really a coder !<\/p>\n<p>Let&#8217;s dream : some bits of this code may even be part of Docpad one day. No modesty here ;p<\/p>\n","protected":false},"excerpt":{"rendered":"<p>EDIT March 2019: this website is now based on WordPress, this article is kept as an archive EDIT : This method for translating DocPad is currently developped as a plugin by Bernhard F.W. Gschaider ! Many thanks to him ! In my way building this website, one the major technical point to solve was internationalization [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"godot_game_meta":[],"pulp_press_meta":[],"pulp_comic_meta":[],"inline_featured_image":false,"nylnook_wp_meta":[null],"footnotes":""},"categories":[1],"tags":[31,52],"class_list":["post-86","post","type-post","status-publish","format-standard","hentry","category-blog","tag-geek","tag-tutorials"],"_links":{"self":[{"href":"https:\/\/nylnook.art\/en\/wp-json\/wp\/v2\/posts\/86","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/nylnook.art\/en\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/nylnook.art\/en\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/nylnook.art\/en\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/nylnook.art\/en\/wp-json\/wp\/v2\/comments?post=86"}],"version-history":[{"count":3,"href":"https:\/\/nylnook.art\/en\/wp-json\/wp\/v2\/posts\/86\/revisions"}],"predecessor-version":[{"id":260,"href":"https:\/\/nylnook.art\/en\/wp-json\/wp\/v2\/posts\/86\/revisions\/260"}],"wp:attachment":[{"href":"https:\/\/nylnook.art\/en\/wp-json\/wp\/v2\/media?parent=86"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/nylnook.art\/en\/wp-json\/wp\/v2\/categories?post=86"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/nylnook.art\/en\/wp-json\/wp\/v2\/tags?post=86"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}