mirror of
https://we.phorge.it/source/phorge.git
synced 2024-11-25 00:02:41 +01:00
Make i18n string extraction faster and more flexible
Summary: Ref T5267. Two general changes: - Make string extraction use a cache, so that it doesn't take several minutes every time you change something. Minor updates now only take a few seconds (like `arc liberate` and similar). - Instead of dumping a sort-of-template file out, write out to a cache (`src/.cache/i18n_strings.json`). I'm planning to add more steps to read this cache and do interesting things with it (emit translatewiki strings, generate or update standalone translation files, etc). Test Plan: - Ran `bin/i18n extract`. - Ran it again, saw it go a lot faster. - Changed stuff, ran it, saw it only look at new stuff. - Examined caches. Reviewers: chad Reviewed By: chad Maniphest Tasks: T5267 Differential Revision: https://secure.phabricator.com/D16227
This commit is contained in:
parent
d09094f4fb
commit
ccc7c1b424
2 changed files with 229 additions and 46 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -4,6 +4,7 @@
|
|||
# Diviner
|
||||
/docs/
|
||||
/.divinercache/
|
||||
/src/.cache/
|
||||
|
||||
# libphutil
|
||||
/src/.phutil_module_cache
|
||||
|
|
|
@ -3,9 +3,13 @@
|
|||
final class PhabricatorInternationalizationManagementExtractWorkflow
|
||||
extends PhabricatorInternationalizationManagementWorkflow {
|
||||
|
||||
const CACHE_VERSION = 1;
|
||||
|
||||
protected function didConstruct() {
|
||||
$this
|
||||
->setName('extract')
|
||||
->setExamples(
|
||||
'**extract** [__options__] __library__')
|
||||
->setSynopsis(pht('Extract translatable strings.'))
|
||||
->setArguments(
|
||||
array(
|
||||
|
@ -13,44 +17,138 @@ final class PhabricatorInternationalizationManagementExtractWorkflow
|
|||
'name' => 'paths',
|
||||
'wildcard' => true,
|
||||
),
|
||||
array(
|
||||
'name' => 'clean',
|
||||
'help' => pht('Drop caches before extracting strings. Slow!'),
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
public function execute(PhutilArgumentParser $args) {
|
||||
$console = PhutilConsole::getConsole();
|
||||
$paths = $args->getArg('paths');
|
||||
|
||||
$futures = array();
|
||||
$paths = $args->getArg('paths');
|
||||
if (!$paths) {
|
||||
$paths = array(getcwd());
|
||||
}
|
||||
|
||||
$targets = array();
|
||||
foreach ($paths as $path) {
|
||||
$root = Filesystem::resolvePath($path);
|
||||
$path_files = id(new FileFinder($root))
|
||||
->withType('f')
|
||||
->withSuffix('php')
|
||||
->find();
|
||||
|
||||
foreach ($path_files as $file) {
|
||||
$full_path = $root.DIRECTORY_SEPARATOR.$file;
|
||||
$data = Filesystem::readFile($full_path);
|
||||
$futures[$full_path] = PhutilXHPASTBinary::getParserFuture($data);
|
||||
if (!Filesystem::pathExists($root) || !is_dir($root)) {
|
||||
throw new PhutilArgumentUsageException(
|
||||
pht(
|
||||
'Path "%s" does not exist, or is not a directory.',
|
||||
$path));
|
||||
}
|
||||
|
||||
$libraries = id(new FileFinder($path))
|
||||
->withPath('*/__phutil_library_init__.php')
|
||||
->find();
|
||||
if (!$libraries) {
|
||||
throw new PhutilArgumentUsageException(
|
||||
pht(
|
||||
'Path "%s" contains no libphutil libraries.',
|
||||
$path));
|
||||
}
|
||||
|
||||
foreach ($libraries as $library) {
|
||||
$targets[] = Filesystem::resolvePath(dirname($library)).'/';
|
||||
}
|
||||
}
|
||||
|
||||
$console->writeErr(
|
||||
"%s\n",
|
||||
pht('Found %s file(s)...', phutil_count($futures)));
|
||||
$targets = array_unique($targets);
|
||||
|
||||
$results = array();
|
||||
foreach ($targets as $library) {
|
||||
echo tsprintf(
|
||||
"**<bg:blue> %s </bg>** %s\n",
|
||||
pht('EXTRACT'),
|
||||
pht(
|
||||
'Extracting "%s"...',
|
||||
Filesystem::readablePath($library)));
|
||||
|
||||
$this->extractLibrary($library);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private function extractLibrary($root) {
|
||||
$files = $this->loadLibraryFiles($root);
|
||||
$cache = $this->readCache($root);
|
||||
|
||||
$modified = $this->getModifiedFiles($files, $cache);
|
||||
$cache['files'] = $files;
|
||||
|
||||
if ($modified) {
|
||||
echo tsprintf(
|
||||
"**<bg:blue> %s </bg>** %s\n",
|
||||
pht('MODIFIED'),
|
||||
pht(
|
||||
'Found %s modified file(s) (of %s total).',
|
||||
phutil_count($modified),
|
||||
phutil_count($files)));
|
||||
|
||||
$old_strings = idx($cache, 'strings');
|
||||
$old_strings = array_select_keys($old_strings, $files);
|
||||
$new_strings = $this->extractFiles($root, $modified);
|
||||
$all_strings = $new_strings + $old_strings;
|
||||
$cache['strings'] = $all_strings;
|
||||
|
||||
$this->writeStrings($root, $all_strings);
|
||||
} else {
|
||||
echo tsprintf(
|
||||
"**<bg:blue> %s </bg>** %s\n",
|
||||
pht('NOT MODIFIED'),
|
||||
pht('Strings for this library are already up to date.'));
|
||||
}
|
||||
|
||||
$cache = id(new PhutilJSON())->encodeFormatted($cache);
|
||||
$this->writeCache($root, 'i18n_files.json', $cache);
|
||||
}
|
||||
|
||||
private function getModifiedFiles(array $files, array $cache) {
|
||||
$known = idx($cache, 'files', array());
|
||||
$known = array_fuse($known);
|
||||
|
||||
$modified = array();
|
||||
foreach ($files as $file => $hash) {
|
||||
|
||||
if (isset($known[$hash])) {
|
||||
continue;
|
||||
}
|
||||
$modified[$file] = $hash;
|
||||
}
|
||||
|
||||
return $modified;
|
||||
}
|
||||
|
||||
private function extractFiles($root_path, array $files) {
|
||||
$hashes = array();
|
||||
|
||||
$futures = array();
|
||||
foreach ($files as $file => $hash) {
|
||||
$full_path = $root_path.DIRECTORY_SEPARATOR.$file;
|
||||
$data = Filesystem::readFile($full_path);
|
||||
$futures[$full_path] = PhutilXHPASTBinary::getParserFuture($data);
|
||||
|
||||
$hashes[$full_path] = $hash;
|
||||
}
|
||||
|
||||
$bar = id(new PhutilConsoleProgressBar())
|
||||
->setTotal(count($futures));
|
||||
|
||||
$messages = array();
|
||||
$results = array();
|
||||
|
||||
$futures = id(new FutureIterator($futures))
|
||||
->limit(8);
|
||||
foreach ($futures as $full_path => $future) {
|
||||
$bar->update(1);
|
||||
|
||||
$hash = $hashes[$full_path];
|
||||
|
||||
try {
|
||||
$tree = XHPASTTree::newFromDataAndResolvedExecFuture(
|
||||
Filesystem::readFile($full_path),
|
||||
|
@ -67,24 +165,27 @@ final class PhabricatorInternationalizationManagementExtractWorkflow
|
|||
$calls = $root->selectDescendantsOfType('n_FUNCTION_CALL');
|
||||
foreach ($calls as $call) {
|
||||
$name = $call->getChildByIndex(0)->getConcreteString();
|
||||
if ($name == 'pht') {
|
||||
$params = $call->getChildByIndex(1, 'n_CALL_PARAMETER_LIST');
|
||||
$string_node = $params->getChildByIndex(0);
|
||||
$string_line = $string_node->getLineNumber();
|
||||
try {
|
||||
$string_value = $string_node->evalStatic();
|
||||
if ($name != 'pht') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$results[$string_value][] = array(
|
||||
'file' => Filesystem::readablePath($full_path),
|
||||
'line' => $string_line,
|
||||
);
|
||||
} catch (Exception $ex) {
|
||||
$messages[] = pht(
|
||||
'WARNING: Failed to evaluate pht() call on line %d in "%s": %s',
|
||||
$call->getLineNumber(),
|
||||
$full_path,
|
||||
$ex->getMessage());
|
||||
}
|
||||
$params = $call->getChildByIndex(1, 'n_CALL_PARAMETER_LIST');
|
||||
$string_node = $params->getChildByIndex(0);
|
||||
$string_line = $string_node->getLineNumber();
|
||||
try {
|
||||
$string_value = $string_node->evalStatic();
|
||||
|
||||
$results[$hash][] = array(
|
||||
'string' => $string_value,
|
||||
'file' => Filesystem::readablePath($full_path, $root_path),
|
||||
'line' => $string_line,
|
||||
);
|
||||
} catch (Exception $ex) {
|
||||
$messages[] = pht(
|
||||
'WARNING: Failed to evaluate pht() call on line %d in "%s": %s',
|
||||
$call->getLineNumber(),
|
||||
$full_path,
|
||||
$ex->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -93,28 +194,109 @@ final class PhabricatorInternationalizationManagementExtractWorkflow
|
|||
$bar->done();
|
||||
|
||||
foreach ($messages as $message) {
|
||||
$console->writeErr("%s\n", $message);
|
||||
echo tsprintf(
|
||||
"**<bg:yellow> %s </bg>** %s\n",
|
||||
pht('WARNING'),
|
||||
$message);
|
||||
}
|
||||
|
||||
ksort($results);
|
||||
return $results;
|
||||
}
|
||||
|
||||
$out = array();
|
||||
$out[] = '<?php';
|
||||
$out[] = '// @no'.'lint';
|
||||
$out[] = 'return array(';
|
||||
foreach ($results as $string => $locations) {
|
||||
foreach ($locations as $location) {
|
||||
$out[] = ' // '.$location['file'].':'.$location['line'];
|
||||
private function writeStrings($root, array $strings) {
|
||||
$map = array();
|
||||
foreach ($strings as $hash => $string_list) {
|
||||
foreach ($string_list as $string_info) {
|
||||
$map[$string_info['string']]['uses'][] = array(
|
||||
'file' => $string_info['file'],
|
||||
'line' => $string_info['line'],
|
||||
);
|
||||
}
|
||||
$out[] = " '".addcslashes($string, "\0..\37\\'\177..\377")."' => null,";
|
||||
$out[] = null;
|
||||
}
|
||||
$out[] = ');';
|
||||
$out[] = null;
|
||||
|
||||
echo implode("\n", $out);
|
||||
ksort($map);
|
||||
|
||||
return 0;
|
||||
$json = id(new PhutilJSON())->encodeFormatted($map);
|
||||
$this->writeCache($root, 'i18n_strings.json', $json);
|
||||
}
|
||||
|
||||
private function loadLibraryFiles($root) {
|
||||
$files = id(new FileFinder($root))
|
||||
->withType('f')
|
||||
->withSuffix('php')
|
||||
->excludePath('*/.*')
|
||||
->setGenerateChecksums(true)
|
||||
->find();
|
||||
|
||||
$map = array();
|
||||
foreach ($files as $file => $hash) {
|
||||
$file = Filesystem::readablePath($file, $root);
|
||||
$file = ltrim($file, '/');
|
||||
|
||||
if (dirname($file) == '.') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (dirname($file) == 'extensions') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$map[$file] = md5($hash.$file);
|
||||
}
|
||||
|
||||
return $map;
|
||||
}
|
||||
|
||||
private function readCache($root) {
|
||||
$path = $this->getCachePath($root, 'i18n_files.json');
|
||||
|
||||
$default = array(
|
||||
'version' => self::CACHE_VERSION,
|
||||
'files' => array(),
|
||||
'strings' => array(),
|
||||
);
|
||||
|
||||
if ($this->getArgv()->getArg('clean')) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
if (!Filesystem::pathExists($path)) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
try {
|
||||
$data = Filesystem::readFile($path);
|
||||
} catch (Exception $ex) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
try {
|
||||
$cache = phutil_json_decode($data);
|
||||
} catch (PhutilJSONParserException $e) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
$version = idx($cache, 'version');
|
||||
if ($version !== self::CACHE_VERSION) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
return $cache;
|
||||
}
|
||||
|
||||
private function writeCache($root, $file, $data) {
|
||||
$path = $this->getCachePath($root, $file);
|
||||
|
||||
$cache_dir = dirname($path);
|
||||
if (!Filesystem::pathExists($cache_dir)) {
|
||||
Filesystem::createDirectory($cache_dir, 0755, true);
|
||||
}
|
||||
|
||||
Filesystem::writeFile($path, $data);
|
||||
}
|
||||
|
||||
private function getCachePath($root, $to_file) {
|
||||
return $root.'/.cache/'.$to_file;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue