From 379e48a4c622796b187dd871a590a8aa0446cd21 Mon Sep 17 00:00:00 2001
From: Qiang Xue <qiang.xue@gmail.com>
Date: Sun, 24 Mar 2013 22:34:45 -0400
Subject: [PATCH] Finished new message translation implementation.

---
 framework/base/ActionFilter.php     |  45 +++++++++++++++++++++++++++++++++++++++++++++
 framework/base/Application.php      |   1 -
 framework/base/View.php             |  80 ++++++++++++++++++++++++++++++++++++++------------------------------------------
 framework/i18n/I18N.php             | 114 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------------------------------------
 framework/i18n/PhpMessageSource.php |  23 +++++++++++++++++++++--
 framework/logging/Target.php        |   3 +--
 framework/widgets/FragmentCache.php | 294 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 todo.md                             |   1 -
 8 files changed, 457 insertions(+), 104 deletions(-)
 create mode 100644 framework/base/ActionFilter.php
 create mode 100644 framework/widgets/FragmentCache.php

diff --git a/framework/base/ActionFilter.php b/framework/base/ActionFilter.php
new file mode 100644
index 0000000..2655c5a
--- /dev/null
+++ b/framework/base/ActionFilter.php
@@ -0,0 +1,45 @@
+<?php
+/**
+ * @link http://www.yiiframework.com/
+ * @copyright Copyright (c) 2008 Yii Software LLC
+ * @license http://www.yiiframework.com/license/
+ */
+
+namespace yii\base;
+
+/**
+ * @author Qiang Xue <qiang.xue@gmail.com>
+ * @since 2.0
+ */
+class Filter extends Behavior
+{
+	/**
+	 * Declares event handlers for the [[owner]]'s events.
+	 * @return array events (array keys) and the corresponding event handler methods (array values).
+	 */
+	public function events()
+	{
+		return array(
+			'beforeAction' => 'beforeAction',
+			'afterAction' => 'afterAction',
+		);
+	}
+
+	/**
+	 * @param ActionEvent $event
+	 * @return boolean
+	 */
+	public function beforeAction($event)
+	{
+		return $event->isValid;
+	}
+
+	/**
+	 * @param ActionEvent $event
+	 * @return boolean
+	 */
+	public function afterAction($event)
+	{
+
+	}
+}
\ No newline at end of file
diff --git a/framework/base/Application.php b/framework/base/Application.php
index 3dcbb26..31087e2 100644
--- a/framework/base/Application.php
+++ b/framework/base/Application.php
@@ -92,7 +92,6 @@ class Application extends Module
 
 	private $_runtimePath;
 	private $_ended = false;
