Skip to content

Commit ccc7c1b

Browse files
author
epriestley
committedJul 4, 2016
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
1 parent d09094f commit ccc7c1b

File tree

2 files changed

+228
-45
lines changed

2 files changed

+228
-45
lines changed
 

‎.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
# Diviner
55
/docs/
66
/.divinercache/
7+
/src/.cache/
78

89
# libphutil
910
/src/.phutil_module_cache

‎src/infrastructure/internationalization/management/PhabricatorInternationalizationManagementExtractWorkflow.php

+227-45
Original file line numberDiff line numberDiff line change
@@ -3,54 +3,152 @@
33
final class PhabricatorInternationalizationManagementExtractWorkflow
44
extends PhabricatorInternationalizationManagementWorkflow {
55

6+
const CACHE_VERSION = 1;
7+
68
protected function didConstruct() {
79
$this
810
->setName('extract')
11+
->setExamples(
12+
'**extract** [__options__] __library__')
913
->setSynopsis(pht('Extract translatable strings.'))
1014
->setArguments(
1115
array(
1216
array(
1317
'name' => 'paths',
1418
'wildcard' => true,
1519
),
20+
array(
21+
'name' => 'clean',
22+
'help' => pht('Drop caches before extracting strings. Slow!'),
23+
),
1624
));
1725
}
1826

1927
public function execute(PhutilArgumentParser $args) {
2028
$console = PhutilConsole::getConsole();
29+
2130
$paths = $args->getArg('paths');
31+
if (!$paths) {
32+
$paths = array(getcwd());
33+
}
2234

23-
$futures = array();
35+
$targets = array();
2436
foreach ($paths as $path) {
2537
$root = Filesystem::resolvePath($path);
26-
$path_files = id(new FileFinder($root))
27-
->withType('f')
28-
->withSuffix('php')
38+
39+
if (!Filesystem::pathExists($root) || !is_dir($root)) {
40+
throw new PhutilArgumentUsageException(
41+
pht(
42+
'Path "%s" does not exist, or is not a directory.',
43+
$path));
44+
}
45+
46+
$libraries = id(new FileFinder($path))
47+
->withPath('*/__phutil_library_init__.php')
2948
->find();
49+
if (!$libraries) {
50+
throw new PhutilArgumentUsageException(
51+
pht(
52+
'Path "%s" contains no libphutil libraries.',
53+
$path));
54+
}
3055

31-
foreach ($path_files as $file) {
32-
$full_path = $root.DIRECTORY_SEPARATOR.$file;
33-
$data = Filesystem::readFile($full_path);
34-
$futures[$full_path] = PhutilXHPASTBinary::getParserFuture($data);
56+
foreach ($libraries as $library) {
57+
$targets[] = Filesystem::resolvePath(dirname($library)).'/';
3558
}
3659
}
3760

38-
$console->writeErr(
39-
"%s\n",
40-
pht('Found %s file(s)...', phutil_count($futures)));
61+
$targets = array_unique($targets);
4162

42-
$results = array();
63+
foreach ($targets as $library) {
64+
echo tsprintf(
65+
"**<bg:blue> %s </bg>** %s\n",
66+
pht('EXTRACT'),
67+
pht(
68+
'Extracting "%s"...',
69+
Filesystem::readablePath($library)));
70+
71+
$this->extractLibrary($library);
72+
}
73+
74+
return 0;
75+
}
76+
77+
private function extractLibrary($root) {
78+
$files = $this->loadLibraryFiles($root);
79+
$cache = $this->readCache($root);
80+
81+
$modified = $this->getModifiedFiles($files, $cache);
82+
$cache['files'] = $files;
83+
84+
if ($modified) {
85+
echo tsprintf(
86+
"**<bg:blue> %s </bg>** %s\n",
87+
pht('MODIFIED'),
88+
pht(
89+
'Found %s modified file(s) (of %s total).',
90+
phutil_count($modified),
91+
phutil_count($files)));
92+
93+
$old_strings = idx($cache, 'strings');
94+
$old_strings = array_select_keys($old_strings, $files);
95+
$new_strings = $this->extractFiles($root, $modified);
96+
$all_strings = $new_strings + $old_strings;
97+
$cache['strings'] = $all_strings;
98+
99+
$this->writeStrings($root, $all_strings);
100+
} else {
101+
echo tsprintf(
102+
"**<bg:blue> %s </bg>** %s\n",
103+
pht('NOT MODIFIED'),
104+
pht('Strings for this library are already up to date.'));
105+
}
106+
107+
$cache = id(new PhutilJSON())->encodeFormatted($cache);
108+
$this->writeCache($root, 'i18n_files.json', $cache);
109+
}
110+
111+
private function getModifiedFiles(array $files, array $cache) {
112+
$known = idx($cache, 'files', array());
113+
$known = array_fuse($known);
114+
115+
$modified = array();
116+
foreach ($files as $file => $hash) {
117+
118+
if (isset($known[$hash])) {
119+
continue;
120+
}
121+
$modified[$file] = $hash;
122+
}
123+
124+
return $modified;
125+
}
126+
127+
private function extractFiles($root_path, array $files) {
128+
$hashes = array();
129+
130+
$futures = array();
131+
foreach ($files as $file => $hash) {
132+
$full_path = $root_path.DIRECTORY_SEPARATOR.$file;
133+
$data = Filesystem::readFile($full_path);
134+
$futures[$full_path] = PhutilXHPASTBinary::getParserFuture($data);
135+
136+
$hashes[$full_path] = $hash;
137+
}
43138

44139
$bar = id(new PhutilConsoleProgressBar())
45140
->setTotal(count($futures));
46141

47142
$messages = array();
143+
$results = array();
48144

49145
$futures = id(new FutureIterator($futures))
50146
->limit(8);
51147
foreach ($futures as $full_path => $future) {
52148
$bar->update(1);
53149

150+
$hash = $hashes[$full_path];
151+
54152
try {
55153
$tree = XHPASTTree::newFromDataAndResolvedExecFuture(
56154
Filesystem::readFile($full_path),
@@ -67,24 +165,27 @@ public function execute(PhutilArgumentParser $args) {
67165
$calls = $root->selectDescendantsOfType('n_FUNCTION_CALL');
68166
foreach ($calls as $call) {
69167
$name = $call->getChildByIndex(0)->getConcreteString();
70-
if ($name == 'pht') {
71-
$params = $call->getChildByIndex(1, 'n_CALL_PARAMETER_LIST');
72-
$string_node = $params->getChildByIndex(0);
73-
$string_line = $string_node->getLineNumber();
74-
try {
75-
$string_value = $string_node->evalStatic();
76-
77-
$results[$string_value][] = array(
78-
'file' => Filesystem::readablePath($full_path),
79-
'line' => $string_line,
80-
);
81-
} catch (Exception $ex) {
82-
$messages[] = pht(
83-
'WARNING: Failed to evaluate pht() call on line %d in "%s": %s',
84-
$call->getLineNumber(),
85-
$full_path,
86-
$ex->getMessage());
87-
}
168+
if ($name != 'pht') {
169+
continue;
170+
}
171+
172+
$params = $call->getChildByIndex(1, 'n_CALL_PARAMETER_LIST');
173+
$string_node = $params->getChildByIndex(0);
174+
$string_line = $string_node->getLineNumber();
175+
try {
176+
$string_value = $string_node->evalStatic();
177+
178+
$results[$hash][] = array(
179+
'string' => $string_value,
180+
'file' => Filesystem::readablePath($full_path, $root_path),
181+
'line' => $string_line,
182+
);
183+
} catch (Exception $ex) {
184+
$messages[] = pht(
185+
'WARNING: Failed to evaluate pht() call on line %d in "%s": %s',
186+
$call->getLineNumber(),
187+
$full_path,
188+
$ex->getMessage());
88189
}
89190
}
90191

@@ -93,28 +194,109 @@ public function execute(PhutilArgumentParser $args) {
93194
$bar->done();
94195

95196
foreach ($messages as $message) {
96-
$console->writeErr("%s\n", $message);
197+
echo tsprintf(
198+
"**<bg:yellow> %s </bg>** %s\n",
199+
pht('WARNING'),
200+
$message);
97201
}
98202

99-
ksort($results);
203+
return $results;
204+
}
100205

101-
$out = array();
102-
$out[] = '<?php';
103-
$out[] = '// @no'.'lint';
104-
$out[] = 'return array(';
105-
foreach ($results as $string => $locations) {
106-
foreach ($locations as $location) {
107-
$out[] = ' // '.$location['file'].':'.$location['line'];
206+
private function writeStrings($root, array $strings) {
207+
$map = array();
208+
foreach ($strings as $hash => $string_list) {
209+
foreach ($string_list as $string_info) {
210+
$map[$string_info['string']]['uses'][] = array(
211+
'file' => $string_info['file'],
212+
'line' => $string_info['line'],
213+
);
108214
}
109-
$out[] = " '".addcslashes($string, "\0..\37\\'\177..\377")."' => null,";
110-
$out[] = null;
111215
}
112-
$out[] = ');';
113-
$out[] = null;
114216

115-
echo implode("\n", $out);
217+
ksort($map);
116218

117-
return 0;
219+
$json = id(new PhutilJSON())->encodeFormatted($map);
220+
$this->writeCache($root, 'i18n_strings.json', $json);
221+
}
222+
223+
private function loadLibraryFiles($root) {
224+
$files = id(new FileFinder($root))
225+
->withType('f')
226+
->withSuffix('php')
227+
->excludePath('*/.*')
228+
->setGenerateChecksums(true)
229+
->find();
230+
231+
$map = array();
232+
foreach ($files as $file => $hash) {
233+
$file = Filesystem::readablePath($file, $root);
234+
$file = ltrim($file, '/');
235+
236+
if (dirname($file) == '.') {
237+
continue;
238+
}
239+
240+
if (dirname($file) == 'extensions') {
241+
continue;
242+
}
243+
244+
$map[$file] = md5($hash.$file);
245+
}
246+
247+
return $map;
248+
}
249+
250+
private function readCache($root) {
251+
$path = $this->getCachePath($root, 'i18n_files.json');
252+
253+
$default = array(
254+
'version' => self::CACHE_VERSION,
255+
'files' => array(),
256+
'strings' => array(),
257+
);
258+
259+
if ($this->getArgv()->getArg('clean')) {
260+
return $default;
261+
}
262+
263+
if (!Filesystem::pathExists($path)) {
264+
return $default;
265+
}
266+
267+
try {
268+
$data = Filesystem::readFile($path);
269+
} catch (Exception $ex) {
270+
return $default;
271+
}
272+
273+
try {
274+
$cache = phutil_json_decode($data);
275+
} catch (PhutilJSONParserException $e) {
276+
return $default;
277+
}
278+
279+
$version = idx($cache, 'version');
280+
if ($version !== self::CACHE_VERSION) {
281+
return $default;
282+
}
283+
284+
return $cache;
285+
}
286+
287+
private function writeCache($root, $file, $data) {
288+
$path = $this->getCachePath($root, $file);
289+
290+
$cache_dir = dirname($path);
291+
if (!Filesystem::pathExists($cache_dir)) {
292+
Filesystem::createDirectory($cache_dir, 0755, true);
293+
}
294+
295+
Filesystem::writeFile($path, $data);
296+
}
297+
298+
private function getCachePath($root, $to_file) {
299+
return $root.'/.cache/'.$to_file;
118300
}
119301

120302
}

0 commit comments

Comments
 (0)
Failed to load comments.