Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/src/infrastructure/internationalization/management/PhabricatorInternationalizationManagementExtractWorkflow.php
12241 views
1
<?php
2
3
final class PhabricatorInternationalizationManagementExtractWorkflow
4
extends PhabricatorInternationalizationManagementWorkflow {
5
6
const CACHE_VERSION = 1;
7
8
protected function didConstruct() {
9
$this
10
->setName('extract')
11
->setExamples(
12
'**extract** [__options__] __library__')
13
->setSynopsis(pht('Extract translatable strings.'))
14
->setArguments(
15
array(
16
array(
17
'name' => 'paths',
18
'wildcard' => true,
19
),
20
array(
21
'name' => 'clean',
22
'help' => pht('Drop caches before extracting strings. Slow!'),
23
),
24
));
25
}
26
27
public function execute(PhutilArgumentParser $args) {
28
$console = PhutilConsole::getConsole();
29
30
$paths = $args->getArg('paths');
31
if (!$paths) {
32
$paths = array(getcwd());
33
}
34
35
$targets = array();
36
foreach ($paths as $path) {
37
$root = Filesystem::resolvePath($path);
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')
48
->find();
49
if (!$libraries) {
50
throw new PhutilArgumentUsageException(
51
pht(
52
'Path "%s" contains no libphutil libraries.',
53
$path));
54
}
55
56
foreach ($libraries as $library) {
57
$targets[] = Filesystem::resolvePath(dirname($path.'/'.$library)).'/';
58
}
59
}
60
61
$targets = array_unique($targets);
62
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
}
138
139
$bar = id(new PhutilConsoleProgressBar())
140
->setTotal(count($futures));
141
142
$messages = array();
143
$results = array();
144
145
$futures = id(new FutureIterator($futures))
146
->limit(8);
147
foreach ($futures as $full_path => $future) {
148
$bar->update(1);
149
150
$hash = $hashes[$full_path];
151
152
try {
153
$tree = XHPASTTree::newFromDataAndResolvedExecFuture(
154
Filesystem::readFile($full_path),
155
$future->resolve());
156
} catch (Exception $ex) {
157
$messages[] = pht(
158
'WARNING: Failed to extract strings from file "%s": %s',
159
$full_path,
160
$ex->getMessage());
161
continue;
162
}
163
164
$root = $tree->getRootNode();
165
$calls = $root->selectDescendantsOfType('n_FUNCTION_CALL');
166
foreach ($calls as $call) {
167
$name = $call->getChildByIndex(0)->getConcreteString();
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
$args = $params->getChildren();
179
$args = array_slice($args, 1);
180
181
$types = array();
182
foreach ($args as $child) {
183
$type = null;
184
185
switch ($child->getTypeName()) {
186
case 'n_FUNCTION_CALL':
187
$call = $child->getChildByIndex(0);
188
if ($call->getTypeName() == 'n_SYMBOL_NAME') {
189
switch ($call->getConcreteString()) {
190
case 'phutil_count':
191
$type = 'number';
192
break;
193
case 'phutil_person':
194
$type = 'person';
195
break;
196
}
197
}
198
break;
199
case 'n_NEW':
200
$class = $child->getChildByIndex(0);
201
if ($class->getTypeName() == 'n_CLASS_NAME') {
202
switch ($class->getConcreteString()) {
203
case 'PhutilNumber':
204
$type = 'number';
205
break;
206
}
207
}
208
break;
209
default:
210
break;
211
}
212
213
$types[] = $type;
214
}
215
216
$results[$hash][] = array(
217
'string' => $string_value,
218
'file' => Filesystem::readablePath($full_path, $root_path),
219
'line' => $string_line,
220
'types' => $types,
221
);
222
} catch (Exception $ex) {
223
$messages[] = pht(
224
'WARNING: Failed to evaluate pht() call on line %d in "%s": %s',
225
$call->getLineNumber(),
226
$full_path,
227
$ex->getMessage());
228
}
229
}
230
231
$tree->dispose();
232
}
233
$bar->done();
234
235
foreach ($messages as $message) {
236
echo tsprintf(
237
"**<bg:yellow> %s </bg>** %s\n",
238
pht('WARNING'),
239
$message);
240
}
241
242
return $results;
243
}
244
245
private function writeStrings($root, array $strings) {
246
$map = array();
247
foreach ($strings as $hash => $string_list) {
248
foreach ($string_list as $string_info) {
249
$string = $string_info['string'];
250
251
$map[$string]['uses'][] = array(
252
'file' => $string_info['file'],
253
'line' => $string_info['line'],
254
);
255
256
if (!isset($map[$string]['types'])) {
257
$map[$string]['types'] = $string_info['types'];
258
} else if ($map[$string]['types'] !== $string_info['types']) {
259
echo tsprintf(
260
"**<bg:yellow> %s </bg>** %s\n",
261
pht('WARNING'),
262
pht(
263
'Inferred types for string "%s" vary across callsites.',
264
$string_info['string']));
265
}
266
}
267
}
268
269
ksort($map);
270
271
$json = id(new PhutilJSON())->encodeFormatted($map);
272
$this->writeCache($root, 'i18n_strings.json', $json);
273
}
274
275
private function loadLibraryFiles($root) {
276
$files = id(new FileFinder($root))
277
->withType('f')
278
->withSuffix('php')
279
->excludePath('*/.*')
280
->setGenerateChecksums(true)
281
->find();
282
283
$map = array();
284
foreach ($files as $file => $hash) {
285
$file = Filesystem::readablePath($file, $root);
286
$file = ltrim($file, '/');
287
288
if (dirname($file) == '.') {
289
continue;
290
}
291
292
if (dirname($file) == 'extensions') {
293
continue;
294
}
295
296
$map[$file] = md5($hash.$file);
297
}
298
299
return $map;
300
}
301
302
private function readCache($root) {
303
$path = $this->getCachePath($root, 'i18n_files.json');
304
305
$default = array(
306
'version' => self::CACHE_VERSION,
307
'files' => array(),
308
'strings' => array(),
309
);
310
311
if ($this->getArgv()->getArg('clean')) {
312
return $default;
313
}
314
315
if (!Filesystem::pathExists($path)) {
316
return $default;
317
}
318
319
try {
320
$data = Filesystem::readFile($path);
321
} catch (Exception $ex) {
322
return $default;
323
}
324
325
try {
326
$cache = phutil_json_decode($data);
327
} catch (PhutilJSONParserException $e) {
328
return $default;
329
}
330
331
$version = idx($cache, 'version');
332
if ($version !== self::CACHE_VERSION) {
333
return $default;
334
}
335
336
return $cache;
337
}
338
339
private function writeCache($root, $file, $data) {
340
$path = $this->getCachePath($root, $file);
341
342
$cache_dir = dirname($path);
343
if (!Filesystem::pathExists($cache_dir)) {
344
Filesystem::createDirectory($cache_dir, 0755, true);
345
}
346
347
Filesystem::writeFile($path, $data);
348
}
349
350
private function getCachePath($root, $to_file) {
351
return $root.'/.cache/'.$to_file;
352
}
353
354
}
355
356