Zend Framework: per module translation sources
Separate modules should be able to have separate Zend_Translate sources. As can be read in my earlier post about module configuration, I am making my modules completely autonomous in their config. That also means they should have their own translation sources that are automatically added to the main Zend_Translate instance for easy reference. In this article I will show you how to set this up.
The standard zend application resource Zend_Application_Resource_Translate doesn’t support this. If you use that resource and include translate settings in your module.ini, a separate Zend_Translate instance will be added to the resource container for that module. By default, the resource also sets the registry key ‘Zend_Translate’ to the new instance every time the resource is called in the bootstrap process. Because you don’t know the module load order, you don’t know what instance the registry will contain.
This has an important drawback: the stock translate view helper, Zend_Form and other Zend_Translate aware ZF components automatically check for a Zend_Translate instance in the registry under the key ‘Zend_Translate’. This will lead to unexpected results.
There are workarounds, but they are not convenient in my opinion. Another example: to get at the translations for your module from your controller, you would have to do this:
$moduleTranslate = $this->getInvokeArg('bootstrap')->getResource('modules') ->offsetGet('modulenamehere')->getResource('translate');
to get to the main translate instance:
$mainTranslate = $this->getInvokeArg('bootstrap')->getResource('translate');
And the shortest version could not be used, because you wouldn’t know which instance it contains!
$unknownTranslate = Zend_Registry::get('Zend_Translate');
My solution was building a slightly different version of Zend_Application_Resource_Translate. It checks if the registry key is already set first. If not, it creates a new instance of Zend_Translate and assigns it to the registry. If there already is a registry key, the resource fetches the existing instance and calls addTranslation on it. This way, all your translation sources are neatly merged.
Usage example
Default translate settings in application.ini:
resources.translate.data = APPLICATION_PATH "/languages" resources.translate.adapter = "Array" resources.translate.options.scan = "filename"
Then, if you use my ModuleSetup resource as described in module config, you can set the module translation options in the module.ini for that module:
resources.translate.data = APPLICATION_PATH "/modules/modulename/languages"
or, you can set the module config in your application.ini, as described in the Zend Framework reference:
modulename.resources.translate.data = APPLICATION_PATH "/modules/modulename/languages"
* note: you can set all options for module translation sources the same way as for the application translate resource, except ‘adapter’. This is because Zend_Translate::addTranslation doesn’t support it.
application/languages/nl.php:
return array( 'application' => 'applicatie', );
application/modules/modulename/languages/nl.php:
return array( 'module example' => 'module voorbeeld', );
After bootstrap, Zend_Registry::get(‘Zend_Translate’) contains the merged tranlation sources. A var_dump of Zend_Registry::get(‘Zend_Translate’)->getMessages() will show:
array 'application' => string 'applicatie' (length=10) 'module example' => string 'module voorbeeld' (length=16)
Code
class Rexus_Application_Resource_Translate extends Zend_Application_Resource_ResourceAbstract { const DEFAULT_REGISTRY_KEY = 'Zend_Translate'; /** * @var Zend_Translate */ protected $_translate; /** * Defined by Zend_Application_Resource_Resource * * @return Zend_Translate */ public function init() { /* * The translate settings for the application.ini should be loaded first * to set all necessary defaults, most importantly the adapter to use * * Since we can't know the load order of modules and/or application bootstrap * we set the application translate as dependecy if we are not the main Bootstrap */ if ('Bootstrap' !== get_class($this->getBootstrap())) { $this->getBootstrap()->getApplication()->bootstrap('translate'); } return $this->getTranslate(); } /** * Retrieve translate object * * @return Zend_Translate */ public function getTranslate() { $options = $this->getOptions(); if (!isset($options['data'])) { throw new Zend_Application_Resource_Exception( 'No translation source data provided in the ini file for: ' . get_class($this->getBootstrap()).'.' ); } $adapter = isset($options['adapter']) ? $options['adapter'] : Zend_Translate::AN_ARRAY; $locale = isset($options['locale']) ? $options['locale'] : null; $translateOptions = isset($options['options']) ? $options['options'] : array(); $key = ( isset ($options['registry_key']) && !is_numeric($options['registry_key'])) ? $options['registry_key'] : self::DEFAULT_REGISTRY_KEY; // If no translate object was set in the registry we create it. if (!Zend_Registry::isRegistered($key)) { $this->_createTranslation($adapter, $options['data'], $locale, $translateOptions); // if there is, we should add a translation source to the existing translate object } elseif (Zend_Registry::isRegistered($key)) { $this->_translate = Zend_Registry::get($key); $this->_addTranslation($options['data'], $locale, $translateOptions); } Zend_Registry::set($key, $this->_translate); return $this->_translate; } protected function _createTranslation($adapter, $data, $locale, $options) { $this->_translate = new Zend_Translate( $adapter, $data, $locale, $options ); } protected function _addTranslation($data, $locale, $options) { $this->_translate->addTranslation( $data, $locale, $options ); } }
To use this resource instead of the stock one, just drop it into your library. It will automatically be chosen over the stock one, because it has the same name. Of course, this only works correctly if you set the prefix and path for your resources in your application.ini like this:
pluginPaths.Rexus_Application_Resource = "Rexus/Application/Resource"
Improvements and comments are always welcome.
Again… You’ve did it! Cool.
Tried and almost working.
My Accept-Language header is:
en-ca,en-us;q=0.8,en;q=0.6,fr-ca;q=0.4,fr;q=0.2
Application.ini
resources.translate.data = APPLICATION_PATH “/languages”
resources.translate.locale = “en”
resources.translate.adapter = “Array”
resources.translate.options.scan = “filename”
I have
application/languages/en.php
application/languages/fr.php
application/languages/nl.php
application/modules/modulename/languages/en.php
application/modules/modulename/languages/fr.php
application/modules/modulename/languages/nl.phh
They are the same as yours!
Here is the results:
var_dump(Zend_Registry::get(‘Zend_Translate’)->getMessages());
array(2) {
["application"]=>
string(10) “applicatie”
["module example"]=>
string(16) “module voorbeeld”
}
var_dump(Zend_Registry::get(‘Zend_Translate’));
object(Zend_Translate)#25 (1) {
["_adapter:private"]=>
object(Zend_Translate_Adapter_Array)#24 (4) {
["_data:private"]=>
array(1) {
["nl"]=>
array(1) {
["module example"]=>
string(16) “module voorbeeld”
}
}
["_automatic:private"]=>
bool(false)
["_options:protected"]=>
array(8) {
["clear"]=>
bool(false)
["disableNotices"]=>
bool(false)
["ignore"]=>
string(1) “.”
["locale"]=>
string(2) “nl”
["log"]=>
NULL
["logMessage"]=>
string(49) “Untranslated message within ‘%locale%’: %message%”
["logUntranslated"]=>
bool(false)
["scan"]=>
string(8) “filename”
}
["_translate:protected"]=>
array(3) {
["en"]=>
array(2) {
["application"]=>
string(11) “application”
["module example"]=>
string(14) “module example”
}
["fr"]=>
array(2) {
["application"]=>
string(11) “application”
["module example"]=>
string(17) “exemple de module”
}
["nl"]=>
array(2) {
["application"]=>
string(10) “applicatie”
["module example"]=>
string(16) “module voorbeeld”
}
}
}
}
The locale in the Zend_Translate object in registry is ‘nl’. Something is wrong.
Maybe it’s me… Maybe not…
oki… I’ve searched a little and found that when I have
resources.translate.data = APPLICATION_PATH “/modules/modulename/languages”
in my module.ini it does load the additional languages BUT it change the locale property of Zend_Translate to the name of the last file loaded.
Tested by adding a zu.php in my module language folder containing
‘Zoulou module example’,);
And
print(Zend_Registry::get(‘Zend_Translate’)->_(“module example”).”\n”);
Do return me
Zoulou module example
Even if Zend_Locale getLanguage return me ‘en’ as expected.
[...] I think you have an error in your ini settings for the translate resource. 'resources.translate.data' is not an array, it should be a string pointing to your data: PHP Code: resources.translate.adapter = "array" resources.translate.data = APPLICATION_PATH "/translations/" resources.translate.options.scan = "directory" resources.translate.options.disableNotices = true resources.translate.options.logUntranslated = true The 'fileExt' and 'directory' settings don't exist. If you want to know more about setting translation for multiple modules using Zend_Application and a custom translate resource, check my article about it: ZF: per module translations sources. [...]
[...] so happens I wrote an article on my blog about exactly this issue: per module translation sources. I think it can help you out. If you have any questions, you can leave a comment on the article or [...]
I was wondering, how are you collecting the terms to translate and build your translation file? When you have a huge website with thousands of sentences and words to translate, how come the Zend_Framework doesn’t include a source scanner to collect everything? Because he does translate some terms alone and it’s pretty much impossible to know which one will be automatically sent to Zend_Translate through another component and which one have to be translated manually.
@Nyk018 may I suggest poedit and enabling the gettext adapter if you have a large website.
)
http://blog.hackix.com/2010/01/configuring-poedit-for-zend-framework-projects/ shows you how.
Should work the same as above in every other respect (I think
I’ll work up a post showing this alternative.
Interesting blog you got here but I can’t seem to find the RSS button.
You can use multiple different adapters by using $adapter1->addTranslation($adapter2);
See here for details: http://www.thomasweidner.com/flatpress/2010/03/25/working-with-multiple-translation-sources/
Interesting blog. I’m just quite confused with how that tool works.