Commit 8bdc437b by Alexander Makarov

Fixes #2079:

- i18n now falls back to `en` from `en-US` if message translation isn't found - View now falls back to `en` from `en-US` if file not found - Default `sourceLanguage` and `language` are now `en`
parent 3bf072a7
...@@ -40,6 +40,8 @@ Format is `ll-CC` where `ll` is two- or three-letter lowercase code for a langu ...@@ -40,6 +40,8 @@ Format is `ll-CC` where `ll` is two- or three-letter lowercase code for a langu
[ISO-639](http://www.loc.gov/standards/iso639-2/) and `CC` is country code according to [ISO-639](http://www.loc.gov/standards/iso639-2/) and `CC` is country code according to
[ISO-3166](http://www.iso.org/iso/en/prods-services/iso3166ma/02iso-3166-code-lists/list-en1.html). [ISO-3166](http://www.iso.org/iso/en/prods-services/iso3166ma/02iso-3166-code-lists/list-en1.html).
If there's no translation for `ru-RU` Yii will try `ru` as well before failing.
> **Note**: you can further customize details specifying language > **Note**: you can further customize details specifying language
> [as documented in ICU project](http://userguide.icu-project.org/locale#TOC-The-Locale-Concept). > [as documented in ICU project](http://userguide.icu-project.org/locale#TOC-The-Locale-Concept).
...@@ -64,7 +66,7 @@ Yii tries to load appropriate translation from one of the message sources define ...@@ -64,7 +66,7 @@ Yii tries to load appropriate translation from one of the message sources define
'app*' => [ 'app*' => [
'class' => 'yii\i18n\PhpMessageSource', 'class' => 'yii\i18n\PhpMessageSource',
//'basePath' => '@app/messages', //'basePath' => '@app/messages',
//'sourceLanguage' => 'en-US', //'sourceLanguage' => 'en',
'fileMap' => [ 'fileMap' => [
'app' => 'app.php', 'app' => 'app.php',
'app/error' => 'error.php', 'app/error' => 'error.php',
...@@ -273,8 +275,8 @@ You can use i18n in your views to provide support for different languages. For e ...@@ -273,8 +275,8 @@ You can use i18n in your views to provide support for different languages. For e
you want to create special case for russian language, you create `ru-RU` folder under the view path of current controller/widget and you want to create special case for russian language, you create `ru-RU` folder under the view path of current controller/widget and
put there file for russian language as follows `views/site/ru-RU/index.php`. put there file for russian language as follows `views/site/ru-RU/index.php`.
> **Note**: You should note that in **Yii2** language id style has changed, now it use dash **ru-RU, en-US, pl-PL** instead of underscore, because of > **Note**: If language is specified as `en-US` and there are no corresponding views, Yii will try views under `en`
> php **intl** library. > before using original ones.
Formatters Formatters
---------- ----------
......
...@@ -81,6 +81,10 @@ Yii Framework 2 Change Log ...@@ -81,6 +81,10 @@ Yii Framework 2 Change Log
- Enh #2043: Added support for custom request body parsers (danschmidt5189, cebe) - Enh #2043: Added support for custom request body parsers (danschmidt5189, cebe)
- Enh #2051: Do not save null data into database when using RBAC (qiangxue) - Enh #2051: Do not save null data into database when using RBAC (qiangxue)
- Enh #2101: Gii is now using model labels when generating search (thiagotalma) - Enh #2101: Gii is now using model labels when generating search (thiagotalma)
- Enh #2079:
- i18n now falls back to `en` from `en-US` if message translation isn't found (samdark)
- View now falls back to `en` from `en-US` if file not found (samdark)
- Default `sourceLanguage` and `language` are now `en` (samdark)
- Enh: Added `favicon.ico` and `robots.txt` to default application templates (samdark) - Enh: Added `favicon.ico` and `robots.txt` to default application templates (samdark)
- Enh: Added `Widget::autoIdPrefix` to support prefixing automatically generated widget IDs (qiangxue) - Enh: Added `Widget::autoIdPrefix` to support prefixing automatically generated widget IDs (qiangxue)
- Enh: Support for file aliases in console command 'message' (omnilight) - Enh: Support for file aliases in console command 'message' (omnilight)
......
...@@ -80,13 +80,13 @@ abstract class Application extends Module ...@@ -80,13 +80,13 @@ abstract class Application extends Module
* @var string the language that is meant to be used for end users. * @var string the language that is meant to be used for end users.
* @see sourceLanguage * @see sourceLanguage
*/ */
public $language = 'en-US'; public $language = 'en';
/** /**
* @var string the language that the application is written in. This mainly refers to * @var string the language that the application is written in. This mainly refers to
* the language that the messages and view files are written in. * the language that the messages and view files are written in.
* @see language * @see language
*/ */
public $sourceLanguage = 'en-US'; public $sourceLanguage = 'en';
/** /**
* @var Controller the currently active controller instance * @var Controller the currently active controller instance
*/ */
......
...@@ -43,8 +43,8 @@ class BaseFileHelper ...@@ -43,8 +43,8 @@ class BaseFileHelper
* a file with the same name will be looked for under the subdirectory * a file with the same name will be looked for under the subdirectory
* whose name is the same as the language code. For example, given the file "path/to/view.php" * whose name is the same as the language code. For example, given the file "path/to/view.php"
* and language code "zh_CN", the localized file will be looked for as * and language code "zh_CN", the localized file will be looked for as
* "path/to/zh_CN/view.php". If the file is not found, the original file * "path/to/zh_CN/view.php". If the file is not found, it will try a fallback with just a language code that is
* will be returned. * "zh" i.e. "path/to/zh/view.php". If it is not found as well the original file will be returned.
* *
* If the target and the source language codes are the same, * If the target and the source language codes are the same,
* the original file will be returned. * the original file will be returned.
...@@ -69,8 +69,17 @@ class BaseFileHelper ...@@ -69,8 +69,17 @@ class BaseFileHelper
return $file; return $file;
} }
$desiredFile = dirname($file) . DIRECTORY_SEPARATOR . $language . DIRECTORY_SEPARATOR . basename($file); $desiredFile = dirname($file) . DIRECTORY_SEPARATOR . $language . DIRECTORY_SEPARATOR . basename($file);
if (is_file($desiredFile)) {
return $desiredFile;
} else {
$language = substr($language, 0, 2);
if ($language === $sourceLanguage) {
return $file;
}
$desiredFile = dirname($file) . DIRECTORY_SEPARATOR . $language . DIRECTORY_SEPARATOR . basename($file);
return is_file($desiredFile) ? $desiredFile : $file; return is_file($desiredFile) ? $desiredFile : $file;
} }
}
/** /**
* Determines the MIME type of the specified file. * Determines the MIME type of the specified file.
......
...@@ -111,8 +111,9 @@ class DbMessageSource extends MessageSource ...@@ -111,8 +111,9 @@ class DbMessageSource extends MessageSource
/** /**
* 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 * If translation for specific locale code such as `en-US` isn't found it
* the specified language and category. * tries more generic `en`.
*
* @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. The keys are original messages, and the values * @return array the loaded messages. The keys are original messages, and the values
...@@ -146,13 +147,25 @@ class DbMessageSource extends MessageSource ...@@ -146,13 +147,25 @@ class DbMessageSource extends MessageSource
*/ */
protected function loadMessagesFromDb($category, $language) protected function loadMessagesFromDb($category, $language)
{ {
$query = new Query(); $mainQuery = new Query();
$messages = $query->select(['t1.message message', 't2.translation translation']) $mainQuery->select(['t1.message message', 't2.translation translation'])
->from([$this->sourceMessageTable . ' t1', $this->messageTable . ' t2']) ->from([$this->sourceMessageTable . ' t1', $this->messageTable . ' t2'])
->where('t1.id = t2.id AND t1.category = :category AND t2.language = :language') ->where('t1.id = t2.id AND t1.category = :category AND t2.language = :language')
->params([':category' => $category, ':language' => $language]) ->params([':category' => $category, ':language' => $language]);
->createCommand($this->db)
->queryAll(); $fallbackLanguage = substr($language, 0, 2);
if ($fallbackLanguage != $language) {
$fallbackQuery = new Query();
$fallbackQuery->select(['t1.message message', 't2.translation translation'])
->from([$this->sourceMessageTable . ' t1', $this->messageTable . ' t2'])
->where('t1.id = t2.id AND t1.category = :category AND t2.language = :fallbackLanguage')
->andWhere('t2.id NOT IN (SELECT id FROM '.$this->messageTable.' WHERE language = :language)')
->params([':category' => $category, ':language' => $language, ':fallbackLanguage' => $fallbackLanguage]);
$mainQuery->union($fallbackQuery);
}
$messages = $mainQuery->createCommand($this->db)->queryAll();
return ArrayHelper::map($messages, 'message', 'translation'); return ArrayHelper::map($messages, 'message', 'translation');
} }
} }
...@@ -50,8 +50,9 @@ class GettextMessageSource extends MessageSource ...@@ -50,8 +50,9 @@ class GettextMessageSource extends MessageSource
/** /**
* 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 * If translation for specific locale code such as `en-US` isn't found it
* the specified language and category. * tries more generic `en`.
*
* @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. The keys are original messages, and the values * @return array the loaded messages. The keys are original messages, and the values
...@@ -59,13 +60,59 @@ class GettextMessageSource extends MessageSource ...@@ -59,13 +60,59 @@ class GettextMessageSource extends MessageSource
*/ */
protected function loadMessages($category, $language) protected function loadMessages($category, $language)
{ {
$messageFile = $this->getMessageFilePath($category, $language);
$messages = $this->loadMessagesFromFile($messageFile);
$fallbackLanguage = substr($language, 0, 2);
if ($fallbackLanguage != $language) {
$fallbackMessageFile = $this->getMessageFilePath($category, $fallbackLanguage);
$fallbackMessages = $this->loadMessagesFromFile($fallbackMessageFile);
if ($messages === null && $fallbackMessages === null && $fallbackLanguage != $this->sourceLanguage) {
Yii::error("The message file for category '$category' does not exist: $messageFile Fallback file does not exist as well: $fallbackMessageFile", __METHOD__);
} else if (empty($messages)) {
return $fallbackMessages;
} else if (!empty($fallbackMessages)) {
foreach ($messages as $key => $value) {
if (empty($value) && !empty($fallbackMessages[$key])) {
$messages[$key] = $fallbackMessages[$key];
}
}
}
} else {
if ($messages === null) {
Yii::error("The message file for category '$category' does not exist: $messageFile", __METHOD__);
}
}
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 . '/' . $this->catalog; $messageFile = Yii::getAlias($this->basePath) . '/' . $language . '/' . $this->catalog;
if ($this->useMoFile) { if ($this->useMoFile) {
$messageFile .= static::MO_FILE_EXT; $messageFile .= static::MO_FILE_EXT;
} else { } else {
$messageFile .= static::PO_FILE_EXT; $messageFile .= static::PO_FILE_EXT;
} }
return $messageFile;
}
/**
* Loads the message translation for the specified language and category or returns null if file doesn't exist.
*
* @param $messageFile string path to message file
* @return array|null array of messages or null if file not found
*/
protected function loadMessagesFromFile($messageFile)
{
if (is_file($messageFile)) { if (is_file($messageFile)) {
if ($this->useMoFile) { if ($this->useMoFile) {
$gettextFile = new GettextMoFile(['useBigEndian' => $this->useBigEndian]); $gettextFile = new GettextMoFile(['useBigEndian' => $this->useBigEndian]);
...@@ -78,8 +125,7 @@ class GettextMessageSource extends MessageSource ...@@ -78,8 +125,7 @@ class GettextMessageSource extends MessageSource
} }
return $messages; return $messages;
} else { } else {
Yii::error("The message file for category '$category' does not exist: $messageFile", __METHOD__); return null;
return [];
} }
} }
} }
...@@ -54,14 +54,14 @@ class I18N extends Component ...@@ -54,14 +54,14 @@ class I18N extends Component
if (!isset($this->translations['yii'])) { if (!isset($this->translations['yii'])) {
$this->translations['yii'] = [ $this->translations['yii'] = [
'class' => 'yii\i18n\PhpMessageSource', 'class' => 'yii\i18n\PhpMessageSource',
'sourceLanguage' => 'en-US', 'sourceLanguage' => 'en',
'basePath' => '@yii/messages', 'basePath' => '@yii/messages',
]; ];
} }
if (!isset($this->translations['app'])) { if (!isset($this->translations['app'])) {
$this->translations['app'] = [ $this->translations['app'] = [
'class' => 'yii\i18n\PhpMessageSource', 'class' => 'yii\i18n\PhpMessageSource',
'sourceLanguage' => 'en-US', 'sourceLanguage' => 'en',
'basePath' => '@app/messages', 'basePath' => '@app/messages',
]; ];
} }
......
...@@ -53,8 +53,9 @@ class MessageSource extends Component ...@@ -53,8 +53,9 @@ 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 * If translation for specific locale code such as `en-US` isn't found it
* the specified language and category. * tries more generic `en`.
*
* @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. The keys are original messages, and the values * @return array the loaded messages. The keys are original messages, and the values
......
...@@ -53,18 +53,69 @@ class PhpMessageSource extends MessageSource ...@@ -53,18 +53,69 @@ class PhpMessageSource extends MessageSource
/** /**
* Loads the message translation for the specified language and category. * 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 $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.
*/ */
protected function loadMessages($category, $language) protected function loadMessages($category, $language)
{ {
$messageFile = $this->getMessageFilePath($category, $language);
$messages = $this->loadMessagesFromFile($messageFile);
$fallbackLanguage = substr($language, 0, 2);
if ($fallbackLanguage != $language) {
$fallbackMessageFile = $this->getMessageFilePath($category, $fallbackLanguage);
$fallbackMessages = $this->loadMessagesFromFile($fallbackMessageFile);
if ($messages === null && $fallbackMessages === null && $fallbackLanguage != $this->sourceLanguage) {
Yii::error("The message file for category '$category' does not exist: $messageFile Fallback file does not exist as well: $fallbackMessageFile", __METHOD__);
} else if (empty($messages)) {
return $fallbackMessages;
} else if (!empty($fallbackMessages)) {
foreach ($messages as $key => $value) {
if (empty($value) && !empty($fallbackMessages[$key])) {
$messages[$key] = $fallbackMessages[$key];
}
}
}
} else {
if ($messages === null) {
Yii::error("The message file for category '$category' does not exist: $messageFile", __METHOD__);
}
}
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/"; $messageFile = Yii::getAlias($this->basePath) . "/$language/";
if (isset($this->fileMap[$category])) { if (isset($this->fileMap[$category])) {
$messageFile .= $this->fileMap[$category]; $messageFile .= $this->fileMap[$category];
} else { } else {
$messageFile .= str_replace('\\', '/', $category) . '.php'; $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 $messageFile string path to message file
* @return array|null array of messages or null if file not found
*/
protected function loadMessagesFromFile($messageFile)
{
if (is_file($messageFile)) { if (is_file($messageFile)) {
$messages = include($messageFile); $messages = include($messageFile);
if (!is_array($messages)) { if (!is_array($messages)) {
...@@ -72,8 +123,7 @@ class PhpMessageSource extends MessageSource ...@@ -72,8 +123,7 @@ class PhpMessageSource extends MessageSource
} }
return $messages; return $messages;
} else { } else {
Yii::error("The message file for category '$category' does not exist: $messageFile", __METHOD__); return null;
return [];
} }
} }
} }
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
?> ?>
<?php if (method_exists($this, 'beginPage')) $this->beginPage(); ?> <?php if (method_exists($this, 'beginPage')) $this->beginPage(); ?>
<!doctype html> <!doctype html>
<html lang="en-us"> <html lang="en">
<head> <head>
<meta charset="utf-8"/> <meta charset="utf-8"/>
......
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