Path: blob/master/src/infrastructure/internationalization/management/PhabricatorInternationalizationManagementExtractWorkflow.php
12241 views
<?php12final class PhabricatorInternationalizationManagementExtractWorkflow3extends PhabricatorInternationalizationManagementWorkflow {45const CACHE_VERSION = 1;67protected function didConstruct() {8$this9->setName('extract')10->setExamples(11'**extract** [__options__] __library__')12->setSynopsis(pht('Extract translatable strings.'))13->setArguments(14array(15array(16'name' => 'paths',17'wildcard' => true,18),19array(20'name' => 'clean',21'help' => pht('Drop caches before extracting strings. Slow!'),22),23));24}2526public function execute(PhutilArgumentParser $args) {27$console = PhutilConsole::getConsole();2829$paths = $args->getArg('paths');30if (!$paths) {31$paths = array(getcwd());32}3334$targets = array();35foreach ($paths as $path) {36$root = Filesystem::resolvePath($path);3738if (!Filesystem::pathExists($root) || !is_dir($root)) {39throw new PhutilArgumentUsageException(40pht(41'Path "%s" does not exist, or is not a directory.',42$path));43}4445$libraries = id(new FileFinder($path))46->withPath('*/__phutil_library_init__.php')47->find();48if (!$libraries) {49throw new PhutilArgumentUsageException(50pht(51'Path "%s" contains no libphutil libraries.',52$path));53}5455foreach ($libraries as $library) {56$targets[] = Filesystem::resolvePath(dirname($path.'/'.$library)).'/';57}58}5960$targets = array_unique($targets);6162foreach ($targets as $library) {63echo tsprintf(64"**<bg:blue> %s </bg>** %s\n",65pht('EXTRACT'),66pht(67'Extracting "%s"...',68Filesystem::readablePath($library)));6970$this->extractLibrary($library);71}7273return 0;74}7576private function extractLibrary($root) {77$files = $this->loadLibraryFiles($root);78$cache = $this->readCache($root);7980$modified = $this->getModifiedFiles($files, $cache);81$cache['files'] = $files;8283if ($modified) {84echo tsprintf(85"**<bg:blue> %s </bg>** %s\n",86pht('MODIFIED'),87pht(88'Found %s modified file(s) (of %s total).',89phutil_count($modified),90phutil_count($files)));9192$old_strings = idx($cache, 'strings');93$old_strings = array_select_keys($old_strings, $files);94$new_strings = $this->extractFiles($root, $modified);95$all_strings = $new_strings + $old_strings;96$cache['strings'] = $all_strings;9798$this->writeStrings($root, $all_strings);99} else {100echo tsprintf(101"**<bg:blue> %s </bg>** %s\n",102pht('NOT MODIFIED'),103pht('Strings for this library are already up to date.'));104}105106$cache = id(new PhutilJSON())->encodeFormatted($cache);107$this->writeCache($root, 'i18n_files.json', $cache);108}109110private function getModifiedFiles(array $files, array $cache) {111$known = idx($cache, 'files', array());112$known = array_fuse($known);113114$modified = array();115foreach ($files as $file => $hash) {116117if (isset($known[$hash])) {118continue;119}120$modified[$file] = $hash;121}122123return $modified;124}125126private function extractFiles($root_path, array $files) {127$hashes = array();128129$futures = array();130foreach ($files as $file => $hash) {131$full_path = $root_path.DIRECTORY_SEPARATOR.$file;132$data = Filesystem::readFile($full_path);133$futures[$full_path] = PhutilXHPASTBinary::getParserFuture($data);134135$hashes[$full_path] = $hash;136}137138$bar = id(new PhutilConsoleProgressBar())139->setTotal(count($futures));140141$messages = array();142$results = array();143144$futures = id(new FutureIterator($futures))145->limit(8);146foreach ($futures as $full_path => $future) {147$bar->update(1);148149$hash = $hashes[$full_path];150151try {152$tree = XHPASTTree::newFromDataAndResolvedExecFuture(153Filesystem::readFile($full_path),154$future->resolve());155} catch (Exception $ex) {156$messages[] = pht(157'WARNING: Failed to extract strings from file "%s": %s',158$full_path,159$ex->getMessage());160continue;161}162163$root = $tree->getRootNode();164$calls = $root->selectDescendantsOfType('n_FUNCTION_CALL');165foreach ($calls as $call) {166$name = $call->getChildByIndex(0)->getConcreteString();167if ($name != 'pht') {168continue;169}170171$params = $call->getChildByIndex(1, 'n_CALL_PARAMETER_LIST');172$string_node = $params->getChildByIndex(0);173$string_line = $string_node->getLineNumber();174try {175$string_value = $string_node->evalStatic();176177$args = $params->getChildren();178$args = array_slice($args, 1);179180$types = array();181foreach ($args as $child) {182$type = null;183184switch ($child->getTypeName()) {185case 'n_FUNCTION_CALL':186$call = $child->getChildByIndex(0);187if ($call->getTypeName() == 'n_SYMBOL_NAME') {188switch ($call->getConcreteString()) {189case 'phutil_count':190$type = 'number';191break;192case 'phutil_person':193$type = 'person';194break;195}196}197break;198case 'n_NEW':199$class = $child->getChildByIndex(0);200if ($class->getTypeName() == 'n_CLASS_NAME') {201switch ($class->getConcreteString()) {202case 'PhutilNumber':203$type = 'number';204break;205}206}207break;208default:209break;210}211212$types[] = $type;213}214215$results[$hash][] = array(216'string' => $string_value,217'file' => Filesystem::readablePath($full_path, $root_path),218'line' => $string_line,219'types' => $types,220);221} catch (Exception $ex) {222$messages[] = pht(223'WARNING: Failed to evaluate pht() call on line %d in "%s": %s',224$call->getLineNumber(),225$full_path,226$ex->getMessage());227}228}229230$tree->dispose();231}232$bar->done();233234foreach ($messages as $message) {235echo tsprintf(236"**<bg:yellow> %s </bg>** %s\n",237pht('WARNING'),238$message);239}240241return $results;242}243244private function writeStrings($root, array $strings) {245$map = array();246foreach ($strings as $hash => $string_list) {247foreach ($string_list as $string_info) {248$string = $string_info['string'];249250$map[$string]['uses'][] = array(251'file' => $string_info['file'],252'line' => $string_info['line'],253);254255if (!isset($map[$string]['types'])) {256$map[$string]['types'] = $string_info['types'];257} else if ($map[$string]['types'] !== $string_info['types']) {258echo tsprintf(259"**<bg:yellow> %s </bg>** %s\n",260pht('WARNING'),261pht(262'Inferred types for string "%s" vary across callsites.',263$string_info['string']));264}265}266}267268ksort($map);269270$json = id(new PhutilJSON())->encodeFormatted($map);271$this->writeCache($root, 'i18n_strings.json', $json);272}273274private function loadLibraryFiles($root) {275$files = id(new FileFinder($root))276->withType('f')277->withSuffix('php')278->excludePath('*/.*')279->setGenerateChecksums(true)280->find();281282$map = array();283foreach ($files as $file => $hash) {284$file = Filesystem::readablePath($file, $root);285$file = ltrim($file, '/');286287if (dirname($file) == '.') {288continue;289}290291if (dirname($file) == 'extensions') {292continue;293}294295$map[$file] = md5($hash.$file);296}297298return $map;299}300301private function readCache($root) {302$path = $this->getCachePath($root, 'i18n_files.json');303304$default = array(305'version' => self::CACHE_VERSION,306'files' => array(),307'strings' => array(),308);309310if ($this->getArgv()->getArg('clean')) {311return $default;312}313314if (!Filesystem::pathExists($path)) {315return $default;316}317318try {319$data = Filesystem::readFile($path);320} catch (Exception $ex) {321return $default;322}323324try {325$cache = phutil_json_decode($data);326} catch (PhutilJSONParserException $e) {327return $default;328}329330$version = idx($cache, 'version');331if ($version !== self::CACHE_VERSION) {332return $default;333}334335return $cache;336}337338private function writeCache($root, $file, $data) {339$path = $this->getCachePath($root, $file);340341$cache_dir = dirname($path);342if (!Filesystem::pathExists($cache_dir)) {343Filesystem::createDirectory($cache_dir, 0755, true);344}345346Filesystem::writeFile($path, $data);347}348349private function getCachePath($root, $to_file) {350return $root.'/.cache/'.$to_file;351}352353}354355356