Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
97.40% |
75 / 77 |
|
71.43% |
5 / 7 |
CRAP | |
0.00% |
0 / 1 |
ImportLokaliseCommand | |
97.40% |
75 / 77 |
|
71.43% |
5 / 7 |
19 | |
0.00% |
0 / 1 |
configure | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
1 | |||
collectSourceFiles | |
92.31% |
12 / 13 |
|
0.00% |
0 / 1 |
5.01 | |||
matchTargetFiles | |
100.00% |
28 / 28 |
|
100.00% |
1 / 1 |
6 | |||
formatLokaliseLine | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
writeToDisk | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
importStrings | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
2 | |||
execute | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
3 |
1 | <?php |
2 | |
3 | /** |
4 | * Language command: ingest and normalise language files exported from Lokalise. |
5 | * |
6 | * PHP version 8 |
7 | * |
8 | * Copyright (C) Villanova University 2023. |
9 | * |
10 | * This program is free software; you can redistribute it and/or modify |
11 | * it under the terms of the GNU General Public License version 2, |
12 | * as published by the Free Software Foundation. |
13 | * |
14 | * This program is distributed in the hope that it will be useful, |
15 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
16 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
17 | * GNU General Public License for more details. |
18 | * |
19 | * You should have received a copy of the GNU General Public License |
20 | * along with this program; if not, write to the Free Software |
21 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
22 | * |
23 | * @category VuFind |
24 | * @package Console |
25 | * @author Demian Katz <demian.katz@villanova.edu> |
26 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
27 | * @link https://vufind.org/wiki/development Wiki |
28 | */ |
29 | |
30 | namespace VuFindConsole\Command\Language; |
31 | |
32 | use Symfony\Component\Console\Attribute\AsCommand; |
33 | use Symfony\Component\Console\Input\InputArgument; |
34 | use Symfony\Component\Console\Input\InputInterface; |
35 | use Symfony\Component\Console\Output\OutputInterface; |
36 | |
37 | use function count; |
38 | use function in_array; |
39 | use function strlen; |
40 | |
41 | /** |
42 | * Language command: ingest and normalise language files exported from Lokalise. |
43 | * |
44 | * @category VuFind |
45 | * @package Console |
46 | * @author Demian Katz <demian.katz@villanova.edu> |
47 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
48 | * @link https://vufind.org/wiki/development Wiki |
49 | */ |
50 | #[AsCommand( |
51 | name: 'language/importlokalise', |
52 | description: 'Lokalise file importer' |
53 | )] |
54 | class ImportLokaliseCommand extends AbstractCommand |
55 | { |
56 | /** |
57 | * Configure the command. |
58 | * |
59 | * @return void |
60 | */ |
61 | protected function configure() |
62 | { |
63 | $this |
64 | ->setHelp( |
65 | 'Loads and normalizes language strings from Lokalise export files' |
66 | )->addArgument( |
67 | 'source', |
68 | InputArgument::REQUIRED, |
69 | 'source directory (containing language files from Lokalise)' |
70 | )->addArgument( |
71 | 'target', |
72 | InputArgument::REQUIRED, |
73 | 'the language directory to update with new strings' |
74 | ); |
75 | } |
76 | |
77 | /** |
78 | * Recurse through a directory collecting all .ini files. |
79 | * |
80 | * @param string $dir Directory to explore |
81 | * |
82 | * @return string[] |
83 | */ |
84 | protected function collectSourceFiles(string $dir): array |
85 | { |
86 | $files = []; |
87 | $dirHandle = opendir($dir); |
88 | while ($file = readdir($dirHandle)) { |
89 | if (strlen(trim($file, '.')) === 0) { |
90 | continue; // skip . and .. |
91 | } |
92 | $next = "$dir/$file"; |
93 | if (is_dir($next)) { |
94 | $files = array_merge($files, $this->collectSourceFiles($next)); |
95 | } elseif (str_ends_with($next, '.ini')) { |
96 | $files[] = $next; |
97 | } |
98 | } |
99 | closedir($dirHandle); |
100 | sort($files); // sort file list for consistent, predictable order of operations |
101 | return $files; |
102 | } |
103 | |
104 | /** |
105 | * Given an array of files in $sourceDir, return an array of equivalent matching filenames |
106 | * in $targetDir. |
107 | * |
108 | * @param string $sourceDir Source directory |
109 | * @param string $targetDir Target directory |
110 | * @param string[] $sourceFiles Source files |
111 | * |
112 | * @return string[] |
113 | */ |
114 | protected function matchTargetFiles(string $sourceDir, string $targetDir, array $sourceFiles): array |
115 | { |
116 | $targetFiles = []; |
117 | foreach ($sourceFiles as $sourceFile) { |
118 | $baseName = basename($sourceFile); |
119 | // Change file name from Lokalise format to VuFind format: |
120 | $normalizedFile = preg_replace( |
121 | '/' . preg_quote($baseName, '/') . '$/', |
122 | str_replace('_', '-', strtolower($baseName)), |
123 | $sourceFile |
124 | ); |
125 | // Determine the equivalent filename in the target directory: |
126 | $targetFile = preg_replace( |
127 | '/^' . preg_quote($sourceDir, '/') . '/', |
128 | $targetDir, |
129 | $normalizedFile |
130 | ); |
131 | // If the target file does not exist, check to see if removing the |
132 | // regional part of the code yields a match; otherwise, accept it as new |
133 | // unless the more general code is already defined separately in the |
134 | // source file list. |
135 | if (!file_exists($targetFile)) { |
136 | $parts = explode('-', $targetFile); |
137 | if (count($parts) > 1) { |
138 | $lastPart = array_pop($parts); |
139 | // If there's a slash in the last part, this means there's a hyphen |
140 | // in a directory name somewhere. We should only process further if |
141 | // the hyphen is in the FILENAME. |
142 | if (!str_contains($lastPart, '/')) { |
143 | $revisedTargetFile = implode('-', $parts) . '.ini'; |
144 | $matchingSourceFile = preg_replace( |
145 | '/^' . preg_quote($targetDir, '/') . '/', |
146 | $sourceDir, |
147 | $revisedTargetFile |
148 | ); |
149 | if (!in_array($matchingSourceFile, $sourceFiles)) { |
150 | $targetFile = $revisedTargetFile; |
151 | } |
152 | } |
153 | } |
154 | } |
155 | $targetFiles[] = $targetFile; |
156 | } |
157 | return $targetFiles; |
158 | } |
159 | |
160 | /** |
161 | * Format a single line from a Lokalise language file so it is ready for further |
162 | * processing by the language file normalizer. |
163 | * |
164 | * @param string $line Line to format |
165 | * |
166 | * @return string |
167 | */ |
168 | protected function formatLokaliseLine(string $line): string |
169 | { |
170 | // Strip single quotes: |
171 | return preg_replace("/^(.* = )'(.*)'(\\n)?\$/", '$1$2$3', $line); |
172 | } |
173 | |
174 | /** |
175 | * Write content to disk. |
176 | * |
177 | * @param string $file Filename |
178 | * @param string $text Text to write |
179 | * |
180 | * @return void |
181 | */ |
182 | protected function writeToDisk(string $file, string $text): void |
183 | { |
184 | // We wrap the file write here for testing/extensibility purposes. |
185 | file_put_contents($file, $text); |
186 | } |
187 | |
188 | /** |
189 | * Add new strings from $sourceFile to $targetFile. |
190 | * |
191 | * @param string $sourceFile New file from Lokalise |
192 | * @param string $targetFile Existing file in VuFind |
193 | * |
194 | * @return void |
195 | */ |
196 | protected function importStrings(string $sourceFile, string $targetFile): void |
197 | { |
198 | $sourceStrings = array_map( |
199 | [$this, 'formatLokaliseLine'], |
200 | $this->normalizer->loadFileIntoArray($sourceFile) |
201 | ); |
202 | $targetStrings = file_exists($targetFile) ? $this->normalizer->loadFileIntoArray($targetFile) : []; |
203 | $this->writeToDisk( |
204 | $targetFile, |
205 | $this->normalizer->normalizeArray(array_merge($targetStrings, $sourceStrings)) |
206 | ); |
207 | } |
208 | |
209 | /** |
210 | * Run the command. |
211 | * |
212 | * @param InputInterface $input Input object |
213 | * @param OutputInterface $output Output object |
214 | * |
215 | * @return int 0 for success |
216 | */ |
217 | protected function execute(InputInterface $input, OutputInterface $output) |
218 | { |
219 | $source = $input->getArgument('source'); |
220 | $target = $input->getArgument('target'); |
221 | |
222 | if (!is_dir($source)) { |
223 | $output->writeln("{$source} does not exist or is not a directory."); |
224 | return 1; |
225 | } |
226 | if (!is_dir($target)) { |
227 | $output->writeln("{$target} does not exist or is not a directory."); |
228 | return 1; |
229 | } |
230 | $sourceFiles = $this->collectSourceFiles($source); |
231 | $targetFiles = $this->matchTargetFiles($source, $target, $sourceFiles); |
232 | array_map([$this, 'importStrings'], $sourceFiles, $targetFiles); |
233 | $output->writeln('Import complete.'); |
234 | return 0; |
235 | } |
236 | } |