MediaWiki  REL1_24
generateJsonI18n.php
Go to the documentation of this file.
00001 <?php
00002 
00028 require_once __DIR__ . '/Maintenance.php';
00029 
00035 class GenerateJsonI18n extends Maintenance {
00036     public function __construct() {
00037         parent::__construct();
00038         $this->mDescription = "Build JSON messages files from a PHP messages file";
00039 
00040         $this->addArg( 'phpfile', 'PHP file defining a $messages array', false );
00041         $this->addArg( 'jsondir', 'Directory to write JSON files to', false );
00042         $this->addOption( 'langcode', 'Language code; only needed for converting core i18n files',
00043             false, true );
00044         $this->addOption( 'extension', 'Perform default conversion on an extension',
00045             false, true );
00046         $this->addOption( 'shim-only', 'Only create or update the backward-compatibility shim' );
00047         $this->addOption( 'supplementary', 'Find supplementary i18n files in subdirs and convert those',
00048             false, false );
00049     }
00050 
00051     public function execute() {
00052         global $IP;
00053 
00054         $phpfile = $this->getArg( 0 );
00055         $jsondir = $this->getArg( 1 );
00056         $extension = $this->getOption( 'extension' );
00057         $convertSupplementaryI18nFiles = $this->hasOption( 'supplementary' );
00058 
00059         if ( $extension ) {
00060             if ( $phpfile ) {
00061                 $this->error( "The phpfile is already specified, conflicts with --extension.\n", 1 );
00062             }
00063             $phpfile = "$IP/extensions/$extension/$extension.i18n.php";
00064         }
00065 
00066         if ( !$phpfile ) {
00067             $this->error( "I'm here for an argument!\n" );
00068             $this->maybeHelp( true );
00069             // dies.
00070         }
00071 
00072         if ( $convertSupplementaryI18nFiles ) {
00073             if ( is_readable( $phpfile ) ) {
00074                 $this->transformI18nFile( $phpfile, $jsondir );
00075             } else {
00076                 // This is non-fatal because we might want to continue searching for
00077                 // i18n files in subdirs even if the extension does not include a
00078                 // primary i18n.php.
00079                 $this->error( "Warning: no primary i18n file was found." );
00080             }
00081             $this->output( "Searching for supplementary i18n files...\n" );
00082             $dir_iterator = new RecursiveDirectoryIterator( dirname( $phpfile ) );
00083             $iterator = new RecursiveIteratorIterator(
00084                 $dir_iterator, RecursiveIteratorIterator::LEAVES_ONLY );
00085             foreach ( $iterator as $path => $fileObject ) {
00086                 if ( fnmatch( "*.i18n.php", $fileObject->getFilename() ) ) {
00087                     $this->output( "Converting $path.\n" );
00088                     $this->transformI18nFile( $path );
00089                 }
00090             }
00091         } else {
00092             // Just convert the primary i18n file.
00093             $this->transformI18nFile( $phpfile, $jsondir );
00094         }
00095     }
00096 
00097     public function transformI18nFile( $phpfile, $jsondir = null ) {
00098         if ( !$jsondir ) {
00099             // Assume the json directory should be in the same directory as the
00100             // .i18n.php file.
00101             $jsondir = dirname( $phpfile ) . "/i18n";
00102         }
00103         if ( !is_dir( $jsondir ) ) {
00104             $this->output( "Creating directory $jsondir.\n" );
00105             $success = mkdir( $jsondir );
00106             if ( !$success ) {
00107                 $this->error( "Could not create directory $jsondir\n", 1 );
00108             }
00109         }
00110 
00111         if ( $this->hasOption( 'shim-only' ) ) {
00112             $this->shimOnly( $phpfile, $jsondir );
00113 
00114             return;
00115         }
00116 
00117         if ( $jsondir === null ) {
00118             $this->error( 'Argument [jsondir] is required unless --shim-only is specified.' );
00119             $this->maybeHelp( true );
00120         }
00121 
00122         if ( !is_readable( $phpfile ) ) {
00123             $this->error( "Error reading $phpfile\n", 1 );
00124         }
00125         include $phpfile;
00126         $phpfileContents = file_get_contents( $phpfile );
00127 
00128         if ( !isset( $messages ) ) {
00129             $this->error( "PHP file $phpfile does not define \$messages array\n", 1 );
00130         }
00131 
00132         $extensionStyle = true;
00133         if ( !isset( $messages['en'] ) || !is_array( $messages['en'] ) ) {
00134             if ( !$this->hasOption( 'langcode' ) ) {
00135                 $this->error( "PHP file $phpfile does not set language codes, --langcode " .
00136                     "is required.\n", 1 );
00137             }
00138             $extensionStyle = false;
00139             $langcode = $this->getOption( 'langcode' );
00140             $messages = array( $langcode => $messages );
00141         } elseif ( $this->hasOption( 'langcode' ) ) {
00142             $this->output( "Warning: --langcode option set but will not be used.\n" );
00143         }
00144 
00145         foreach ( $messages as $langcode => $langmsgs ) {
00146             $authors = $this->getAuthorsFromComment( $this->findCommentBefore(
00147                 $extensionStyle ? "\$messages['$langcode'] =" : '$messages =',
00148                 $phpfileContents
00149             ) );
00150             // Make sure the @metadata key is the first key in the output
00151             $langmsgs = array_merge(
00152                 array( '@metadata' => array( 'authors' => $authors ) ),
00153                 $langmsgs
00154             );
00155 
00156             $jsonfile = "$jsondir/$langcode.json";
00157             $success = file_put_contents(
00158                 $jsonfile,
00159                 FormatJson::encode( $langmsgs, "\t", FormatJson::ALL_OK ) . "\n"
00160             );
00161             if ( $success === false ) {
00162                 $this->error( "FAILED to write $jsonfile", 1 );
00163             }
00164             $this->output( "$jsonfile\n" );
00165         }
00166 
00167         if ( !$this->hasOption( 'langcode' ) ) {
00168             $shim = $this->doShim( $jsondir );
00169             file_put_contents( $phpfile, $shim );
00170         }
00171 
00172         $this->output( "All done.\n" );
00173         $this->output( "Also add \$wgMessagesDirs['YourExtension'] = __DIR__ . '/i18n';\n" );
00174     }
00175 
00176     protected function shimOnly( $phpfile, $jsondir ) {
00177         if ( file_exists( $phpfile ) ) {
00178             if ( !is_readable( $phpfile ) ) {
00179                 $this->error( "Error reading $phpfile\n", 1 );
00180             }
00181 
00182             $phpfileContents = file_get_contents( $phpfile );
00183             $m = array();
00184             if ( !preg_match( '!"/([^"$]+)/\$csCode.json";!', $phpfileContents, $m ) ) {
00185                 $this->error( "Cannot recognize $phpfile as a shim.\n", 1 );
00186             }
00187 
00188             if ( $jsondir === null ) {
00189                 $jsondir = $m[1];
00190             }
00191 
00192             $this->output( "Updating existing shim $phpfile\n" );
00193         } elseif ( $jsondir === null ) {
00194             $this->error( "$phpfile does not exist.\n" .
00195                 "Argument [jsondir] is required in order to create a new shim.\n", 1 );
00196         } else {
00197             $this->output( "Creating new shim $phpfile\n" );
00198         }
00199 
00200         $shim = $this->doShim( $jsondir );
00201         file_put_contents( $phpfile, $shim );
00202         $this->output( "All done.\n" );
00203     }
00204 
00205     protected function doShim( $jsondir ) {
00206         $shim = <<<'PHP'
00207 <?php
00219 $messages = array();
00220 if ( !function_exists( '{{FUNC}}' ) ) {
00221     function {{FUNC}}( $cache, $code, &$cachedData ) {
00222         $codeSequence = array_merge( array( $code ), $cachedData['fallbackSequence'] );
00223         foreach ( $codeSequence as $csCode ) {
00224             $fileName = dirname( __FILE__ ) . "/{{OUT}}/$csCode.json";
00225             if ( is_readable( $fileName ) ) {
00226                 $data = FormatJson::decode( file_get_contents( $fileName ), true );
00227                 foreach ( array_keys( $data ) as $key ) {
00228                     if ( $key === '' || $key[0] === '@' ) {
00229                         unset( $data[$key] );
00230                     }
00231                 }
00232                 $cachedData['messages'] = array_merge( $data, $cachedData['messages'] );
00233             }
00234 
00235             $cachedData['deps'][] = new FileDependency( $fileName );
00236         }
00237         return true;
00238     }
00239 
00240     $GLOBALS['wgHooks']['LocalisationCacheRecache'][] = '{{FUNC}}';
00241 }
00242 
00243 PHP;
00244 
00245         $jsondir = str_replace( '\\', '/', $jsondir );
00246         $shim = str_replace( '{{OUT}}', $jsondir, $shim );
00247         $shim = str_replace( '{{FUNC}}', 'wfJsonI18nShim' . wfRandomString( 16 ), $shim );
00248 
00249         return $shim;
00250     }
00251 
00258     protected function findCommentBefore( $needle, $haystack ) {
00259         $needlePos = strpos( $haystack, $needle );
00260         if ( $needlePos === false ) {
00261             return '';
00262         }
00263         // Need to pass a negative offset to strrpos() so it'll search backwards from the
00264         // offset
00265         $startPos = strrpos( $haystack, '
00278