Magento Translate System

In this blog post we will look into the details of magento translate system.

Basics

Magento translate system is used to setup a single website in multiple languages.
To setup a simple multi language system, we can create either a new store view or a new store or a new website from magento admin. For this go to System -> Manage Stores and create a new store view/store/website. Next go to System -> Configuration -> General -> Locale and set the language you want the new store to be in. We will not cover in detail how to set this up properly as its not relevant to the current blog.

There are two ways using which we can setup translations

1. Using translation csv files
2. Inline Translate Feature

Translate csv file are located in app/locale/en_US by default. You can further add translational files for each language as well. In this folder translation files are setup per module, also for each module in the config.xml file we can define the translation file to use

<frontend>
   <translate>
        <modules>
                <Mage_Catalog>
                    <files>
                        <default>Mage_Catalog.csv</default>
                    </files>
                </Mage_Catalog>
        </modules>
   </translate>
</frontend> 

similarly we can define a different translate file to “adminhtml” area as well.
There is a second location where translate file is located: “app/design/frontend/default/{theme}/locale/en_US/translate.csv”. In this location we can have only a single translate file and not multiple files.

TIP: Translate files are simple key value pair files. Key is the input string and value is output in the desired language.

Inline Translate feature needs to be enabled from “System -> Configuration -> Advanced -> Developer -> Translate Inline”. Here you can turn on translate for admin or frontend. Turning this on allows us to directly edit/save translations from magento web ui itself. These translations are saved in magento database in table “core/translate”

Using Translation In Custom Modules and Themes

We have seen above how to include a custom translation file for our theme and module. To enable translation in our template files we need to use the function __(''); provided by magento. The function “__()” can take in n-number of arguments and works same the as php’s sprintffunction.
e.g

echo __('Having Fun?');
echo __('Having Fun? %s','Ya');
echo __('Having Fun? %s %d','Ya',date('h'));

This function is available in .phtml files, block files, controllers, helpers. So basically it available everywhere except model files.

We can also use translation in our configuration files
e.g

<checkout translate="label" module="checkout">
            <label>Checkout</label>

This says to use the translate file of checkout module to translate the label “Checkout”

Digging Deep

Lets now look at the code level on how magento implements translations.

Files to Look at:
1. Mage_Core_Model_Translate
2. Mage_Core_Model_Locale

Lets first look at the definition of the function “__()”

    public function __()
    {
        $args = func_get_args();
        $expr = new Mage_Core_Model_Translate_Expr(array_shift($args), $this->getModuleName());
        array_unshift($args, $expr);
        return $this->_getApp()->getTranslator()->translate($args);
    }

This function definition is located in files
“Mage_Core_Block_Abstract”,”Mage_Adminhtml_Controller_Action”, “Mage_Core_Controller_Front_Action”, “Mage_Core_Helper_Abstract”
This function ultimately calls the “translate()” function in “Mage_Core_Model_Translate”

There is an “init()” function the class “Mage_Core_Model_Translate” which initializes all translations, so lets look at that function in detail. The function is divided into 3 parts “_loadModuleTranslation” , “_loadThemeTranslation”, “_loadDbTranslation”. The purpose of each method is evident from its name.

1. _loadModuleTranslation():

        foreach ($this->getModulesConfig() as $moduleName=>$info) {
            $info = $info->asArray();
            $this->_loadModuleTranslation($moduleName, $info['files'], $forceReload);
        }
        protected function _loadModuleTranslation($moduleName, $files, $forceReload=false)
        {
             foreach ($files as $file) {
                 $file = $this->_getModuleFilePath($moduleName, $file);
                 $this->_addData($this->_getFileData($file), $moduleName, $forceReload);
             }
             return $this;
        }
        protected function _getModuleFilePath($module, $fileName)
        {
            $file = Mage::getBaseDir('locale');
            $file.= DS.$this->getLocale().DS.$fileName;
            return $file;
         }

The above function reads configuration information for each module from “frontend/translate/module” or “adminhtml/translate/module” depending on the area. Loads the file from the ‘locale’ folder and from the current locale set for the store, and add the translation using “_addData()” function.

TIP: It can be seen here that for each module we can specify multiple files in the “files” tag in config.xml

