This commit is contained in:
2020-02-01 16:47:12 +07:00
commit 4c619ad6e6
16739 changed files with 3329179 additions and 0 deletions

View File

@@ -0,0 +1,189 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\i18n;
use Yii;
use yii\base\InvalidConfigException;
use yii\caching\CacheInterface;
use yii\db\Connection;
use yii\db\Expression;
use yii\db\Query;
use yii\di\Instance;
use yii\helpers\ArrayHelper;
/**
* DbMessageSource extends [[MessageSource]] and represents a message source that stores translated
* messages in database.
*
* The database must contain the following two tables: source_message and message.
*
* The `source_message` table stores the messages to be translated, and the `message` table stores
* the translated messages. The name of these two tables can be customized by setting [[sourceMessageTable]]
* and [[messageTable]], respectively.
*
* The database connection is specified by [[db]]. Database schema could be initialized by applying migration:
*
* ```
* yii migrate --migrationPath=@yii/i18n/migrations/
* ```
*
* If you don't want to use migration and need SQL instead, files for all databases are in migrations directory.
*
* @author resurtm <resurtm@gmail.com>
* @since 2.0
*/
class DbMessageSource extends MessageSource
{
/**
* Prefix which would be used when generating cache key.
* @deprecated This constant has never been used and will be removed in 2.1.0.
*/
const CACHE_KEY_PREFIX = 'DbMessageSource';
/**
* @var Connection|array|string the DB connection object or the application component ID of the DB connection.
*
* After the DbMessageSource object is created, if you want to change this property, you should only assign
* it with a DB connection object.
*
* Starting from version 2.0.2, this can also be a configuration array for creating the object.
*/
public $db = 'db';
/**
* @var CacheInterface|array|string the cache object or the application component ID of the cache object.
* The messages data will be cached using this cache object.
* Note, that to enable caching you have to set [[enableCaching]] to `true`, otherwise setting this property has no effect.
*
* After the DbMessageSource object is created, if you want to change this property, you should only assign
* it with a cache object.
*
* Starting from version 2.0.2, this can also be a configuration array for creating the object.
* @see cachingDuration
* @see enableCaching
*/
public $cache = 'cache';
/**
* @var string the name of the source message table.
*/
public $sourceMessageTable = '{{%source_message}}';
/**
* @var string the name of the translated message table.
*/
public $messageTable = '{{%message}}';
/**
* @var int the time in seconds that the messages can remain valid in cache.
* Use 0 to indicate that the cached data will never expire.
* @see enableCaching
*/
public $cachingDuration = 0;
/**
* @var bool whether to enable caching translated messages
*/
public $enableCaching = false;
/**
* Initializes the DbMessageSource component.
* This method will initialize the [[db]] property to make sure it refers to a valid DB connection.
* Configured [[cache]] component would also be initialized.
* @throws InvalidConfigException if [[db]] is invalid or [[cache]] is invalid.
*/
public function init()
{
parent::init();
$this->db = Instance::ensure($this->db, Connection::className());
if ($this->enableCaching) {
$this->cache = Instance::ensure($this->cache, 'yii\caching\CacheInterface');
}
}
/**
* Loads the message translation for the specified language and category.
* If translation for specific locale code such as `en-US` isn't found it
* tries more generic `en`.
*
* @param string $category the message category
* @param string $language the target language
* @return array the loaded messages. The keys are original messages, and the values
* are translated messages.
*/
protected function loadMessages($category, $language)
{
if ($this->enableCaching) {
$key = [
__CLASS__,
$category,
$language,
];
$messages = $this->cache->get($key);
if ($messages === false) {
$messages = $this->loadMessagesFromDb($category, $language);
$this->cache->set($key, $messages, $this->cachingDuration);
}
return $messages;
}
return $this->loadMessagesFromDb($category, $language);
}
/**
* Loads the messages from database.
* You may override this method to customize the message storage in the database.
* @param string $category the message category.
* @param string $language the target language.
* @return array the messages loaded from database.
*/
protected function loadMessagesFromDb($category, $language)
{
$mainQuery = (new Query())->select(['message' => 't1.message', 'translation' => 't2.translation'])
->from(['t1' => $this->sourceMessageTable, 't2' => $this->messageTable])
->where([
't1.id' => new Expression('[[t2.id]]'),
't1.category' => $category,
't2.language' => $language,
]);
$fallbackLanguage = substr($language, 0, 2);
$fallbackSourceLanguage = substr($this->sourceLanguage, 0, 2);
if ($fallbackLanguage !== $language) {
$mainQuery->union($this->createFallbackQuery($category, $language, $fallbackLanguage), true);
} elseif ($language === $fallbackSourceLanguage) {
$mainQuery->union($this->createFallbackQuery($category, $language, $fallbackSourceLanguage), true);
}
$messages = $mainQuery->createCommand($this->db)->queryAll();
return ArrayHelper::map($messages, 'message', 'translation');
}
/**
* The method builds the [[Query]] object for the fallback language messages search.
* Normally is called from [[loadMessagesFromDb]].
*
* @param string $category the message category
* @param string $language the originally requested language
* @param string $fallbackLanguage the target fallback language
* @return Query
* @see loadMessagesFromDb
* @since 2.0.7
*/
protected function createFallbackQuery($category, $language, $fallbackLanguage)
{
return (new Query())->select(['message' => 't1.message', 'translation' => 't2.translation'])
->from(['t1' => $this->sourceMessageTable, 't2' => $this->messageTable])
->where([
't1.id' => new Expression('[[t2.id]]'),
't1.category' => $category,
't2.language' => $fallbackLanguage,
])->andWhere([
'NOT IN', 't2.id', (new Query())->select('[[id]]')->from($this->messageTable)->where(['language' => $language]),
]);
}
}

1740
vendor/yiisoft/yii2/i18n/Formatter.php vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,37 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\i18n;
use yii\base\Component;
/**
* GettextFile is the base class for representing a Gettext message file.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
abstract class GettextFile extends Component
{
/**
* Loads messages from a file.
* @param string $filePath file path
* @param string $context message context
* @return array message translations. Array keys are source messages and array values are translated messages:
* source message => translated message.
*/
abstract public function load($filePath, $context);
/**
* Saves messages to a file.
* @param string $filePath file path
* @param array $messages message translations. Array keys are source messages and array values are
* translated messages: source message => translated message. Note if the message has a context,
* the message ID must be prefixed with the context with chr(4) as the separator.
*/
abstract public function save($filePath, $messages);
}

View File