-	private $_language;
 
 	/**
 	 * @var string Used to reserve memory for fatal error handler. This memory
diff --git a/framework/base/View.php b/framework/base/View.php
index 36c90ad..baa1d10 100644
--- a/framework/base/View.php
+++ b/framework/base/View.php
@@ -322,46 +322,42 @@ class View extends Component
 		$this->endWidget();
 	}
 
-	//
-	//	/**
-	//	 * Begins fragment caching.
-	//	 * This method will display cached content if it is available.
-	//	 * If not, it will start caching and would expect an [[endCache()]]
-	//	 * call to end the cache and save the content into cache.
-	//	 * A typical usage of fragment caching is as follows,
-	//	 *
-	//	 * ~~~
-	//	 * if($this->beginCache($id)) {
-	//	 *     // ...generate content here
-	//	 *     $this->endCache();
-	//	 * }
-	//	 * ~~~
-	//	 *
-	//	 * @param string $id a unique ID identifying the fragment to be cached.
-	//	 * @param array $properties initial property values for [[yii\widgets\OutputCache]]
-	//	 * @return boolean whether we need to generate content for caching. False if cached version is available.
-	//	 * @see endCache
-	//	 */
-	//	public function beginCache($id, $properties = array())
-	//	{
-	//		$properties['id'] = $id;
-	//		$cache = $this->beginWidget('yii\widgets\OutputCache', $properties);
-	//		if ($cache->getIsContentCached()) {
-	//			$this->endCache();
-	//			return false;
-	//		} else {
-	//			return true;
-	//		}
-	//	}
-	//
-	//	/**
-	//	 * Ends fragment caching.
-	//	 * This is an alias to [[endWidget()]]
-	//	 * @see beginCache
-	//	 */
-	//	public function endCache()
-	//	{
-	//		$this->endWidget();
-	//	}
-	//
+	/**
+	 * Begins fragment caching.
+	 * This method will display cached content if it is available.
+	 * If not, it will start caching and would expect an [[endCache()]]
+	 * call to end the cache and save the content into cache.
+	 * A typical usage of fragment caching is as follows,
+	 *
+	 * ~~~
+	 * if($this->beginCache($id)) {
+	 *     // ...generate content here
+	 *     $this->endCache();
+	 * }
+	 * ~~~
+	 *
+	 * @param string $id a unique ID identifying the fragment to be cached.
+	 * @param array $properties initial property values for [[\yii\widgets\OutputCache]]
+	 * @return boolean whether you should generate the content for caching.
+	 * False if the cached version is available.
+	 */
+	public function beginCache($id, $properties = array())
+	{
+		$properties['id'] = $id;
+		$cache = $this->beginWidget('yii\widgets\OutputCache', $properties);
+		if ($cache->getIsContentCached()) {
+			$this->endCache();
+			return false;
+		} else {
+			return true;
+		}
+	}
+
+	/**
+	 * Ends fragment caching.
+	 */
+	public function endCache()
+	{
+		$this->endWidget();
+	}
 }
\ No newline at end of file
diff --git a/framework/i18n/I18N.php b/framework/i18n/I18N.php
index ab87dfc..0409da3 100644
--- a/framework/i18n/I18N.php
+++ b/framework/i18n/I18N.php
@@ -4,91 +4,93 @@ namespace yii\i18n;
 
 use Yii;
 use yii\base\Component;
+use yii\base\InvalidConfigException;
 
 class I18N extends Component
 {
+	/**
+	 * @var array list of [[MessageSource]] configurations or objects. The array keys are message
+	 * categories, and the array values are the corresponding [[MessageSource]] objects or the configurations
+	 * for creating the [[MessageSource]] objects. The message categories can contain the wildcard '*' at the end
+	 * to match multiple categories with the same prefix. For example, 'app\*' matches both 'app\cat1' and 'app\cat2'.
+	 */
+	public $translations;
+
+	public function init()
+	{
+		if (!isset($this->translations['yii'])) {
+			$this->translations['yii'] = array(
+				'class' => 'yii\i18n\PhpMessageSource',
+				'sourceLanguage' => 'en_US',
+				'basePath' => '@yii/messages',
+			);
+		}
+		if (!isset($this->translations['app'])) {
+			$this->translations['app'] = array(
+				'class' => 'yii\i18n\PhpMessageSource',
+				'sourceLanguage' => 'en_US',
+				'basePath' => '@app/messages',
+			);
+		}
+	}
+
 	public function translate($message, $params = array(), $language = null)
 	{
 		if ($language === null) {
 			$language = Yii::$app->language;
 		}
 
-		if (strpos($message, '|') !== false && preg_match('/^([\w\-\.]+)\|(.*)/', $message, $matches)) {
+		// allow chars for category: word chars, ".", "-", "/","\"
+		if (strpos($message, '|') !== false && preg_match('/^([\w\-\\/\.\\\\]+)\|(.*)/', $message, $matches)) {
 			$category = $matches[1];
 			$message = $matches[2];
 		} else {
 			$category = 'app';
 		}
 
-//		$message = $this->getMessageSource($category)->translate($category, $message, $language);
-//
-//		if (!is_array($params)) {
-//			$params = array($params);
-//		}
-//
-//		if (isset($params[0])) {
-//			$message = $this->getPluralFormat($message, $params[0], $language);
-//			if (!isset($params['{n}'])) {
-//				$params['{n}'] = $params[0];
-//			}
-//			unset($params[0]);
-//		}
+		$message = $this->getMessageSource($category)->translate($category, $message, $language);
 
-		return $params === array() ? $message : strtr($message, $params);
-	}
+		if (!is_array($params)) {
+			$params = array($params);
+		}
 
-	public function getLocale($language)
-	{
+		if (isset($params[0])) {
+			$message = $this->getPluralForm($message, $params[0], $language);
+			if (!isset($params['{n}'])) {
+				$params['{n}'] = $params[0];
+			}
+			unset($params[0]);
+		}
 
+		return $params === array() ? $message : strtr($message, $params);
 	}
 
 	public function getMessageSource($category)
 	{
-		return $category === 'yii' ? $this->getMessages() : $this->getCoreMessages();
-	}
-
-	private $_coreMessages;
-	private $_messages;
-
-	public function getCoreMessages()
-	{
-		if (is_object($this->_coreMessages)) {
-			return $this->_coreMessages;
-		} elseif ($this->_coreMessages === null) {
-			return $this->_coreMessages = new PhpMessageSource(array(
-				'sourceLanguage' => 'en_US',
-				'basePath' => '@yii/messages',
-			));
+		if (isset($this->translations[$category])) {
+			$source = $this->translations[$category];
 		} else {
-			return $this->_coreMessages = Yii::createObject($this->_coreMessages);
+			// try wildcard matching
+			foreach ($this->translations as $pattern => $config) {
+				if (substr($pattern, -1) === '*' && strpos($category, rtrim($pattern, '*')) === 0) {
+					$source = $config;
+					break;
+				}
+			}
 		}
-	}
-
-	public function setCoreMessages($config)
-	{
-		$this->_coreMessages = $config;
-	}
-
-	public function getMessages()
-	{
-		if (is_object($this->_messages)) {
-			return $this->_messages;
-		} elseif ($this->_messages === null) {
-			return $this->_messages = new PhpMessageSource(array(
-				'sourceLanguage' => 'en_US',
-				'basePath' => '@app/messages',
-			));
+		if (isset($source)) {
+			return $source instanceof MessageSource ? $source : Yii::createObject($source);
 		} else {
-			return $this->_messages = Yii::createObject($this->_messages);
+			throw new InvalidConfigException("Unable to locate message source for category '$category'.");
 		}
 	}
 
-	public function setMessages($config)
+	public function getLocale($language)
 	{
-		$this->_messages = $config;
+
 	}
 
-	protected function getPluralFormat($message, $number, $language)
+	protected function getPluralForm($message, $number, $language)
 	{
 		if (strpos($message, '|') === false) {
 			return $message;
@@ -96,7 +98,7 @@ class I18N extends Component
 		$chunks = explode('|', $message);
 		$rules = $this->getLocale($language)->getPluralRules();
 		foreach ($rules as $i => $rule) {
-			if (isset($chunks[$i]) && self::evaluate($rule, $number)) {
+			if (isset($chunks[$i]) && $this->evaluate($rule, $number)) {
 				return $chunks[$i];
 			}
 		}
@@ -110,7 +112,7 @@ class I18N extends Component
 	 * @param mixed $n the number value
 	 * @return boolean the expression result
 	 */
-	protected static function evaluate($expression, $n)
+	protected function evaluate($expression, $n)
 	{
 		return @eval("return $expression;");
 	}
diff --git a/framework/i18n/PhpMessageSource.php b/framework/i18n/PhpMessageSource.php
index 5c7374a..6b12353 100644
--- a/framework/i18n/PhpMessageSource.php
+++ b/framework/i18n/PhpMessageSource.php
@@ -36,6 +36,18 @@ class PhpMessageSource extends MessageSource
 	 * the "messages" subdirectory of the application directory (e.g. "protected/messages").
 	 */
 	public $basePath = '@app/messages';
+	/**
+	 * @var array mapping between message categories and the corresponding message file paths.
+	 * The file paths are relative to [[basePath]]. For example,
+	 *
+	 * ~~~
+	 * array(
+	 *     'core' => 'core.php',
+	 *     'ext' => 'extensions.php',
+	 * )
+	 * ~~~
+	 */
+	public $fileMap;
 
 	/**
 	 * Loads the message translation for the specified language and category.
@@ -45,7 +57,14 @@ class PhpMessageSource extends MessageSource
 	 */
 	protected function loadMessages($category, $language)
 	{
-		$messageFile = Yii::getAlias($this->basePath) . "/$language/$category.php";
+		$messageFile = Yii::getAlias($this->basePath) . "/$language/";
+		if (isset($this->fileMap[$category])) {
+			$messageFile .= $this->fileMap[$category];
+		} elseif (($pos = strrpos($category, '\\')) !== false) {
+			$messageFile .= (substr($category, $pos) . '.php');
+		} else {
+			$messageFile .= "$category.php";
+		}
 		if (is_file($messageFile)) {
 			$messages = include($messageFile);
 			if (!is_array($messages)) {
@@ -53,7 +72,7 @@ class PhpMessageSource extends MessageSource
 			}
 			return $messages;
 		} else {
-			Yii::error("Message file not found: $messageFile", __CLASS__);
+			Yii::error("The message file for category '$category' does not exist: $messageFile", __CLASS__);
 			return array();
 		}
 	}
diff --git a/framework/logging/Target.php b/framework/logging/Target.php
index 32d12d8..b88e78d 100644
--- a/framework/logging/Target.php
+++ b/framework/logging/Target.php
@@ -192,8 +192,7 @@ abstract class Target extends \yii\base\Component
 
 			$matched = empty($this->categories);
 			foreach ($this->categories as $category) {
-				$prefix = rtrim($category, '*');
-				if (strpos($message[2], $prefix) === 0 && ($message[2] === $category || $prefix !== $category)) {
+				if ($message[2] === $category || substr($category, -1) === '*' && strpos($message[2], rtrim($category, '*')) === 0) {
 					$matched = true;
 					break;
 				}
diff --git a/framework/widgets/FragmentCache.php b/framework/widgets/FragmentCache.php
new file mode 100644
index 0000000..e6805f5
--- /dev/null
+++ b/framework/widgets/FragmentCache.php
@@ -0,0 +1,294 @@
+<?php
+/**
+ * @link http://www.yiiframework.com/
+ * @copyright Copyright (c) 2008 Yii Software LLC
+ * @license http://www.yiiframework.com/license/
+ */
+
+namespace yii\widgets;
+
+use yii\base\Widget;
+
+/**
+ * @author Qiang Xue <qiang.xue@gmail.com>
+ * @since 2.0
+ */
+class FragmentCache extends Widget
+{
+	/**
+	 * Prefix to the keys for storing cached data
+	 */
+	const CACHE_KEY_PREFIX = 'Yii.COutputCache.';
+
+	/**
+	 * @var string the ID of the cache application component. Defaults to 'cache' (the primary cache application component.)
+	 */
+	public $cacheID = 'cache';
+	/**
+	 * @var integer number of seconds that the data can remain in cache. Defaults to 60 seconds.
+	 * If it is 0, existing cached content would be removed from the cache.
+	 * If it is a negative value, the cache will be disabled (any existing cached content will
+	 * remain in the cache.)
+	 *
+	 * Note, if cache dependency changes or cache space is limited,
+	 * the data may be purged out of cache earlier.
+	 */
+	public $duration = 60;
+	/**
+	 * @var mixed the dependency that the cached content depends on.
+	 * This can be either an object implementing {@link ICacheDependency} interface or an array
+	 * specifying the configuration of the dependency object. For example,
+	 * <pre>
+	 * array(
+	 *     'class'=>'CDbCacheDependency',
+	 *     'sql'=>'SELECT MAX(lastModified) FROM Post',
+	 * )
+	 * </pre>
+	 * would make the output cache depends on the last modified time of all posts.
+	 * If any post has its modification time changed, the cached content would be invalidated.
+	 */
+	public $dependency;
+	/**
+	 * @var boolean whether the content being cached should be differentiated according to route.
+	 * A route consists of the requested controller ID and action ID.
+	 * Defaults to true.
+	 */
+	public $varyByRoute = true;
+	/**
+	 * @var boolean whether the content being cached should be differentiated according to user's language.
+	 * A language is retrieved via Yii::app()->language.
+	 * Defaults to false.
+	 * @since 1.1.14
+	 */
+	public $varyByLanguage = false;
+	/**
+	 * @var array list of GET parameters that should participate in cache key calculation.
+	 * By setting this property, the output cache will use different cached data
+	 * for each different set of GET parameter values.
+	 */
+	public $varyByParam;
+	/**
+	 * @var string a PHP expression whose result is used in the cache key calculation.
+	 * By setting this property, the output cache will use different cached data
+	 * for each different expression result.
+	 * The expression can also be a valid PHP callback,
+	 * including class method name (array(ClassName/Object, MethodName)),
+	 * or anonymous function (PHP 5.3.0+). The function/method signature should be as follows:
+	 * <pre>
+	 * function foo($cache) { ... }
+	 * </pre>
+	 * where $cache refers to the output cache component.
+	 */
+	public $varyByExpression;
+	/**
+	 * @var array list of request types (e.g. GET, POST) for which the cache should be enabled only.
+	 * Defaults to null, meaning all request types.
+	 */
+	public $requestTypes;
+
+	private $_key;
+	private $_cache;
+	private $_contentCached;
+	private $_content;
+	private $_actions;
+
+	/**
+	 * Marks the start of content to be cached.
+	 * Content displayed after this method call and before {@link endCache()}
+	 * will be captured and saved in cache.
+	 * This method does nothing if valid content is already found in cache.
+	 */
+	public function init()
+	{
+		if ($this->getIsContentCached()) {
+			$this->replayActions();
+		} elseif ($this->_cache !== null) {
+			$this->getController()->getCachingStack()->push($this);
+			ob_start();
+			ob_implicit_flush(false);
+		}
+	}
+
+	/**
+	 * Marks the end of content to be cached.
+	 * Content displayed before this method call and after {@link init()}
+	 * will be captured and saved in cache.
+	 * This method does nothing if valid content is already found in cache.
+	 */
+	public function run()
+	{
+		if ($this->getIsContentCached()) {
+			if ($this->getController()->isCachingStackEmpty()) {
+				echo $this->getController()->processDynamicOutput($this->_content);
+			} else {
+				echo $this->_content;
+			}
+		} elseif ($this->_cache !== null) {
+			$this->_content = ob_get_clean();
+			$this->getController()->getCachingStack()->pop();
+			$data = array($this->_content, $this->_actions);
+			if (is_array($this->dependency)) {
+				$this->dependency = Yii::createComponent($this->dependency);
+			}
+			$this->_cache->set($this->getCacheKey(), $data, $this->duration, $this->dependency);
+
+			if ($this->getController()->isCachingStackEmpty()) {
+				echo $this->getController()->processDynamicOutput($this->_content);
+			} else {
+				echo $this->_content;
+			}
+		}
+	}
+
+	/**
+	 * @return boolean whether the content can be found from cache
+	 */
+	public function getIsContentCached()
+	{
+		if ($this->_contentCached !== null) {
+			return $this->_contentCached;
+		} else {
+			return $this->_contentCached = $this->checkContentCache();
+		}
+	}
+
+	/**
+	 * Looks for content in cache.
+	 * @return boolean whether the content is found in cache.
+	 */
+	protected function checkContentCache()
+	{
+		if ((empty($this->requestTypes) || in_array(Yii::app()->getRequest()->getRequestType(), $this->requestTypes))
+			&& ($this->_cache = $this->getCache()) !== null
+		) {
+			if ($this->duration > 0 && ($data = $this->_cache->get($this->getCacheKey())) !== false) {
+				$this->_content = $data[0];
+				$this->_actions = $data[1];
+				return true;
+			}
+			if ($this->duration == 0) {
+				$this->_cache->delete($this->getCacheKey());
+			}
+			if ($this->duration <= 0) {
+				$this->_cache = null;
+			}
+		}
+		return false;
+	}
+
+	/**
+	 * @return ICache the cache used for caching the content.
+	 */
+	protected function getCache()
+	{
+		return Yii::app()->getComponent($this->cacheID);
+	}
+
+	/**
+	 * Caclulates the base cache key.
+	 * The calculated key will be further variated in {@link getCacheKey}.
+	 * Derived classes may override this method if more variations are needed.
+	 * @return string basic cache key without variations
+	 */
+	protected function getBaseCacheKey()
+	{
+		return self::CACHE_KEY_PREFIX . $this->getId() . '.';
+	}
+
+	/**
+	 * Calculates the cache key.
+	 * The key is calculated based on {@link getBaseCacheKey} and other factors, including
+	 * {@link varyByRoute}, {@link varyByParam}, {@link varyBySession} and {@link varyByLanguage}.
+	 * @return string cache key
+	 */
+	protected function getCacheKey()
+	{
+		if ($this->_key !== null) {
+			return $this->_key;
+		} else {
+			$key = $this->getBaseCacheKey() . '.';
+			if ($this->varyByRoute) {
+				$controller = $this->getController();
+				$key .= $controller->getUniqueId() . '/';
+				if (($action = $controller->getAction()) !== null) {
+					$key .= $action->getId();
+				}
+			}
+			$key .= '.';
+
+			if ($this->varyBySession) {
+				$key .= Yii::app()->getSession()->getSessionID();
+			}
+			$key .= '.';
+
+			if (is_array($this->varyByParam) && isset($this->varyByParam[0])) {
+				$params = array();
+				foreach ($this->varyByParam as $name) {
+					if (isset($_GET[$name])) {
+						$params[$name] = $_GET[$name];
+					} else {
+						$params[$name] = '';
+					}
+				}
+				$key .= serialize($params);
+			}
+			$key .= '.';
+
+			if ($this->varyByExpression !== null) {
+				$key .= $this->evaluateExpression($this->varyByExpression);
+			}
+			$key .= '.';
+
+			if ($this->varyByLanguage) {
+				$key .= Yii::app()->language;
+			}
+			$key .= '.';
+
+			return $this->_key = $key;
+		}
+	}
+
+	/**
+	 * Records a method call when this output cache is in effect.
+	 * When the content is served from the output cache, the recorded
+	 * method will be re-invoked.
+	 * @param string $context a property name of the controller. The property should refer to an object
+	 * whose method is being recorded. If empty it means the controller itself.
+	 * @param string $method the method name
+	 * @param array $params parameters passed to the method
+	 */
+	public function recordAction($context, $method, $params)
+	{
+		$this->_actions[] = array($context, $method, $params);
+	}
+
+	/**
+	 * Replays the recorded method calls.
+	 */
+	protected function replayActions()
+	{
+		if (empty($this->_actions)) {
+			return;
+		}
+		$controller = $this->getController();
+		$cs = Yii::app()->getClientScript();
+		foreach ($this->_actions as $action) {
+			if ($action[0] === 'clientScript') {
+				$object = $cs;
+			} elseif ($action[0] === '') {
+				$object = $controller;
+			} else {
+				$object = $controller->{$action[0]};
+			}
+			if (method_exists($object, $action[1])) {
+				call_user_func_array(array($object, $action[1]), $action[2]);
+			} elseif ($action[0] === '' && function_exists($action[1])) {
+				call_user_func_array($action[1], $action[2]);
+			} else {
+				throw new CException(Yii::t('yii', 'Unable to replay the action "{object}.{method}". The method does not exist.',
+					array('object' => $action[0],
+						'method' => $action[1])));
+			}
+		}
+	}
+}
\ No newline at end of file
diff --git a/todo.md b/todo.md
index 4d5343a..f102f41 100644
--- a/todo.md
+++ b/todo.md
@@ -39,7 +39,6 @@ memo
 		* consider to be released as a separate tool for user app docs
 - i18n
 	* consider using PHP built-in support and data
-	* message translations, choice format
 	* formatting: number and date
 	* parsing??
 	* make dates/date patterns uniform application-wide including JUI, formats etc.
--
libgit2 0.27.1