Monday, February 11, 2013

Yii Translations - With Great Power ...

The ability of Yii to translate with ease is one of its primary benefits for many people. You have the ability to translate individual strings or entire views based on the current language selected for the application, and the definitions that you have provided. It's a tremendously powerful tool, when used responsibly. I discovered today that when used with less than extreme care, the results can be ... unexpected.

The core documentation for Yii::t() can be found here: http://www.yiiframework.com/doc/api/1.1/YiiBase#t-detail

Recently, I encountered a bug in some production code that was causing 0s to be stripped from within strings before relaying the ActiveRecord's attribute values. After some convoluted research, I discovered that the problematic code was in an overloaded getAttribute() method, which was calling Yii::t() to translate the attribute value before returning it. Normally, that would not be a problem, however, the model had actually added a method for t, so that it could be referenced as $this->t( $message ); and avoid having to type in the category for the translation, since the translations were being based on the __CLASS__ of the model. Again, this would be fine ... except that the default parameters established for the method were incorrect.

Passing NULL to the $params value of the Yii::t() method in no way equates to the default empty array that the method actually uses. If you pass NULL through as the param value, it will in fact continue through all of the remainder of the Yii::t() method, and execute the final line, which is:
return $params!==array() ? strtr($message,$params) : $message;
Side note: It should be noted that if $params == array() the translated message is returned after the first if block ( https://github.com/yiisoft/yii/blob/1.1.13/framework/YiiBase.php#L580 ). If that same conditional also checked for === null, well, this would be a different post ;)

Now, imagine that the original string was something like:
myemail2001@example.com

Should email addresses be going through Yii::t()? No, not really. The code in question was more on the order of translating the favorite color of the user to the proper language, but when applied universally, without proper thorough research (no, it's NOT safe to assume that null will work as a default value ...), bad things can happen.

The following statements are all true, though the last is a bit shocking:
$attributeValue = 'myemail2001@example.com';

// Translate the value with no parameters - assuming translations are stored 
// in the messages/(lang)/user_preferences.php file
$valueTranslated = Yii::t( 'user_preferences', $attributeValue );

// At this point, $valueTranslated and $attributeValue are the same, since 
// it's an email address, and there is no direct translation.
echo $valueTranslated ,' equals ', $attributeValue;

// Now, lets assume that you presumed the params parameter was null, 
// rather than array() ...
$valueTranslatedWrong = Yii::t( 'user_preferences', $attributeValue, null ); 

// At this point, $valueTranslatedWrong is: myemail21@example.com
// Note that the 0s have all been stripped out of the string
echo $valueTranslatedWrong , ' does not equal ' , $attributeValue;

What?! Where are my 0s? Well, as we all know, in PHP, 0 is considered equal to null, unless you're using ===. So, when you strtr out NULL, you're pulling out the 0s.

Moral of the story? Always double check the method declaration.