Home > Coding, Blog > Multilingual Zend Framework and WordPress

Multilingual Zend Framework and WordPress

I could not make myself start moving towards to multi-language imlementation of my website … This problem remained unsolved for two years. It is caused mostly by the fact that the website engine, based on Zend Framework. Many times I tried to touch the problem of  Zend Framework localization, but nothing . On the official resource (zendframework.ru) there is an entire forum thread, but I could not find a perfect solution. As a result, I had to compose it from various sources. And here is the solution I’ve finally got.
First I’d like to say a couple of workd about multilingual WordPress

It’s all quite simple here: there are some decent plugins, one of which I hurried up to take advantage of. It calls qTranslate. Easy to install, easy to configure. After installation go to the Settings menu -> Languages, and if there is no Russian language, add it.The most important thing for me was the parameter settings plug URL Modification Mode This option defines how URL dispatching will be done to determine the language. There are three options
/en/article_human_uri/ or
?lang=en or
en.mydomain.com

I’ve chosen first option, since similar concept was proposed for Zend-segment.The second important parameter – Default Language . This is the language that is selected, if the URL is not specifying language. Also there is an option for auto-detection the language using data provided by user’s browser. I also implemented this mechanism in the Zend-part, but will describe it a bit lower. The plugin also allows to connect, translation services (like google translate), with help of which you can receive automatic translation. Although I have not tried this stuff yet – it’s a dubious idea, machine translation as for me. The most interesting thing begins when it deal with Zend Framework. This is very peculiar puzzle. On the one hand I wanted to make an elegant solution, on the other side I didn’t want to do something complicated like piling a bunch of new tables and fields.The main problem was how to store multilingual content in existing tables. For example, the table for the portfolio (/product/). Initially it was not designed for multilingual. The table structure is simple. Conventionally, it looks like this:

item_id,
title,
description

What can be done so it supports the multiple languages? First option is to add few fields:

title_ru,
description_ru,
title_en,
description_en

But what if we want to add more languages? The second option is to save the content in separate normalized table. But it’s far from simplicity, I didn’t want to deal with it. That’s why I’ve made third descicion. All keep the same but the content will be stored wrapped to special tags. For instance:

<!--lang:ru-->Это русский контент<--:--><!--lang:en-->This is english content<!--:-->

