Commit fcf3f2c8 by Qiang Xue

i18n WIP

parent 3c1452e8
...@@ -20,7 +20,7 @@ class I18N extends Component ...@@ -20,7 +20,7 @@ class I18N extends Component
$category = 'app'; $category = 'app';
} }
$message = $this->getSource($category)->translate($category, $message, $language); $message = $this->getMessageSource($category)->translate($category, $message, $language);
if (!is_array($params)) { if (!is_array($params)) {
$params = array($params); $params = array($params);
...@@ -42,7 +42,7 @@ class I18N extends Component ...@@ -42,7 +42,7 @@ class I18N extends Component
} }
public function getSource($category) public function getMessageSource($category)
{ {
return $category === 'yii' ? $this->getMessages() : $this->getCoreMessages(); return $category === 'yii' ? $this->getMessages() : $this->getCoreMessages();
} }
......
...@@ -15,29 +15,36 @@ use yii\base\Component; ...@@ -15,29 +15,36 @@ use yii\base\Component;
/** /**
* MessageSource is the base class for message translation repository classes. * MessageSource is the base class for message translation repository classes.
* *
* A message source is an application component that provides message internationalization (i18n). * A message source stores message translations in some persistent storage.
* It stores messages translated in different languages and provides
* these translated versions when requested.
* *
* A concrete class must implement {@link loadMessages} or override {@link translateMessage}. * Child classes should override [[loadMessages()]] to provide translated messages.
*
* @property string $language The language that the source messages are written in.
* Defaults to {@link CApplication::language application language}.
* *
* @author Qiang Xue <qiang.xue@gmail.com> * @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0 * @since 2.0
*/ */
abstract class MessageSource extends Component class MessageSource extends Component
{ {
/** /**
* @event MissingTranslationEvent an event that is triggered when a message translation is not found.
*/
const EVENT_MISSING_TRANSLATION = 'missingTranslation';
/**
* @var boolean whether to force message translation when the source and target languages are the same. * @var boolean whether to force message translation when the source and target languages are the same.
* Defaults to false, meaning translation is only performed when source and target languages are different. * Defaults to false, meaning translation is only performed when source and target languages are different.
*/ */
public $forceTranslation = false; public $forceTranslation = false;
/**
* @var string the language that the original messages are in. If not set, it will use the value of
* [[\yii\base\Application::sourceLanguage]].
*/
public $sourceLanguage; public $sourceLanguage;
private $_messages = array(); private $_messages = array();
/**
* Initializes this component.
*/
public function init() public function init()
{ {
parent::init(); parent::init();
...@@ -48,26 +55,30 @@ abstract class MessageSource extends Component ...@@ -48,26 +55,30 @@ abstract class MessageSource extends Component
/** /**
* Loads the message translation for the specified language and category. * Loads the message translation for the specified language and category.
* Child classes should override this method to return the message translations of
* the specified language and category.
* @param string $category the message category * @param string $category the message category
* @param string $language the target language * @param string $language the target language
* @return array the loaded messages * @return array the loaded messages. The keys are original messages, and the values
* are translated messages.
*/ */
abstract protected function loadMessages($category, $language); protected function loadMessages($category, $language)
{
return array();
}
/** /**
* Translates a message to the specified language. * Translates a message to the specified language.
* *
* Note, if the specified language is the same as * Note that unless [[forceTranslation]] is true, if the target language
* the {@link getLanguage source message language}, messages will NOT be translated. * is the same as the [[sourceLanguage|source language]], the message
* will NOT be translated.
* *
* If the message is not found in the translations, an {@link onMissingTranslation} * If a translation is not found, a [[missingTranslation]] event will be triggered.
* event will be raised. Handlers can mark this message or do some
* default handling. The {@link CMissingTranslationEvent::message}
* property of the event parameter will be returned.
* *
* @param string $category the message category * @param string $category the message category
* @param string $message the message to be translated * @param string $message the message to be translated
* @param string $language the target language. If null (default), the {@link CApplication::getLanguage application language} will be used. * @param string $language the target language
* @return string the translated message (or the original message if translation is not needed) * @return string the translated message (or the original message if translation is not needed)
*/ */
public function translate($category, $message, $language) public function translate($category, $message, $language)
...@@ -81,8 +92,8 @@ abstract class MessageSource extends Component ...@@ -81,8 +92,8 @@ abstract class MessageSource extends Component
/** /**
* Translates the specified message. * Translates the specified message.
* If the message is not found, an {@link onMissingTranslation} * If the message is not found, a [[missingTranslation]] event will be triggered
* event will be raised. * and the original message will be returned.
* @param string $category the category that the message belongs to * @param string $category the category that the message belongs to
* @param string $message the message to be translated * @param string $message the message to be translated
* @param string $language the target language * @param string $language the target language
...@@ -96,63 +107,17 @@ abstract class MessageSource extends Component ...@@ -96,63 +107,17 @@ abstract class MessageSource extends Component
} }
if (isset($this->_messages[$key][$message]) && $this->_messages[$key][$message] !== '') { if (isset($this->_messages[$key][$message]) && $this->_messages[$key][$message] !== '') {
return $this->_messages[$key][$message]; return $this->_messages[$key][$message];
} elseif ($this->hasEventHandler('onMissingTranslation')) { } elseif ($this->hasEventHandlers('missingTranslation')) {
$event = new CMissingTranslationEvent($this, $category, $message, $language); $event = new MissingTranslationEvent(array(
$this->onMissingTranslation($event); 'category' => $category,
return $event->message; 'message' => $message,
'language' => $language,
));
$this->trigger(self::EVENT_MISSING_TRANSLATION, $event);
return $this->_messages[$key] = $event->message;
} else { } else {
return $message; return $message;
} }
} }
/**
* Raised when a message cannot be translated.
* Handlers may log this message or do some default handling.
* The {@link CMissingTranslationEvent::message} property
* will be returned by {@link translateMessage}.
* @param CMissingTranslationEvent $event the event parameter
*/
public function onMissingTranslation($event)
{
$this->raiseEvent('onMissingTranslation', $event);
}
} }
/**
* CMissingTranslationEvent represents the parameter for the {@link MessageSource::onMissingTranslation onMissingTranslation} event.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @package system.i18n
* @since 1.0
*/
class CMissingTranslationEvent extends CEvent
{
/**
* @var string the message to be translated
*/
public $message;
/**
* @var string the category that the message belongs to
*/
public $category;
/**
* @var string the ID of the language that the message is to be translated to
*/
public $language;
/**
* Constructor.
* @param mixed $sender sender of this event
* @param string $category the category that the message belongs to
* @param string $message the message to be translated
* @param string $language the ID of the language that the message is to be translated to
*/
public function __construct($sender, $category, $message, $language)
{
parent::__construct($sender);
$this->message = $message;
$this->category = $category;
$this->language = $language;
}
}
<?php
/**
* MissingTranslationEvent class file.
*
* @link http://www.yiiframework.com/
* @copyright Copyright &copy; 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\i18n;
use yii\base\Event;
/**
* MissingTranslationEvent represents the parameter for the [[MessageSource::missingTranslation]] event.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
class MissingTranslationEvent extends Event
{
/**
* @var string the message to be translated. An event handler may overwrite this property
* with a translated version if possible.
*/
public $message;
/**
* @var string the category that the message belongs to
*/
public $category;
/**
* @var string the language ID (e.g. en_US) that the message is to be translated to
*/
public $language;
}
...@@ -9,121 +9,35 @@ ...@@ -9,121 +9,35 @@
namespace yii\i18n; namespace yii\i18n;
use Yii;
/** /**
* PhpMessageSource represents a message source that stores translated messages in PHP scripts. * PhpMessageSource represents a message source that stores translated messages in PHP scripts.
* *
* PhpMessageSource uses PHP files and arrays to keep message translations. * PhpMessageSource uses PHP arrays to keep message translations.
* <ul> *
* <li>All translations are saved under the {@link basePath} directory.</li> * - Each PHP script contains one array which stores the message translations in one particular
* <li>Translations in one language are kept as PHP files under an individual subdirectory * language and for a single message category;
* whose name is the same as the language ID. Each PHP file contains messages * - Each PHP script is saved as a file named as `[[basePath]]/LanguageID/CategoryName.php`;
* belonging to the same category, and the file name is the same as the category name.</li> * - Within each PHP script, the message translations are returned as an array like the following:
* <li>Within a PHP file, an array of (source, translation) pairs is returned. *
* For example: * ~~~
* <pre>
* return array( * return array(
* 'original message 1' => 'translated message 1', * 'original message 1' => 'translated message 1',
* 'original message 2' => 'translated message 2', * 'original message 2' => 'translated message 2',
* ); * );
* </pre> * ~~~
* </li>
* </ul>
* When {@link cachingDuration} is set as a positive number, message translations will be cached.
*
* Messages for an extension class (e.g. a widget, a module) can be specially managed and used.
* In particular, if a message belongs to an extension whose class name is Xyz, then the message category
* can be specified in the format of 'Xyz.categoryName'. And the corresponding message file
* is assumed to be 'BasePath/messages/LanguageID/categoryName.php', where 'BasePath' refers to
* the directory that contains the extension class file. When using Yii::t() to translate an extension message,
* the category name should be set as 'Xyz.categoryName'.
* *
* @author Qiang Xue <qiang.xue@gmail.com> * @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0 * @since 2.0
*/ */
class PhpMessageSource extends MessageSource class PhpMessageSource extends MessageSource
{ {
const CACHE_KEY_PREFIX='Yii.CPhpMessageSource.';
/**
* @var integer the time in seconds that the messages can remain valid in cache.
* Defaults to 0, meaning the caching is disabled.
*/
public $cachingDuration=0;
/**
* @var string the ID of the cache application component that is used to cache the messages.
* Defaults to 'cache' which refers to the primary cache application component.
* Set this property to false if you want to disable caching the messages.
*/
public $cacheID='cache';
/** /**
* @var string the base path for all translated messages. Defaults to null, meaning * @var string the base path for all translated messages. Defaults to null, meaning
* the "messages" subdirectory of the application directory (e.g. "protected/messages"). * the "messages" subdirectory of the application directory (e.g. "protected/messages").
*/ */
public $basePath; public $basePath = '@app/messages';
/**
* @var array the message paths for extensions that do not have a base class to use as category prefix.
* The format of the array should be:
* <pre>
* array(
* 'ExtensionName' => 'ext.ExtensionName.messages',
* )
* </pre>
* Where the key is the name of the extension and the value is the alias to the path
* of the "messages" subdirectory of the extension.
* When using Yii::t() to translate an extension message, the category name should be
* set as 'ExtensionName.categoryName'.
* Defaults to an empty array, meaning no extensions registered.
* @since 1.1.13
*/
public $extensionPaths=array();
private $_files=array();
/**
* Initializes the application component.
* This method overrides the parent implementation by preprocessing
* the user request data.
*/
public function init()
{
parent::init();
if($this->basePath===null)
$this->basePath=Yii::getPathOfAlias('application.messages');
}
/**
* Determines the message file name based on the given category and language.
* If the category name contains a dot, it will be split into the module class name and the category name.
* In this case, the message file will be assumed to be located within the 'messages' subdirectory of
* the directory containing the module class file.
* Otherwise, the message file is assumed to be under the {@link basePath}.
* @param string $category category name
* @param string $language language ID
* @return string the message file path
*/
protected function getMessageFile($category,$language)
{
if(!isset($this->_files[$category][$language]))
{
if(($pos=strpos($category,'.'))!==false)
{
$extensionClass=substr($category,0,$pos);
$extensionCategory=substr($category,$pos+1);
// First check if there's an extension registered for this class.
if(isset($this->extensionPaths[$extensionClass]))
$this->_files[$category][$language]=Yii::getPathOfAlias($this->extensionPaths[$extensionClass]).DIRECTORY_SEPARATOR.$language.DIRECTORY_SEPARATOR.$extensionCategory.'.php';
else
{
// No extension registered, need to find it.
$class=new ReflectionClass($extensionClass);
$this->_files[$category][$language]=dirname($class->getFileName()).DIRECTORY_SEPARATOR.'messages'.DIRECTORY_SEPARATOR.$language.DIRECTORY_SEPARATOR.$extensionCategory.'.php';
}
}
else
$this->_files[$category][$language]=$this->basePath.DIRECTORY_SEPARATOR.$language.DIRECTORY_SEPARATOR.$category.'.php';
}
return $this->_files[$category][$language];
}
/** /**
* Loads the message translation for the specified language and category. * Loads the message translation for the specified language and category.
...@@ -131,30 +45,18 @@ class PhpMessageSource extends MessageSource ...@@ -131,30 +45,18 @@ class PhpMessageSource extends MessageSource
* @param string $language the target language * @param string $language the target language
* @return array the loaded messages * @return array the loaded messages
*/ */
protected function loadMessages($category,$language) protected function loadMessages($category, $language)
{
$messageFile=$this->getMessageFile($category,$language);
if($this->cachingDuration>0 && $this->cacheID!==false && ($cache=Yii::app()->getComponent($this->cacheID))!==null)
{ {
$key=self::CACHE_KEY_PREFIX . $messageFile; $messageFile = Yii::getAlias($this->basePath) . "/$language/$category.php";
if(($data=$cache->get($key))!==false) if (is_file($messageFile)) {
return unserialize($data); $messages = include($messageFile);
} if (!is_array($messages)) {
$messages = array();
if(is_file($messageFile))
{
$messages=include($messageFile);
if(!is_array($messages))
$messages=array();
if(isset($cache))
{
$dependency=new CFileCacheDependency($messageFile);
$cache->set($key,serialize($messages),$this->cachingDuration,$dependency);
} }
return $messages; return $messages;
} } else {
else Yii::error("Message file not found: $messageFile", __CLASS__);
return array(); return array();
} }
}
} }
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment