Issues

ZF-9976: Zend_Translate_Adapter rerouting problem

Description

Hello!

When there are 2 (or more) untranslated MessageIDs (one directly after the other), I got only the first one translated in the "routed" "fallback" language.

it's about these lines of code: "if (array_key_exists($locale, $this->_options['route']) && !array_key_exists($locale, $this->_routed)" and: "$this->_routed[$locale] = true;" in line 738 - 740 (and also 683 - 685) of Zend/Translate/Adapter.php

Example: to translate: ID1 (available in user-language) ID2 (not available in user-language, routing to standard-language) ID3 (not available in user-language, routing to standard-language)

ID1 is found in user-language-source and successfully returned - OK!

ID2 is NOT found in user-language-source; setting "$this->_routed[USER_LANGUAGE] = true" and successfully return text from standard language - OK

ID3 is NOT found in user-language-source; "$this->_routed[USER_LANGUAGE]" already is true in lines 738/739 we have: "if (array_key_exists($locale, $this->_options['route']) && !array_key_exists($locale, $this->_routed)" an because of "!array_key_exists($locale, $this->_routed)" we won't route, because we thougth, we already did... we don't return the translation from the standard language, so only the MessageID (ID3) will be returned... -ERROR!!!

it's eroneous, that "$this->_routed" will be resetted AFTER: "return $this->translate($messageId, $this->_options['route'][$locale]); }" by "$this->_routed = array();" in line 744 but because of that "return $this->translate(..." we don't reach that point.

Best regards, René Kerner.

Comments

The code works as expected. It would be nice if you give a real example instead of framework code.

This is a simple recursive methodcall as used in many other components and projects. When you follow the workflow you would see that _route is resetted before the rerouted translation is returned.

I can't see a failure wether in code nor within the testbed which tests the code.

Ok, I'll explain a bit precisely.

In my application german (de) is the standard-locale and the german language-file would always be the complete one. for test-reasons, i changed my firefox-language-option to "en", so Zend_Locale retrieves english as the actual locale. but it's not completly translated. so i wanted to reroute "en" to "de" if the identifiers aren't found in the "en"-languagefile.



...

;----------------------------
; Zend_Locale
;----------------------------
resources.locale.default = "de"

;----------------------------
; Zend_Translate
;----------------------------
resources.translate.adapter = "Tacticx_Translate_Adapter_Gettext"
resources.translate.data = APPLICATION_PATH "/languages"
resources.translate.default = "de"
resources.translate.locale = "auto"
resources.translate.route.en = "de"
resources.translate.options.disableNotices = false
resources.translate.options.logUntranslated = true
resources.translate.options.scan = "directory"
resources.translate.options.ignore[] = "."
resources.translate.options.ignore.regex = "/\.po$/i"