Then when displaying content we extract the current language with simple regular expression.The decision, by the way, I spied in qTranslate. There was still a significant chunk – the news. There have already been a previous decision not to manage. I decided to connect english news feeds. After much searching and comparison I chose to tape REUTERS. By the way, I incidentally solved the problem of pictures to the news – Yandex from which I take the Russian news have very poor situation with pictures for news and I came up with a good idea – take pictures from google-images. Making google request by specifying the news title and getting a list of relevant images. It works in 99% of cases. So, the news are sorted out now. What’s next? Static content remains: -tatic text in the view file (view in the concept of Model-View-Controller). In Zend Framework the role of view is played by phtml scripts in views/scripts folder. In order to work with multilingual I designed singleton object and put it to library – one of Zend Framework architecture elements. This Singleton (I named it Lang) performs a number of functions, providing processing of multilingual data. When you create an instance of it is determined by the current language in the internal property singleton. Determining language is performed by followingalgorithm:
1. The URL request is analyzed for seeking language-component (eg, http://heximal.ru/en/product). I’ll tell about multiligual URL dispatching in Zend Framework a bit later.
2. If the language is not defined from the URL, a scan cookie, which is set to the user when the first successful language definition.
3. If the language is not determined by the cookie, it tries to determine the language of the http-request headers. Accept-Language header is analyzed. If the header contains ‘ru’ value then Russian is set. Otherwise English.
4. If for some reason the Accept-Language header is missing default language defined global settings is set. So when we instantiate singloton specific language is stored in internal variable. Singleton and the property of the current language are available at any place in the code. Thus, we can at least make the script view as the following:

<?php
  if ($this->lang->_lang=='ru') {
?>
  Русский статичный контент
<?php  
  } else {
?>
  English static content
<?php  
  }
?>

When the localized content is not very large, for example, one line, it makes sense to use the classical technique Multilingual – localization. The point is this: for each language we create the file of specific format, which stores key-value pairs, such as

"COMMON_ERR_UNKNOWNERR": "Unknown error",
"COMMON_ERR_WRONGCAPTCHA": "Invalid confirmation code",
"COMMON_ERR_BADFIELDS": "Form fields contain errors",

"COMMON_ERR_UNKNOWNERR": "Неопределенная ошибка",
"COMMON_ERR_WRONGCAPTCHA": "Неверный код подтверждения",
"COMMON_ERR_BADFIELDS": "Поля формы содержат ошибки",

Next trick is the following. Implement the function e.g. ls (short for language select – it’s too long to write language_select each time) in language singleton clas. And further to the right place submission call, not thinking about what is installed language:

echo($this->lang->ls("COMMON_ERR_BADFIELDS"));

Another major problem – the links that appear on the site. In the original form, they looked like this:
/product/takemehome/
If nothing is changed, then when you click on a link a chain of determining the language of the second step is starting. It seems there is nothing bad and you can live with: remember the language in user cookie, and use it. Switching the language would have produced a simple rebinding cookie value.
I must say, I hesitated a long time between the URL decision, and much simpler solution with cookie. The most important argument in favor of choosing URL-way was the following. If English visitor came to my site, and decided to give his friend a link to my site, that would have been received when I opened the link, does not contain the language-component? Not guaranteed.
So I decided to do things right.
So, the pURL roblem is solved with lang-Singleton. Write a function, call it href

     public function href($href) {
       $hr = explode("/",$href);
       if ($hr[0]=='http:')
         $f=3;
       else
         $f = $href[0]=='/'?1:0;
       $ins = array($this->_lang);
       if (isset($hr[$f])&&$hr[$f]=='blog')
         array_splice($hr, $f+1, 0, $ins);
       else
         array_splice($hr, $f, 0, $ins);
       return implode('/',$hr);
     }

I think the idea is clear. The function takes /seoscan/ url as input and produces either /ru/seoscan/ or /en/seoscan/ depending on the current language.
In the required place, we just do

 echo ($ this-> lang-> href ('/ seoscan /'));

Lot of changes? In fact, not so much. Eyes are afraid, and the hands are doing. If the site was originally designed as multilingual, generally would not have had much to change.
In the exact singlotone declaring the function for retrieving content from the combined values

public function extr_loc($str) {
        preg_match_all('/<!--:'.$this->_lang.'-->(.*?)/si', $str, $matches);
        if (count($matches[1])==0)                  
         return $str;
        $res = "";
        for ($i=0,$c=count($matches[1]);$i<$c;$i++) 
          $res.=$matches[1][$i];
        return $res;
     }

The most hard thing was to setup Zend Framework routing so it could understand current website structure with url language component. So if the route in classic model was specified as

$router->addRoute('product',
     new Zend_Controller_Router_Route('product/:productId', 
       array('controller' => 'product', 'action' => 'view')
     )
);

after moving to /en/profuct/productid/ URLs this controller stopped to be able to work. On the internet I found very good and suitable solution. I publish it here. We need Zend_Controller_Router_Route subclass (keepeing authorship):

<?php
/**
 * This class has been inherited to bring an additional feature to the
 * Zend_Controller_Router_Route class. It detects a route segment named
 * locale and sets a default locale for the runnig application based on
 * the segment's value
 *
 * If the value is not a valid Locale identifier the locale is not set
 * into the registry
 *
 * @author Jiri Helmich 
 */
class Heximal_Route extends Zend_Controller_Router_Route
{
	/**
	 * Matches a user submitted path with parts defined by a map. Assigns and
	 * returns an array of variables on a successful match.
	 *
	 * @param string $path Path used to match against this routing map
	 * @return array|false An array of assigned values or a false on a mismatch
	 * @author Jiri Helmich 
	 */
	public function match($path, $partial = false)	{
		//make a copy of that for the parental class
		$originalPath = $path;
		//if the path is empty, there is no locale :-)
		if ($path !== '') {
 
			//path begins with a delimiter, so trim that and explode the path
			$path = trim($path, $this->_urlDelimiter);
			$path = explode($this->_urlDelimiter, $path);
			//loop over each part of the path
			foreach ($path as $pos => $value) {
				//a simple test if this could be a matching route
				if (!array_key_exists($pos, $this->_variables))
						break; //the route is probably longer than the path, not our business
 
				//get a name of current route segment
				$name = $this->_variables[$pos];
 
				//locale segment, that's the interesting stuff
				if ($name === 'locale' && !empty($value)) {
					try {
						//if the given value is not a valid locale identifier
						//an exception is thrown
						$locale = Zend_Locale::findLocale($value);
 
						//otherwise, we construct a new locale instance based on the identifier ...
						$locale = new Zend_Locale($locale);
						// ... and set that into the registry
						Zend_Registry::set("Zend_Locale",$locale);
						//BUT the default translator already has a locale set,
						//so we need to override that
						//we would also like if the assemble method of the
						//router would have the locale value automatically
						Zend_Controller_Front::getInstance()->getRouter()->setGlobalParam('locale',$locale);
 
						//that's all we need to do, the rest is parent's job
						array_splice($path,0,1);
						$path="/".implode("/",$path)."/";
						break;
 
					}catch(Zend_Locale_Exception $e) {
						//this could throw an exception
						//but without doing that, the standard match method
						//is executed the and default locale is used
					}
				}
			}
		}
		//let the parental class to do its job
		return parent::match($originalPath, $partial);
	}
 
}

Place it in our library branch and add it to lazy load (in Bootstrap.php):

Zend_Loader::loadClass('Heximal_Route');

I implemented routes.php as followed (just showing a piece of code):

<?php   
$router = Zend_Controller_Front::getInstance()- -->getRouter();
 
$router->addRoute('pages_loc',new Heximal_Route(
	':locale/:controller/:action/*',
	array(
		'controller' => 'index',
		'action'	 => 'index',
		'locale'	 => 'ru'
	)
));
 
$routes = array(
   'prod_index' => array(
       'url'=>'/product/', 
       'params'=>array('controller' => 'product', 'action' => 'index')
   ),
/*
add all necessary routes here
*/   
);
  foreach ($routes as $r_name => $info) {
    $router->addRoute($r_name.'_noloc',
       new Zend_Controller_Router_Route($info["url"], $info["params"],isset($info["validate"])?$info["validate"]:false)
    );
    $info["params"]["locale"]="ru";
    $router->addRoute($r_name,
       new Heximal_Route(':locale'.$info["url"],$info["params"],isset($info["validate"])?$info["validate"]:false)
    );  
  }

And thus, section by section, page by page, I turned the site on multilingual tracks.

Categories: Coding, Blog Tags: , ,
  1. No comments yet.