I18n subdomains and url-rules

This guide is for more or less advanced users. If you just started with Yii or didn't worked with any web-framework before, you may should come back later :-)

Some thoughts

In Yii, we normally define (static) url rules in the main config file. Currently there's no core support for subdomains or dynamic url rules based on informations like cookies.

When you develop a multilanguage website, you will probably define the url rules in english language, regardless of the client language. That works well, but for professional portal style sites, you may want to use another approach regarding better user experience and SEO reasons.

Let's think of a website in english and german language. The rule for the contact formular would probably look something like this:

<?php
...

'components' => array(
  'urlManager' => array(
    'rules' => array(
      'contact' => 'site/contact',
    ),
  ),
),

...
?>

That means we could access the contact formular via:

http://www.example.com/contact

Now think about this:

http://en.example.com/contact
http://de.example.com/kontakt

As you can see, we use the proper language code as subdomain (en for english, de for german) and the word "contact" as url rule. Like already noted, this approach is unfortunately not supported by the Yii core.

Before I'll show you a solution that will solve this scenario, please keep the following disadvantages and benefits in mind. Evaluate conscientious for yourself if it's woth for your project.

Disadvantages
  • If you use different subdomains for one site, the pagerank of your main domain (eg example.com) will probably suffer. Also you may run into a duplicate content problem when for example the page titles are always the same regardless of the active language subdomain.

  • Google itself states that it's better to use top-level domains instead of subdomains. So if you indeed own "example.com" and "example.de", you shouldn't use the following solution. But you're of course free to modify the solution for the use of top-level domains instead of subdomains.

  • For small multilanguage sites, I would suggest to just use english url rules with one main domain. The real benefits probably won't show up for a website with less than 10.000 unqiue visitors a day.

Benefits
  • You can split your website into a real multilanguage portal like for example YouTube and measure the performance with one Google Analytics account per subdomain.

  • Google will display sitelinks for each language instead of probably mixed up stuff. In general, the whole SEO possibilities will be better.

  • The user experience will be better.

The Solution

First, we create a new application component that will include all the special properties and functions we need.

<?php

class I18n extends CApplicationComponent
{

    public $supportedLanguages;
    public $urlRulesPath;
    public $activeLanguage;

    public function init()
    {

		if (true === (bool)preg_match("/^(?<protocol>(http|https):\/\/)(((?<languageCode>[a-z]{2})\.)*)((.*\.)*(?<domain>.+\.[a-z]+))$/", Yii::app()->request->hostInfo, $matches))
		{
			
			if (2 !== strlen($matches['languageCode']) && false !== ($language = $this->isSupportedLanguage($matches['languageCode'])))
			{
				$this->activeLanguage = $language;	
			}
			else
			{
				
				$this->activeLanguage = $this->guessClientLanguage();
				
				$redirectUrl = "{$matches['protocol']}{$this->activeLanguage['code']}.{$matches['domain']}";
				
			}
			
		}
		else
		{
			throw new CException("Unable to parse host info - please request this page with a domain (eg http://example.com/) instead of an ip-address!");
		}
			
		Yii::app()->setLanguage($this->activeLanguage['code']);
		
		Yii::app()->urlManager->rules = include("{$this->urlRulesPath}/{$this->activeLanguage['code']}.php");
						
		if (false !== Yii::app()->urlManager->cacheID && null !== ($cache = Yii::app()->getComponent(Yii::app()->urlManager->cacheID)))
		{
			$keyPrefix = $cache->keyPrefix;
			$cache->keyPrefix = "{$keyPrefix}.{$this->activeLanguage['code']}";
			Yii::app()->urlManager->init();
			$cache->keyPrefix = $keyPrefix;					
		}
		else
		{
			Yii::app()->urlManager->init();
		}
		
		if (true === isset($redirectUrl))
		{
			Yii::app()->request->redirect($redirectUrl);
		}

	}
	
	public function guessClientLanguage()
	{

		if (false === isset($_SERVER['HTTP_ACCEPT_LANGUAGE']) || false === (bool)preg_match("/([a-z]{2}+)[-_][A-z]{2}+/", $_SERVER['HTTP_ACCEPT_LANGUAGE'], $match) || false === ($language = $this->isSupportedLanguage($match[1])))
		{
			if (false === ($language = $this->getFallbackLanguage()))
			{
				throw new CException("Unable to return the fallback language - please set it in config!");
			}
		}
		
		return $language;
		
	}
	
	public function getFallbackLanguage()
	{

		foreach ($this->supportedLanguages as $supportedLanguage)
		{
			if (true === isset($supportedLanguage['fallback']) && true === $supportedLanguage['fallback'])
			{
				return $supportedLanguage;
			}
		}

		return false;

	}

	public function isSupportedLanguage($languageCode)
	{

		foreach ($this->supportedLanguages as $supportedLanguage)
		{
			if ($supportedLanguage['code'] === $languageCode)
			{
				return $supportedLanguage;
			}
		}

		return false;

	}

}

?>

In the main config, we will add this custom application component to the preload array.

<?php
...

preload => array('log', 'i18n'),

...
?>

Also we add it to the components array with some example properties.

<?php
...

'components' => array(
  'i18n' => array(
    'class' => 'I18n',
    'urlRulesPath' => dirname(__FILE__) . '/i18n/urlRules',
    'supportedLanguages' => array(
      array('code' => 'en', 'name' => 'english', 'fallback' => true),
      array('code' => 'de', 'name' => 'german'),
    ),
  ),
),

...
?>

The url rule files in "protected/i18n/urlRules" have to be like this:

<?php

/* ../protected/i18n/urlRules/de.php */

return array(
  'kontakt' => 'site/contact',
),

?>

On a request, this chain will apply:

  • The web application will preload and init() our I18n component.

  • Now our I18n component comes in play and parses the host info.

  • If we're not already on a valid subdomain, the component tries to guess the client language and checks if that language is supported. If that's not the case, the fallback language is used as active language. After that a redirect-url gets generated.

  • The application language will be set to the just figured out active language.

  • The custom url rules will be included and passed to the urlManager component. The urlManager will be re-initialized to apply the custom rules.

  • If a redirect-url was previously set, a redirect to that url will be executed.

Summary

This is a simple solution. For example I use a more advanced I18n component that can do translations, guesses the client language by country-code and so on. Also I can set different work-modes (top-level domain, subdomain, path).

It's in the end up to you what you need, but I hope this example solution could give you a clue of how this can be done.

Feel free to share your thoughts and remarks - thank you.

Links

Russian version

7 0
17 followers
Viewed: 45 082 times
Version: 1.1
Category: Tutorials
Tags: i18n, URL
Written by: Y!!
Last updated by: atrandafir
Created on: Nov 7, 2009
Last updated: 12 years ago
Update Article

Revisions

View all history

Related Articles