You're in the army now
translate($this)
So I needed to set up a translatable website and thought I would re-visit the way I use the translate component from the Zend framework. Previously I set up the translate component and would then create a plugin to handle the language change which I would trigger by changing a session variable.
Although this works I tended to feel it was a bit slap dash and that there must be a more efficient and useful way of utilising the component without the need to utilise a plugin.
So having had a nose around the net I decided that I wanted to make the language URL driven. I also wanted to steer away from using a subdomain to drive the language selection (i.e. en.website.com/pagename) and wanted it to be a part of the url (i.e. website.com/en/pagename) as this was more in keeping of the style of site I like to use.
So with a little tweaking code and utilising both the Locale, Translate and Router Resources here is how I did it.
Router
First things first I need to set up my router. I did this using the application.ini file and the default router resource that gets shipped with the framework
resources.router.routes.default.route = "/:slug/*"
resources.router.routes.default.defaults.module = "default"
resources.router.routes.default.defaults.controller = "index"
resources.router.routes.default.defaults.action = "index"
resources.router.routes.i18ndefault.route = "/:i18n/:slug/*"
resources.router.routes.i18ndefault.defaults.i18n = "en"
resources.router.routes.i18ndefault.defaults.module = "default"
resources.router.routes.i18ndefault.defaults.controller = "index"
resources.router.routes.i18ndefault.defaults.action = "index"
resources.router.chainNameSeparator = "_"
as you can see I have created two routes, the normal route and a “translateable” route.
Locale
Next we need to create a new resource to handle Locale in the way we want it to.
class Zucchi_Application_Resource_Locale
extends Zend_Application_Resource_ResourceAbstract
{
const DEFAULT_REGISTRY_KEY = 'Zend_Locale';
protected $_locale;
public function init()
{
return $this->getLocale();
}
public function getLocale()
{
if (null === $this->_locale) {
$options = $this->getOptions(); // get resource config options
$bootstrap = $this->getBootstrap();
$bootstrap->bootstrap('Router'); // ensure router loaded
$router = $bootstrap->getContainer()->router; // get router
# clone router so as not to cause issues with router
# by populating with request outside lifecycle
$router2 = clone $router;
$request = new Zend_Controller_Request_Http(); // get request object
$router->route($request); // populate router with request
// if i18n param presnt then test and load locale
if (Zend_Locale::isLocale($i18n = $request->getParam('i18n', false))) {
$this->_locale = new Zend_Locale($i18n);
} else {
try { // attempt auto determination of locale
$this->_locale = new Zend_Locale('auto');
} catch (Zend_Locale_Exception $e) {
// load default locale
Zend_Locale::setDefault($options['default']);
$this->_locale = new Zend_Locale($options['default']);
}
}
$key = (isset($options['registry_key']) && !is_numeric($options['registry_key']))
? $options['registry_key']
: self::DEFAULT_REGISTRY_KEY;
Zend_Registry::set($key, $this->_locale);
}
return $this->_locale;
}
}
Quite a lot is happening here so I’ll go through it a step at a time.
$bootstrap = $this->getBootstrap();
$bootstrap->bootstrap('Router'); // ensure router loaded
$router = $bootstrap->getContainer()->router; // get router
# clone router so as not to cause issues with router
# by populating with request outside lifecycle
$router2 = clone $router;
$request = new Zend_Controller_Request_Http(); // get request object
$router->route($request); // populate router with request
Here we get the router which we will clone. the reason for cloning is that we do not want to populate the original router before it would normally be populated. This prevent the possibility of problems occuring later on.
We then create a new request object which will grab all the correct data we need to populate our cloned router.
By populating the router we can now access the parameters we set up in the route resource.
// if i18n param presnt then test and load locale
if (Zend_Locale::isLocale($i18n = $request->getParam('i18n', false))) {
$this->_locale = new Zend_Locale($i18n);
} else {
try { // attempt auto determination of locale
$this->_locale = new Zend_Locale('auto');
} catch (Zend_Locale_Exception $e) {
// load default locale
Zend_Locale::setDefault($options['default']);
$this->_locale = new Zend_Locale($options['default']);
}
}
We can now check that the param i18n exists… Now you may be think that this is odd as we should then really be populating the url with the locale rather than the language.. And your absolutely right. By doing it this way we can also dynamically set the locale as well as the translation. some example urls we can use are
- website.com/en_gb/page-name
- website.com/en_us/page-name
- website.com/de_DE/page-name
- website.com/it_IT/page-name
- website.com/ja_JP/page-name
This will allow you to use the locale components ability to adapt your website by locale as well as language.
We also test to make sure that the locale exists and if it doesn’t then we default to the auto detection and failing that we can then revert to a default set in our application.ini file
; locale resource
resources.locale.default = "en_GB"
Translate
Now we move onto the translate resource
; translate resource
resources.translate.adapter = "gettext"
resources.translate.data = APPLICATION_PATH "/../data/languages"
resources.translate.cache = true
resources.translate.options.clear = 0
resources.translate.options.disableNotices = 0
resources.translate.options.ignore = "."
resources.translate.options.scan = "filename"
resources.translate.log.active = false
resources.translate.log.file = APPLICATION_PATH "/../data/logs/translations/missing.log"
resources.translate.log.message = "Missing '%message%' within locale '%locale%'"
I tend to use gettext as its the most applicable for my needs.
I also tend to scan by filename as that means it becomes a lot easier for me to add and remove languages accordingly.
lass Zucchi_Application_Resource_Translate extends Zend_Application_Resource_Translate {
const DEFAULT_REGISTRY_KEY = 'Zend_Translate';
public function getTranslate()
{
if (null === $this->_translate) {
// make sure local loaded
$bootstrap = $this->getBootstrap();
$bootstrap->bootstrap('Locale');
$locale = $bootstrap->getContainer()->locale; // get locale
$options = $this->getOptions();
if (!isset($options['data'])) {
throw new Zend_Application_Resource_Exception('No translation source data provided.');
}
if (isset($options['cache']) && $options['cache']) {
$bootstrap = $this->getBootstrap()->bootstrap('cache');
$cache = $bootstrap->getResource('cache');
Zend_Translate::setCache($cache);
}
if ('production' == APPLICATION_PATH) {
$options = $this->getOptions();
$options['disableNotices'] = true;
$this->setOptions($options);
}
$adapter = isset($options['adapter']) ? $options['adapter'] : Zend_Translate::AN_ARRAY;
$translateOptions = isset($options['options']) ? $options['options'] : array();
$this->_translate = new Zend_Translate(
$adapter, $options['data'], $locale->getLanguage(), $translateOptions
);
if (isset($options['log']) && $options['log']['active'] && $options['log']['file']) {
// Create a log instance
$writer = new Zend_Log_Writer_Stream($options['log']['file']);
$log = new Zend_Log($writer);
$logOptions = array('log' => $log,
'logUntranslated' => true);
if (isset($options['log']['message'])) {
$logOptions['logMessage'] = $options['log']['message'];
}
$this->_translate->setOptions($logOptions);
}
$key = (isset($options['registry_key']) && !is_numeric($options['registry_key']))
? $options['registry_key']
: self::DEFAULT_REGISTRY_KEY;
Zend_Registry::set($key, $this->_translate);
}
return $this->_translate;
}
}
As you can see we first make sure that the dependancy for the locale resource is met. Set up some caching and other settings.
Then we get to setting the locale. Now I’m all for letting the TRanslate component use the Value we set in the Zend Registry… but I find that with the system being set up like I have it I would need a language file for each varient of english. So instead of relying on that I use the Zend Locales buil in “getLanguage” method to return the correct language code. so en_GB and en_US will both return ‘en’ which we can then use in the naming conventions for our files (if your scanning for translation files that is).
$this->_translate = new Zend_Translate(
$adapter, $options['data'], $locale->getLanguage(), $translateOptions
);
We then set a little bit of logging and place the translate into the Rgeistry so that other components can detect it and hey presto your all done.
To test whats happening place this in your controllers action and try out some combinations for your URLs.
$bootstrap = $this->getInvokeArg('bootstrap');
Zend_Debug::dump($this->_getAllParams());
Zend_Debug::dump($bootstrap->getResource('locale'));
You should now have a valid parameter you can test against ‘i18n’ and your locale & languages will automatically adjust.
| Print article | This entry was posted by Matt Cockayne on November 26, 2009 at 8:25 pm, and is filed under Spec Ops. Follow any responses to this post through RSS 2.0. You can leave a response or trackback from your own site. |


