2008-10-16 22:42:41 +02:00
< ? php
/**
2008-11-01 14:26:08 +01:00
* SilverStripe - variant of the " gettext " tool :
* Parses the string content of all PHP - files and SilverStripe templates
* for ocurrences of the _t () translation method . Also uses the { @ link i18nEntityProvider }
* interface to get dynamically defined entities by executing the
* { @ link provideI18nEntities ()} method on all implementors of this interface .
*
* Collects all found entities ( and their natural language text for the default locale )
* into language - files for each module in an array notation . Creates or overwrites these files ,
* e . g . sapphire / lang / en_US . php .
*
* The collector needs to be run whenever you make new translatable
* entities available . Please don ' t alter the arrays in language tables manually .
*
2009-11-10 22:50:27 +01:00
* Usage through URL : http :// localhost / dev / tasks / i18nTextCollectorTast
* Usage through URL ( module - specific ) : http :// localhost / dev / tasks / i18nTextCollectorTask / ? module = mymodule
* Usage on CLI : sake dev / tasks / i18nTextCollectorTask
* Usage on CLI ( module - specific ) : sake dev / tasks / i18nTextCollectorTask module = mymodule
2008-11-01 15:49:37 +01:00
*
* Requires PHP 5.1 + due to class_implements () limitations
2008-11-01 14:26:08 +01:00
*
2008-10-16 22:42:41 +02:00
* @ author Bernat Foj Capell < bernat @ silverstripe . com >
2008-10-17 17:21:33 +02:00
* @ author Ingo Schommer < FIRSTNAME @ silverstripe . com >
2008-10-16 22:42:41 +02:00
* @ package sapphire
2008-11-01 14:26:08 +01:00
* @ subpackage i18n
* @ uses i18nEntityProvider
* @ uses i18n
2008-10-16 22:42:41 +02:00
*/
2009-11-10 22:50:27 +01:00
class i18nTextCollector extends Object {
2008-10-16 22:42:41 +02:00
protected $defaultLocale ;
2008-10-17 17:21:33 +02:00
/**
* @ var string $basePath The directory base on which the collector should act .
* Usually the webroot set through { @ link Director :: baseFolder ()} .
* @ todo Fully support changing of basePath through { @ link SSViewer } and { @ link ManifestBuilder }
*/
public $basePath ;
/**
* @ var string $basePath The directory base on which the collector should create new lang folders and files .
* Usually the webroot set through { @ link Director :: baseFolder ()} .
* Can be overwritten for testing or export purposes .
* @ todo Fully support changing of basePath through { @ link SSViewer } and { @ link ManifestBuilder }
*/
public $baseSavePath ;
2008-10-16 22:42:41 +02:00
/**
* @ param $locale
*/
function __construct ( $locale = null ) {
$this -> defaultLocale = ( $locale ) ? $locale : i18n :: default_locale ();
2008-10-17 17:21:33 +02:00
$this -> basePath = Director :: baseFolder ();
$this -> baseSavePath = Director :: baseFolder ();
2008-10-16 22:42:41 +02:00
parent :: __construct ();
}
/**
* This is the main method to build the master string tables with the original strings .
* It will search for existent modules that use the i18n feature , parse the _t () calls
* and write the resultant files in the lang folder of each module .
*
* @ uses DataObject -> collectI18nStatics ()
2009-01-05 07:19:48 +01:00
*
* @ param array $restrictToModules
2008-10-16 22:42:41 +02:00
*/
2009-01-05 07:19:48 +01:00
public function run ( $restrictToModules = null ) {
2008-10-20 14:39:49 +02:00
//Debug::message("Collecting text...", false);
2008-10-16 22:42:41 +02:00
2009-01-05 07:19:48 +01:00
$modules = array ();
2008-10-16 22:42:41 +02:00
// A master string tables array (one mst per module)
2008-10-17 17:21:33 +02:00
$entitiesByModule = array ();
2008-10-16 22:42:41 +02:00
//Search for and process existent modules, or use the passed one instead
2009-11-10 22:50:27 +01:00
if ( $restrictToModules && count ( $restrictToModules )) {
foreach ( $restrictToModules as $restrictToModule ) {
$modules [] = basename ( $restrictToModule );
}
} else {
2009-01-05 07:19:48 +01:00
$modules = scandir ( $this -> basePath );
2009-11-10 22:50:27 +01:00
}
2008-10-17 17:21:33 +02:00
foreach ( $modules as $module ) {
// Only search for calls in folder with a _config.php file (which means they are modules)
$isValidModuleFolder = (
is_dir ( " $this->basePath / $module " )
&& is_file ( " $this->basePath / $module /_config.php " )
&& substr ( $module , 0 , 1 ) != '.'
);
if ( ! $isValidModuleFolder ) continue ;
// we store the master string tables
2009-01-05 07:19:48 +01:00
$processedEntities = $this -> processModule ( $module );
2010-10-15 03:19:58 +02:00
2009-01-05 07:19:48 +01:00
if ( isset ( $entitiesByModule [ $module ])) {
$entitiesByModule [ $module ] = array_merge_recursive ( $entitiesByModule [ $module ], $processedEntities );
} else {
$entitiesByModule [ $module ] = $processedEntities ;
}
// extract all entities for "foreign" modules (fourth argument)
foreach ( $entitiesByModule [ $module ] as $fullName => $spec ) {
2010-10-15 03:19:58 +02:00
if ( isset ( $spec [ 3 ]) && $spec [ 3 ] && $spec [ 3 ] != $module ) {
2009-01-05 07:19:48 +01:00
$othermodule = $spec [ 3 ];
if ( ! isset ( $entitiesByModule [ $othermodule ])) $entitiesByModule [ $othermodule ] = array ();
unset ( $spec [ 3 ]);
$entitiesByModule [ $othermodule ][ $fullName ] = $spec ;
unset ( $entitiesByModule [ $module ][ $fullName ]);
}
2010-10-15 03:19:58 +02:00
}
2008-10-16 22:42:41 +02:00
}
2009-01-05 07:19:48 +01:00
2008-10-16 22:42:41 +02:00
// Write the generated master string tables
2008-10-17 17:21:33 +02:00
$this -> writeMasterStringFile ( $entitiesByModule );
2008-10-16 22:42:41 +02:00
2008-10-20 14:39:49 +02:00
//Debug::message("Done!", false);
2008-10-16 22:42:41 +02:00
}
/**
* Build the module ' s master string table
*
* @ param string $module Module ' s name
*/
2008-10-17 17:21:33 +02:00
protected function processModule ( $module ) {
$entitiesArr = array ();
2008-10-16 22:42:41 +02:00
2008-10-20 14:39:49 +02:00
//Debug::message("Processing Module '{$module}'", false);
2008-10-17 17:21:33 +02:00
// Search for calls in code files if these exists
if ( is_dir ( " $this->basePath / $module /code " )) {
$fileList = $this -> getFilesRecursive ( " $this->basePath / $module /code " );
2008-10-17 19:44:14 +02:00
} else if ( $module == 'sapphire' ) {
2008-10-17 17:21:33 +02:00
// sapphire doesn't have the usual module structure, so we'll scan all subfolders
$fileList = $this -> getFilesRecursive ( " $this->basePath / $module " );
}
foreach ( $fileList as $filePath ) {
// exclude ss-templates, they're scanned separately
if ( substr ( $filePath , - 3 ) == 'php' ) {
$content = file_get_contents ( $filePath );
$entitiesArr = array_merge ( $entitiesArr ,( array ) $this -> collectFromCode ( $content , $module ));
2008-10-29 22:07:17 +01:00
$entitiesArr = array_merge ( $entitiesArr , ( array ) $this -> collectFromEntityProviders ( $filePath , $module ));
2008-10-16 22:42:41 +02:00
}
2008-10-17 17:21:33 +02:00
}
// Search for calls in template files if these exists
if ( is_dir ( " $this->basePath / $module /templates " )) {
$fileList = $this -> getFilesRecursive ( " $this->basePath / $module /templates " );
foreach ( $fileList as $index => $filePath ) {
$content = file_get_contents ( $filePath );
// templates use their filename as a namespace
$namespace = basename ( $filePath );
$entitiesArr = array_merge ( $entitiesArr , ( array ) $this -> collectFromTemplate ( $content , $module , $namespace ));
2008-10-16 22:42:41 +02:00
}
2008-10-17 17:21:33 +02:00
}
// sort for easier lookup and comparison with translated files
2008-10-17 19:44:14 +02:00
ksort ( $entitiesArr );
2008-10-17 17:21:33 +02:00
return $entitiesArr ;
2008-10-16 22:42:41 +02:00
}
2008-10-17 17:21:33 +02:00
public function collectFromCode ( $content , $module ) {
$entitiesArr = array ();
$regexRule = '_t[[:space:]]*\(' .
2009-07-09 03:02:43 +02:00
'[[:space:]]*("[^"]*"|\\\'[^\']*\\\')[[:space:]]*,' . // namespace.entity
'[[:space:]]*(("([^"]|\\\")*"|\'([^\']|\\\\\')*\')' . // value
'([[:space:]]*\\.[[:space:]]*("([^"]|\\\")*"|\'([^\']|\\\\\')*\'))*)' . // concatenations
'([[:space:]]*,[[:space:]]*[^,)]*)?([[:space:]]*,' . // priority (optional)
'[[:space:]]*("([^"]|\\\")*"|\'([^\']|\\\\\')*\'))?[[:space:]]*' . // comment (optional)
2008-10-17 17:21:33 +02:00
'\)' ;
2009-07-09 04:37:01 +02:00
2008-10-17 17:21:33 +02:00
while ( ereg ( $regexRule , $content , $regs )) {
$entitiesArr = array_merge ( $entitiesArr , ( array ) $this -> entitySpecFromRegexMatches ( $regs ));
2008-10-16 22:42:41 +02:00
2008-10-17 17:21:33 +02:00
// remove parsed content to continue while() loop
2008-10-16 22:42:41 +02:00
$content = str_replace ( $regs [ 0 ], " " , $content );
}
2008-10-17 19:44:14 +02:00
ksort ( $entitiesArr );
2008-10-17 17:21:33 +02:00
return $entitiesArr ;
2008-10-16 22:42:41 +02:00
}
2008-10-17 17:21:33 +02:00
public function collectFromTemplate ( $content , $module , $fileName ) {
$entitiesArr = array ();
2008-10-16 22:42:41 +02:00
// Search for included templates
2008-10-17 17:21:33 +02:00
preg_match_all ( '/<' . '% include +([A-Za-z0-9_]+) +%' . '>/' , $content , $regs , PREG_SET_ORDER );
foreach ( $regs as $reg ) {
$includeName = $reg [ 1 ];
$includeFileName = " { $includeName } .ss " ;
$filePath = SSViewer :: getTemplateFileByType ( $includeName , 'Includes' );
2009-05-25 08:59:21 +02:00
if ( ! $filePath ) $filePath = SSViewer :: getTemplateFileByType ( $includeName , 'main' );
if ( $filePath ) {
$includeContent = file_get_contents ( $filePath );
$entitiesArr = array_merge ( $entitiesArr ,( array ) $this -> collectFromTemplate ( $includeContent , $module , $includeFileName ));
}
2008-10-17 17:21:33 +02:00
// @todo Will get massively confused if you include the includer -> infinite loop
2008-10-16 22:42:41 +02:00
}
2009-03-04 04:44:11 +01:00
// @todo respect template tags (< % _t() % > instead of _t())
2008-10-17 17:21:33 +02:00
$regexRule = '_t[[:space:]]*\(' .
2009-07-09 03:02:43 +02:00
'[[:space:]]*("[^"]*"|\\\'[^\']*\\\')[[:space:]]*,' . // namespace.entity
'[[:space:]]*(("([^"]|\\\")*"|\'([^\']|\\\\\')*\')' . // value
'([[:space:]]*\\.[[:space:]]*("([^"]|\\\")*"|\'([^\']|\\\\\')*\'))*)' . // concatenations
'([[:space:]]*,[[:space:]]*[^,)]*)?([[:space:]]*,' . // priority (optional)
'[[:space:]]*("([^"]|\\\")*"|\'([^\']|\\\\\')*\'))?[[:space:]]*' . // comment (optional)
2008-10-17 17:21:33 +02:00
'\)' ;
while ( ereg ( $regexRule , $content , $regs )) {
$entitiesArr = array_merge ( $entitiesArr ,( array ) $this -> entitySpecFromRegexMatches ( $regs , $fileName ));
// remove parsed content to continue while() loop
$content = str_replace ( $regs [ 0 ], " " , $content );
}
2008-10-17 19:44:14 +02:00
ksort ( $entitiesArr );
2008-10-17 17:21:33 +02:00
return $entitiesArr ;
}
2008-11-02 00:16:45 +01:00
/**
* @ uses i18nEntityProvider
*/
2008-11-01 19:56:39 +01:00
function collectFromEntityProviders ( $filePath ) {
$entitiesArr = array ();
$classes = ClassInfo :: classes_for_file ( $filePath );
if ( $classes ) foreach ( $classes as $class ) {
// Not all classes can be instanciated without mandatory arguments,
// so entity collection doesn't work for all SilverStripe classes currently
// Requires PHP 5.1+
if ( class_exists ( $class ) && in_array ( 'i18nEntityProvider' , class_implements ( $class ))) {
2008-11-02 01:29:11 +01:00
$reflectionClass = new ReflectionClass ( $class );
if ( $reflectionClass -> isAbstract ()) continue ;
2009-02-02 00:49:53 +01:00
2008-11-01 19:56:39 +01:00
$obj = singleton ( $class );
$entitiesArr = array_merge ( $entitiesArr ,( array ) $obj -> provideI18nEntities ());
}
}
ksort ( $entitiesArr );
return $entitiesArr ;
}
2008-10-17 17:21:33 +02:00
/**
* @ todo Fix regexes so the deletion of quotes , commas and newlines from wrong matches isn ' t necessary
*/
protected function entitySpecFromRegexMatches ( $regs , $_namespace = null ) {
// remove wrapping quotes
$fullName = substr ( $regs [ 1 ], 1 , - 1 );
// split fullname into entity parts
$entityParts = explode ( '.' , $fullName );
if ( count ( $entityParts ) > 1 ) {
// templates don't have a custom namespace
2008-10-16 22:42:41 +02:00
$entity = array_pop ( $entityParts );
2008-10-17 17:21:33 +02:00
// namespace might contain dots, so we explode
$namespace = implode ( '.' , $entityParts );
} else {
$entity = array_pop ( $entityParts );
$namespace = $_namespace ;
}
2008-10-29 22:07:17 +01:00
// If a dollar sign is used in the entity name,
// we can't resolve without running the method,
// and skip the processing. This is mostly used for
// dynamically translating static properties, e.g. looping
// through $db, which are detected by {@link collectFromEntityProviders}.
if ( strpos ( '$' , $entity ) !== FALSE ) return false ;
2008-10-17 17:21:33 +02:00
// remove wrapping quotes
$value = ( $regs [ 2 ]) ? substr ( $regs [ 2 ], 1 , - 1 ) : null ;
2009-07-09 04:37:01 +02:00
$value = ereg_replace ( " ([^ \\ ])[' \" ][[:space:]]* \ .[[:space:]]*[' \" ] " , '\\1' , $value );
2008-10-16 22:42:41 +02:00
2008-10-17 17:21:33 +02:00
// only escape quotes when wrapped in double quotes, to make them safe for insertion
// into single-quoted PHP code. If they're wrapped in single quotes, the string should
// be properly escaped already
2009-07-09 04:37:01 +02:00
if ( substr ( $regs [ 2 ], 0 , 1 ) == '"' ) {
// Double quotes don't need escaping
$value = str_replace ( '\\"' , '"' , $value );
// But single quotes do
$value = str_replace ( " ' " , " \\ ' " , $value );
}
2008-10-17 17:21:33 +02:00
// remove starting comma and any newlines
2009-11-21 02:43:54 +01:00
$eol = PHP_EOL ;
$prio = ( $regs [ 10 ]) ? trim ( preg_replace ( " / $eol / " , '' , substr ( $regs [ 10 ], 1 ))) : null ;
2008-10-17 17:21:33 +02:00
// remove wrapping quotes
2009-07-09 03:02:43 +02:00
$comment = ( $regs [ 12 ]) ? substr ( $regs [ 12 ], 1 , - 1 ) : null ;
2008-10-16 22:42:41 +02:00
2008-10-17 17:21:33 +02:00
return array (
" { $namespace } . { $entity } " => array (
$value ,
$prio ,
$comment
)
);
}
/**
* Input for langArrayCodeForEntitySpec () should be suitable for insertion
* into single - quoted strings , so needs to be escaped already .
*
* @ param string $entity The entity name , e . g . CMSMain . BUTTONSAVE
*/
public function langArrayCodeForEntitySpec ( $entityFullName , $entitySpec ) {
$php = '' ;
2009-11-21 02:43:54 +01:00
$eol = PHP_EOL ;
2008-10-17 17:21:33 +02:00
$entityParts = explode ( '.' , $entityFullName );
if ( count ( $entityParts ) > 1 ) {
// templates don't have a custom namespace
$entity = array_pop ( $entityParts );
// namespace might contain dots, so we implode back
$namespace = implode ( '.' , $entityParts );
} else {
user_error ( " i18nTextCollector::langArrayCodeForEntitySpec(): Wrong entity format for $entityFullName with values " . var_export ( $entitySpec , true ), E_USER_WARNING );
return false ;
}
$value = $entitySpec [ 0 ];
$prio = ( isset ( $entitySpec [ 1 ])) ? addcslashes ( $entitySpec [ 1 ], '\'' ) : null ;
$comment = ( isset ( $entitySpec [ 2 ])) ? addcslashes ( $entitySpec [ 2 ], '\'' ) : null ;
$php .= '$lang[\'' . $this -> defaultLocale . '\'][\'' . $namespace . '\'][\'' . $entity . '\'] = ' ;
if ( $prio ) {
2009-11-21 02:43:54 +01:00
$php .= " array( $eol\t ' " . $value . " ', $eol\t " . $prio ;
2008-10-17 17:21:33 +02:00
if ( $comment ) {
2009-11-21 02:43:54 +01:00
$php .= " , $eol\t ' " . $comment . '\'' ;
2008-10-17 17:21:33 +02:00
}
2009-11-21 02:43:54 +01:00
$php .= " $eol ); " ;
2008-10-17 17:21:33 +02:00
} else {
$php .= '\'' . $value . '\';' ;
2008-10-16 22:42:41 +02:00
}
2009-11-21 02:43:54 +01:00
$php .= " $eol " ;
2008-10-17 17:21:33 +02:00
return $php ;
}
2008-11-01 19:56:39 +01:00
/**
* Write the master string table of every processed module
*/
protected function writeMasterStringFile ( $entitiesByModule ) {
// Write each module language file
if ( $entitiesByModule ) foreach ( $entitiesByModule as $module => $entities ) {
$php = '' ;
2009-11-21 02:43:54 +01:00
$eol = PHP_EOL ;
2008-11-01 19:56:39 +01:00
// Create folder for lang files
$langFolder = $this -> baseSavePath . '/' . $module . '/lang' ;
if ( ! file_exists ( $langFolder )) {
Filesystem :: makeFolder ( $langFolder , Filesystem :: $folder_create_mask );
touch ( $langFolder . '/_manifest_exclude' );
}
// Open the English file and write the Master String Table
2008-11-01 20:05:16 +01:00
$langFile = $langFolder . '/' . $this -> defaultLocale . '.php' ;
if ( $fh = fopen ( $langFile , " w " )) {
2008-11-01 19:56:39 +01:00
if ( $entities ) foreach ( $entities as $fullName => $spec ) {
$php .= $this -> langArrayCodeForEntitySpec ( $fullName , $spec );
}
// test for valid PHP syntax by eval'ing it
try {
2008-11-01 20:05:16 +01:00
eval ( $php );
2008-11-01 19:56:39 +01:00
} catch ( Exception $e ) {
user_error ( 'i18nTextCollector->writeMasterStringFile(): Invalid PHP language file. Error: ' . $e -> toString (), E_USER_ERROR );
}
2009-11-21 02:43:54 +01:00
fwrite ( $fh , " < " . " ?php { $eol } { $eol } global \$ lang; { $eol } { $eol } " . $php . " { $eol } ? " . " > " );
2008-11-01 19:56:39 +01:00
fclose ( $fh );
//Debug::message("Created file: $langFolder/" . $this->defaultLocale . ".php", false);
} else {
user_error ( " Cannot write language file! Please check permissions of $langFolder / " . $this -> defaultLocale . " .php " , E_USER_ERROR );
2008-10-17 17:21:33 +02:00
}
}
2008-11-01 19:56:39 +01:00
}
/**
* Helper function that searches for potential files to be parsed
*
* @ param string $folder base directory to scan ( will scan recursively )
* @ param array $fileList Array where potential files will be added to
*/
protected function getFilesRecursive ( $folder , & $fileList = null ) {
if ( ! $fileList ) $fileList = array ();
$items = scandir ( $folder );
$isValidFolder = (
! in_array ( '_manifest_exclude' , $items )
&& ! preg_match ( '/\/tests$/' , $folder )
);
if ( $items && $isValidFolder ) foreach ( $items as $item ) {
if ( substr ( $item , 0 , 1 ) == '.' ) continue ;
if ( substr ( $item , - 4 ) == '.php' ) $fileList [ substr ( $item , 0 , - 4 )] = " $folder / $item " ;
else if ( substr ( $item , - 3 ) == '.ss' ) $fileList [ $item ] = " $folder / $item " ;
else if ( is_dir ( " $folder / $item " )) $this -> getFilesRecursive ( " $folder / $item " , $fileList );
}
return $fileList ;
2008-10-17 17:21:33 +02:00
}
public function getDefaultLocale () {
return $this -> defaultLocale ;
}
public function setDefaultLocale ( $locale ) {
$this -> defaultLocale = $locale ;
2008-10-16 22:42:41 +02:00
}
}
?>