Multilingual ExpressionEngine® (ver 2), Part 2

This is the second installment of articles that focuses on using ExpressionEngine as a multilingual content management system. In the previous article we began with an overview of what types of content need to be translated, the different methods of translations, and took our first step by setting up channel content for multilingual data entry. In this article we will cover static translations, categories, navigation, URLs, and third-party translation add-on systems.

It is important to understand the methods discussed in the previous article as we will now build upon them to begin setting up translations for all of the other translatable data in EE. Aside from channel content we will also need a way to include multilingual content for the following:

  1. Locale-Based Formatting
  2. Static Content
  3. URL Structures
  4. Navigation

We will be taking a “do-it-yourself” approach to setting up these bits of data for translation where possible, rather than module-based frameworks like Transcribe or Publisher. At the end of this article we will point out the merits of these add-ons and how quick they are to implement, however, I feel it is always good to know different methods for doing the same thing in the case you do not want to, or cannot, adopt one of these frameworks. So let’s get on with finishing up the setup of our ExpressionEngine multilingual site.

Locale-Based Formatting

One of the more overlooked areas when creating a multilingual site is locale specific formatting. A good example of this are dates and numbers. I’m sure you can understand how numeric date formats can be confusing considering the American “mm/dd/yyyy” format, in contrast to the more globally used “dd/mm/yyyy” format, especially with a date like “06/05/2014”. Is it “June 5th 2014” or the “6th of May 2014”? Another example are numeric formats. The imperial system uses the comma (,) as a convenience separator (to make numbers easier for us humans to read), and the period (.) as a decimal separator. You guessed it, the rest of the world uses the exact opposite (i.e.; 2.540,95 = 2,540.95). So, now that I’ve drilled into your brain how important this stuff is, let me show you one way of handling it in ExpressionEngine.

From our previous article, the first part of our setup included creating multiple index.php files and keeping them within their own directory at our web root (i.e.; /es/index.php for Spanish, /de/index.php for German, etc.). Within the index.php files we were able to define language keys as global variables which we used as a sort of “switch-board” to know what language the user was viewing the site in and get the appropriate field or channel. Since these global variables are rendered early in the ExpressionEngine parsing process, we can also take advantage of them to solve our formatting problem. Let’s add a few more global variables to each of these files. So for example in our Spanish version we will now have:

