- i18n 2017-03-19T16:58:15+01:00 https://fadeit.dk/blog/tag/i18n.html Loading translations from Google Spreadsheets 2015-09-04T11:16:30+02:00 https://fadeit.dk/blog /2015/09/04/managing-translations-with-gulp-or-grunt <h2 id="introduction">Introduction</h2> <p>This is a follow-up post to our <a href="https://fadeit.dk/blog/post/managing-angular-translate-translations">previous</a> translation management article. To recap - in that article we solved the issue of loading ‘fresh’ translation on development server using nginx and proxying translation requests to the api which in turn will fetch new translations from Google Spreadsheets. It can be seen as overkill - this flow complicates nginx configuration, slows down translation loading on every refresh and will make a ton of (unnecessary) requests while developing. While it’s nice to always get the latest translations, truth is that most of the time you don’t need to fetch translations from the cloud. In this post we’ll explore loading translations only during the build process, thus eliminating the need to proxy requests and write api endpoint in the first place.</p> <h2 id="workflow">Workflow</h2> <p>Since writing previous post, I have also changed my mind about lazy-loading translations using partial loader. This is primarily due to the lengths I had to go to cloak translations while they’re loading, especially considering that the size of the translations file is marginal for most applications. By packaging translations in the same minified file as the application I don’t have to worry about <a href="https://fadeit.dk/blog/post/preload-angular-translate-partial-loader">pre-loading</a> translations or flickering as they are being loaded. So we will introduce an extra task to our workflow that will fetch translations from Google Spreadsheets and converts it to a javascript file ready to be used by angular-translate.</p> <h2 id="using-gulp">Using Gulp</h2> <p>Although here at fadeit we started out with using Grunt exclusively, we’ve come to appreciate Gulp more in our recent projects. Primarily since writing custom tasks in good ol’ JavaScript is more intuitive as opposed to Grunt’s configuration approach. Surely piping the result of previous task directly to next one without having to write it to disk is a plus, but not in current context.</p> <div class="language-javascript highlighter-rouge"><pre class="highlight"><code><span class="kd">var</span> <span class="nx">gulp</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'gulp'</span><span class="p">);</span> <span class="kd">var</span> <span class="nx">request</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'request'</span><span class="p">);</span> <span class="kd">var</span> <span class="nx">fs</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'fs'</span><span class="p">);</span> <span class="nx">gulp</span><span class="p">.</span><span class="nx">task</span><span class="p">(</span><span class="s1">'translations'</span><span class="p">,</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">cb</span><span class="p">)</span> <span class="p">{</span> <span class="kd">var</span> <span class="nx">uuid</span> <span class="o">=</span> <span class="s1">'1FsVuRLbtgxMZvWd4mpnKiAhqVYap-ZAx08LBeZ9HFJk'</span><span class="p">;</span> <span class="kd">var</span> <span class="nx">page</span> <span class="o">=</span> <span class="s1">'od6'</span> <span class="kd">var</span> <span class="nx">url</span> <span class="o">=</span> <span class="s1">'https://spreadsheets.google.com/feeds/list/'</span> <span class="o">+</span> <span class="nx">uuid</span> <span class="o">+</span> <span class="s1">'/'</span> <span class="o">+</span> <span class="nx">page</span> <span class="o">+</span> <span class="s1">'/public/values?alt=json'</span><span class="p">;</span> <span class="nx">request</span><span class="p">(</span><span class="nx">url</span><span class="p">,</span> <span class="kd">function</span><span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">res</span><span class="p">,</span> <span class="nx">body</span><span class="p">){</span> <span class="kd">var</span> <span class="nx">json</span> <span class="o">=</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="nx">body</span><span class="p">);</span> <span class="kd">var</span> <span class="nx">translations</span> <span class="o">=</span> <span class="p">{</span> <span class="s1">'en'</span><span class="p">:</span> <span class="p">{},</span> <span class="s1">'da'</span><span class="p">:</span> <span class="p">{}</span> <span class="p">};</span> <span class="k">for</span><span class="p">(</span><span class="kd">var</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="nx">json</span><span class="p">.</span><span class="nx">feed</span><span class="p">.</span><span class="nx">entry</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">){</span> <span class="kd">var</span> <span class="nx">entry</span> <span class="o">=</span> <span class="nx">json</span><span class="p">.</span><span class="nx">feed</span><span class="p">.</span><span class="nx">entry</span><span class="p">[</span><span class="nx">i</span><span class="p">];</span> <span class="kd">var</span> <span class="nx">key</span> <span class="o">=</span> <span class="nx">entry</span><span class="p">.</span><span class="nx">gsx$key</span><span class="p">.</span><span class="nx">$t</span><span class="p">;</span> <span class="kd">var</span> <span class="nx">enValue</span> <span class="o">=</span> <span class="nx">entry</span><span class="p">.</span><span class="nx">gsx$en</span><span class="p">.</span><span class="nx">$t</span><span class="p">;</span> <span class="kd">var</span> <span class="nx">daValue</span> <span class="o">=</span> <span class="nx">entry</span><span class="p">.</span><span class="nx">gsx$da</span><span class="p">.</span><span class="nx">$t</span><span class="p">;</span> <span class="nx">translations</span><span class="p">.</span><span class="nx">en</span><span class="p">[</span><span class="nx">key</span><span class="p">]</span> <span class="o">=</span> <span class="nx">enValue</span><span class="p">;</span> <span class="nx">translations</span><span class="p">.</span><span class="nx">da</span><span class="p">[</span><span class="nx">key</span><span class="p">]</span> <span class="o">=</span> <span class="nx">daValue</span><span class="p">;</span> <span class="p">}</span> <span class="nx">writeTranslations</span><span class="p">(</span><span class="nx">translations</span><span class="p">,</span> <span class="nx">options</span><span class="p">.</span><span class="nx">src</span> <span class="o">+</span> <span class="s1">'/app/translations.js'</span><span class="p">,</span> <span class="nx">cb</span><span class="p">);</span> <span class="p">});</span> <span class="p">});</span> <span class="kd">function</span> <span class="nx">writeTranslations</span><span class="p">(</span><span class="nx">translations</span><span class="p">,</span> <span class="nx">file</span><span class="p">,</span> <span class="nx">cb</span><span class="p">){</span> <span class="kd">var</span> <span class="nx">contents</span> <span class="o">=</span> <span class="s2">"var appModule = angular.module('fadeit');\n"</span><span class="p">;</span> <span class="nx">contents</span> <span class="o">+=</span> <span class="s2">"appModule.config(function($translateProvider){\n"</span><span class="p">;</span> <span class="k">for</span><span class="p">(</span><span class="kd">var</span> <span class="nx">property</span> <span class="k">in</span> <span class="nx">translations</span><span class="p">){</span> <span class="kd">var</span> <span class="nx">lang</span> <span class="o">=</span> <span class="nx">translations</span><span class="p">[</span><span class="nx">property</span><span class="p">];</span> <span class="nx">contents</span> <span class="o">+=</span> <span class="s2">"$translateProvider.translations('"</span> <span class="o">+</span> <span class="nx">property</span> <span class="o">+</span> <span class="s2">"',\n"</span><span class="p">;</span> <span class="nx">contents</span> <span class="o">+=</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">lang</span><span class="p">);</span> <span class="nx">contents</span> <span class="o">+=</span> <span class="s2">");\n"</span><span class="p">;</span> <span class="p">}</span> <span class="nx">contents</span> <span class="o">+=</span> <span class="s2">"});"</span><span class="p">;</span> <span class="nx">fs</span><span class="p">.</span><span class="nx">writeFile</span><span class="p">(</span><span class="nx">file</span><span class="p">,</span> <span class="nx">contents</span><span class="p">,</span> <span class="kd">function</span><span class="p">(</span><span class="nx">err</span><span class="p">){</span> <span class="k">if</span><span class="p">(</span><span class="nx">err</span><span class="p">){</span> <span class="nx">console</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="s1">'Writing translations failed:'</span><span class="p">);</span> <span class="nx">console</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span> <span class="p">}</span> <span class="nx">cb</span><span class="p">();</span> <span class="p">});</span> <span class="p">}</span> </code></pre> </div> <p>As seen from the code, couple modules are used - namely <code class="highlighter-rouge">request</code> to fetch the JSON data and <code class="highlighter-rouge">fs</code> to write a file. The task won’t only write the file, but it will also generate an angular.js module file that will configure translations ready to be used. It can be seen a bit hacky to generate javascript file like that, but I’ve yet to come up with more clean solution. The upside is that this code is not prone to change and I haven’t touched it since writing it. There’s caveat though - if you are using <a href="http://www.browsersync.io/">browsersync</a>, then it may trigger refresh before the translations are actually pulled (because other files changed before). To fix this, we need to make sure that translations are loaded before any other tasks take place. It is easily solved with gulp-sync module, to execute certain tasks synchronously. Same would also apply if the process will concatenate files together - we need to make sure new translations are loaded before that happens. In the sample build process below I’ve split up the build process in two portions where each of them is executed asynchronously. The callback at the end of the gulp task is necessary so that gulp-sync knows when to start processing next batch of tasks.</p> <div class="language-javascript highlighter-rouge"><pre class="highlight"><code><span class="kd">var</span> <span class="nx">gulpsync</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'gulp-sync'</span><span class="p">)(</span><span class="nx">gulp</span><span class="p">);</span> <span class="nx">gulp</span><span class="p">.</span><span class="nx">task</span><span class="p">(</span><span class="s1">'build'</span><span class="p">,</span> <span class="nx">gulpsync</span><span class="p">.</span><span class="nx">sync</span><span class="p">([[</span><span class="s1">'translations'</span><span class="p">,</span> <span class="s1">'clean'</span><span class="p">],</span> <span class="p">[</span><span class="s1">'html'</span><span class="p">,</span> <span class="s1">'scripts'</span><span class="p">]]));</span> </code></pre> </div> <h2 id="using-grunt">Using Grunt</h2> <p>Firstly I must apologise as I did not take time to develop a pure grunt way of doing so. This is because it started out as a python script which I later incorporated in the grunt workflow. The script will serve same purpose as the one above, with a minor difference of calling an external python executeable instead.</p> <div class="language-python highlighter-rouge"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">json</span> <span class="kn">import</span> <span class="nn">urllib.request</span> <span class="kn">from</span> <span class="nn">collections</span> <span class="kn">import</span> <span class="n">OrderedDict</span> <span class="n">sheet_id</span> <span class="o">=</span> <span class="s">'1FsVuRLbtgxMZvWd4mpnKiAhqVYap-ZAx08LBeZ9HFJk'</span><span class="p">;</span> <span class="n">page_id</span> <span class="o">=</span> <span class="s">'od6'</span> <span class="n">base</span> <span class="o">=</span> <span class="s">'https://spreadsheets.google.com/feeds/list/{0}/{1}/public/values?alt=json'</span> <span class="n">translation_config</span> <span class="o">=</span> <span class="s">'src/app/translations.js'</span> <span class="n">url</span> <span class="o">=</span> <span class="n">base</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">sheet_id</span><span class="p">,</span> <span class="n">page_id</span><span class="p">)</span> <span class="n">json_data</span> <span class="o">=</span> <span class="n">json</span><span class="o">.</span><span class="n">loads</span><span class="p">(</span><span class="n">urllib</span><span class="o">.</span><span class="n">request</span><span class="o">.</span><span class="n">urlopen</span><span class="p">(</span><span class="n">url</span><span class="p">)</span><span class="o">.</span><span class="n">read</span><span class="p">()</span><span class="o">.</span><span class="n">decode</span><span class="p">(</span><span class="s">'utf8'</span><span class="p">))</span> <span class="c">#we need to build different data structure</span> <span class="n">en_translation_map</span> <span class="o">=</span> <span class="n">OrderedDict</span><span class="p">()</span> <span class="n">da_translation_map</span> <span class="o">=</span> <span class="n">OrderedDict</span><span class="p">()</span> <span class="k">for</span> <span class="n">entry</span> <span class="ow">in</span> <span class="n">json_data</span><span class="p">[</span><span class="s">'feed'</span><span class="p">][</span><span class="s">'entry'</span><span class="p">]:</span> <span class="n">key</span> <span class="o">=</span> <span class="n">entry</span><span class="p">[</span><span class="s">'gsx$key'</span><span class="p">][</span><span class="s">'$t'</span><span class="p">]</span> <span class="n">en_value</span> <span class="o">=</span> <span class="n">entry</span><span class="p">[</span><span class="s">'gsx$en'</span><span class="p">][</span><span class="s">'$t'</span><span class="p">]</span> <span class="n">da_value</span> <span class="o">=</span> <span class="n">entry</span><span class="p">[</span><span class="s">'gsx$da'</span><span class="p">][</span><span class="s">'$t'</span><span class="p">]</span> <span class="n">en_translation_map</span><span class="p">[</span><span class="n">key</span><span class="p">]</span> <span class="o">=</span> <span class="n">en_value</span> <span class="n">da_translation_map</span><span class="p">[</span><span class="n">key</span><span class="p">]</span> <span class="o">=</span> <span class="n">da_value</span> <span class="c">#Javascript for translations file we are generating</span> <span class="n">config</span> <span class="o">=</span> <span class="s">""" var appModule = angular.module('fadeit'); appModule.config(function($translateProvider){ """</span> <span class="n">en_translations</span> <span class="o">=</span> <span class="n">json</span><span class="o">.</span><span class="n">dumps</span><span class="p">(</span><span class="n">en_translation_map</span><span class="p">,</span> <span class="n">indent</span><span class="o">=</span><span class="mi">4</span><span class="p">)</span> <span class="n">da_translations</span> <span class="o">=</span> <span class="n">json</span><span class="o">.</span><span class="n">dumps</span><span class="p">(</span><span class="n">da_translation_map</span><span class="p">,</span> <span class="n">indent</span><span class="o">=</span><span class="mi">4</span><span class="p">)</span> <span class="n">config</span> <span class="o">=</span> <span class="n">config</span> <span class="o">+</span> <span class="s">"</span><span class="se">\n</span><span class="s">$translateProvider.translations('en',"</span> <span class="o">+</span> <span class="n">en_translations</span> <span class="o">+</span> <span class="s">');'</span> <span class="n">config</span> <span class="o">=</span> <span class="n">config</span> <span class="o">+</span> <span class="s">"</span><span class="se">\n</span><span class="s">$translateProvider.translations('da',"</span> <span class="o">+</span> <span class="n">da_translations</span> <span class="o">+</span> <span class="s">');'</span> <span class="n">config</span> <span class="o">=</span> <span class="n">config</span> <span class="o">+</span> <span class="s">"</span><span class="se">\n</span><span class="s">});"</span> <span class="c">#close function</span> <span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="n">translation_config</span><span class="p">,</span> <span class="s">'w'</span><span class="p">)</span> <span class="k">as</span> <span class="n">outfile</span><span class="p">:</span> <span class="n">outfile</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="n">config</span><span class="p">)</span> </code></pre> </div> <p>Now we can save this file in the project (in our case src/translations.py) and call it using grunt-shell during build time.</p> <div class="language-javascript highlighter-rouge"><pre class="highlight"><code><span class="nx">shell</span><span class="err">:</span> <span class="p">{</span> <span class="nl">translations</span><span class="p">:</span> <span class="p">{</span> <span class="nl">options</span><span class="p">:</span> <span class="p">{</span> <span class="nl">stdout</span><span class="p">:</span> <span class="kc">true</span> <span class="p">},</span> <span class="nx">command</span><span class="err">:</span> <span class="s1">'python3 src/translations.py'</span> <span class="p">}</span> <span class="p">}</span> </code></pre> </div> <p>Since grunt executes tasks synchronously, all there is left to do is call this task in the beginning of the build process and it’ll generate a following file.</p> <div class="language-javascript highlighter-rouge"><pre class="highlight"><code><span class="kd">var</span> <span class="nx">appModule</span> <span class="o">=</span> <span class="nx">angular</span><span class="p">.</span><span class="nx">module</span><span class="p">(</span><span class="s1">'fadeit'</span><span class="p">);</span> <span class="nx">appModule</span><span class="p">.</span><span class="nx">config</span><span class="p">(</span><span class="kd">function</span><span class="p">(</span><span class="nx">$translateProvider</span><span class="p">)</span> <span class="p">{</span> <span class="nx">$translateProvider</span><span class="p">.</span><span class="nx">translations</span><span class="p">(</span><span class="s1">'en'</span><span class="p">,</span> <span class="p">{</span> <span class="s2">"HELLO"</span><span class="p">:</span> <span class="s2">"Hello"</span> <span class="p">}</span> <span class="p">);</span> <span class="nx">$translateProvider</span><span class="p">.</span><span class="nx">translations</span><span class="p">(</span><span class="s1">'da'</span><span class="p">,</span> <span class="p">{</span> <span class="s2">"HELLO"</span><span class="p">:</span> <span class="s2">"Hej"</span> <span class="p">}</span> <span class="p">);</span> <span class="p">});</span> </code></pre> </div> <h2 id="closing-words">Closing words</h2> <p>Over-all I’ve come to prefer loading translations during build time instead. While it will add few seconds to the build process, I will make it up by not having to wait for translations being proxied on every reload. If I already have a server running and just wish to reload translations, I can issue the command directly instead of re-building everything. For gulp it would be <code class="highlighter-rouge">gulp translations</code> and for grunt <code class="highlighter-rouge">grunt shell:translations</code>. I’ve also decided to ditch versioning translations and generally will add <code class="highlighter-rouge">src/translations.js</code> to <code class="highlighter-rouge">.gitignore</code>.</p> How to manage multilingual website translations? 2015-05-08T12:25:54+02:00 https://fadeit.dk/blog /2015/05/08/managing-angular-translate-translations <p><strong>Update</strong>: I have written a <a href="https://fadeit.dk/blog/post/managing-translations-with-gulp-or-grunt">follow-up post</a> where I load translations during gulp/grunt build process instead of nginx proxy.</p> <h2 id="introduction">Introduction</h2> <p>This article focuses on translation management for AngularJS, however it can be applied to any translation management process. Truth is… I haven’t been a fan of the approach <a href="https://en.wikipedia.org/wiki/Gettext">gettext</a> takes, primarily because of the translation management process. I find the work-flow of having to extract translations from the code (.po file), send them to translator, compiling (.mo file) and deploying to consume significant amount of time. Key based translation like angular-translate may simplify certain aspects, but JSON files are still a limitation. They are uncomfortable to edit, prone to merge conflicts and have a strict format.</p> <h2 id="our-apporach">Our apporach</h2> <p>Fortunately there is a tool out there that offers flexible user interface, real-time editing, it fast and easy to integrate with - Google Sheets. You may know that it is possible to publish spreadsheet as HTML, however what they don’t tell you is that there is also an endpoint for returning the data in JSON format.</p> <p><img src="/blog/assets/managing-angular-translate-translations/publish.png" alt="Publishing Google Sheets" /></p> <p>Google will then give you a URL that looks like this:</p> <p><a href="https://docs.google.com/spreadsheets/d/1FsVuRLbtgxMZvWd4mpnKiAhqVYap-ZAx08LBeZ9HFJk/pubhtml">https://docs.google.com/spreadsheets/d/1FsVuRLbtgxMZvWd4mpnKiAhqVYap-ZAx08LBeZ9HFJk/pubhtml</a>.</p> <p>To access JSON variant, take the id of document - <code class="highlighter-rouge">1FsVuRLbtgxMZvWd4mpnKiAhqVYap-ZAx08LBeZ9HFJk</code> and use it in spreadsheet feed service URL:</p> <p><a href="https://spreadsheets.google.com/feeds/list/1FsVuRLbtgxMZvWd4mpnKiAhqVYap-ZAx08LBeZ9HFJk/od6/public/values?alt=json">https://spreadsheets.google.com/feeds/list/1FsVuRLbtgxMZvWd4mpnKiAhqVYap-ZAx08LBeZ9HFJk/od6/public/values?alt=json</a>.</p> <p>That URL however will only display data for first page only - by default first page id in Google Docs is <code class="highlighter-rouge">od6</code>. In order to organize translations we want to store them on multiple pages (I have created Common &amp; Register pages in the screenshot above), therefore we need to get IDs of other pages using yet another feed:</p> <p><a href="https://spreadsheets.google.com/feeds/worksheets/1FsVuRLbtgxMZvWd4mpnKiAhqVYap-ZAx08LBeZ9HFJk/private/full">https://spreadsheets.google.com/feeds/worksheets/1FsVuRLbtgxMZvWd4mpnKiAhqVYap-ZAx08LBeZ9HFJk/private/full</a>.</p> <p>From the XML we can search for <code class="highlighter-rouge">&lt;/id&gt;</code> to extract other sheet IDs. As we see, the ID for second sheet is no longer following any pattern - <a href="https://spreadsheets.google.com/feeds/list/1FsVuRLbtgxMZvWd4mpnKiAhqVYap-ZAx08LBeZ9HFJk/o14w8rc/public/values?alt=json">o14w8rc</a>.</p> <p>Even more feeds are covered on the <a href="https://developers.google.com/google-apps/spreadsheets/">Official API documentation</a>.</p> <p><img src="/blog/assets/managing-angular-translate-translations/formatted.png" alt="Formatted JSON from Google Spreadsheet feed" /></p> <p>While JSON representation is good starting point, it is a serialized representation of the document that contains a lot of meta-data. We are only interesed in the nested values, so data-structure to be transformed into what i18n framework can read. For that we are going to implement an API endpoint that will fetch translations from Google Docs, transform the structure and return translations. As seen from the screenshot above, the pattern for extracting a value is <code class="highlighter-rouge">feed-&gt;entry-&gt;gsx${language}-&gt;$t</code>. The minimal example below utilizes <a href="http://flask.pocoo.org/">Flask microframework</a> written in Python</p> <div class="language-python highlighter-rouge"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">os</span> <span class="kn">import</span> <span class="nn">json</span> <span class="kn">import</span> <span class="nn">urllib</span> <span class="kn">from</span> <span class="nn">flask</span> <span class="kn">import</span> <span class="n">Flask</span><span class="p">,</span> <span class="n">abort</span> <span class="kn">from</span> <span class="nn">collections</span> <span class="kn">import</span> <span class="n">OrderedDict</span> <span class="n">app</span> <span class="o">=</span> <span class="n">Flask</span><span class="p">(</span><span class="n">__name__</span><span class="p">)</span> <span class="nd">@app.route</span><span class="p">(</span><span class="s">'/translation/&lt;lang&gt;/&lt;filename&gt;'</span><span class="p">)</span> <span class="k">def</span> <span class="nf">fetch_translation</span><span class="p">(</span><span class="n">lang</span><span class="p">,</span> <span class="n">filename</span><span class="p">):</span> <span class="c"># Mapping Google Docs page ID to the translation file</span> <span class="n">page_map</span> <span class="o">=</span> <span class="p">{</span> <span class="s">'common.json'</span><span class="p">:</span> <span class="s">'od6'</span><span class="p">,</span> <span class="s">'register.json'</span><span class="p">:</span> <span class="s">'o14w8rc'</span> <span class="p">}</span> <span class="n">sheet_id</span> <span class="o">=</span> <span class="s">'1FsVuRLbtgxMZvWd4mpnKiAhqVYap-ZAx08LBeZ9HFJk'</span> <span class="n">base</span> <span class="o">=</span> <span class="s">'https://spreadsheets.google.com/feeds/list/{0}/{1}/public/values?alt=json'</span> <span class="k">if</span> <span class="n">filename</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">page_map</span><span class="p">:</span> <span class="n">abort</span><span class="p">(</span><span class="mi">404</span><span class="p">)</span> <span class="n">page_id</span> <span class="o">=</span> <span class="n">page_map</span><span class="p">[</span><span class="n">filename</span><span class="p">]</span> <span class="n">url</span> <span class="o">=</span> <span class="n">base</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">sheet_id</span><span class="p">,</span> <span class="n">page_id</span><span class="p">)</span> <span class="n">json_data</span> <span class="o">=</span> <span class="n">json</span><span class="o">.</span><span class="n">loads</span><span class="p">(</span><span class="n">urllib</span><span class="o">.</span><span class="n">request</span><span class="o">.</span><span class="n">urlopen</span><span class="p">(</span><span class="n">url</span><span class="p">)</span><span class="o">.</span><span class="n">read</span><span class="p">()</span><span class="o">.</span><span class="n">decode</span><span class="p">(</span><span class="s">'utf8'</span><span class="p">))</span> <span class="c"># We need to build different data structure</span> <span class="n">translation_map</span> <span class="o">=</span> <span class="n">OrderedDict</span><span class="p">()</span> <span class="k">for</span> <span class="n">entry</span> <span class="ow">in</span> <span class="n">json_data</span><span class="p">[</span><span class="s">'feed'</span><span class="p">][</span><span class="s">'entry'</span><span class="p">]:</span> <span class="n">key</span> <span class="o">=</span> <span class="n">entry</span><span class="p">[</span><span class="s">'gsx$key'</span><span class="p">][</span><span class="s">'$t'</span><span class="p">]</span> <span class="n">value</span> <span class="o">=</span> <span class="n">entry</span><span class="p">[</span><span class="s">'gsx${0}'</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">lang</span><span class="p">)][</span><span class="s">'$t'</span><span class="p">]</span> <span class="k">if</span> <span class="n">value</span><span class="p">:</span> <span class="n">translation_map</span><span class="p">[</span><span class="n">key</span><span class="p">]</span> <span class="o">=</span> <span class="n">value</span> <span class="k">return</span> <span class="n">json</span><span class="o">.</span><span class="n">dumps</span><span class="p">(</span><span class="n">translation_map</span><span class="p">)</span> <span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">'__main__'</span><span class="p">:</span> <span class="n">app</span><span class="o">.</span><span class="n">run</span><span class="p">()</span> </code></pre> </div> <p>Now let’s run the server and fetch translations at http://127.0.0.1:5000/translation/en-us/register.json</p> <div class="language-bash highlighter-rouge"><pre class="highlight"><code>curl http://127.0.0.1:5000/translation/en-us/register.json <span class="o">{</span><span class="s2">"WELCOME"</span>: <span class="s2">"Welcome"</span><span class="o">}</span> </code></pre> </div> <p>Fetching translations is convenient while developing and translating, however we wouldn’t want production server to fetch translations from Google Docs every time it gets a request. Easiest solution would be to cache translations for certain amount of time, we also want to version control translations, therefore the strategy is a little different. Production server would be serving static JSON files from the disc, whereas on development instance NGINX will intercept any requests going to /translation and proxy them to the application server instead:</p> <p><img src="/blog/assets/managing-angular-translate-translations/flow.png" alt="Translation loading flow" /></p> <p>For development NGINX configuration we need to add a location block like so:</p> <div class="language-bash highlighter-rouge"><pre class="highlight"><code><span class="c">#proxy translations from Google Docs on development</span> location /translation <span class="o">{</span> proxy_pass http://127.0.0.1:5000<span class="nv">$uri</span>; proxy_redirect off; proxy_set_header Host <span class="nv">$host</span>; proxy_set_header X-Real-IP <span class="nv">$remote_addr</span>; proxy_set_header X-Forwarded-For <span class="nv">$proxy_add_x_forwarded_for</span>; <span class="o">}</span> </code></pre> </div> <h2 id="conclusion">Conclusion</h2> <p>All translations now come from one consistent source, no more mismatches and conflicts.</p> <p>With Git we get:</p> <ul> <li>Accidental modifications in Google Docs don’t automatically end up in production</li> <li>Each translation has a revision history&lt;/li&gt;&lt;li&gt;Finding differences between edits is simple</li> <li>Blame view helps finding wrongdoer</li> </ul> <p>Google Docs brings to the table:</p> <ul> <li>Spotting missing translations is easy because all languages are visible on same sheet (.json files are separate for each language)</li> <li>Access and control management of editors is simple</li> <li>Multiple people can edit same document in realtime</li> <li>Changes to translations are visible immediately (on development setup)</li> <li>Sheets supports highlighting and commenting</li> </ul> <p>Dependencies needed to run the flask server:</p> <div class="language-bash highlighter-rouge"><pre class="highlight"><code><span class="c">#Install dependencies</span> sudo apt-get install virtualenv python3.4-dev python-pip <span class="c">#Create virtual environment</span> virtualenv -p /usr/bin/python3.4 venv <span class="c">#Activate virtual environment</span> <span class="nb">source </span>venv/bin/activate <span class="c">#Install libraries</span> pip install flask <span class="c">#Run server</span> python server.py <span class="c">#Fetch translations</span> http://localhost:5000/translation/en-us/register.json </code></pre> </div> Pre-loading partial loader translations 2015-04-09T16:20:16+02:00 https://fadeit.dk/blog /2015/04/09/preload-angular-translate-partial-loader <h2 id="introduction">Introduction</h2> <p><a href="https://angular-translate.github.io/">Angular-translate</a> is the de-facto internationalisation module for AngularJS applications. Basically it works by replacing translation keys from the translation dictionary. The problem is that larger applications have a lot of content, thus a lot of translations so it doesn’t make sense to send the entire translations file to the client on initial page load. The good news is that there is a solution - <a href="https://github.com/angular-translate/angular-translate/blob/master/src/service/loader-partial.js">partial loader</a>. It works as advertised - we can define translation files that are required to load for that page, thus lowering the amount of data required to transfer. The downside to partial loading is that first browser must initialize angular, which in turn will start loading the translation partials. The issue becomes apparent on slower connections where translation keys are either shown or invisible until partial loader finishes fetching the files. It can be a factor when trying to make a first impression. What if we could pre-load some parts that are common throughout our application? It would help improve the user experience on most pages, but will not eliminate the use of partial translations.</p> <p>Behind the scenes the partial loader uses angular’s <code class="highlighter-rouge">$http</code> service to fetch a JSON file and store it in an array. Fortunately for us, angular has a built-in caching mechanism for <code class="highlighter-rouge">$http</code> service that we could leverage for our purpose. We are going to store the parts we want to pre-load in the $http cache, so when partial loader tries to fetch the files, it will hit the cache.<br />By default the caching mechanism for <code class="highlighter-rouge">$http</code> service is using <code class="highlighter-rouge">$cacheFactory</code>’s cache with <code class="highlighter-rouge">'$http'</code> key, although caching mechanism can be overridden. Cache key is the request URL and value is an array containing 4 items:</p> <ul> <li>response status - 200 for success</li> <li>response headers - we leave it blank</li> <li>data - JSON for our needs</li> <li>status text - ‘OK’ for fulfilled request</li> </ul> <p>Assuming the translation file we want to pre-load is available at http://example.com/assets/translations/da-dk/common.json, let’s test out the concept by creating a function that fakes cache entry for us:</p> <div class="language-javascript highlighter-rouge"><pre class="highlight"><code><span class="kd">var</span> <span class="nx">app</span> <span class="o">=</span> <span class="nx">angular</span><span class="p">.</span><span class="nx">module</span><span class="p">(</span><span class="s1">'app'</span><span class="p">,</span> <span class="p">[</span><span class="s1">'pascalprecht.translate'</span><span class="p">]);</span> <span class="nx">app</span><span class="p">.</span><span class="nx">config</span><span class="p">(</span><span class="kd">function</span><span class="p">(</span><span class="nx">$translateProvider</span><span class="p">)</span> <span class="p">{</span> <span class="nx">$translateProvider</span><span class="p">.</span><span class="nx">useLoader</span><span class="p">(</span><span class="s1">'$translatePartialLoader'</span><span class="p">,</span> <span class="p">{</span> <span class="na">urlTemplate</span><span class="p">:</span> <span class="s1">'/assets/translations/{lang}/{part}.json'</span><span class="p">,</span> <span class="na">$http</span><span class="p">:</span> <span class="p">{</span> <span class="na">cache</span><span class="p">:</span> <span class="kc">true</span> <span class="p">}</span> <span class="c1">//Enable caching for PartialLoader</span> <span class="p">});</span> <span class="p">});</span> <span class="nx">app</span><span class="p">.</span><span class="nx">run</span><span class="p">(</span><span class="kd">function</span> <span class="nx">run</span> <span class="p">(</span><span class="nx">$cacheFactory</span><span class="p">)</span> <span class="p">{</span> <span class="kd">var</span> <span class="nx">httpCache</span> <span class="o">=</span> <span class="nx">$cacheFactory</span><span class="p">.</span><span class="nx">get</span><span class="p">(</span><span class="s1">'$http'</span><span class="p">);</span> <span class="kd">var</span> <span class="nx">putHttpCache</span> <span class="o">=</span> <span class="kd">function</span> <span class="nx">putHttpCache</span><span class="p">(</span><span class="nx">cacheKey</span><span class="p">,</span> <span class="nx">cacheValue</span><span class="p">){</span> <span class="kd">var</span> <span class="nx">cacheObj</span> <span class="o">=</span> <span class="p">[</span><span class="mi">200</span><span class="p">,</span> <span class="nx">cacheValue</span><span class="p">,</span> <span class="p">{},</span> <span class="s2">"OK"</span><span class="p">];</span> <span class="nx">httpCache</span><span class="p">.</span><span class="nx">put</span><span class="p">(</span><span class="nx">cacheKey</span><span class="p">,</span> <span class="nx">cacheObj</span><span class="p">);</span> <span class="p">};</span> <span class="kd">var</span> <span class="nx">cacheValue</span> <span class="o">=</span> <span class="nx">angular</span><span class="p">.</span><span class="nx">toJson</span><span class="p">({</span><span class="na">TEST_KEY</span><span class="p">:</span> <span class="s2">"Cached key test"</span><span class="p">});</span> <span class="nx">putHttpCache</span><span class="p">(</span><span class="s1">'/assets/translations/da-dk/common.json'</span><span class="p">,</span> <span class="nx">cacheValue</span><span class="p">);</span> <span class="p">});</span> </code></pre> </div> <p>It works, however we have translation keys in code. Now we need to come up with a way to ship the desired JSON files at initial application load. An approach would be to keep translations in a .js file instead (an angular service, returning JSON string on call), however that would require us to migrate existing JSON files into Javascript objects and reduce translation maintainability. But wait, there is a Grunt plugin that already stores HTML templates in a similar way (<a href="https://github.com/karlgoldstein/grunt-html2js">html2js</a>). The only difference is that the plugin uses <code class="highlighter-rouge">$templateCache</code> instead. Since <code class="highlighter-rouge">html2js</code> is part of our build process already, what we can do is pass the JSON translations to the angular app using the existing Grunt build task. Once angular is bootstrapped, we’ll load translations from <code class="highlighter-rouge">$templateCache</code> and store them into the <code class="highlighter-rouge">$cacheFactory</code> cache as well. Here’s how the Grunt task for storing .json files in <code class="highlighter-rouge">$templateCache</code> looks like:</p> <div class="language-javascript highlighter-rouge"><pre class="highlight"><code><span class="nx">html2js</span><span class="err">:</span> <span class="p">{</span> <span class="c1">//...template caching task omitted...</span> <span class="nl">translations</span><span class="p">:</span> <span class="p">{</span> <span class="nl">options</span><span class="p">:</span> <span class="p">{</span> <span class="nl">htmlmin</span><span class="p">:</span> <span class="p">{},</span> <span class="c1">//override minification settings</span> <span class="nx">base</span><span class="err">:</span> <span class="s1">'src'</span> <span class="p">},</span> <span class="nx">src</span><span class="err">:</span> <span class="p">[</span> <span class="s1">'src/assets/translations/**/*.json'</span> <span class="p">],</span> <span class="nx">dest</span><span class="err">:</span> <span class="s1">'&lt;%= build_dir %&gt;/templates-translations.js'</span> <span class="p">}</span> <span class="p">}</span> </code></pre> </div> <p>Let’s now ‘teach’ the <code class="highlighter-rouge">putHttpCache</code> method to take translations from <code class="highlighter-rouge">$templateCache</code>:</p> <div class="language-javascript highlighter-rouge"><pre class="highlight"><code><span class="nx">app</span><span class="p">.</span><span class="nx">run</span><span class="p">(</span><span class="kd">function</span> <span class="nx">run</span> <span class="p">(</span><span class="nx">$cacheFactory</span><span class="p">,</span> <span class="nx">$templateCache</span><span class="p">)</span> <span class="p">{</span> <span class="kd">var</span> <span class="nx">httpCache</span> <span class="o">=</span> <span class="nx">$cacheFactory</span><span class="p">.</span><span class="nx">get</span><span class="p">(</span><span class="s1">'$http'</span><span class="p">);</span> <span class="kd">var</span> <span class="nx">putHttpCache</span> <span class="o">=</span> <span class="kd">function</span> <span class="nx">putHttpCache</span><span class="p">(</span><span class="nx">cacheKey</span><span class="p">){</span> <span class="kd">var</span> <span class="nx">cacheObj</span> <span class="o">=</span> <span class="p">[</span><span class="mi">200</span><span class="p">,</span> <span class="nx">$templateCache</span><span class="p">.</span><span class="nx">get</span><span class="p">(</span><span class="nx">cacheKey</span><span class="p">),</span> <span class="p">{},</span> <span class="s2">"OK"</span><span class="p">];</span> <span class="nx">httpCache</span><span class="p">.</span><span class="nx">put</span><span class="p">(</span><span class="s1">'/'</span> <span class="o">+</span> <span class="nx">cacheKey</span><span class="p">,</span> <span class="nx">cacheObj</span><span class="p">);</span> <span class="c1">//prepend '/' to make url's match</span> <span class="p">};</span> <span class="nx">putHttpCache</span><span class="p">(</span><span class="s1">'assets/translations/da-dk/common.json'</span><span class="p">);</span> <span class="p">});</span> </code></pre> </div> <h2 id="conclusion">Conclusion</h2> <p>Now we can take the best of both worlds - preload most frequently used translations and lazy-load translations for less visited pages. Our html2js build process works in similar manner - all templates ending with <code class="highlighter-rouge">*.tpl2js.html</code> are stored in <code class="highlighter-rouge">$templateCache</code>, whereas templates ending with <code class="highlighter-rouge">*.tpl.html</code> are only fetched on demand.</p> AngularJS SEO friendly translation URLs with ui-router 2015-03-30T18:10:55+02:00 https://fadeit.dk/blog /2015/03/30/angular-translate-ui-router-seo <h2 id="introduction">Introduction</h2> <p>In a <a href="https://fadeit.dk/blog/post/angularjs-seo-for-angular-translate">previous</a> post we explored how to introduce unique URLs per language using primarily NGINX and Grunt configuration. To recap, that approach is reasonable if you already have a working application and prefer not to re-factor every state and view that links to it. While it may be easier to implement in an existing application, it forces users to perform a refresh when switching between languages - though not a big issue (how often do users switch back-and-forth anyway), having a Single Page Application (SPA) that forces users to reload pages is sub-optimal. In this article we will explore possibilities of implementing SEO friendly URLs using ui-router.</p> <p>Nested states is the key to introducing language as a URL variable to all states. We start off by creating the ‘app’ state that every other state will be child of. The app state is abstract, so that the state by itself can not be activated. Next we want the route to only match the languages we intend to support (in this example ‘da’ and ‘en’) to reduce possible false-positive matches e.g example.com/<b>da</b>ta. As ui-router injects the content of the child states into ui-view, we define it as an inline template for the parent route. We also define ‘app.home’ state that will handle example.com/en and example.com/da pages:</p> <div class="language-javascript highlighter-rouge"><pre class="highlight"><code><span class="nx">$stateProvider</span><span class="p">.</span><span class="nx">state</span><span class="p">(</span><span class="s1">'app'</span><span class="p">,</span> <span class="p">{</span> <span class="na">abstract</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="na">url</span><span class="p">:</span> <span class="s1">'/{lang:(?:da|en)}'</span><span class="p">,</span> <span class="na">template</span><span class="p">:</span> <span class="s1">'&lt;ui-view/&gt;'</span> <span class="p">});</span> <span class="nx">$stateProvider</span><span class="p">.</span><span class="nx">state</span><span class="p">(</span><span class="s1">'app.home'</span><span class="p">,</span> <span class="p">{</span> <span class="na">url</span><span class="p">:</span> <span class="s1">''</span><span class="p">,</span> <span class="na">templateUrl</span><span class="p">:</span> <span class="s1">'views/home-page.html'</span><span class="p">,</span> <span class="p">});</span> </code></pre> </div> <p>Now every state that we want to translate should be a child of app state:</p> <div class="language-javascript highlighter-rouge"><pre class="highlight"><code><span class="nx">$stateProvider</span><span class="p">.</span><span class="nx">state</span><span class="p">(</span><span class="s1">'app.about'</span><span class="p">,</span> <span class="p">{</span> <span class="na">url</span><span class="p">:</span> <span class="s1">'/about'</span><span class="p">,</span> <span class="na">templateUrl</span><span class="p">:</span> <span class="s1">'views/about-page.html'</span><span class="p">,</span> <span class="p">});</span> </code></pre> </div> <p>Since the child states contain a language path variable, we need to alter the links pointing to the child states to pass in <code class="highlighter-rouge">lang</code> parameter. But before we do so, we need to set <code class="highlighter-rouge">activeLang</code> on <code class="highlighter-rouge">$rootScope</code> so we can read it from there when generating links with <code class="highlighter-rouge">ui-sref</code>. Due to the fact that <code class="highlighter-rouge">activeLang</code> can change at state change, we need to subscribe to <code class="highlighter-rouge">stateChangeSuccess</code> event. While at it, we can also generate links for the same page in other languages - ‘In English’ link at example.com/da/about should point to example.com/en/about. In this simple example we use only two languages, so we’ll be using only one variable <code class="highlighter-rouge">otherLangURL</code>:</p> <div class="language-javascript highlighter-rouge"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">navigationController</span><span class="p">(</span><span class="nx">$scope</span><span class="p">,</span> <span class="nx">$rootScope</span><span class="p">,</span> <span class="nx">$stateParams</span><span class="p">,</span> <span class="nx">$translate</span><span class="p">){</span> <span class="nx">$scope</span><span class="p">.</span><span class="nx">$on</span><span class="p">(</span><span class="s1">'$stateChangeSuccess'</span><span class="p">,</span> <span class="kd">function</span> <span class="nx">rootStateChangeSuccess</span><span class="p">(</span><span class="nx">event</span><span class="p">,</span> <span class="nx">toState</span><span class="p">){</span> <span class="k">if</span><span class="p">(</span><span class="nx">$stateParams</span><span class="p">.</span><span class="nx">lang</span> <span class="o">!==</span> <span class="kc">undefined</span><span class="p">){</span> <span class="kd">var</span> <span class="nx">otherLang</span> <span class="o">=</span> <span class="nx">$stateParams</span><span class="p">.</span><span class="nx">lang</span> <span class="o">===</span> <span class="s1">'da'</span> <span class="p">?</span> <span class="s1">'en'</span> <span class="p">:</span> <span class="s1">'da'</span><span class="p">;</span> <span class="nx">$rootScope</span><span class="p">.</span><span class="nx">activeLang</span> <span class="o">=</span> <span class="nx">$stateParams</span><span class="p">.</span><span class="nx">lang</span><span class="p">;</span> <span class="nx">$rootScope</span><span class="p">.</span><span class="nx">otherLangURL</span> <span class="o">=</span> <span class="nx">$location</span><span class="p">.</span><span class="nx">absUrl</span><span class="p">().</span><span class="nx">replace</span><span class="p">(</span><span class="s1">'/'</span> <span class="o">+</span> <span class="nx">$stateParams</span><span class="p">.</span><span class="nx">lang</span><span class="p">,</span> <span class="s1">'/'</span> <span class="o">+</span><span class="nx">otherLang</span><span class="p">);</span> <span class="nx">$translate</span><span class="p">.</span><span class="nx">use</span><span class="p">(</span><span class="nx">$stateParams</span><span class="p">.</span><span class="nx">lang</span><span class="p">);</span> <span class="p">}</span> <span class="p">});</span> <span class="p">}</span> </code></pre> </div> <p>It would be nice to use <code class="highlighter-rouge">ui-sref</code> to dynamically generate the links to same page in other languages, but unfortunately <code class="highlighter-rouge">ui-sref</code> does not support variables in state names, therefore we have to fall back to <code class="highlighter-rouge">ng-href</code> for generating the links. Fortunately path variable substitution is supported, therefore we can generate menu links using <code class="highlighter-rouge">activeLang</code> variable we stored on <code class="highlighter-rouge">$rootScope</code>:</p> <div class="language-html highlighter-rouge"><pre class="highlight"><code><span class="nt">&lt;ul</span> <span class="na">class=</span><span class="s">"menu-navigation"</span><span class="nt">&gt;</span> ... <span class="nt">&lt;li</span> <span class="na">ui-sref-active=</span><span class="s">"activeMenu"</span><span class="nt">&gt;</span> <span class="nt">&lt;a</span> <span class="na">href</span> <span class="na">ui-sref=</span><span class="s">"app.about({lang: activeLang})"</span> <span class="na">translate</span><span class="nt">&gt;</span> NAV_ABOUT <span class="nt">&lt;/a&gt;</span> <span class="nt">&lt;/li&gt;</span> <span class="nt">&lt;li&gt;</span> <span class="nt">&lt;a</span> <span class="na">ng-href=</span><span class="s">""</span> <span class="na">translate</span><span class="nt">&gt;</span> IN_LANGUAGE <span class="nt">&lt;/a&gt;</span> <span class="nt">&lt;/li&gt;</span> <span class="nt">&lt;/ul&gt;</span> </code></pre> </div> <h2 id="conclusion">Conclusion</h2> <p>Incorporating language in the application routes from very start will save time in the long run as re-factoring hundreds of <code class="highlighter-rouge">ui-sref</code> links will take it’s time. Ui-router fully supports nested sub-states, so there will not be any conflict if your application is already using nested states, just make sure that there is <code class="highlighter-rouge">ui-view</code> for each parent state to inject the content into. I was unable to make the lang parameter in the parent state optional, thus language will be enforced in every path (as opposed to <a href="https://fadeit.dk/blog/post/angularjs-seo-for-angular-translate">previous post</a> where we were able to use paths without language defined in them). Have a better approach? leave a comment!</p> AngularJS SEO friendly translations 2015-03-12T16:30:16+01:00 https://fadeit.dk/blog /2015/03/12/angularjs-seo-for-angular-translate <p><strong>Update</strong>: I have written a follow-up post solving the same problem using ui-router <a href="https://fadeit.dk/blog/post/angular-translate-ui-router-seo">here</a></p> <h2 id="introduction">Introduction</h2> <p>Instant translations, this is what <a href="http://angular-translate.github.io/">angular-translate</a> brings to the table. While it is significantly more convenient than waiting one more site load cycle for a different language to load (especially on sites that are slow to begin with), it doesn’t come without complications. If each translation language does not have a unique URL, search engines can not link search results to it, let alone crawl the site properly.</p> <p>Here’s what Google has to say on the matter:</p> <blockquote> <p><strong>Make sure each language version is easily discoverable</strong> <em>Keep the content for each language on separate URL’s. Don’t use cookies to show translated versions of the page. Consider cross-linking each language version of a page.</em> <a href="https://support.google.com/webmasters/answer/182192">https://support.google.com/webmasters/answer/182192</a></p> </blockquote> <h2 id="options">Options</h2> <p>Okay, perhaps it’s a nice practice to have language in the URL. Here are the options:</p> <ol> <li>Store the language in a GET parameter e.g: <strong>example.com/about?lang=en</strong></li> <li>Store the language in a path e.g: <strong>example.com/en/about</strong></li> <li>Store the language in a sub-domain e.g: <strong>en.example.com/about</strong></li> </ol> <p>While the first option would be the easiest to implement, it is also the ugliest. The other two are relatively similar in terms of appearance and it boils down to your personal preference. An important distincion to make is that if language is stored in a sub-domain, it excludes the possibility of handling the routing with angular as subdomain is out of application router scope. We decided to explore the second option - having the language in a URL path.</p> <p>The preferred approach would be to introduce a path variable on a router base path and have all children be relative to it or escape the language before routing is performed. While it is possible, it seems that the only way to achieve it is to nest all states in the following manner:</p> <div class="language-javascript highlighter-rouge"><pre class="highlight"><code><span class="nx">angular</span><span class="p">.</span><span class="nx">module</span><span class="p">(</span><span class="s1">'app'</span><span class="p">).</span><span class="nx">config</span><span class="p">([</span> <span class="s1">'$stateProvider'</span><span class="p">,</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">$stateProvider</span><span class="p">)</span> <span class="p">{</span> <span class="nx">$stateProvider</span><span class="p">.</span><span class="nx">state</span><span class="p">(</span><span class="s1">'app'</span><span class="p">,</span> <span class="p">{</span> <span class="na">abstract</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="na">url</span><span class="p">:</span> <span class="s1">'/{lang:(?:da|en)}'</span><span class="p">,</span> <span class="na">template</span><span class="p">:</span> <span class="s1">'&lt;ui-view/&gt;'</span><span class="p">,</span> <span class="na">controller</span><span class="p">:</span> <span class="s1">'RootController'</span> <span class="p">});</span> <span class="nx">$stateProvider</span><span class="p">.</span><span class="nx">state</span><span class="p">(</span><span class="s1">'app.root'</span><span class="p">,</span> <span class="p">{</span> <span class="na">url</span><span class="p">:</span> <span class="s1">''</span><span class="p">,</span> <span class="na">templateUrl</span><span class="p">:</span> <span class="s1">'views/home.html'</span><span class="p">,</span> <span class="p">});</span> <span class="nx">$stateProvider</span><span class="p">.</span><span class="nx">state</span><span class="p">(</span><span class="s1">'app.about'</span><span class="p">,</span> <span class="p">{</span> <span class="na">url</span><span class="p">:</span> <span class="s1">''</span><span class="p">,</span> <span class="na">templateUrl</span><span class="p">:</span> <span class="s1">'views/about.html'</span><span class="p">,</span> <span class="p">});</span> <span class="p">}</span> <span class="p">]);</span> </code></pre> </div> <p>It is likely that by the time SEO becomes a priority, the application is already built and refactoring code does not sound appealing. To name a few, using ui-router has following downsides:</p> <ul> <li>Language slug becomes mandatory for each state, so example.com/about would become example.com/da/about</li> <li>All child state names need to be changed</li> <li>All links have to be refactored</li> </ul> <p>We’ll be taking a closer look at ui-router in a separate post soon, while this article focuses on finding an alternative configuration-based approach.</p> <h2 id="automation-to-the-rescue">Automation to the rescue!</h2> <p>Hosting Angular application is just serving static files, where each URL maps to a directory/file by default. Therefore, if we are to host the same application in a different directory for each language, we get SEO friendly URLs without a major overhaul of the code. If you are working on a big Angular project, the chances are that either Grunt or Gulp is in the mix. Grunt is our weapon of choice, so we need to add an extra copy task at the end of the build process that would copy all files into a subdirectory:</p> <div class="language-javascript highlighter-rouge"><pre class="highlight"><code><span class="nx">copy</span><span class="err">:</span> <span class="p">{</span> <span class="nl">build_en_lang</span><span class="p">:</span> <span class="p">{</span> <span class="nl">files</span><span class="p">:</span> <span class="p">[</span> <span class="p">{</span> <span class="na">src</span><span class="p">:</span> <span class="p">[</span> <span class="s1">'**'</span><span class="p">,</span> <span class="s1">'!en/**'</span><span class="p">],</span> <span class="na">dest</span><span class="p">:</span> <span class="s1">'&lt;%= build_dir %&gt;/en'</span><span class="p">,</span> <span class="na">cwd</span><span class="p">:</span> <span class="s1">'&lt;%= build_dir %&gt;'</span><span class="p">,</span> <span class="na">expand</span><span class="p">:</span> <span class="kc">true</span> <span class="p">}</span> <span class="p">]</span> <span class="p">}</span> <span class="p">}</span> </code></pre> </div> <p><code class="highlighter-rouge">!en/**</code> tells Grunt not to copy itself every time that task is called.</p> <p>The directory structure should be following:</p> <ul> <li>/path/to/app/index.html (will default to Danish)</li> <li>/path/to/app/other/files</li> <li>/path/to/app/en/index.html</li> <li>/path/to/app/en/other/files</li> </ul> <p>Now we need to tell Angular to check for URL when determining language:</p> <div class="language-javascript highlighter-rouge"><pre class="highlight"><code><span class="kd">var</span> <span class="nx">absUrl</span> <span class="o">=</span> <span class="nx">$location</span><span class="p">.</span><span class="nx">absUrl</span><span class="p">();</span> <span class="k">if</span><span class="p">(</span><span class="nx">absUrl</span><span class="p">.</span><span class="nx">indexOf</span><span class="p">(</span><span class="s1">'/en/'</span><span class="p">)</span> <span class="o">!==</span> <span class="o">-</span><span class="mi">1</span><span class="p">){</span> <span class="nx">$scope</span><span class="p">.</span><span class="nx">activeLang</span> <span class="o">=</span> <span class="s1">'en-us'</span><span class="p">;</span> <span class="p">}</span> <span class="k">else</span><span class="p">{</span> <span class="nx">$scope</span><span class="p">.</span><span class="nx">activeLang</span> <span class="o">=</span> <span class="s1">'da-dk'</span><span class="p">;</span> <span class="p">}</span> <span class="nx">$translate</span><span class="p">.</span><span class="nx">use</span><span class="p">(</span><span class="nx">$scope</span><span class="p">.</span><span class="nx">activeLang</span><span class="p">);</span> </code></pre> </div> <p><code class="highlighter-rouge">activeLang</code> is stored on scope because it will be later used to determine the class of language switching button</p> <p>Google also suggests cross-linking each language version of the page. If user navigates to a different page, we should also update the link that points to the same page in other languages. For that we use $stateChangeSuccess event:</p> <div class="language-javascript highlighter-rouge"><pre class="highlight"><code><span class="c1">//Construct url base</span> <span class="kd">var</span> <span class="nx">port</span> <span class="o">=</span> <span class="nx">$location</span><span class="p">.</span><span class="nx">port</span><span class="p">();</span> <span class="c1">//Port can be omitted on 80 or 443</span> <span class="nx">port</span> <span class="o">=</span> <span class="p">(</span><span class="nx">port</span> <span class="o">===</span> <span class="mi">80</span> <span class="o">||</span> <span class="nx">port</span> <span class="o">===</span> <span class="mi">443</span><span class="p">)</span> <span class="p">?</span> <span class="s1">''</span> <span class="p">:</span> <span class="s1">':'</span> <span class="o">+</span> <span class="nx">port</span><span class="p">;</span> <span class="kd">var</span> <span class="nx">urlBase</span> <span class="o">=</span> <span class="nx">$location</span><span class="p">.</span><span class="nx">protocol</span><span class="p">()</span> <span class="o">+</span> <span class="s1">'://'</span> <span class="o">+</span> <span class="nx">$location</span><span class="p">.</span><span class="nx">host</span><span class="p">()</span> <span class="o">+</span> <span class="nx">port</span><span class="p">;</span> <span class="nx">$rootScope</span><span class="p">.</span><span class="nx">$on</span><span class="p">(</span><span class="s1">'$stateChangeSuccess'</span><span class="p">,</span><span class="kd">function</span><span class="p">(){</span> <span class="nx">$scope</span><span class="p">.</span><span class="nx">enUrl</span> <span class="o">=</span> <span class="nx">urlBase</span> <span class="o">+</span> <span class="s1">'/en'</span> <span class="o">+</span> <span class="nx">$location</span><span class="p">.</span><span class="nx">url</span><span class="p">();</span> <span class="nx">$scope</span><span class="p">.</span><span class="nx">daUrl</span> <span class="o">=</span> <span class="nx">urlBase</span> <span class="o">+</span> <span class="nx">$location</span><span class="p">.</span><span class="nx">url</span><span class="p">();</span> <span class="p">});</span> </code></pre> </div> <p>The URLs need to be absolute, otherwise if this code was executed at example.com/en, it would calculate the ‘en’ URL to be at example.com/en/en and so on.</p> <p>Next we need to display our cross-links in the template using <code class="highlighter-rouge">ng-href</code> directive:</p> <div class="language-html highlighter-rouge"><pre class="highlight"><code><span class="nt">&lt;a</span> <span class="na">ng-href=</span><span class="s">""</span> <span class="na">target=</span><span class="s">"_self"</span> <span class="na">ng-class=</span><span class="s">"{'active': activeLang === 'en-us'}"</span><span class="nt">&gt;</span> <span class="nt">&lt;img</span> <span class="na">src=</span><span class="s">"assets/images/gb.svg"</span> <span class="na">alt=</span><span class="s">"Change language to English"</span><span class="nt">/&gt;</span> <span class="nt">&lt;/a&gt;</span> <span class="nt">&lt;a</span> <span class="na">ng-href=</span><span class="s">""</span> <span class="na">target=</span><span class="s">"_self"</span> <span class="na">ng-class=</span><span class="s">"{'active': activeLang === 'da-dk'}"</span><span class="nt">&gt;</span> <span class="nt">&lt;img</span> <span class="na">src=</span><span class="s">"assets/images/dk.svg"</span> <span class="na">alt=</span><span class="s">"Change language to Danish"</span><span class="nt">/&gt;</span> <span class="nt">&lt;/a&gt;</span> </code></pre> </div> <p><code class="highlighter-rouge">target='_self'</code> is used to tell Angular not to load this link in ajax but to navigate to it the Angular way. We need the browser to fetch appropriate index.html page and bootstrap the application (since we have one application per language).</p> <p>The application is now SEO friendly, but we are not quite done yet. We are also using HTML5 mode to have pretty URLs. When using HTML5 mode, we must define base tag in the index file so Angular knows where the application path starts from:</p> <div class="language-html highlighter-rouge"><pre class="highlight"><code><span class="cp">&lt;!DOCTYPE html&gt;</span> <span class="nt">&lt;html</span> <span class="na">lang=</span><span class="s">"da"</span> <span class="na">ng-app=</span><span class="s">"example"</span> <span class="na">ng-controller=</span><span class="s">"AppCtrl"</span><span class="nt">&gt;</span> <span class="nt">&lt;head&gt;</span> <span class="nt">&lt;base</span> <span class="na">href=</span><span class="s">"/"</span><span class="nt">&gt;</span> <span class="nt">&lt;link</span> <span class="na">rel=</span><span class="s">"alternate"</span> <span class="na">hreflang=</span><span class="s">"en"</span> <span class="na">href=</span><span class="s">"http://example.com/en"</span><span class="nt">/&gt;</span> <span class="nt">&lt;link</span> <span class="na">rel=</span><span class="s">"alternate"</span> <span class="na">hreflang=</span><span class="s">"da"</span> <span class="na">href=</span><span class="s">"http://example.com"</span><span class="nt">/&gt;</span> ... </code></pre> </div> <p>In case <code class="highlighter-rouge">hreflang</code> caught your eye, you can read more about it <a href="https://support.google.com/webmasters/answer/182192">here</a>.</p> <p>However, now we also have a copy of the site running at /en, so the base tag needs to be appropriate.</p> <h2 id="grunt-to-the-rescue-again">Grunt to the rescue, again!</h2> <p>We need to update the base tag when making a copy of the site to match the path. Grunt’s copy task features a process option that is called for each file copied. The process option enables us to transform the contents of the file:</p> <div class="language-javascript highlighter-rouge"><pre class="highlight"><code><span class="nx">copy</span><span class="err">:</span> <span class="p">{</span> <span class="nl">options</span><span class="p">:</span> <span class="p">{</span> <span class="nl">processContent</span><span class="p">:</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">contents</span><span class="p">,</span> <span class="nx">srcpath</span><span class="p">)</span> <span class="p">{</span> <span class="k">if</span><span class="p">(</span><span class="nx">srcpath</span><span class="p">.</span><span class="nx">indexOf</span><span class="p">(</span><span class="s1">'index.html'</span><span class="p">)</span> <span class="o">&gt;=</span> <span class="mi">0</span><span class="p">){</span> <span class="c1">//note spaces around in order not to match 'hreflang' tag</span> <span class="nx">contents</span> <span class="o">=</span> <span class="nx">contents</span><span class="p">.</span><span class="nx">replace</span><span class="p">(</span><span class="s1">' lang="da" '</span><span class="p">,</span> <span class="s1">' lang="en" '</span><span class="p">);</span> <span class="nx">contents</span> <span class="o">=</span> <span class="nx">contents</span><span class="p">.</span><span class="nx">replace</span><span class="p">(</span><span class="s1">'&lt;base href="/"&gt;'</span><span class="p">,</span> <span class="s1">'&lt;base href="/en/"&gt;'</span><span class="p">);</span> <span class="p">}</span> <span class="k">return</span> <span class="nx">contents</span><span class="p">;</span> <span class="p">}</span> <span class="p">}</span> <span class="c1">//build_en_lang omitted</span> <span class="p">}</span> </code></pre> </div> <p>We are using older version of Grunt, so the option is called <code class="highlighter-rouge">processContent</code> instead of <code class="highlighter-rouge">process</code>.</p> <p>HTML5 mode also requires URL rewriting on the server side. If a request is made at example.com/about, the server should still return the index.html page and it is up to Angular router to figure out what to do with the ‘about’ path variable. In Nginx, this is typically achieved with try_files directive:</p> <div class="language-bash highlighter-rouge"><pre class="highlight"><code>location / <span class="o">{</span> try_files <span class="nv">$uri</span> <span class="nv">$uri</span>/ <span class="nv">$uri</span>/index.html /index.html; <span class="o">}</span> </code></pre> </div> <p>If a request is made at example.com/en/about, the configuration above will result in Nginx trying following files:</p> <ul> <li>en/about –&gt; not found</li> <li>en/about/ –&gt; not found</li> <li>en/about/index.html –&gt; not found</li> <li>index.html –&gt; found</li> </ul> <p>This results in the server returning the index.html file of the root rather than of ‘en’ directory. Now the router is trying to figure out which state to match the <code class="highlighter-rouge">en/about</code> path to, which it will of course fail to do accomplish. We need to define another location to load proper index.html, if $uri is not a match:</p> <div class="language-bash highlighter-rouge"><pre class="highlight"><code>location /en/ <span class="o">{</span> try_files <span class="nv">$uri</span> <span class="nv">$uri</span>/ <span class="nv">$uri</span>/index.html /en/index.html; <span class="o">}</span> </code></pre> </div> <p>Finally, SEO friendly multilingual application!<br />Now that we’ve established that there is no way around configuring web server, let’s make most of it! Since the only file that actually changed is index.html, there is no real need to copy other resources. We start out by changing the grunt copy task to only copy index file to same directory and rename it to index.en.html:</p> <div class="language-javascript highlighter-rouge"><pre class="highlight"><code><span class="nx">copy</span><span class="p">{</span> <span class="nl">build_en_lang</span><span class="p">:</span> <span class="p">{</span> <span class="nl">options</span><span class="p">:</span> <span class="p">{</span> <span class="nl">processContent</span><span class="p">:</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">contents</span><span class="p">)</span> <span class="p">{</span> <span class="c1">//note spaces around in order not to match 'hreflang' tag</span> <span class="nx">contents</span> <span class="o">=</span> <span class="nx">contents</span><span class="p">.</span><span class="nx">replace</span><span class="p">(</span><span class="s1">' lang="da" '</span><span class="p">,</span> <span class="s1">' lang="en" '</span><span class="p">);</span> <span class="nx">contents</span> <span class="o">=</span> <span class="nx">contents</span><span class="p">.</span><span class="nx">replace</span><span class="p">(</span><span class="s1">'&lt;base href="/"&gt;'</span><span class="p">,</span> <span class="s1">'&lt;base href="/en/"&gt;'</span><span class="p">);</span> <span class="k">return</span> <span class="nx">contents</span><span class="p">;</span> <span class="p">},</span> <span class="p">},</span> <span class="nx">files</span><span class="err">:</span> <span class="p">[</span> <span class="p">{</span> <span class="na">src</span><span class="p">:</span> <span class="p">[</span> <span class="s1">'index.html'</span><span class="p">],</span> <span class="na">dest</span><span class="p">:</span> <span class="s1">'&lt;%= build_dir %&gt;/'</span><span class="p">,</span> <span class="na">cwd</span><span class="p">:</span> <span class="s1">'&lt;%= build_dir %&gt;'</span><span class="p">,</span> <span class="na">expand</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="na">rename</span><span class="p">:</span> <span class="kd">function</span><span class="p">(</span><span class="nx">dest</span><span class="p">,</span> <span class="nx">src</span><span class="p">){</span> <span class="k">return</span> <span class="nx">dest</span> <span class="o">+</span> <span class="nx">src</span><span class="p">.</span><span class="nx">replace</span><span class="p">(</span><span class="s1">'.html'</span><span class="p">,</span> <span class="s1">'.en.html'</span><span class="p">);</span> <span class="p">}</span> <span class="p">}</span> <span class="p">]</span> <span class="p">}</span> <span class="p">}</span> </code></pre> </div> <p>Now Nginx ‘en’ location needs to serve index.en.html instead of index.html, otherwise it should ignore /en/ part of the URL when serving static files. We will use regular expression to match /en and capture everything that comes after into $1. Then we use captured group in try_files directive:</p> <div class="language-bash highlighter-rouge"><pre class="highlight"><code>location ~ ^/en/?<span class="o">(</span>.<span class="k">*</span><span class="o">)</span><span class="nv">$ </span><span class="o">{</span> index index.en.html; try_files /<span class="nv">$1</span> /<span class="nv">$1</span>/ /index.en.html; <span class="o">}</span> </code></pre> </div> <p>We have jumped through several hoops in order to get rid of the comfort angular-translate provides. Fortunately we can bring some of it back - authenticated users are not crawlers, so we can display ‘quick buttons’ for them while displaying SEO friendly cross-links to unauthenticated users:</p> <div class="language-html highlighter-rouge"><pre class="highlight"><code><span class="nt">&lt;div</span> <span class="na">ng-if=</span><span class="s">"!auth.isAuthenticated"</span><span class="nt">&gt;</span> <span class="nt">&lt;a</span> <span class="na">ng-href=</span><span class="s">""</span> <span class="na">target=</span><span class="s">"_self"</span> <span class="na">ng-class=</span><span class="s">"{'active': activeLang === 'en-us'}"</span><span class="nt">&gt;</span> <span class="nt">&lt;img</span> <span class="na">src=</span><span class="s">"assets/images/gb.svg"</span> <span class="na">alt=</span><span class="s">"Change language to English"</span><span class="nt">/&gt;</span> <span class="nt">&lt;/a&gt;</span> ... <span class="nt">&lt;/div&gt;</span> <span class="nt">&lt;div</span> <span class="na">ng-if=</span><span class="s">"auth.isAuthenticated"</span><span class="nt">&gt;</span> <span class="nt">&lt;a</span> <span class="na">ng-href=</span><span class="s">"#"</span> <span class="na">ng-click=</span><span class="s">"changeLang('en-us')"</span> <span class="na">ng-class=</span><span class="s">"{'active': activeLang === 'en-us'}"</span><span class="nt">&gt;</span> <span class="nt">&lt;img</span> <span class="na">src=</span><span class="s">"assets/images/gb.svg"</span> <span class="na">alt=</span><span class="s">"Change language to English"</span><span class="nt">/&gt;</span> <span class="nt">&lt;/a&gt;</span> ... <span class="nt">&lt;/div&gt;</span> </code></pre> </div> <h2 id="conclusion">Conclusion</h2> <p>With this approach we have tried to solve as much as possible with configuration, so we’ve managed not to entangle language awareness with every state in the application. While ui-router configuration would be preferred, it does not come without limitations: ui-router can not be used for sub-domains and ui-router enforces using language in every url rather than having it as optional parameter.</p> <p>At the end of day there is no good way to to play by SEO rules, so hopefully AngularJS 2.0 will change this.</p>