...


    protected function _initTranslation()
    {
        $this->bootstrap('locale');
        $this->bootstrap('translate');
        $translate = Zend_Registry::get('Zend_Translate');
        $writer = new Zend_Log_Writer_Stream(APPLICATION_ROOT_PATH
                . '/data/logs/translation.log');
        $log = new Zend_Log($writer);
        // Diese der Übersetzungs-Instanz hinzufügen
        $translate->setOptions(array('log' => $log));
    }

    protected function _initNavigationMenu()
    {
        $this->bootstrap('locale');
        $this->bootstrap('translate');
        $this->bootstrap('translation');
        require_once(APPLICATION_ROOT_PATH . DIRECTORY_SEPARATOR . 'data'
                    . DIRECTORY_SEPARATOR . 'menu' . DIRECTORY_SEPARATOR
                    . 'menu.inc.php');
        // only stores an navigation-menu array in variable $menu
        Zend_Registry::set('Zend_Navigation', $menu);

$menu = new Zend_Navigation(
array(
    array(
        'label' => 'navigation::home',
        'uri' => '/',
        'pages' => array(
                       array(
                           'label' => 'navigation::login',
                           'controller' => 'access',
                       ),
                       array(
                           'label' => 'navigation::logout',
                           'action' => 'logout',
                           'controller' => 'access',
                       )
        ),
    ),
    array(
        'label' => 'navigation::organization_editor',
        'module' => 'mgr',
        'action' => 'index',
        'controller' => 'organization',
    ),

...

as you can see, the navigation labels are "identifiers", which should be translated by Zend_Translate.

The Zend_View_Helper_SOMETHING (?) is used to create the menu-structure and there the labels got translated.

In that example the View_Helper_???... (i'm @home at the moment and can't say, which is used. tomorrow i'll again debug that and add that information) gets the Translator (I expect Zend_Translate from Zend_Registry) and calls ->translate("navigation::login"); locale is null and gets the currently user-locale (line 656, Zend/Translate/Adapter.php).

the next lines until line 707 are skipped, because the conditions don't match. after that, because of missing languagefile-entries in the english-language-file, the next lines are skipped (conditions don't match) until line 735/736:


        $this->_log($messageId, $locale);
        // use rerouting when enabled

then we come to the routing-part (lines 737 - 745):


        if (!empty($this->_options['route'])) // isn't empty --> true
        {  // --> go in that if
            if (array_key_exists($locale, $this->_options['route']) &&  // exists --> true 
                !array_key_exists($locale, $this->_routed))         // doesn't exists --> true
            {   // --> go in that if
                $this->_routed[$locale] = true; // for that translator-object (let's call it: TO1) we set routed["en"] to true
                return $this->translate($messageId, $this->_options['route'][$locale]); // we call ->translate("...", "de") and it will return the right german languagefile-entry for that identifier
            }

            $this->_routed = array();  // is not called, because of "return $this->translate(...)" before
        }

in that navigation-menu, the next identifier also isn't translated in the english-language-file.

so we are in the same helper and have the same translator-object (TO1), which "$this->_routed[$locale]" value is still not resetted (I debugged that!!!). so: $this->_routed["en"] is true.

the view helper now calls with that translator (TO1) ->translate("the_next_identifier"); as before no line matches one of the if-conditions and we come to lines 737 - 745:


        if (!empty($this->_options['route'])) // isn't empty --> true
        {  // --> go in that if
            if (array_key_exists($locale, $this->_options['route']) &&  // exists --> true 
                !array_key_exists($locale, $this->_routed))         // already EXISTS --> false
            {   // --> NOT go in that if
                $this->_routed[$locale] = true;
                return $this->translate($messageId, $this->_options['route'][$locale]);
            }

            $this->_routed = array();  // now it's resetted, but we didn't translate that identifier. for that translator-object only the next identifier will be translated...
        }

the reason is, that the reset:


$this->_routed = array();

is only resetted on three positions: line 53, on initialization: private $_routed = array(); line 689, $this->_routed = array(); after Zend_Locale::isLocale-checks line 744, $this->_routed = array(); after rerouting

more specific code-examples I can only support from work tomorrow.

what code examples do you need?

best regards, rené

wrong translation example (application code follows!)

now some explanation for my screenshots:

I hava a StartController with an index-action.


STARTSEITE
<?php echo $this->translate('test1') . '
' . PHP_EOL; echo $this->translate('test2') . '
' . PHP_EOL; echo $this->translate('test3') . '
' . PHP_EOL; echo $this->translate('test4') . '
' . PHP_EOL; echo $this->translate('test5') . '
' . PHP_EOL; echo $this->translate('test6') . '
' . PHP_EOL; echo $this->translate('test7') . '
' . PHP_EOL; echo $this->translate('test8') . '
' . PHP_EOL; echo $this->translate('test9') . '
' . PHP_EOL; echo $this->translate('test10') . '
' . PHP_EOL; echo $this->translate('test11') . '
' . PHP_EOL; echo $this->translate('test12') . '
' . PHP_EOL; echo $this->translate('test13') . '
' . PHP_EOL; echo $this->translate('test14') . '
' . PHP_EOL;

In german PO/MO-files I translated identifiers test1 - test4 and test7 - test12. test5, test6, test13 and test14 aren't translated. This is correctly shown in picture one: "TRANSLATE_DE.png" !TRANSLATE_DE.png|thumbnail!

Then I switched my browser LANGUAGE_ACCEPT to "English [en]" and Zend_Locale will give this to Zend_Translate. In the english languagefile nothing is translated at this time. !TRANSLATE_EN_no-translations.png|thumbnail! This should look like picture one and there should always be the german translation: ü1 - ü4 test5, test6 ü7 - ü12 test13, test14 BUT I got: ü1 - OK test2 - ERROR ü3 - OK test4 - ERROR test5 - OK test6 - OK ü7 - OK test8 - ERROR ü9 - OK test10 - ERROR ü11 - OK test12 - ERROR test13 - OK test14 - OK As you can see, only every second untranslated translation is correctly routed!

Then I translated some identifiers in the english languagefile, but only identifiers test4 - test7. As you can see, !TRANSLATE_EN_translated-ID-4-5-6-7.png|thumbnail! I should get: ü1 - ü3 en4 - en7 ü8 - ü12 test13, test14

BUT I got: ü1 - OK test2 - ERROR ü3 - OK en4 - OK en5 - OK en6 - OK en7 - OK test8 - ERROR ü9 - OK test10 - ERROR ü11 - OK test12 - ERROR test13 - OK test14 - OK

So you can see only every second translation in case of language-rerouting is correct.

The reason is in the recursion. Because of the RETURN command in "return $this->translate($messageId, $route_language)" the reset of $this->_routed[$routed_language] will only be reseted in a later call of $this->translate() for the next identifier. The right solution would be to save the returned value of the $this->translate($messageId, $route_language) to a variable. Then after the recursion reset or in code "unset($this->_routed[$currently_routed_language])". So you can prevent endless recursion in cyclic routing definitions (like EN => DE => FR => EN) when the identifier is not found.

And more precisly in german / Und auf Deutsch: Es liegt daran, wie ich bereits beschrieben habe, dass im beim re-routen der rekursive Abstieg direkt mit einem RETURN gemacht wird. Richtig wäre doch, das Ergebnis des rekursiven Abstiegs von $this->translate($messageId, $route_language) in eine variable zu speichern. Nach erfolgreichem rekursivem Aufstieg "unset($this->_routed[$geroutete_sprache])" zu machen. und dann erst das RETURN. So kann man verhindern, dass es eine endlose Rekursionschleife gibt, wenn der Bezeichner/Identifier bei einer zyklischen Routen-Definition (z.B. EN => DE => FR => EN) nicht gefunden wird.

wrong screenshot replaced

In my project I'm using a self extended Zend_Translate_Adaper_Gettext. It's called Tacticx_Translate_Adaper_Gettext. It's not the right place because it should be in a Tacticx_Translate_Adaper class, but for me it's acceptable there atm. Here is an example for a corrected Zend/Translate/Adapter.php I only show the added function and the corrected translate(...)-function.


    private function _routeTranslate($_messageId, $_locale)
    {
        $this->_routed[$_locale] = true;
        $foreign_lang = $this->translate($_messageId, $this->_options['route'][$_locale]);
        unset($this->_routed[$_locale]);
        if(!isset($this->_routed))
            $this->_routed = array();
        return $foreign_lang;
    }

    /**
     * Translates the given string
     * returns the translation
     *
     * @see Zend_Locale
     * @param  string|array       $messageId Translation string, or Array for plural translations
     * @param  string|Zend_Locale $locale    (optional) Locale/Language to use, identical with
     *                                       locale identifier, @see Zend_Locale for more information
     * @return string
     */
    public function translate($messageId, $locale = null)
    {
        if($locale === null)
            $locale = $this->_options['locale'];
        $plural = null;
        if(is_array($messageId))
        {
            if(count($messageId) > 2)
            {
                $number = array_pop($messageId);
                if(!is_numeric($number))
                {
                    $plocale = $number;
                    $number  = array_pop($messageId);
                }
                else
                    $plocale = 'en';

                $plural    = $messageId;
                $messageId = $messageId[0];
            }
            else
                $messageId = $messageId[0];
        }

        // check if chosen locale is a valid locale
        if(!Zend_Locale::isLocale($locale, true, false))
        {
            if (!Zend_Locale::isLocale($locale, false, false))
            {
                // language does not exist, return original string
                $this->_log($messageId, $locale);
                // use rerouting when enabled
                if(!empty($this->_options['route']))
                {
                    if(array_key_exists($locale, $this->_options['route']) &&
                        !array_key_exists($locale, $this->_routed))
                    {
                        return $this->_routeTranslate($messageId, $locale);
                    }
                    $this->_routed = array();
                }

                if($plural === null)
                    return $messageId;

                $rule = Zend_Translate_Plural::getPlural($number, $plocale);
                if(!isset($plural[$rule]))
                    $rule = 0;
                return $plural[$rule];
            }
            $locale = new Zend_Locale($locale);
        }

        $locale = (string) $locale;
        if((is_string($messageId) || is_int($messageId)) && isset($this->_translate[$locale][$messageId]))
        {
            // return original translation
            if($plural === null)
                return $this->_translate[$locale][$messageId];
            $rule = Zend_Translate_Plural::getPlural($number, $locale);
            if(isset($this->_translate[$locale][$plural[0]][$rule]))
                return $this->_translate[$locale][$plural[0]][$rule];
        }
        else if(strlen($locale) != 2)
        { // faster than creating a new locale and separate the leading part
            $locale = substr($locale, 0, -strlen(strrchr($locale, '_')));

            if((is_string($messageId) || is_int($messageId))
                && isset($this->_translate[$locale][$messageId]))
            { // return regionless translation (en_US -> en)
                if($plural === null)
                    return $this->_translate[$locale][$messageId];
                $rule = Zend_Translate_Plural::getPlural($number, $locale);
                if (isset($this->_translate[$locale][$plural[0]][$rule]))
                    return $this->_translate[$locale][$plural[0]][$rule];
            }
        }

        $this->_log($messageId, $locale);
        // use rerouting when enabled
        if (!empty($this->_options['route']))
        {
            if (array_key_exists($locale, $this->_options['route']) &&
                !array_key_exists($locale, $this->_routed))
            {
                return $this->_routeTranslate($messageId, $locale);
            }
            $this->_routed = array();
        }
        if ($plural === null)
            return $messageId;

        $rule = Zend_Translate_Plural::getPlural($number, $plocale);
        if (!isset($plural[$rule]))
            $rule = 0;
        return $plural[$rule];
    }

Please write your issues in english and not in other languages which people don't understand. Thnx.

Fixed with r22425.