$assign_to_config['global_vars']['lang_code'] = 'es';
$assign_to_config['global_vars']['lang_name'] = 'spanish';
$assign_to_config['global_vars']['lang_alias'] = 'Español';
$assign_to_config['global_vars']['lang_date'] = '%d/%m/%Y';
$assign_to_config['global_vars']['lang_time'] = '%H:%i';
$assign_to_config['global_vars']['lang_num_decimal'] = '.';
$assign_to_config['global_vars']['lang_num_separator’] = ',';

So as you can see, aside from our language code, name, and alias variables, we now have date, time, and number formatting strings which we can use in our templates as well. In the case of dates and times we could specify the format in our templates as:

{current_time format="{lang_date}"}
{current_time format="{lang_time}"}
{current_time format="{lang_date} {lang_time}"}

Since our global variables are rendered early in the EE parsing process, we are just using the strings we defined for the format parameter. If you need to output dates in word format (i.e.; “Domingo, 27 Agosto”) we could use the free “Date/Time Language Converter” plugin in conjunction with our language code variable like so:

{exp:datetime_convert language="{lang_code}" format="$A %e %B %Y"}
{current_time}
{/exp:datetime_convert}

In the case of number formatting we could use our “decimal” and “separator” variables in conjunction with a number formatting plugin like “Number Formatter”:

{exp:number_formatter:format dec_point='{lang_num_decimal}' thousands_sep='{lang_num_separator}'}
{value}
{/exp:number_formatter:format}

There are all kinds of locale specific formatting problems that can be addressed with this method. Currency symbols, international phone prefixes, the list goes on. Taking things further, you could even include RegEx strings to handle things like phone number formats:

$assign_to_config['global_vars']['lang_phone']  = '^[^0-9]([0-9][^0-9]){8,10}$';

Just keep in mind your locale specific global variables should be related to formatting only. You could go crazy and start putting in bits of text here too, but that becomes unmanageable very quickly. For all those small pieces of static texts like “Submit”, “Enviar”, etc., we’ll need to translate them elsewhere …

Static Content

In any website, multilingual or not, there are tons of micro-copy to deal with. These are the small bits of text that live in our “submit” buttons, error messages, table headers, any text that can be regarded as “static”, and in all probability, it will not change that often.

If you come from a backend development background like PHP, you would know that small pieces of text like this should be managed via a .PO (Portable Object) file, and using a native method like PHP’s “gettext” to retrieve them in the correct locale would be the way to go. However, since we don’t want to have to build our own interface for our clients so they can interact with .PO files, let’s make it easy and let EE and a few add-ons handle the multilingual micro copy for us.

At this point we are going to deviate from the “do-it-yourself” method and rely on an add-on. The following is a list of add-ons I have used in the past that will allow you to manage multilingual micro copy from within the ExpressionEngine CP. Some are created specifically for the task, while others (Low Variables) are more general tools that can be adapted to the task.

Biber Multi-language Support

This add-on is specifically created to manage multilingual micro-copy. This add-on will give us an extension page within the CP with a table that we can use to insert, edit and delete our translated versions of micro-copy.

Outputting a piece of micro-copy in our templates is as simple as calling it by the “key” value we give it with a “bbr-mls-“ prefix in a tag, which will act as a global variable :

{bbr-mls-telephone}

The setup process will also create our multiple language based index.php files and folders for us (basically doing the same thing we did manually in the first article of this series) and will also give us the option to set the default site language either by the users browser, from a cookie, or a default language we select.

Republic Variables

Similar to the previous add-on, Republic Variables will also allow us to manage our multilingual micro-copy from a user friendly table making inserting, editing and deleting a breeze. Variables can also be placed into groups to make it easier for clients to find the piece of text they need to change quickly.

Outputting the content in our template in the correct language requires a little more work as we just need to use our global “lang_code” to define the correct language version :

{{lang_code}-telephone}

Contrary to the Biber extension, Republic Variables does not setup our multiple index.php files and folders in the root, so if we already have that setup manually, this may be a better option, plus it has that magic price tag … free!

Low Variables

This is an add-on that you probably already have installed in your EE build. It’s so useful that if you don’t have it, you are probably going to purchase it anyways to manage other pieces of content in your site that are not channel based. So why not use it to manage your multilingual micro-copy as well? To use Low Variables to manage multilingual content you must rely on a naming convention for your variables. In fact, Low has included a “Variable Suffix” field just for this purpose.

By including the language codes that our site supports, Low Variables will automatically create duplicate variable fields for each language with the appropriate language code suffix. Calling these variables from within our templates is just a matter of using our global language code again to get the correct language version of what we need :

{telephone_{lang_code}}

This add-on has a bigger price tag than the previous ones, however it is so versatile that you are probably going to want it anyways for other things, and believe me it’s well worth the price.

Transcribe & Publisher

These two add-ons are the main module-based multilingual frameworks that exist for ExpressionEngine at the moment and as such, also include areas for managing multilingual micro-content among other things. At the end of this article we will discuss them in a bit more detail but I thought I would include them here as well.

URL Structures

Aside from the content on the page that needs to get translated, we also have to think about how our URL structures will appear in each language. This can have an important impact on search engine optimisation so it’s not something we should take for granted.

Here the advantage of using a module-based framework like Transcribe or Publisher is that it’s already baked-in, and we don’t really need to worry too much about it. Both of these packages provide template aliases for each language which makes it easy for us to setup URL’s like “example.com/en/products/t-shirt” or “example.com/es/productos/camiseta”, that point to the same template group. However if we want to “roll-our-own” multilingual template group solution there are a couple of options we can look at.

Embedding Template Groups

If we have a small number of languages to support on our site (say 2 or 3) it might be feasible to just create separate template groups for each language whose templates simply embed our main templates that do all the work. So we could have the following template groups setup for a product channel that supports English and Spanish versions :

/products/index
/products/view
/productos/index
/productos/ver

In the Spanish versions of our templates we could then just call the templates that are doing all the work. In our “/productos/ver” template we would just include the following embed :

{embed="products/view"}

There are some disadvantages to this method however. Every time an embed is called in EE the template parsing engine is initialised again which can be a performance hit. Another problem is that this method can become unmanageable if you are using a lot of template groups to organise your templates and/or you need to support more than a few languages. I tend to see this method useful only if a small part of your project needs multilingual support, or you need to “tack-on” multilingual support to an already existing small ExpressionEngine site.

Alternate Template Routes

If you are creating a multilingual site from start to finish rather than inheriting one, a better method would be to just create instructions for EE to point to the same template using different translations of the template group and template. This is where the Resource Router extension (formerly “Template Routes”) can help us out. Installing the extension will allow us to define routes to existing templates within our site configuration file using familiar Codeigniter style routing rules including variable placeholders like pagination, url titles, and category triggers. So for example we could easily setup routes to a “products” template group to point to their Spanish counterparts by placing the following in our sites config file:

$config['resource_router'] = array(
   'productos' => 'products/index’
   'productos/:pagination' => 'products/index',
   'productos/:category_url_title/:pagination' => 'products/category_index',
   'productos/:url_title' => 'products/view',
);

Instead of having to create and maintain separate template groups for each language we are now just telling ExpressionEngine if a GET request contains “productos” and a URL title, just use the “products/view” template instead. You could also accomplish the same thing using routes in your sites .htaccess file on a server (Apache) level however, I prefer keeping this sort of thing encapsulated in a sites configuration instead of relegating it to an external .htaccess file that could be out of sync sometime between development, staging, and production servers.

Navigation

Creating easily client-editable navigation for ExpressionEngine can be a bit of a challenge sometimes. So taking it to the next level by adding the dimension of multiple languages, can be even more overwhelming to a client than it needs to be. In my experience I tend to use the following methods depending on if a particular site uses Structure or not.

With Structure

If you have gone down the road of using Structure for your site build, it’s best to work with that framework and use the recommended method from their own documentation. Basically you will need to keep entries separate, removing the possibility of using the “1-to-1” approach of channel content translation, but that may be okay for your project. We just need to base where we start our Structure navigation from in the tree based on our URL segments :

{if segment_1  'es' OR segment_1  'fr'}
   {!-- START FROM SEGMENT 2 IF NOT DEFAULT LANGUAGE --}
   {exp:structure:nav start_from='/{segment_1}/{segment_2}'}
{if:else}
   {!-- START FROM SEGMENT 1 IF DEFAULT LANGUAGE --}
   {exp:structure:nav start_from='/{segment_1}'}
{/if}

As stated above, if you are already planning to train your client to use Structure to input and manage a site hierarchy, I suggest you stick with this method as visually in the CP it will make sense to them, with each language being a branch of the navigation tree.

Without Structure

If the site does not warrant it, or if you are not a fan of the Structure paradigm, I would suggest using an add-on that is dedicated to managing navigation in EE. There are a few out there, however my experience with NavEE has been the best so far. You get the same “visual” representation of a sites hierarchy however you do not need to rely on breaking a single tree down into multiple sections. You can basically create as many navigation trees as you want and output them in your templates however you want. This is perfect for multilingual sites.

Since NavEE supports multiple navigation trees, the basic concept we will use is to create a navigation tree for each language, and then call the correct navigation using our global language code variable :

{exp:navee:custom nav_title='main_nav_{lang_code}' wrap_type='ul'}
   <li>
       <a href="{link}">{text}</a>
       {kids}
   </li>
{/exp:navee:custom}

The flexibility this method offers is it will let us mix both “1-to-1” and “1-to-None” channel content translation paradigms in a single language menu. The downside is it will require your client to understand that navigation can differ from content.

Module-Based Frameworks

No multilingual ExpressionEngine article can be published without mentioning the two great module-based frameworks that exist for ExpressionEngine: Transcribe and Publisher. So far these articles have taken the “do-it-yourself” approach to making ExpressionEngine handle multilingual content. But even if you are planning to take a similar approach, it is always good to know these add-ons exist and what they can do.

One of the things that comes to mind are ExpressionEngine categories. Translating categories can be a bit of a problem. You could use custom category fields for each language to translate the names correctly, however the category URL title will be the same. You could create separate category groups for each language, but that can become cumbersome. Turns out, Transcribe and Publisher both have easier ways of handling this.

A sticking point in our translated URL structures is the entries URL title will be the same if we took the “1-to-1” translation method in a specific channel. Both of these add-ons get around this problem by removing this method completely and simply inserting multiple entries in the same channel for each language yet relating them to each other.

And finally, another big advantage is that almost all of the content that needs to be translated that we have discussed is included in one big package by these systems. The trade-off of course is that we need to follow their logic in our templates and build, and maybe the price tag if your project’s budget is already tight.

Stay tuned for the season finale

I hope I have given you some ideas on content translation in ExpressionEngine with this series. In our next and final instalment we will discuss preparing the ExpressionEngine Control Panel for multiple languages and making content translators jobs easier. Until then, Tot ziens!