2. _loadThemeTranslation():

    protected function _loadThemeTranslation($forceReload = false)
    {
        $file = Mage::getDesign()->getLocaleFileName('translate.csv');
        $this->_addData($this->_getFileData($file), false, $forceReload);
        return $this;
    }

In this we can see that translate.csv file is loaded for the current theme.

3. _loadDbTranslation():

    protected function _loadDbTranslation($forceReload = false)
    {
        $arr = $this->getResource()->getTranslationArray(null, $this->getLocale());
        $this->_addData($arr, $this->getConfig(self::CONFIG_KEY_STORE), $forceReload);
        return $this;
    }

In this Inline Translations are loaded from database table “core_translate” and then added to the object using _addData() function.

Next lets look at the “_addData()” function

    protected function _addData($data, $scope, $forceReload=false)
    {
        foreach ($data as $key => $value) {
            if ($key === $value) {
                continue;
            }
            $key    = $this->_prepareDataString($key);
            $value  = $this->_prepareDataString($value);
            if ($scope && isset($this->_dataScope[$key]) && !$forceReload ) {
                /**
                 * Checking previos value
                 */
                $scopeKey = $this->_dataScope[$key] . self::SCOPE_SEPARATOR . $key;
                if (!isset($this->_data[$scopeKey])) {
                    if (isset($this->_data[$key])) {
                        $this->_data[$scopeKey] = $this->_data[$key];
                        /**
                         * Not allow use translation not related to module
                         */
                        if (Mage::getIsDeveloperMode()) {
                            unset($this->_data[$key]);
                        }
                    }
                }
                $scopeKey = $scope . self::SCOPE_SEPARATOR . $key;
                $this->_data[$scopeKey] = $value;
            }
            else {
                $this->_data[$key]     = $value;
                $this->_dataScope[$key]= $scope;
            }
        }
        return $this;
    }

The add data function store data in two arrays “_data” and “_dataScope”. $scope is an important variable in this function
$scope => module name in case of module translate files
$scope => false in case of theme translate files
$scope => store name in case of database translate files

Its important to see how data is stored in the “$this->_data” array. To understand lets assume an translate key,value pair
“Add To Cart”,”Add To Basket”
So first this is stored as
$this->_data[‘Add To Cart’] = ‘Add To Basket’;

But if this same translate key is found in another module file then in that case
$this->_data[‘module1::Add To Cart’] = ‘Add To Basket’;
$this->_data[‘module2::Add To Cart’] = ‘Add To Basket Module2’;
$this->_data[‘Add To Cart’] = ‘Add To Basket Module’;

and if developer mode is turned on non module translate is removed which means, $this->_data[‘Add To Cart’] is removed.

The final important function which is actually used to translate the string is

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

As its easy to see here, first translate string is search according to code (code here is module::text). If code is not found then it searches according to text.

Magento Locale Class

There is another important class which we need to look at “Mage_Core_Model_Locale”. This is class which is used to get all locale based information like language codes, timezone information, currency etc and it gets its data from “Zend_Locale” class.

“Mage_Core_Model_Locale_Config” this class contains all the locale and currency you see in admin system configuration.

There are two important function in this “emulate” and “revert”

    public function emulate($storeId)
    {
        if ($storeId) {
            $this->_emulatedLocales[] = clone $this->getLocale();
            $this->_locale = new Zend_Locale(Mage::getStoreConfig(self::XML_PATH_DEFAULT_LOCALE, $storeId));
            $this->_localeCode = $this->_locale->toString();
            Mage::getSingleton('core/translate')->setLocale($this->_locale)->init('frontend', true);
        }
        else {
            $this->_emulatedLocales[] = false;
        }
    }

In this function we can pass a store and it will load all translate information for that particular locale. As we can see it reinitialize the entire ‘core/translate’ object with new translate information as per store locale.

    public function revert()
    {
        if ($locale = array_pop($this->_emulatedLocales)) {
            $this->_locale = $locale;
            $this->_localeCode = $this->_locale->toString();
            Mage::getSingleton('core/translate')->setLocale($this->_locale)->init('adminhtml', true);
        }
    }

The revert function undo’s the effect of emulate. It loads the previous locale and its translation.
But it can be seen here that “emulate()” function loads the ‘frontend’ area while revert loads the ‘adminhtml’ area.
Not sure if this a bug by magento or this is how it was intended because i was not able to figure out the reason for this.