MessageController.php 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  1. <?php
  2. /**
  3. * @author Qiang Xue <[email protected]>
  4. * @link http://www.yiiframework.com/
  5. * @copyright Copyright (c) 2008 Yii Software LLC
  6. * @license http://www.yiiframework.com/license/
  7. */
  8. namespace yii\console\controllers;
  9. use Yii;
  10. use yii\console\Controller;
  11. use yii\console\Exception;
  12. use yii\helpers\FileHelper;
  13. /**
  14. * This command extracts messages to be translated from source files.
  15. * The extracted messages are saved either as PHP message source files
  16. * or ".po" files under the specified directory. Format depends on `format`
  17. * setting in config file.
  18. *
  19. * Usage:
  20. * 1. Create a configuration file using the 'message/config' command:
  21. * yii message/config /path/to/myapp/messages/config.php
  22. * 2. Edit the created config file, adjusting it for your web application needs.
  23. * 3. Run the 'message/extract' command, using created config:
  24. * yii message /path/to/myapp/messages/config.php
  25. *
  26. * @author Qiang Xue <[email protected]>
  27. * @since 2.0
  28. */
  29. class MessageController extends Controller
  30. {
  31. /**
  32. * @var string controller default action ID.
  33. */
  34. public $defaultAction = 'extract';
  35. /**
  36. * Creates a configuration file for the "extract" command.
  37. *
  38. * The generated configuration file contains detailed instructions on
  39. * how to customize it to fit for your needs. After customization,
  40. * you may use this configuration file with the "extract" command.
  41. *
  42. * @param string $filePath output file name or alias.
  43. * @throws Exception on failure.
  44. */
  45. public function actionConfig($filePath)
  46. {
  47. $filePath = Yii::getAlias($filePath);
  48. if (file_exists($filePath)) {
  49. if (!$this->confirm("File '{$filePath}' already exists. Do you wish to overwrite it?")) {
  50. return;
  51. }
  52. }
  53. copy(Yii::getAlias('@yii/views/messageConfig.php'), $filePath);
  54. echo "Configuration file template created at '{$filePath}'.\n\n";
  55. }
  56. /**
  57. * Extracts messages to be translated from source code.
  58. *
  59. * This command will search through source code files and extract
  60. * messages that need to be translated in different languages.
  61. *
  62. * @param string $configFile the path or alias of the configuration file.
  63. * You may use the "yii message/config" command to generate
  64. * this file and then customize it for your needs.
  65. * @throws Exception on failure.
  66. */
  67. public function actionExtract($configFile)
  68. {
  69. $configFile = Yii::getAlias($configFile);
  70. if (!is_file($configFile)) {
  71. throw new Exception("The configuration file does not exist: $configFile");
  72. }
  73. $config = array_merge([
  74. 'translator' => 'Yii::t',
  75. 'overwrite' => false,
  76. 'removeUnused' => false,
  77. 'sort' => false,
  78. 'format' => 'php',
  79. ], require($configFile));
  80. if (!isset($config['sourcePath'], $config['messagePath'], $config['languages'])) {
  81. throw new Exception('The configuration file must specify "sourcePath", "messagePath" and "languages".');
  82. }
  83. if (!is_dir($config['sourcePath'])) {
  84. throw new Exception("The source path {$config['sourcePath']} is not a valid directory.");
  85. }
  86. if (!is_dir($config['messagePath'])) {
  87. throw new Exception("The message path {$config['messagePath']} is not a valid directory.");
  88. }
  89. if (empty($config['languages'])) {
  90. throw new Exception("Languages cannot be empty.");
  91. }
  92. if (empty($config['format']) || !in_array($config['format'], ['php', 'po'])) {
  93. throw new Exception('Format should be either "php" or "po".');
  94. }
  95. $files = FileHelper::findFiles(realpath($config['sourcePath']), $config);
  96. $messages = [];
  97. foreach ($files as $file) {
  98. $messages = array_merge_recursive($messages, $this->extractMessages($file, $config['translator']));
  99. }
  100. foreach ($config['languages'] as $language) {
  101. $dir = $config['messagePath'] . DIRECTORY_SEPARATOR . $language;
  102. if (!is_dir($dir)) {
  103. @mkdir($dir);
  104. }
  105. foreach ($messages as $category => $msgs) {
  106. $file = str_replace("\\", '/', "$dir/$category." . $config['format']);
  107. $path = dirname($file);
  108. if (!is_dir($path)) {
  109. mkdir($path, 0755, true);
  110. }
  111. $msgs = array_values(array_unique($msgs));
  112. $this->generateMessageFile($msgs, $file, $config['overwrite'], $config['removeUnused'], $config['sort'], $config['format']);
  113. }
  114. }
  115. }
  116. /**
  117. * Extracts messages from a file
  118. *
  119. * @param string $fileName name of the file to extract messages from
  120. * @param string $translator name of the function used to translate messages
  121. * @return array
  122. */
  123. protected function extractMessages($fileName, $translator)
  124. {
  125. echo "Extracting messages from $fileName...\n";
  126. $subject = file_get_contents($fileName);
  127. $messages = [];
  128. if (!is_array($translator)) {
  129. $translator = [$translator];
  130. }
  131. foreach ($translator as $currentTranslator) {
  132. $n = preg_match_all(
  133. '/\b' . $currentTranslator . '\s*\(\s*(\'.*?(?<!\\\\)\'|".*?(?<!\\\\)")\s*,\s*(\'.*?(?<!\\\\)\'|".*?(?<!\\\\)")\s*[,\)]/s',
  134. $subject, $matches, PREG_SET_ORDER);
  135. for ($i = 0; $i < $n; ++$i) {
  136. if (($pos = strpos($matches[$i][1], '.')) !== false) {
  137. $category = substr($matches[$i][1], $pos + 1, -1);
  138. } else {
  139. $category = substr($matches[$i][1], 1, -1);
  140. }
  141. $message = $matches[$i][2];
  142. $messages[$category][] = eval("return $message;"); // use eval to eliminate quote escape
  143. }
  144. }
  145. return $messages;
  146. }
  147. /**
  148. * Writes messages into file
  149. *
  150. * @param array $messages
  151. * @param string $fileName name of the file to write to
  152. * @param boolean $overwrite if existing file should be overwritten without backup
  153. * @param boolean $removeUnused if obsolete translations should be removed
  154. * @param boolean $sort if translations should be sorted
  155. * @param string $format output format
  156. */
  157. protected function generateMessageFile($messages, $fileName, $overwrite, $removeUnused, $sort, $format)
  158. {
  159. echo "Saving messages to $fileName...";
  160. if (is_file($fileName)) {
  161. if($format === 'po'){
  162. $translated = file_get_contents($fileName);
  163. preg_match_all('/(?<=msgid ").*(?="\n(#*)msgstr)/', $translated, $keys);
  164. preg_match_all('/(?<=msgstr ").*(?="\n\n)/', $translated, $values);
  165. $translated = array_combine($keys[0], $values[0]);
  166. } else {
  167. $translated = require($fileName);
  168. }
  169. sort($messages);
  170. ksort($translated);
  171. if (array_keys($translated) == $messages) {
  172. echo "nothing new...skipped.\n";
  173. return;
  174. }
  175. $merged = [];
  176. $untranslated = [];
  177. foreach ($messages as $message) {
  178. if($format === 'po'){
  179. $message = preg_replace('/\"/', '\"', $message);
  180. }
  181. if (array_key_exists($message, $translated) && strlen($translated[$message]) > 0) {
  182. $merged[$message] = $translated[$message];
  183. } else {
  184. $untranslated[] = $message;
  185. }
  186. }
  187. ksort($merged);
  188. sort($untranslated);
  189. $todo = [];
  190. foreach ($untranslated as $message) {
  191. $todo[$message] = '';
  192. }
  193. ksort($translated);
  194. foreach ($translated as $message => $translation) {
  195. if (!isset($merged[$message]) && !isset($todo[$message]) && !$removeUnused) {
  196. if (substr($translation, 0, 2) === '@@' && substr($translation, -2) === '@@') {
  197. $todo[$message] = $translation;
  198. } else {
  199. $todo[$message] = '@@' . $translation . '@@';
  200. }
  201. }
  202. }
  203. $merged = array_merge($todo, $merged);
  204. if ($sort) {
  205. ksort($merged);
  206. }
  207. if (false === $overwrite) {
  208. $fileName .= '.merged';
  209. }
  210. if ($format === 'po'){
  211. $out_str = '';
  212. foreach ($merged as $k => $v){
  213. $k = preg_replace('/(\")|(\\\")/', "\\\"", $k);
  214. $v = preg_replace('/(\")|(\\\")/', "\\\"", $v);
  215. if (substr($v, 0, 2) === '@@' && substr($v, -2) === '@@') {
  216. $out_str .= "#msgid \"$k\"\n";
  217. $out_str .= "#msgstr \"$v\"\n";
  218. } else {
  219. $out_str .= "msgid \"$k\"\n";
  220. $out_str .= "msgstr \"$v\"\n";
  221. }
  222. $out_str .= "\n";
  223. }
  224. $merged = $out_str;
  225. }
  226. echo "translation merged.\n";
  227. } else {
  228. if ($format === 'po') {
  229. $merged = '';
  230. sort($messages);
  231. foreach($messages as $message) {
  232. $message = preg_replace('/(\")|(\\\")/', '\\\"', $message);
  233. $merged .= "msgid \"$message\"\n";
  234. $merged .= "msgstr \"\"\n";
  235. $merged .= "\n";
  236. }
  237. } else {
  238. $merged = [];
  239. foreach ($messages as $message) {
  240. $merged[$message] = '';
  241. }
  242. ksort($merged);
  243. }
  244. echo "saved.\n";
  245. }
  246. if ($format === 'po') {
  247. $content = $merged;
  248. } else {
  249. $array = str_replace("\r", '', var_export($merged, true));
  250. $content = <<<EOD
  251. <?php
  252. /**
  253. * Message translations.
  254. *
  255. * This file is automatically generated by 'yii {$this->id}' command.
  256. * It contains the localizable messages extracted from source code.
  257. * You may modify this file by translating the extracted messages.
  258. *
  259. * Each array element represents the translation (value) of a message (key).
  260. * If the value is empty, the message is considered as not translated.
  261. * Messages that no longer need translation will have their translations
  262. * enclosed between a pair of '@@' marks.
  263. *
  264. * Message string can be used with plural forms format. Check i18n section
  265. * of the guide for details.
  266. *
  267. * NOTE: this file must be saved in UTF-8 encoding.
  268. */
  269. return $array;
  270. EOD;
  271. }
  272. file_put_contents($fileName, $content);
  273. }
  274. }