Magento Therapy: Multi-Stores and Internationalization

Posted on: April 16th, 2010 by Rob Zienert 1 Comment

This is part of the Magento Therapy blog series.

Before I get started, Crucial Web has an extremely helpful tutorial on how to setup multiple Magento stores. They provide many different avenues to get the job done and they’re all pretty relevant, but we opt for a hybrid method which we’ll go over shortly.

As noted in the Crucial Web article, setting up multi-stores in Magento through the admin is only half of the solution. The rest has you getting a little dirty in the code with switch statements and whatever else. Not a big deal, I prefer that it’s a partially implemented solution because you can fit it to your needs instead of being shackled down.

Multi-Stores

Through popular demand from our larger clients, we’ve globally adopted a url structure as follows: "http://COUNTRY.domain.com/LANGUAGE/". We chose this method to accomodate countries with potentially more than one language, for example a Canadian site would probably have English and French versions. This is the format that quite a few large E-Commerce companies use, so we followed suit.

For this post, we’ll be using Canada as our new multi-store. Canada is a great choice because I love hockey and it allows us to address a hybrid subdomain- / subdirectory-based multi-store.

Setting Up the Stores in Magento

I’ll breeze over this portion really fast, as it’s very well explained at Crucial Web. You’ll want to create the following from the System -> Manage Stores page:

  • A Website named "Canada Website" (code: canada)
  • A Store named "Canada"
  • Two Store View Names: "Canada EN", "Canada FR". (codes canada_en and canada_fr, respectively)

Modifying Magneto Store paths

Now that the stores are setup, you’ll need to get your paths in order. This is done inside of the Web tab in Configuration.

The Base URLs will be "http://ca.magento.local". You can make this global to all Canada Stores by modifying the setting at the Website level. You’ll then want to go into each Store configuration individually and set the Base Link URL to:

  • "{{unsecure_base_url}}en/" and "{{secure_base_url}}en/"
  • "{{unsecure_base_url}}fr/" and "{{secure_base_url}}fr/"

We’ll get to the design tab later.

Modifying the Magento index.php file

I’m not a particular fan of how Crucial Web tells you to copy/paste .htaccess files and index.php files around for the subdirectories to work. Instead, I prefer to modify the original index.php file so that I can just require it in the subdirectories:

First, lets just setup the subdirectories. We’ll need a "fr" and "en" directory on the root. Inside, we’ll have a very simple index.php file:

// en/index.php, fr/index.php
require_once dirname(__FILE__) . '/../index.php';

That’s it. Unfortunately, Magento doesn’t play nicely with a setup like this, so we need to edit their index.php file. Where ever a file is included into the index.php file, we need to add dirname(__FILE__) before it. Like so…

// index.php
$compilerConfig = dirname(__FILE__) . '/includes/config.php';
$mageFilename = dirname(__FILE__) . '/app/Mage.php';

I’ve also created a function to help handle the language subdirectories. I just throw this in right above the Mage::run() code at the bottom of the file.

function mageLangSubdir(array $storeCodes, $default) {
    $code = $default;

    preg_match('#^/([a-zA-Z]{2})/?#', $_SERVER['REQUEST_URI'], $matches);
    if (count($matches) > 0) {
        $matches[1] = strtolower($matches[1]);
        if (array_key_exists($matches[1], $storeCodes)) {
            $code = $storeCodes[$matches[1]];
        }
    }

    return $code;
}

It’s not glamourous by any means, but it does get the job done. It pulls out the "/fr/" or "/en/" portion the request URI and will match it against acceptable codes (provided in a moment). If no matches are found, it goes to the default.

Now the bootstrapping. As you add more sites, this switch statement grows.

// Enable multi-site support
switch ($_SERVER['HTTP_HOST']) {
    case 'ca.magento.local':
        $codes = array(
            'en' => 'canada_en',
            'fr' => 'canada_fr',
        );
        $store = mageLangSubdir($codes, 'canada_en');
        Mage::run($store, 'store');
        break;

    case 'magento.local':
    default:
        Mage::run();
}

As you can see, we’re using the switch statement to determine the website, but then parse the request URI with the function above to route us to the correct store.

If you’ve setup everything properly, you should now be able to navigate to your websites. Depending on your setup, you may get 404 pages regarding your homepage. At this point, it’s just a matter of creating site content.

International Themes

In all likelihood, your client will want the same design across all your Magento Stores. I’m all for that. It’s cheaper for the client, a lot easier to manage and brings cohesion to the client brand across all sites. Fortunately, in all of Varien’s wisdom, Magento knows you want to do this and gives you the tools to easily make it happen.

We’ve adopted using a parent theme called, "intl" for our international sites (or at least for the left-to-right languages). No store directly uses this theme, but instead, all of our international themes fallback (default) to this theme. So for example, our English Canada site will have a Themes tab that looks like this:

We’ve adopted the naming convention of "clientname_country_LOCALE" so it’s easy for us to navigate around the themes directory. You can name your themes whatever you please, but we tend to stick to conventions where possible just to make things more predictable.

Your goal in using the intl theme is to make it as reusable as possible so that any files inside of "magento_en_CA" are few in number. We sometimes have a completely empty folder except for perhaps a different newsletter signup callout that submits to a different destination. Everything else falls back to the intl theme.

Unfortunately, life isn’t so wonderful going to the skins folder for the theme. Right now, we have to copy/paste the entire directory for each theme. It’s terrible. It’s depressing. It’s a massive waste of an initial 30 seconds and an unquantifiable increase in maintenance time. It’s something we have been meaning to build onto Magento, but haven’t had the time. So changes to say, the CSS file or custom Javascript will have to be duplicated. Too bad. Your only saving grace is possibly using symlinks, but when files start getting theme-specific changes you’re out of luck.

Translations

The last thing is that we keep our translations inside of the intl theme’s translate.csv files. Of course, we use the translation files in app/locale where applicable, but any custom translations we keep in the theme folder. Just general preference, but it came at the small cost of overriding Mage_Core_Model_Translate::_getTranslatedString().

We found that the files inside of app/locale were getting precedence over the theme translate.csv files. This probably makes a whole lot of sense and works great for some people, but our workflow doesn’t accomodate too well structured like this.

We’ll be going over overriding core Magento classes in a different blog post, but it was simply a matter of checking for the $text instead of $code first:

class Prpl_Core_Model_Translate extends Mage_Core_Model_Translate
{
    /**
     * Reverses the priority of translations to check translate.csv first, and
     * then the locale packages second.
     *
     * @param text $text
     * @param text $code
     * @return text
     */
    protected function _getTranslatedString($text, $code)
    {
        $translated = '';

        if (array_key_exists($text, $this->getData())) {
            $translated = $this->_data[$text];
        } elseif (array_key_exists($code, $this->getData())) {
            $translated = $this->_data[$code];
        } else {
            $translated = $text;
        }
        return $translated;
    }
}

I’m not a personal fan of using their inline translation editor. It’s a fantastic tool don’t get me wrong, but I don’t really like that it saves to the database… nor that it takes longer to get things done. It’s easy to parse a content inventory spreadsheet to generate translate CSV’s, it’s another thing to surf a website changing strings manually only to not have it in version control.

Wrap Up

That’s about it. If you wanted to do something more fancy with multi-stores, it certainly wouldn’t be difficult to add in GeoIP functionality for automatically selecting stores based on a visitor’s location and so-on.

If you’d like to read more about setting up multiple stores in Magento, I’d suggest checking out the multi-store knowledge base at MagentoCommerce.com.