@@ -0,0 +1,169 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\i18n;
use Yii;
/**
* GettextMessageSource represents a message source that is based on GNU Gettext.
*
* Each GettextMessageSource instance represents the message translations
* for a single domain. And each message category represents a message context
* in Gettext. Translated messages are stored as either a MO or PO file,
* depending on the [[useMoFile]] property value.
*
* All translations are saved under the [[basePath]] directory.
*
* Translations in one language are kept as MO or PO files under an individual
* subdirectory whose name is the language ID. The file name is specified via
* [[catalog]] property, which defaults to 'messages'.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
class GettextMessageSource extends MessageSource
{
const MO_FILE_EXT = '.mo';
const PO_FILE_EXT = '.po';
/**
* @var string
*/
public $basePath = '@app/messages';
/**
* @var string
*/
public $catalog = 'messages';
/**
* @var bool
*/
public $useMoFile = true;
/**
* @var bool
*/
public $useBigEndian = false;
/**
* Loads the message translation for the specified $language and $category.
* If translation for specific locale code such as `en-US` isn't found it
* tries more generic `en`. When both are present, the `en-US` messages will be merged
* over `en`. See [[loadFallbackMessages]] for details.
* If the $language is less specific than [[sourceLanguage]], the method will try to
* load the messages for [[sourceLanguage]]. For example: [[sourceLanguage]] is `en-GB`,
* $language is `en`. The method will load the messages for `en` and merge them over `en-GB`.
*
* @param string $category the message category
* @param string $language the target language
* @return array the loaded messages. The keys are original messages, and the values are translated messages.
* @see loadFallbackMessages
* @see sourceLanguage
*/
protected function loadMessages($category, $language)
{
$messageFile = $this->getMessageFilePath($language);
$messages = $this->loadMessagesFromFile($messageFile, $category);
$fallbackLanguage = substr($language, 0, 2);
$fallbackSourceLanguage = substr($this->sourceLanguage, 0, 2);
if ($fallbackLanguage !== $language) {
$messages = $this->loadFallbackMessages($category, $fallbackLanguage, $messages, $messageFile);
} elseif ($language === $fallbackSourceLanguage) {
$messages = $this->loadFallbackMessages($category, $this->sourceLanguage, $messages, $messageFile);
} else {
if ($messages === null) {
Yii::error("The message file for category '$category' does not exist: $messageFile", __METHOD__);
}
}
return (array) $messages;
}
/**
* The method is normally called by [[loadMessages]] to load the fallback messages for the language.
* Method tries to load the $category messages for the $fallbackLanguage and adds them to the $messages array.
*
* @param string $category the message category
* @param string $fallbackLanguage the target fallback language
* @param array $messages the array of previously loaded translation messages.
* The keys are original messages, and the values are the translated messages.
* @param string $originalMessageFile the path to the file with messages. Used to log an error message
* in case when no translations were found.
* @return array the loaded messages. The keys are original messages, and the values are the translated messages.
* @since 2.0.7
*/
protected function loadFallbackMessages($category, $fallbackLanguage, $messages, $originalMessageFile)
{
$fallbackMessageFile = $this->getMessageFilePath($fallbackLanguage);
$fallbackMessages = $this->loadMessagesFromFile($fallbackMessageFile, $category);
if (
$messages === null && $fallbackMessages === null
&& $fallbackLanguage !== $this->sourceLanguage
&& $fallbackLanguage !== substr($this->sourceLanguage, 0, 2)
) {
Yii::error("The message file for category '$category' does not exist: $originalMessageFile "
. "Fallback file does not exist as well: $fallbackMessageFile", __METHOD__);
} elseif (empty($messages)) {
return $fallbackMessages;
} elseif (!empty($fallbackMessages)) {
foreach ($fallbackMessages as $key => $value) {
if (!empty($value) && empty($messages[$key])) {
$messages[$key] = $fallbackMessages[$key];
}
}
}
return (array) $messages;
}
/**
* Returns message file path for the specified language and category.
*
* @param string $language the target language
* @return string path to message file
*/
protected function getMessageFilePath($language)
{
$messageFile = Yii::getAlias($this->basePath) . '/' . $language . '/' . $this->catalog;
if ($this->useMoFile) {
$messageFile .= self::MO_FILE_EXT;
} else {
$messageFile .= self::PO_FILE_EXT;
}
return $messageFile;
}
/**
* Loads the message translation for the specified language and category or returns null if file doesn't exist.
*
* @param string $messageFile path to message file
* @param string $category the message category
* @return array|null array of messages or null if file not found
*/
protected function loadMessagesFromFile($messageFile, $category)
{
if (is_file($messageFile)) {
if ($this->useMoFile) {
$gettextFile = new GettextMoFile(['useBigEndian' => $this->useBigEndian]);
} else {
$gettextFile = new GettextPoFile();
}
$messages = $gettextFile->load($messageFile, $category);
if (!is_array($messages)) {
$messages = [];
}
return $messages;
}
return null;
}
}

View File

@@ -0,0 +1,275 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\i18n;
use yii\base\Exception;
/**
* GettextMoFile represents an MO Gettext message file.
*
* This class is written by adapting Michael's Gettext_MO class in PEAR.
* Please refer to the following license terms.
*
* Copyright (c) 2004-2005, Michael Wallner <mike@iworks.at>.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
class GettextMoFile extends GettextFile
{
/**
* @var bool whether to use big-endian when reading and writing an integer.
*/
public $useBigEndian = false;
/**
* Loads messages from an MO file.
* @param string $filePath file path
* @param string $context message context
* @return array message translations. Array keys are source messages and array values are translated messages:
* source message => translated message.
* @throws Exception if unable to read the MO file
*/
public function load($filePath, $context)
{
if (false === ($fileHandle = @fopen($filePath, 'rb'))) {
throw new Exception('Unable to read file "' . $filePath . '".');
}
if (false === @flock($fileHandle, LOCK_SH)) {
throw new Exception('Unable to lock file "' . $filePath . '" for reading.');
}
// magic
$array = unpack('c', $this->readBytes($fileHandle, 4));
$magic = current($array);
if ($magic == -34) {
$this->useBigEndian = false;
} elseif ($magic == -107) {
$this->useBigEndian = true;
} else {
throw new Exception('Invalid MO file: ' . $filePath . ' (magic: ' . $magic . ').');
}
// revision
$revision = $this->readInteger($fileHandle);
if ($revision !== 0) {
throw new Exception('Invalid MO file revision: ' . $revision . '.');
}
$count = $this->readInteger($fileHandle);
$sourceOffset = $this->readInteger($fileHandle);
$targetOffset = $this->readInteger($fileHandle);
$sourceLengths = [];
$sourceOffsets = [];
fseek($fileHandle, $sourceOffset);
for ($i = 0; $i < $count; ++$i) {
$sourceLengths[] = $this->readInteger($fileHandle);
$sourceOffsets[] = $this->readInteger($fileHandle);
}
$targetLengths = [];
$targetOffsets = [];
fseek($fileHandle, $targetOffset);
for ($i = 0; $i < $count; ++$i) {
$targetLengths[] = $this->readInteger($fileHandle);
$targetOffsets[] = $this->readInteger($fileHandle);
}
$messages = [];
for ($i = 0; $i < $count; ++$i) {
$id = $this->readString($fileHandle, $sourceLengths[$i], $sourceOffsets[$i]);
$separatorPosition = strpos($id, chr(4));
if ((!$context && $separatorPosition === false) || ($context && $separatorPosition !== false && strncmp($id, $context, $separatorPosition) === 0)) {
if ($separatorPosition !== false) {
$id = substr($id, $separatorPosition + 1);
}
$message = $this->readString($fileHandle, $targetLengths[$i], $targetOffsets[$i]);
$messages[$id] = $message;
}
}
@flock($fileHandle, LOCK_UN);
@fclose($fileHandle);
return $messages;
}
/**
* Saves messages to an MO file.
* @param string $filePath file path
* @param array $messages message translations. Array keys are source messages and array values are
* translated messages: source message => translated message. Note if the message has a context,
* the message ID must be prefixed with the context with chr(4) as the separator.
* @throws Exception if unable to save the MO file
*/
public function save($filePath, $messages)
{
if (false === ($fileHandle = @fopen($filePath, 'wb'))) {
throw new Exception('Unable to write file "' . $filePath . '".');
}
if (false === @flock($fileHandle, LOCK_EX)) {
throw new Exception('Unable to lock file "' . $filePath . '" for reading.');
}
// magic
if ($this->useBigEndian) {
$this->writeBytes($fileHandle, pack('c*', 0x95, 0x04, 0x12, 0xde)); // -107
} else {
$this->writeBytes($fileHandle, pack('c*', 0xde, 0x12, 0x04, 0x95)); // -34
}
// revision
$this->writeInteger($fileHandle, 0);
// message count
$messageCount = count($messages);
$this->writeInteger($fileHandle, $messageCount);
// offset of source message table
$offset = 28;
$this->writeInteger($fileHandle, $offset);
$offset += $messageCount * 8;
$this->writeInteger($fileHandle, $offset);
// hashtable size, omitted
$this->writeInteger($fileHandle, 0);
$offset += $messageCount * 8;
$this->writeInteger($fileHandle, $offset);
// length and offsets for source messages
foreach (array_keys($messages) as $id) {
$length = strlen($id);
$this->writeInteger($fileHandle, $length);
$this->writeInteger($fileHandle, $offset);
$offset += $length + 1;
}
// length and offsets for target messages
foreach ($messages as $message) {
$length = strlen($message);
$this->writeInteger($fileHandle, $length);
$this->writeInteger($fileHandle, $offset);
$offset += $length + 1;
}
// source messages
foreach (array_keys($messages) as $id) {
$this->writeString($fileHandle, $id);
}
// target messages
foreach ($messages as $message) {
$this->writeString($fileHandle, $message);
}
@flock($fileHandle, LOCK_UN);
@fclose($fileHandle);
}
/**
* Reads one or several bytes.
* @param resource $fileHandle to read from
* @param int $byteCount to be read
* @return string bytes
*/
protected function readBytes($fileHandle, $byteCount = 1)
{
if ($byteCount > 0) {
return fread($fileHandle, $byteCount);
}
return null;
}
/**
* Write bytes.
* @param resource $fileHandle to write to
* @param string $bytes to be written
* @return int how many bytes are written
*/
protected function writeBytes($fileHandle, $bytes)
{
return fwrite($fileHandle, $bytes);
}
/**
* Reads a 4-byte integer.
* @param resource $fileHandle to read from
* @return int the result
*/
protected function readInteger($fileHandle)
{
$array = unpack($this->useBigEndian ? 'N' : 'V', $this->readBytes($fileHandle, 4));
return current($array);
}
/**
* Writes a 4-byte integer.
* @param resource $fileHandle to write to
* @param int $integer to be written
* @return int how many bytes are written
*/
protected function writeInteger($fileHandle, $integer)
{
return $this->writeBytes($fileHandle, pack($this->useBigEndian ? 'N' : 'V', (int) $integer));
}
/**
* Reads a string.
* @param resource $fileHandle file handle
* @param int $length of the string
* @param int $offset of the string in the file. If null, it reads from the current position.
* @return string the result
*/
protected function readString($fileHandle, $length, $offset = null)
{
if ($offset !== null) {
fseek($fileHandle, $offset);
}
return $this->readBytes($fileHandle, $length);
}
/**
* Writes a string.
* @param resource $fileHandle to write to
* @param string $string to be written
* @return int how many bytes are written
*/
protected function writeString($fileHandle, $string)
{
return $this->writeBytes($fileHandle, $string . "\0");
}
}

View File

@@ -0,0 +1,113 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\i18n;
use Yii;
/**
* GettextPoFile represents a PO Gettext message file.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
class GettextPoFile extends GettextFile
{
/**
* Loads messages from a PO file.
* @param string $filePath file path
* @param string $context message context
* @return array message translations. Array keys are source messages and array values are translated messages:
* source message => translated message.
*/
public function load($filePath, $context)
{
$pattern = '/(msgctxt\s+"(.*?(?<!\\\\))")?\s+' // context
. 'msgid\s+((?:".*(?<!\\\\)"\s*)+)\s+' // message ID, i.e. original string
. 'msgstr\s+((?:".*(?<!\\\\)"\s*)+)/'; // translated string
$content = file_get_contents($filePath);
$matches = [];
$matchCount = preg_match_all($pattern, $content, $matches);
$messages = [];
for ($i = 0; $i < $matchCount; ++$i) {
if ($matches[2][$i] === $context) {
$id = $this->decode($matches[3][$i]);
$message = $this->decode($matches[4][$i]);
$messages[$id] = $message;
}
}
return $messages;
}
/**
* Saves messages to a PO file.
* @param string $filePath file path
* @param array $messages message translations. Array keys are source messages and array values are
* translated messages: source message => translated message. Note if the message has a context,
* the message ID must be prefixed with the context with chr(4) as the separator.
*/
public function save($filePath, $messages)
{
$language = str_replace('-', '_', basename(dirname($filePath)));
$headers = [
'msgid ""',
'msgstr ""',
'"Project-Id-Version: \n"',
'"POT-Creation-Date: \n"',
'"PO-Revision-Date: \n"',
'"Last-Translator: \n"',
'"Language-Team: \n"',
'"Language: ' . $language . '\n"',
'"MIME-Version: 1.0\n"',
'"Content-Type: text/plain; charset=' . Yii::$app->charset . '\n"',
'"Content-Transfer-Encoding: 8bit\n"',
];
$content = implode("\n", $headers) . "\n\n";
foreach ($messages as $id => $message) {
$separatorPosition = strpos($id, chr(4));
if ($separatorPosition !== false) {
$content .= 'msgctxt "' . substr($id, 0, $separatorPosition) . "\"\n";
$id = substr($id, $separatorPosition + 1);
}
$content .= 'msgid "' . $this->encode($id) . "\"\n";
$content .= 'msgstr "' . $this->encode($message) . "\"\n\n";
}
file_put_contents($filePath, $content);
}
/**
* Encodes special characters in a message.
* @param string $string message to be encoded
* @return string the encoded message
*/
protected function encode($string)
{
return str_replace(
['"', "\n", "\t", "\r"],
['\\"', '\\n', '\\t', '\\r'],
$string
);
}
/**
* Decodes special characters in a message.
* @param string $string message to be decoded
* @return string the decoded message
*/
protected function decode($string)
{
$string = preg_replace(
['/"\s+"/', '/\\\\n/', '/\\\\r/', '/\\\\t/', '/\\\\"/'],
['', "\n", "\r", "\t", '"'],
$string
);
return substr(rtrim($string), 1, -1);
}
}

203
vendor/yiisoft/yii2/i18n/I18N.php vendored Normal file
View File

