PageRenderTime 51ms CodeModel.GetById 23ms RepoModel.GetById 1ms app.codeStats 0ms

/maintenance/generateJsonI18n.php

https://gitlab.com/qiusct/mediawiki-i
PHP | 286 lines | 230 code | 14 blank | 42 comment | 6 complexity | 070dce004fe18846d0b673039161600c MD5 | raw file
Possible License(s): Apache-2.0, MIT, GPL-2.0
  1. <?php
  2. /**
  3. * Convert a PHP messages file to a set of JSON messages files.
  4. *
  5. * Usage:
  6. * php generateJsonI18n.php ExtensionName.i18n.php i18n/
  7. *
  8. * This program is free software; you can redistribute it and/or modify
  9. * it under the terms of the GNU General Public License as published by
  10. * the Free Software Foundation; either version 2 of the License, or
  11. * (at your option) any later version.
  12. *
  13. * This program is distributed in the hope that it will be useful,
  14. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. * GNU General Public License for more details.
  17. *
  18. * You should have received a copy of the GNU General Public License along
  19. * with this program; if not, write to the Free Software Foundation, Inc.,
  20. * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  21. * http://www.gnu.org/copyleft/gpl.html
  22. *
  23. * @file
  24. * @ingroup Maintenance
  25. */
  26. require_once __DIR__ . '/Maintenance.php';
  27. /**
  28. * Maintenance script to generate JSON i18n files from a PHP i18n file.
  29. *
  30. * @ingroup Maintenance
  31. */
  32. class GenerateJsonI18n extends Maintenance {
  33. public function __construct() {
  34. parent::__construct();
  35. $this->mDescription = "Build JSON messages files from a PHP messages file";
  36. $this->addArg( 'phpfile', 'PHP file defining a $messages array', false );
  37. $this->addArg( 'jsondir', 'Directory to write JSON files to', false );
  38. $this->addOption( 'langcode', 'Language code; only needed for converting core i18n files',
  39. false, true );
  40. $this->addOption( 'extension', 'Perform default conversion on an extension',
  41. false, true );
  42. $this->addOption( 'shim-only', 'Only create or update the backward-compatibility shim' );
  43. $this->addOption( 'supplementary', 'Find supplementary i18n files in subdirs and convert those',
  44. false, false );
  45. }
  46. public function execute() {
  47. global $IP;
  48. $phpfile = $this->getArg( 0 );
  49. $jsondir = $this->getArg( 1 );
  50. $extension = $this->getOption( 'extension' );
  51. $convertSupplementaryI18nFiles = $this->hasOption( 'supplementary' );
  52. if ( $extension ) {
  53. if ( $phpfile ) {
  54. $this->error( "The phpfile is already specified, conflicts with --extension.\n", 1 );
  55. }
  56. $phpfile = "$IP/extensions/$extension/$extension.i18n.php";
  57. }
  58. if ( !$phpfile ) {
  59. $this->error( "I'm here for an argument!\n" );
  60. $this->maybeHelp( true );
  61. // dies.
  62. }
  63. if ( $convertSupplementaryI18nFiles ) {
  64. if ( is_readable( $phpfile ) ) {
  65. $this->transformI18nFile( $phpfile, $jsondir );
  66. } else {
  67. // This is non-fatal because we might want to continue searching for
  68. // i18n files in subdirs even if the extension does not include a
  69. // primary i18n.php.
  70. $this->error( "Warning: no primary i18n file was found." );
  71. }
  72. $this->output( "Searching for supplementary i18n files...\n" );
  73. $dir_iterator = new RecursiveDirectoryIterator( dirname( $phpfile ) );
  74. $iterator = new RecursiveIteratorIterator( $dir_iterator, RecursiveIteratorIterator::LEAVES_ONLY );
  75. foreach ( $iterator as $path => $fileObject ) {
  76. if ( fnmatch( "*.i18n.php", $fileObject->getFilename() ) ) {
  77. $this->output( "Converting $path.\n" );
  78. $this->transformI18nFile( $path );
  79. }
  80. }
  81. } else {
  82. // Just convert the primary i18n file.
  83. $this->transformI18nFile( $phpfile, $jsondir );
  84. }
  85. }
  86. public function transformI18nFile( $phpfile, $jsondir = null ) {
  87. if ( !$jsondir ) {
  88. // Assume the json directory should be in the same directory as the
  89. // .i18n.php file.
  90. $jsondir = dirname( $phpfile ) . "/i18n";
  91. }
  92. if ( !is_dir( $jsondir ) ) {
  93. $this->output( "Creating directory $jsondir.\n" );
  94. $success = mkdir( $jsondir );
  95. if ( !$success ) {
  96. $this->error( "Could not create directory $jsondir\n", 1 );
  97. }
  98. }
  99. if ( $this->hasOption( 'shim-only' ) ) {
  100. $this->shimOnly( $phpfile, $jsondir );
  101. return;
  102. }
  103. if ( $jsondir === null ) {
  104. $this->error( 'Argument [jsondir] is required unless --shim-only is specified.' );
  105. $this->maybeHelp( true );
  106. }
  107. if ( !is_readable( $phpfile ) ) {
  108. $this->error( "Error reading $phpfile\n", 1 );
  109. }
  110. include $phpfile;
  111. $phpfileContents = file_get_contents( $phpfile );
  112. if ( !isset( $messages ) ) {
  113. $this->error( "PHP file $phpfile does not define \$messages array\n", 1 );
  114. }
  115. $extensionStyle = true;
  116. if ( !isset( $messages['en'] ) || !is_array( $messages['en'] ) ) {
  117. if ( !$this->hasOption( 'langcode' ) ) {
  118. $this->error( "PHP file $phpfile does not set language codes, --langcode " .
  119. "is required.\n", 1 );
  120. }
  121. $extensionStyle = false;
  122. $langcode = $this->getOption( 'langcode' );
  123. $messages = array( $langcode => $messages );
  124. } elseif ( $this->hasOption( 'langcode' ) ) {
  125. $this->output( "Warning: --langcode option set but will not be used.\n" );
  126. }
  127. foreach ( $messages as $langcode => $langmsgs ) {
  128. $authors = $this->getAuthorsFromComment( $this->findCommentBefore(
  129. $extensionStyle ? "\$messages['$langcode'] =" : '$messages =',
  130. $phpfileContents
  131. ) );
  132. // Make sure the @metadata key is the first key in the output
  133. $langmsgs = array_merge(
  134. array( '@metadata' => array( 'authors' => $authors ) ),
  135. $langmsgs
  136. );
  137. $jsonfile = "$jsondir/$langcode.json";
  138. $success = file_put_contents(
  139. $jsonfile,
  140. FormatJson::encode( $langmsgs, "\t", FormatJson::ALL_OK ) . "\n"
  141. );
  142. if ( $success === false ) {
  143. $this->error( "FAILED to write $jsonfile", 1 );
  144. }
  145. $this->output( "$jsonfile\n" );
  146. }
  147. if ( !$this->hasOption( 'langcode' ) ) {
  148. $shim = $this->doShim( $jsondir );
  149. file_put_contents( $phpfile, $shim );
  150. }
  151. $this->output( "All done.\n" );
  152. $this->output( "Also add \$wgMessagesDirs['YourExtension'] = __DIR__ . '/i18n';\n" );
  153. }
  154. protected function shimOnly( $phpfile, $jsondir ) {
  155. if ( file_exists( $phpfile ) ) {
  156. if ( !is_readable( $phpfile ) ) {
  157. $this->error( "Error reading $phpfile\n", 1 );
  158. }
  159. $phpfileContents = file_get_contents( $phpfile );
  160. $m = array();
  161. if ( !preg_match( '!"/([^"$]+)/\$csCode.json";!', $phpfileContents, $m ) ) {
  162. $this->error( "Cannot recognize $phpfile as a shim.\n", 1 );
  163. }
  164. if ( $jsondir === null ) {
  165. $jsondir = $m[1];
  166. }
  167. $this->output( "Updating existing shim $phpfile\n" );
  168. } elseif ( $jsondir === null ) {
  169. $this->error( "$phpfile does not exist.\n" .
  170. "Argument [jsondir] is required in order to create a new shim.\n", 1 );
  171. } else {
  172. $this->output( "Creating new shim $phpfile\n" );
  173. }
  174. $shim = $this->doShim( $jsondir );
  175. file_put_contents( $phpfile, $shim );
  176. $this->output( "All done.\n" );
  177. }
  178. protected function doShim( $jsondir ) {
  179. $shim = <<<'PHP'
  180. <?php
  181. /**
  182. * This is a backwards-compatibility shim, generated by:
  183. * https://git.wikimedia.org/blob/mediawiki%2Fcore.git/HEAD/maintenance%2FgenerateJsonI18n.php
  184. *
  185. * Beginning with MediaWiki 1.23, translation strings are stored in json files,
  186. * and the EXTENSION.i18n.php file only exists to provide compatibility with
  187. * older releases of MediaWiki. For more information about this migration, see:
  188. * https://www.mediawiki.org/wiki/Requests_for_comment/Localisation_format
  189. *
  190. * This shim maintains compatibility back to MediaWiki 1.17.
  191. */
  192. $messages = array();
  193. if ( !function_exists( '{{FUNC}}' ) ) {
  194. function {{FUNC}}( $cache, $code, &$cachedData ) {
  195. $codeSequence = array_merge( array( $code ), $cachedData['fallbackSequence'] );
  196. foreach ( $codeSequence as $csCode ) {
  197. $fileName = dirname( __FILE__ ) . "/{{OUT}}/$csCode.json";
  198. if ( is_readable( $fileName ) ) {
  199. $data = FormatJson::decode( file_get_contents( $fileName ), true );
  200. foreach ( array_keys( $data ) as $key ) {
  201. if ( $key === '' || $key[0] === '@' ) {
  202. unset( $data[$key] );
  203. }
  204. }
  205. $cachedData['messages'] = array_merge( $data, $cachedData['messages'] );
  206. }
  207. $cachedData['deps'][] = new FileDependency( $fileName );
  208. }
  209. return true;
  210. }
  211. $GLOBALS['wgHooks']['LocalisationCacheRecache'][] = '{{FUNC}}';
  212. }
  213. PHP;
  214. $jsondir = str_replace( '\\', '/', $jsondir );
  215. $shim = str_replace( '{{OUT}}', $jsondir, $shim );
  216. $shim = str_replace( '{{FUNC}}', 'wfJsonI18nShim' . wfRandomString( 16 ), $shim );
  217. return $shim;
  218. }
  219. /**
  220. * Find the documentation comment immediately before a given search string
  221. * @param string $needle String to search for
  222. * @param string $haystack String to search in
  223. * @return string Substring of $haystack starting at '/**' ending right before $needle, or empty
  224. */
  225. protected function findCommentBefore( $needle, $haystack ) {
  226. $needlePos = strpos( $haystack, $needle );
  227. if ( $needlePos === false ) {
  228. return '';
  229. }
  230. // Need to pass a negative offset to strrpos() so it'll search backwards from the
  231. // offset
  232. $startPos = strrpos( $haystack, '/**', $needlePos - strlen( $haystack ) );
  233. if ( $startPos === false ) {
  234. return '';
  235. }
  236. return substr( $haystack, $startPos, $needlePos - $startPos );
  237. }
  238. /**
  239. * Get an array of author names from a documentation comment containing @author declarations.
  240. * @param string $comment Documentation comment
  241. * @return array Array of author names (strings)
  242. */
  243. protected function getAuthorsFromComment( $comment ) {
  244. $matches = null;
  245. preg_match_all( '/@author (.*?)$/m', $comment, $matches );
  246. return $matches && $matches[1] ? $matches[1] : array();
  247. }
  248. }
  249. $maintClass = "GenerateJsonI18n";
  250. require_once RUN_MAINTENANCE_IF_MAIN;