1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
|
4
|
|
|
namespace Printful\GettextCms; |
5
|
|
|
|
6
|
|
|
|
7
|
|
|
use Gettext\Extractors\Extractor; |
8
|
|
|
use Gettext\Extractors\JsCode; |
9
|
|
|
use Gettext\Extractors\PhpCode; |
10
|
|
|
use Gettext\Extractors\VueJs; |
11
|
|
|
use Gettext\Translations; |
12
|
|
|
use League\Flysystem\Adapter\Local; |
13
|
|
|
use League\Flysystem\Filesystem; |
14
|
|
|
use League\Flysystem\Plugin\ListFiles; |
15
|
|
|
use Printful\GettextCms\Exceptions\GettextCmsException; |
16
|
|
|
use Printful\GettextCms\Exceptions\InvalidPathException; |
17
|
|
|
use Printful\GettextCms\Exceptions\UnknownExtractorException; |
18
|
|
|
use Printful\GettextCms\Interfaces\MessageConfigInterface; |
19
|
|
|
use Printful\GettextCms\Structures\ScanItem; |
20
|
|
|
|
21
|
|
|
class MessageExtractor |
22
|
|
|
{ |
23
|
|
|
const EXTRACTORS = [ |
24
|
|
|
'js' => JsCode::class, |
25
|
|
|
'vue' => VueJs::class, |
26
|
|
|
'php' => PhpCode::class, |
27
|
|
|
]; |
28
|
|
|
|
29
|
|
|
/** @var MessageConfigInterface */ |
30
|
|
|
private $config; |
31
|
|
|
|
32
|
|
|
/** |
33
|
|
|
* @param MessageConfigInterface $config |
34
|
|
|
*/ |
35
|
|
|
public function __construct(MessageConfigInterface $config) |
36
|
|
|
{ |
37
|
|
|
$this->config = $config; |
38
|
|
|
} |
39
|
|
|
|
40
|
|
|
/** |
41
|
|
|
* @param ScanItem[] $items |
42
|
|
|
* @return Translations[] List of translation files extracted (for each domain) |
43
|
|
|
* @throws GettextCmsException |
44
|
|
|
*/ |
45
|
|
|
public function extract(array $items) |
46
|
|
|
{ |
47
|
|
|
$defaultDomain = $this->config->getDefaultDomain(); |
48
|
|
|
$domains = $this->config->getOtherDomains(); |
49
|
|
|
$domains[] = $defaultDomain; |
50
|
|
|
|
51
|
|
|
$allTranslations = []; |
52
|
|
|
|
53
|
|
|
foreach ($domains as $domain) { |
54
|
|
|
$translations = new Translations; |
55
|
|
|
|
56
|
|
|
// When we scan for default domain, we have to specify an empty value |
57
|
|
|
// otherwise we would search for domain function calls with this domain |
58
|
|
|
// For example, empty domain will find string like __("message") |
59
|
|
|
// But if a domain is specified, it will look for dgettext("custom-domain", "message") |
60
|
|
|
if ($domain !== $defaultDomain) { |
61
|
|
|
$translations->setDomain($domain); |
62
|
|
|
} |
63
|
|
|
|
64
|
|
|
foreach ($items as $item) { |
65
|
|
|
// Scan for this item, translations will be merged with all domain translations |
66
|
|
|
$this->extractForDomain($item, $translations); |
67
|
|
|
} |
68
|
|
|
|
69
|
|
|
// Always set the domain even if it is the default one |
70
|
|
|
$translations->setDomain($domain); |
71
|
|
|
|
72
|
|
|
$allTranslations[] = $translations; |
73
|
|
|
} |
74
|
|
|
|
75
|
|
|
return $allTranslations; |
76
|
|
|
} |
77
|
|
|
|
78
|
|
|
/** |
79
|
|
|
* @param ScanItem $scanItem |
80
|
|
|
* @param Translations $translations |
81
|
|
|
* @return Translations |
82
|
|
|
* @throws InvalidPathException |
83
|
|
|
* @throws UnknownExtractorException |
84
|
|
|
*/ |
85
|
|
|
private function extractForDomain(ScanItem $scanItem, Translations $translations) |
86
|
|
|
{ |
87
|
|
|
$pathnames = $this->resolvePathnames($scanItem); |
88
|
|
|
|
89
|
|
|
foreach ($pathnames as $pathname) { |
90
|
|
|
// Translations will be merged with given object |
91
|
|
|
$this->extractFileMessages($pathname, $translations, $scanItem->functions); |
92
|
|
|
} |
93
|
|
|
|
94
|
|
|
return $translations; |
95
|
|
|
} |
96
|
|
|
|
97
|
|
|
/** |
98
|
|
|
* @param $pathname |
99
|
|
|
* @param Translations $translations |
100
|
|
|
* @param array|null $functions Optional functions to scan for |
101
|
|
|
* @throws UnknownExtractorException |
102
|
|
|
*/ |
103
|
|
|
private function extractFileMessages($pathname, Translations $translations, array $functions = null) |
104
|
|
|
{ |
105
|
|
|
$extractor = $this->getExtractor($pathname); |
106
|
|
|
|
107
|
|
|
$options = [ |
108
|
|
|
'extractComments' => '', // This extracts comments above function call |
109
|
|
|
'domainOnly' => $translations->hasDomain(), // We scan for messages that match our needed domain only |
110
|
|
|
]; |
111
|
|
|
|
112
|
|
|
if ($functions) { |
113
|
|
|
$options['functions'] = $functions; |
114
|
|
|
} |
115
|
|
|
|
116
|
|
|
$extractor::fromFile($pathname, $translations, $options); |
117
|
|
|
} |
118
|
|
|
|
119
|
|
|
/** |
120
|
|
|
* Returns a list of files that match this item (if single file, then an array with a single pathname) |
121
|
|
|
* |
122
|
|
|
* @param ScanItem $item |
123
|
|
|
* @return array List of matching pathnames |
124
|
|
|
* @throws InvalidPathException |
125
|
|
|
*/ |
126
|
|
|
public function resolvePathnames(ScanItem $item): array |
127
|
|
|
{ |
128
|
|
|
if (is_file($item->path)) { |
129
|
|
|
return [$item->path]; |
130
|
|
|
} |
131
|
|
|
|
132
|
|
|
if (!is_dir($item->path)) { |
133
|
|
|
throw new InvalidPathException('Path "' . $item->path . '" does not exist'); |
134
|
|
|
} |
135
|
|
|
|
136
|
|
|
return $this->resolveDirectoryFiles($item); |
137
|
|
|
} |
138
|
|
|
|
139
|
|
|
/** |
140
|
|
|
* If scan item is for a directory, this will create a list of matching files |
141
|
|
|
* |
142
|
|
|
* @param ScanItem $item |
143
|
|
|
* @return string[] List of pathnames to files |
144
|
|
|
*/ |
145
|
|
|
private function resolveDirectoryFiles(ScanItem $item) |
146
|
|
|
{ |
147
|
|
|
$dir = realpath($item->path); |
148
|
|
|
|
149
|
|
|
$adapter = new Local($dir); |
150
|
|
|
$filesystem = new Filesystem($adapter); |
151
|
|
|
$filesystem->addPlugin(new ListFiles); |
152
|
|
|
|
153
|
|
|
$files = $filesystem->listFiles('', $item->recursive); |
154
|
|
|
|
155
|
|
|
// If extensions are set, filter other files out |
156
|
|
|
if ($item->extensions) { |
|
|
|
|
157
|
|
|
$files = array_filter($files, function ($file) use ($item) { |
158
|
|
|
return in_array($file['extension'], $item->extensions); |
159
|
|
|
}); |
160
|
|
|
} |
161
|
|
|
|
162
|
|
|
return array_map(function ($file) use ($dir) { |
163
|
|
|
return $dir . DIRECTORY_SEPARATOR . $file['path']; |
164
|
|
|
}, $files); |
165
|
|
|
} |
166
|
|
|
|
167
|
|
|
/** |
168
|
|
|
* @param string $pathname Full path to file |
169
|
|
|
* @return Extractor |
170
|
|
|
* @throws UnknownExtractorException |
171
|
|
|
*/ |
172
|
|
|
private function getExtractor($pathname) |
173
|
|
|
{ |
174
|
|
|
$extension = pathinfo($pathname, PATHINFO_EXTENSION); |
175
|
|
|
|
176
|
|
|
if (isset(self::EXTRACTORS[$extension])) { |
177
|
|
|
return self::EXTRACTORS[$extension]; |
178
|
|
|
} |
179
|
|
|
|
180
|
|
|
throw new UnknownExtractorException('Extractor is not know for file extension "' . $extension . '"'); |
181
|
|
|
} |
182
|
|
|
} |
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.