@@ -0,0 +1,203 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\i18n;
use Yii;
use yii\base\Component;
use yii\base\InvalidConfigException;
/**
* I18N provides features related with internationalization (I18N) and localization (L10N).
*
* I18N is configured as an application component in [[\yii\base\Application]] by default.
* You can access that instance via `Yii::$app->i18n`.
*
* @property MessageFormatter $messageFormatter The message formatter to be used to format message via ICU
* message format. Note that the type of this property differs in getter and setter. See
* [[getMessageFormatter()]] and [[setMessageFormatter()]] for details.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
class I18N extends Component
{
/**
* @var array list of [[MessageSource]] configurations or objects. The array keys are message
* category patterns, and the array values are the corresponding [[MessageSource]] objects or the configurations
* for creating the [[MessageSource]] objects.
*
* The message category patterns 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`.
*
* The `*` category pattern will match all categories that do not match any other category patterns.
*
* This property may be modified on the fly by extensions who want to have their own message sources
* registered under their own namespaces.
*
* The category `yii` and `app` are always defined. The former refers to the messages used in the Yii core
* framework code, while the latter refers to the default message category for custom application code.
* By default, both of these categories use [[PhpMessageSource]] and the corresponding message files are
* stored under `@yii/messages` and `@app/messages`, respectively.
*
* You may override the configuration of both categories.
*/
public $translations;
/**
* Initializes the component by configuring the default message categories.
*/
public function init()
{
parent::init();
if (!isset($this->translations['yii']) && !isset($this->translations['yii*'])) {
$this->translations['yii'] = [
'class' => 'yii\i18n\PhpMessageSource',
'sourceLanguage' => 'en-US',
'basePath' => '@yii/messages',
];
}
if (!isset($this->translations['app']) && !isset($this->translations['app*'])) {
$this->translations['app'] = [
'class' => 'yii\i18n\PhpMessageSource',
'sourceLanguage' => Yii::$app->sourceLanguage,
'basePath' => '@app/messages',
];
}
}
/**
* Translates a message to the specified language.
*
* After translation the message will be formatted using [[MessageFormatter]] if it contains
* ICU message format and `$params` are not empty.
*
* @param string $category the message category.
* @param string $message the message to be translated.
* @param array $params the parameters that will be used to replace the corresponding placeholders in the message.
* @param string $language the language code (e.g. `en-US`, `en`).
* @return string the translated and formatted message.
*/
public function translate($category, $message, $params, $language)
{
$messageSource = $this->getMessageSource($category);
$translation = $messageSource->translate($category, $message, $language);
if ($translation === false) {
return $this->format($message, $params, $messageSource->sourceLanguage);
}
return $this->format($translation, $params, $language);
}
/**
* Formats a message using [[MessageFormatter]].
*
* @param string $message the message to be formatted.
* @param array $params the parameters that will be used to replace the corresponding placeholders in the message.
* @param string $language the language code (e.g. `en-US`, `en`).
* @return string the formatted message.
*/
public function format($message, $params, $language)
{
$params = (array) $params;
if ($params === []) {
return $message;
}
if (preg_match('~{\s*[\w.]+\s*,~u', $message)) {
$formatter = $this->getMessageFormatter();
$result = $formatter->format($message, $params, $language);
if ($result === false) {
$errorMessage = $formatter->getErrorMessage();
Yii::warning("Formatting message for language '$language' failed with error: $errorMessage. The message being formatted was: $message.", __METHOD__);
return $message;
}
return $result;
}
$p = [];
foreach ($params as $name => $value) {
$p['{' . $name . '}'] = $value;
}
return strtr($message, $p);
}
/**
* @var string|array|MessageFormatter
*/
private $_messageFormatter;
/**
* Returns the message formatter instance.
* @return MessageFormatter the message formatter to be used to format message via ICU message format.
*/
public function getMessageFormatter()
{
if ($this->_messageFormatter === null) {
$this->_messageFormatter = new MessageFormatter();
} elseif (is_array($this->_messageFormatter) || is_string($this->_messageFormatter)) {
$this->_messageFormatter = Yii::createObject($this->_messageFormatter);
}
return $this->_messageFormatter;
}
/**
* @param string|array|MessageFormatter $value the message formatter to be used to format message via ICU message format.
* Can be given as array or string configuration that will be given to [[Yii::createObject]] to create an instance
* or a [[MessageFormatter]] instance.
*/
public function setMessageFormatter($value)
{
$this->_messageFormatter = $value;
}
/**
* Returns the message source for the given category.
* @param string $category the category name.
* @return MessageSource the message source for the given category.
* @throws InvalidConfigException if there is no message source available for the specified category.
*/
public function getMessageSource($category)
{
if (isset($this->translations[$category])) {
$source = $this->translations[$category];
if ($source instanceof MessageSource) {
return $source;
}
return $this->translations[$category] = Yii::createObject($source);
}
// try wildcard matching
foreach ($this->translations as $pattern => $source) {
if (strpos($pattern, '*') > 0 && strpos($category, rtrim($pattern, '*')) === 0) {
if ($source instanceof MessageSource) {
return $source;
}
return $this->translations[$category] = $this->translations[$pattern] = Yii::createObject($source);
}
}
// match '*' in the last
if (isset($this->translations['*'])) {
$source = $this->translations['*'];
if ($source instanceof MessageSource) {
return $source;
}
return $this->translations[$category] = $this->translations['*'] = Yii::createObject($source);
}
throw new InvalidConfigException("Unable to locate message source for category '$category'.");
}
}

64
vendor/yiisoft/yii2/i18n/Locale.php vendored Normal file
View File

@@ -0,0 +1,64 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\i18n;
use Yii;
use yii\base\Component;
use yii\base\InvalidConfigException;
/**
* Locale provides various locale information via convenient methods.
*
* The class requires [PHP intl extension](http://php.net/manual/en/book.intl.php) to be installed.
* @since 2.0.14
*
* @property string $currencySymbol This property is read-only.
*
*/
class Locale extends Component
{
/**
* @var string the locale ID.
* If not set, [[\yii\base\Application::language]] will be used.
*/
public $locale;
/**
* {@inheritdoc}
*/
public function init()
{
if (!extension_loaded('intl')) {
throw new InvalidConfigException('Locale component requires PHP intl extension to be installed.');
}
if ($this->locale === null) {
$this->locale = Yii::$app->language;
}
}
/**
* Returns a currency symbol
*
* @param string $currencyCode the 3-letter ISO 4217 currency code to get symbol for. If null,
* method will attempt using currency code from [[locale]].
* @return string
*/
public function getCurrencySymbol($currencyCode = null)
{
$locale = $this->locale;
if ($currencyCode !== null) {
$locale .= '@currency=' . $currencyCode;
}
$formatter = new \NumberFormatter($locale, \NumberFormatter::CURRENCY);
return $formatter->getSymbol(\NumberFormatter::CURRENCY_SYMBOL);
}
}

View File

@@ -0,0 +1,440 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\i18n;
use Yii;
use yii\base\Component;
use yii\base\NotSupportedException;
/**
* MessageFormatter allows formatting messages via [ICU message format](http://userguide.icu-project.org/formatparse/messages).
*
* This class enhances the message formatter class provided by the PHP intl extension.
*
* The following enhancements are provided:
*
* - It accepts named arguments and mixed numeric and named arguments.
* - Issues no error when an insufficient number of arguments have been provided. Instead, the placeholders will not be
* substituted.
* - Fixes PHP 5.5 weird placeholder replacement in case no arguments are provided at all (https://bugs.php.net/bug.php?id=65920).
* - Offers limited support for message formatting in case PHP intl extension is not installed.
* However it is highly recommended that you install [PHP intl extension](http://php.net/manual/en/book.intl.php) if you want
* to use MessageFormatter features.
*
* The fallback implementation only supports the following message formats:
* - plural formatting for english ('one' and 'other' selectors)
* - select format
* - simple parameters
* - integer number parameters
*
* The fallback implementation does NOT support the ['apostrophe-friendly' syntax](http://www.php.net/manual/en/messageformatter.formatmessage.php).
* Also messages that are working with the fallback implementation are not necessarily compatible with the
* PHP intl MessageFormatter so do not rely on the fallback if you are able to install intl extension somehow.
*
* @property string $errorCode Code of the last error. This property is read-only.
* @property string $errorMessage Description of the last error. This property is read-only.
*
* @author Alexander Makarov <sam@rmcreative.ru>
* @author Carsten Brandt <mail@cebe.cc>
* @since 2.0
*/
class MessageFormatter extends Component
{
private $_errorCode = 0;
private $_errorMessage = '';
/**
* Get the error code from the last operation.
* @link http://php.net/manual/en/messageformatter.geterrorcode.php
* @return string Code of the last error.
*/
public function getErrorCode()
{
return $this->_errorCode;
}
/**
* Get the error text from the last operation.
* @link http://php.net/manual/en/messageformatter.geterrormessage.php
* @return string Description of the last error.
*/
public function getErrorMessage()
{
return $this->_errorMessage;
}
/**
* Formats a message via [ICU message format](http://userguide.icu-project.org/formatparse/messages).
*
* It uses the PHP intl extension's [MessageFormatter](http://www.php.net/manual/en/class.messageformatter.php)
* and works around some issues.
* If PHP intl is not installed a fallback will be used that supports a subset of the ICU message format.
*
* @param string $pattern The pattern string to insert parameters into.
* @param array $params The array of name value pairs to insert into the format string.
* @param string $language The locale to use for formatting locale-dependent parts
* @return string|false The formatted pattern string or `false` if an error occurred
*/
public function format($pattern, $params, $language)
{
$this->_errorCode = 0;
$this->_errorMessage = '';
if ($params === []) {
return $pattern;
}
if (!class_exists('MessageFormatter', false)) {
return $this->fallbackFormat($pattern, $params, $language);
}
// replace named arguments (https://github.com/yiisoft/yii2/issues/9678)
$newParams = [];
$pattern = $this->replaceNamedArguments($pattern, $params, $newParams);
$params = $newParams;
try {
$formatter = new \MessageFormatter($language, $pattern);
if ($formatter === null) {
// formatter may be null in PHP 5.x
$this->_errorCode = intl_get_error_code();
$this->_errorMessage = 'Message pattern is invalid: ' . intl_get_error_message();
return false;
}
} catch (\IntlException $e) {
// IntlException is thrown since PHP 7
$this->_errorCode = $e->getCode();
$this->_errorMessage = 'Message pattern is invalid: ' . $e->getMessage();
return false;
} catch (\Exception $e) {
// Exception is thrown by HHVM
$this->_errorCode = $e->getCode();
$this->_errorMessage = 'Message pattern is invalid: ' . $e->getMessage();
return false;
}
$result = $formatter->format($params);
if ($result === false) {
$this->_errorCode = $formatter->getErrorCode();
$this->_errorMessage = $formatter->getErrorMessage();
return false;
}
return $result;
}
/**
* Parses an input string according to an [ICU message format](http://userguide.icu-project.org/formatparse/messages) pattern.
*
* It uses the PHP intl extension's [MessageFormatter::parse()](http://www.php.net/manual/en/messageformatter.parsemessage.php)
* and adds support for named arguments.
* Usage of this method requires PHP intl extension to be installed.
*
* @param string $pattern The pattern to use for parsing the message.
* @param string $message The message to parse, conforming to the pattern.
* @param string $language The locale to use for formatting locale-dependent parts
* @return array|bool An array containing items extracted, or `FALSE` on error.
* @throws \yii\base\NotSupportedException when PHP intl extension is not installed.
*/
public function parse($pattern, $message, $language)
{
$this->_errorCode = 0;
$this->_errorMessage = '';
if (!class_exists('MessageFormatter', false)) {
throw new NotSupportedException('You have to install PHP intl extension to use this feature.');
}
// replace named arguments
if (($tokens = self::tokenizePattern($pattern)) === false) {
$this->_errorCode = -1;
$this->_errorMessage = 'Message pattern is invalid.';
return false;
}
$map = [];
foreach ($tokens as $i => $token) {
if (is_array($token)) {
$param = trim($token[0]);
if (!isset($map[$param])) {
$map[$param] = count($map);
}
$token[0] = $map[$param];
$tokens[$i] = '{' . implode(',', $token) . '}';
}
}
$pattern = implode('', $tokens);
$map = array_flip($map);
$formatter = new \MessageFormatter($language, $pattern);
if ($formatter === null) {
$this->_errorCode = -1;
$this->_errorMessage = 'Message pattern is invalid.';
return false;
}
$result = $formatter->parse($message);
if ($result === false) {
$this->_errorCode = $formatter->getErrorCode();
$this->_errorMessage = $formatter->getErrorMessage();
return false;
}
$values = [];
foreach ($result as $key => $value) {
$values[$map[$key]] = $value;
}
return $values;
}
/**
* Replace named placeholders with numeric placeholders and quote unused.
*
* @param string $pattern The pattern string to replace things into.
* @param array $givenParams The array of values to insert into the format string.
* @param array $resultingParams Modified array of parameters.
* @param array $map
* @return string The pattern string with placeholders replaced.
*/
private function replaceNamedArguments($pattern, $givenParams, &$resultingParams = [], &$map = [])
{
if (($tokens = self::tokenizePattern($pattern)) === false) {
return false;
}
foreach ($tokens as $i => $token) {
if (!is_array($token)) {
continue;
}
$param = trim($token[0]);
if (array_key_exists($param, $givenParams)) {
// if param is given, replace it with a number
if (!isset($map[$param])) {
$map[$param] = count($map);
// make sure only used params are passed to format method
$resultingParams[$map[$param]] = $givenParams[$param];
}
$token[0] = $map[$param];
$quote = '';
} else {
// quote unused token
$quote = "'";
}
$type = isset($token[1]) ? trim($token[1]) : 'none';
// replace plural and select format recursively
if ($type === 'plural' || $type === 'select') {
if (!isset($token[2])) {
return false;
}
if (($subtokens = self::tokenizePattern($token[2])) === false) {
return false;
}
$c = count($subtokens);
for ($k = 0; $k + 1 < $c; $k++) {
if (is_array($subtokens[$k]) || !is_array($subtokens[++$k])) {
return false;
}
$subpattern = $this->replaceNamedArguments(implode(',', $subtokens[$k]), $givenParams, $resultingParams, $map);
$subtokens[$k] = $quote . '{' . $quote . $subpattern . $quote . '}' . $quote;
}
$token[2] = implode('', $subtokens);
}
$tokens[$i] = $quote . '{' . $quote . implode(',', $token) . $quote . '}' . $quote;
}
return implode('', $tokens);
}
/**
* Fallback implementation for MessageFormatter::formatMessage.
* @param string $pattern The pattern string to insert things into.
* @param array $args The array of values to insert into the format string
* @param string $locale The locale to use for formatting locale-dependent parts
* @return false|string The formatted pattern string or `false` if an error occurred
*/
protected function fallbackFormat($pattern, $args, $locale)
{
if (($tokens = self::tokenizePattern($pattern)) === false) {
$this->_errorCode = -1;
$this->_errorMessage = 'Message pattern is invalid.';
return false;
}
foreach ($tokens as $i => $token) {
if (is_array($token)) {
if (($tokens[$i] = $this->parseToken($token, $args, $locale)) === false) {
$this->_errorCode = -1;
$this->_errorMessage = 'Message pattern is invalid.';
return false;
}
}
}
return implode('', $tokens);
}
/**
* Tokenizes a pattern by separating normal text from replaceable patterns.
* @param string $pattern patter to tokenize
* @return array|bool array of tokens or false on failure
*/
private static function tokenizePattern($pattern)
{
$charset = Yii::$app ? Yii::$app->charset : 'UTF-8';
$depth = 1;
if (($start = $pos = mb_strpos($pattern, '{', 0, $charset)) === false) {
return [$pattern];
}
$tokens = [mb_substr($pattern, 0, $pos, $charset)];
while (true) {
$open = mb_strpos($pattern, '{', $pos + 1, $charset);
$close = mb_strpos($pattern, '}', $pos + 1, $charset);
if ($open === false && $close === false) {
break;
}
if ($open === false) {
$open = mb_strlen($pattern, $charset);
}
if ($close > $open) {
$depth++;
$pos = $open;
} else {
$depth--;
$pos = $close;
}
if ($depth === 0) {
$tokens[] = explode(',', mb_substr($pattern, $start + 1, $pos - $start - 1, $charset), 3);
$start = $pos + 1;
$tokens[] = mb_substr($pattern, $start, $open - $start, $charset);
$start = $open;
}
if ($depth !== 0 && ($open === false || $close === false)) {
break;
}
}
if ($depth !== 0) {
return false;
}
return $tokens;
}
/**
* Parses a token.
* @param array $token the token to parse
* @param array $args arguments to replace
* @param string $locale the locale
* @return bool|string parsed token or false on failure
* @throws \yii\base\NotSupportedException when unsupported formatting is used.
*/
private function parseToken($token, $args, $locale)
{
// parsing pattern based on ICU grammar:
// http://icu-project.org/apiref/icu4c/classMessageFormat.html#details
$charset = Yii::$app ? Yii::$app->charset : 'UTF-8';
$param = trim($token[0]);
if (isset($args[$param])) {
$arg = $args[$param];
} else {
return '{' . implode(',', $token) . '}';
}
$type = isset($token[1]) ? trim($token[1]) : 'none';
switch ($type) {
case 'date':
case 'time':
case 'spellout':
case 'ordinal':
case 'duration':
case 'choice':
case 'selectordinal':
throw new NotSupportedException("Message format '$type' is not supported. You have to install PHP intl extension to use this feature.");
case 'number':
$format = isset($token[2]) ? trim($token[2]) : null;
if (is_numeric($arg) && ($format === null || $format === 'integer')) {
$number = number_format($arg);
if ($format === null && ($pos = strpos($arg, '.')) !== false) {
// add decimals with unknown length
$number .= '.' . substr($arg, $pos + 1);
}
return $number;
}
throw new NotSupportedException("Message format 'number' is only supported for integer values. You have to install PHP intl extension to use this feature.");
case 'none':
return $arg;
case 'select':
/* http://icu-project.org/apiref/icu4c/classicu_1_1SelectFormat.html
selectStyle = (selector '{' message '}')+
*/
if (!isset($token[2])) {
return false;
}
$select = self::tokenizePattern($token[2]);
$c = count($select);
$message = false;
for ($i = 0; $i + 1 < $c; $i++) {
if (is_array($select[$i]) || !is_array($select[$i + 1])) {
return false;
}
$selector = trim($select[$i++]);
if ($message === false && $selector === 'other' || $selector == $arg) {
$message = implode(',', $select[$i]);
}
}
if ($message !== false) {
return $this->fallbackFormat($message, $args, $locale);
}
break;
case 'plural':
/* http://icu-project.org/apiref/icu4c/classicu_1_1PluralFormat.html
pluralStyle = [offsetValue] (selector '{' message '}')+
offsetValue = "offset:" number
selector = explicitValue | keyword
explicitValue = '=' number // adjacent, no white space in between
keyword = [^[[:Pattern_Syntax:][:Pattern_White_Space:]]]+
message: see MessageFormat
*/
if (!isset($token[2])) {
return false;
}
$plural = self::tokenizePattern($token[2]);
$c = count($plural);
$message = false;
$offset = 0;
for ($i = 0; $i + 1 < $c; $i++) {
if (is_array($plural[$i]) || !is_array($plural[$i + 1])) {
return false;
}
$selector = trim($plural[$i++]);
if ($i == 1 && strncmp($selector, 'offset:', 7) === 0) {
$offset = (int) trim(mb_substr($selector, 7, ($pos = mb_strpos(str_replace(["\n", "\r", "\t"], ' ', $selector), ' ', 7, $charset)) - 7, $charset));
$selector = trim(mb_substr($selector, $pos + 1, mb_strlen($selector, $charset), $charset));
}
if ($message === false && $selector === 'other' ||
$selector[0] === '=' && (int) mb_substr($selector, 1, mb_strlen($selector, $charset), $charset) === $arg ||
$selector === 'one' && $arg - $offset == 1
) {
$message = implode(',', str_replace('#', $arg - $offset, $plural[$i]));
}
}
if ($message !== false) {
return $this->fallbackFormat($message, $args, $locale);
}
break;
}
return false;
}
}

View File

@@ -0,0 +1,125 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\i18n;
use Yii;
use yii\base\Component;
/**
* MessageSource is the base class for message translation repository classes.
*
* A message source stores message translations in some persistent storage.
*
* Child classes should override [[loadMessages()]] to provide translated messages.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
class MessageSource extends Component
{
/**
* @event MissingTranslationEvent an event that is triggered when a message translation is not found.
*/
const EVENT_MISSING_TRANSLATION = 'missingTranslation';
/**
* @var bool 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.
*/
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;
private $_messages = [];
/**
* Initializes this component.
*/
public function init()
{
parent::init();
if ($this->sourceLanguage === null) {
$this->sourceLanguage = Yii::$app->sourceLanguage;
}
}
/**
* Loads the message translation for the specified language and category.
* If translation for specific locale code such as `en-US` isn't found it
* tries more generic `en`.
*
* @param string $category the message category
* @param string $language the target language
* @return array the loaded messages. The keys are original messages, and the values
* are translated messages.
*/
protected function loadMessages($category, $language)
{
return [];
}
/**
* Translates a message to the specified language.
*
* Note that unless [[forceTranslation]] is true, if the target language
* is the same as the [[sourceLanguage|source language]], the message
* will NOT be translated.
*
* If a translation is not found, a [[EVENT_MISSING_TRANSLATION|missingTranslation]] event will be triggered.
*
* @param string $category the message category
* @param string $message the message to be translated
* @param string $language the target language
* @return string|bool the translated message or false if translation wasn't found or isn't required
*/
public function translate($category, $message, $language)
{
if ($this->forceTranslation || $language !== $this->sourceLanguage) {
return $this->translateMessage($category, $message, $language);
}
return false;
}
/**
* Translates the specified message.
* If the message is not found, a [[EVENT_MISSING_TRANSLATION|missingTranslation]] event will be triggered.
* If there is an event handler, it may provide a [[MissingTranslationEvent::$translatedMessage|fallback translation]].
* If no fallback translation is provided this method will return `false`.
* @param string $category the category that the message belongs to.
* @param string $message the message to be translated.
* @param string $language the target language.
* @return string|bool the translated message or false if translation wasn't found.
*/
protected function translateMessage($category, $message, $language)
{
$key = $language . '/' . $category;
if (!isset($this->_messages[$key])) {
$this->_messages[$key] = $this->loadMessages($category, $language);
}
if (isset($this->_messages[$key][$message]) && $this->_messages[$key][$message] !== '') {
return $this->_messages[$key][$message];
} elseif ($this->hasEventHandlers(self::EVENT_MISSING_TRANSLATION)) {
$event = new MissingTranslationEvent([
'category' => $category,
'message' => $message,
'language' => $language,
]);
$this->trigger(self::EVENT_MISSING_TRANSLATION, $event);
if ($event->translatedMessage !== null) {
return $this->_messages[$key][$message] = $event->translatedMessage;
}
}
return $this->_messages[$key][$message] = false;
}
}

View File

@@ -0,0 +1,38 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\i18n;
use yii\base\Event;
/**
* MissingTranslationEvent represents the parameter for the [[MessageSource::EVENT_MISSING_TRANSLATION]] 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 use this to provide a fallback translation
* and set [[translatedMessage]] if possible.
*/
public $message;
/**
* @var string the translated message. An event handler may overwrite this property
* with a translated version of [[message]] if possible. If not set (null), it means the message is not translated.
*/
public $translatedMessage;
/**
* @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;
}

View File

@@ -0,0 +1,166 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\i18n;
use Yii;
/**
* PhpMessageSource represents a message source that stores translated messages in PHP scripts.
*
* PhpMessageSource uses PHP arrays to keep message translations.
*
* - Each PHP script contains one array which stores the message translations in one particular
* language and for a single message category;
* - Each PHP script is saved as a file named as "[[basePath]]/LanguageID/CategoryName.php";
* - Within each PHP script, the message translations are returned as an array like the following:
*
* ```php
* return [
* 'original message 1' => 'translated message 1',
* 'original message 2' => 'translated message 2',
* ];
* ```
*
* You may use [[fileMap]] to customize the association between category names and the file names.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
class PhpMessageSource extends MessageSource
{
/**
* @var string the base path for all translated messages. Defaults to '@app/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,
*
* ```php
* [
* 'core' => 'core.php',
* 'ext' => 'extensions.php',
* ]
* ```
*/
public $fileMap;
/**
* Loads the message translation for the specified $language and $category.
* If translation for specific locale code such as `en-US` isn't found it
* tries more generic `en`. When both are present, the `en-US` messages will be merged
* over `en`. See [[loadFallbackMessages]] for details.
* If the $language is less specific than [[sourceLanguage]], the method will try to
* load the messages for [[sourceLanguage]]. For example: [[sourceLanguage]] is `en-GB`,
* $language is `en`. The method will load the messages for `en` and merge them over `en-GB`.
*
* @param string $category the message category
* @param string $language the target language
* @return array the loaded messages. The keys are original messages, and the values are the translated messages.
* @see loadFallbackMessages
* @see sourceLanguage
*/
protected function loadMessages($category, $language)
{
$messageFile = $this->getMessageFilePath($category, $language);
$messages = $this->loadMessagesFromFile($messageFile);
$fallbackLanguage = substr($language, 0, 2);
$fallbackSourceLanguage = substr($this->sourceLanguage, 0, 2);
if ($language !== $fallbackLanguage) {
$messages = $this->loadFallbackMessages($category, $fallbackLanguage, $messages, $messageFile);
} elseif ($language === $fallbackSourceLanguage) {
$messages = $this->loadFallbackMessages($category, $this->sourceLanguage, $messages, $messageFile);
} else {
if ($messages === null) {
Yii::warning("The message file for category '$category' does not exist: $messageFile", __METHOD__);
}
}
return (array) $messages;
}
/**
* The method is normally called by [[loadMessages]] to load the fallback messages for the language.
* Method tries to load the $category messages for the $fallbackLanguage and adds them to the $messages array.
*
* @param string $category the message category
* @param string $fallbackLanguage the target fallback language
* @param array $messages the array of previously loaded translation messages.
* The keys are original messages, and the values are the translated messages.
* @param string $originalMessageFile the path to the file with messages. Used to log an error message
* in case when no translations were found.
* @return array the loaded messages. The keys are original messages, and the values are the translated messages.
* @since 2.0.7
*/
protected function loadFallbackMessages($category, $fallbackLanguage, $messages, $originalMessageFile)
{
$fallbackMessageFile = $this->getMessageFilePath($category, $fallbackLanguage);
$fallbackMessages = $this->loadMessagesFromFile($fallbackMessageFile);
if (
$messages === null && $fallbackMessages === null
&& $fallbackLanguage !== $this->sourceLanguage
&& $fallbackLanguage !== substr($this->sourceLanguage, 0, 2)
) {
Yii::error("The message file for category '$category' does not exist: $originalMessageFile "
. "Fallback file does not exist as well: $fallbackMessageFile", __METHOD__);
} elseif (empty($messages)) {
return $fallbackMessages;
} elseif (!empty($fallbackMessages)) {
foreach ($fallbackMessages as $key => $value) {
if (!empty($value) && empty($messages[$key])) {
$messages[$key] = $fallbackMessages[$key];
}
}
}
return (array) $messages;
}
/**
* Returns message file path for the specified language and category.
*
* @param string $category the message category
* @param string $language the target language
* @return string path to message file
*/
protected function getMessageFilePath($category, $language)
{
$messageFile = Yii::getAlias($this->basePath) . "/$language/";
if (isset($this->fileMap[$category])) {
$messageFile .= $this->fileMap[$category];
} else {
$messageFile .= str_replace('\\', '/', $category) . '.php';
}
return $messageFile;
}
/**
* Loads the message translation for the specified language and category or returns null if file doesn't exist.
*
* @param string $messageFile path to message file
* @return array|null array of messages or null if file not found
*/
protected function loadMessagesFromFile($messageFile)
{
if (is_file($messageFile)) {
$messages = include $messageFile;
if (!is_array($messages)) {
$messages = [];
}
return $messages;
}
return null;
}
}

View File

@@ -0,0 +1,52 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
use yii\db\Migration;
/**
* Initializes i18n messages tables.
*
*
*
* @author Dmitry Naumenko <d.naumenko.a@gmail.com>
* @since 2.0.7
*/
class m150207_210500_i18n_init extends Migration
{
public function up()
{
$tableOptions = null;
if ($this->db->driverName === 'mysql') {
// http://stackoverflow.com/questions/766809/whats-the-difference-between-utf8-general-ci-and-utf8-unicode-ci
$tableOptions = 'CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE=InnoDB';
}
$this->createTable('{{%source_message}}', [
'id' => $this->primaryKey(),
'category' => $this->string(),
'message' => $this->text(),
], $tableOptions);
$this->createTable('{{%message}}', [
'id' => $this->integer()->notNull(),
'language' => $this->string(16)->notNull(),
'translation' => $this->text(),
], $tableOptions);
$this->addPrimaryKey('pk_message_id_language', '{{%message}}', ['id', 'language']);
$this->addForeignKey('fk_message_source_message', '{{%message}}', 'id', '{{%source_message}}', 'id', 'CASCADE', 'RESTRICT');
$this->createIndex('idx_source_message_category', '{{%source_message}}', 'category');
$this->createIndex('idx_message_language', '{{%message}}', 'language');
}
public function down()
{
$this->dropForeignKey('fk_message_source_message', '{{%message}}');
$this->dropTable('{{%message}}');
$this->dropTable('{{%source_message}}');
}
}

View File

@@ -0,0 +1,35 @@
/**
* Database schema required by \yii\i18n\DbMessageSource.
*
* @author Dmitry Naumenko <d.naumenko.a@gmail.com>
* @link http://www.yiiframework.com/
* @copyright 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
* @since 2.0.7
*/
if object_id('[source_message]', 'U') is not null
drop table [source_message];
if object_id('[message]', 'U') is not null
drop table [message];
CREATE TABLE [source_message]
(
[id] integer IDENTITY PRIMARY KEY,
[category] varchar(255),
[message] text
);
CREATE TABLE [message]
(
[id] integer NOT NULL,
[language] varchar(16) NOT NULL,
[translation] text
);
ALTER TABLE [message] ADD CONSTRAINT [pk_message_id_language] PRIMARY KEY ([id], [language]);
ALTER TABLE [message] ADD CONSTRAINT [fk_message_source_message] FOREIGN KEY ([id]) REFERENCES [source_message] ([id]) ON UPDATE CASCADE ON DELETE NO ACTION;
CREATE INDEX [idx_message_language] on [message] ([language]);
CREATE INDEX [idx_source_message_category] on [source_message] ([category]);

View File

@@ -0,0 +1,33 @@
/**
* Database schema required by \yii\i18n\DbMessageSource.
*
* @author Dmitry Naumenko <d.naumenko.a@gmail.com>
* @link http://www.yiiframework.com/
* @copyright 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
* @since 2.0.7
*/
drop table if exists `source_message`;
drop table if exists `message`;
CREATE TABLE `source_message`
(
`id` integer NOT NULL AUTO_INCREMENT PRIMARY KEY,
`category` varchar(255),
`message` text
);
CREATE TABLE `message`
(
`id` integer NOT NULL,
`language` varchar(16) NOT NULL,
`translation` text
);
ALTER TABLE `message` ADD CONSTRAINT `pk_message_id_language` PRIMARY KEY (`id`, `language`);
ALTER TABLE `message` ADD CONSTRAINT `fk_message_source_message` FOREIGN KEY (`id`) REFERENCES `source_message` (`id`) ON UPDATE CASCADE ON DELETE RESTRICT;
CREATE INDEX idx_message_language ON message (language);
CREATE INDEX idx_source_message_category ON source_message (category);

View File

@@ -0,0 +1,33 @@
/**
* Database schema required by \yii\i18n\DbMessageSource.
*
* @author Dmitry Naumenko <d.naumenko.a@gmail.com>
* @link http://www.yiiframework.com/
* @copyright 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
* @since 2.0.7
*/
drop table if exists "source_message";
drop table if exists "message";
CREATE TABLE "source_message"
(
"id" integer NOT NULL PRIMARY KEY,
"category" varchar(255),
"message" clob
);
CREATE SEQUENCE "source_message_SEQ";
CREATE TABLE "message"
(
"id" integer NOT NULL,
"language" varchar(16) NOT NULL,
"translation" clob,
primary key ("id", "language"),
foreign key ("id") references "source_message" ("id") on delete cascade
);
CREATE INDEX idx_message_language ON "message"("language");
CREATE INDEX idx_source_message_category ON "source_message"("category");

View File

@@ -0,0 +1,38 @@
/**
* Database schema required by \yii\i18n\DbMessageSource.
*
* @author Dmitry Naumenko <d.naumenko.a@gmail.com>
* @link http://www.yiiframework.com/
* @copyright 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
* @since 2.0.7
*/
drop table if exists "source_message";
drop table if exists "message";
CREATE SEQUENCE source_message_seq;
CREATE TABLE "source_message"
(
"id" integer NOT NULL PRIMARY KEY DEFAULT nextval('source_message_seq'),
"category" varchar(255),
"message" text
);
CREATE TABLE "message"
(
"id" integer NOT NULL,
"language" varchar(16) NOT NULL,
"translation" text
);
ALTER TABLE "message" ADD CONSTRAINT "pk_message_id_language" PRIMARY KEY ("id", "language");
ALTER TABLE "message" ADD CONSTRAINT "fk_message_source_message" FOREIGN KEY ("id") REFERENCES "source_message" ("id") ON UPDATE CASCADE ON DELETE RESTRICT;
CREATE INDEX "idx_message_language" ON "message" USING btree (language);
ALTER TABLE "message" CLUSTER ON "idx_message_language";
CREATE INDEX "idx_source_message_category" ON "source_message" USING btree (category);
ALTER TABLE "source_message" CLUSTER ON "idx_source_message_category";

View File

@@ -0,0 +1,30 @@
/**
* Database schema required by \yii\i18n\DbMessageSource.
*
* @author Dmitry Naumenko <d.naumenko.a@gmail.com>
* @link http://www.yiiframework.com/
* @copyright 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
* @since 2.0.7
*/
drop table if exists `source_message`;
drop table if exists `message`;
CREATE TABLE `source_message`
(
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`category` varchar(255),
`message` text
);
CREATE TABLE `message`
(
`id` integer NOT NULL REFERENCES `source_message` (`id`) ON UPDATE CASCADE ON DELETE NO ACTION,
`language` varchar(16) NOT NULL,
`translation` text,
PRIMARY KEY (`id`, `language`)
);
CREATE INDEX idx_message_language ON message (language);
CREATE INDEX idx_source_message_category ON source_message (category);