]> BookStack Code Mirror - bookstack/blob - resources/lang/format.php
Cleaned up script and formatted remaining EN files
[bookstack] / resources / lang / format.php
1 #!/usr/bin/env php
2 <?php
3
4 /**
5  * Format a language file in the same way as the EN equivalent.
6  * Matches the line numbers of translated content.
7  * Potentially destructive, Ensure you have a backup of your translation content before running.
8  */
9
10 $args = array_slice($argv, 1);
11
12 if (count($args) < 2) {
13     errorOut("Please provide a language code as the first argument and a translation file name, or '--all', as the second (./format.php fr activities)");
14 }
15
16 $lang = formatLocale($args[0]);
17 $fileName = explode('.', $args[1])[0];
18 $filesNames = [$fileName];
19 if ($fileName === '--all') {
20     $fileNames = getTranslationFileNames();
21 }
22
23 foreach ($fileNames as $fileName) {
24     $formatted = formatFileContents($lang, $fileName);
25     writeLangFile($lang, $fileName, $formatted);
26 }
27
28
29 /**
30  * Format the contents of a single translation file in the given language.
31  * @param string $lang
32  * @param string $fileName
33  * @return string
34  */
35 function formatFileContents(string $lang, string $fileName) : string {
36     $enLines = loadLangFileLines('en', $fileName);
37     $langContent = loadLang($lang, $fileName);
38     $enContent = loadLang('en', $fileName);
39
40     // Calculate the longest top-level key length
41     $longestKeyLength = calculateKeyPadding($enContent);
42
43     // Start formatted content
44     $formatted = [];
45     $mode = 'header';
46     $arrayKeys = [];
47
48     foreach($enLines as $index => $line) {
49         $trimLine = trim($line);
50         if ($mode === 'header') {
51             $formatted[$index] = $line;
52             if (str_replace(' ', '', $trimLine) === 'return[') $mode = 'body';
53         }
54
55         if ($mode === 'body') {
56             $matches = [];
57
58             // Comment
59             if (strpos($trimLine, '//') === 0) {
60                 $formatted[$index] = "\t" . $trimLine;
61                 continue;
62             }
63
64             // Arrays
65             $arrayStartMatch = preg_match('/^\'(.*)\'\s+?=>\s+?\[(\],)?\s*?$/', $trimLine, $matches);
66             $arrayEndMatch = preg_match('/]\s*,\s*$/', $trimLine);
67             $indent = count($arrayKeys) + 1;
68             if ($arrayStartMatch === 1) {
69                 $arrayKeys[] = $matches[1];
70                 $formatted[$index] = str_repeat(" ", $indent * 4) . str_pad("'{$matches[1]}'", $longestKeyLength) . "=> [";
71                 if ($arrayEndMatch !== 1) continue;
72             }
73             if ($arrayEndMatch === 1) {
74                 unsetArrayByKeys($langContent, $arrayKeys);
75                 array_pop($arrayKeys);
76                 if (isset($formatted[$index])) {
77                     $formatted[$index] .= '],';
78                 } else {
79                     $formatted[$index] = str_repeat(" ", ($indent-1) * 4) . "],";
80                 }
81                 continue;
82             }
83
84             // Translation
85             $translationMatch = preg_match('/^\'(.*)\'\s+?=>\s+?\'(.*)?\'.+?$/', $trimLine, $matches);
86             if ($translationMatch === 1) {
87                 $key = $matches[1];
88                 $keys = array_merge($arrayKeys, [$key]);
89                 $langVal = getTranslationByKeys($langContent, $keys);
90                 if (empty($langVal)) continue;
91
92                 $keyPad = $longestKeyLength;
93                 if (count($arrayKeys) === 0) {
94                     unset($langContent[$key]);
95                 } else {
96                     $keyPad = calculateKeyPadding(getTranslationByKeys($enContent, $arrayKeys));
97                 }
98
99                 $formatted[$index] = formatTranslationLine($key, $langVal, $indent, $keyPad);
100                 continue;
101             }
102         }
103
104     }
105
106     // Fill missing lines
107     $arraySize = max(array_keys($formatted));
108     $formatted = array_replace(array_fill(0, $arraySize, ''), $formatted);
109
110     // Add remaining translations
111     $langContent = array_filter($langContent, function($item) {
112         return !is_null($item) && !empty($item);
113     });
114     if (count($langContent) > 0) {
115         $formatted[] = '';
116         $formatted[] = "\t// Unmatched";
117     }
118     foreach ($langContent as $key => $value) {
119         if (is_array($value)) {
120             $formatted[] = formatTranslationArray($key, $value);
121         } else {
122             $formatted[] = formatTranslationLine($key, $value);
123         }
124     }
125
126     // Add end line
127     $formatted[] = '];';
128     return implode("\n", $formatted);
129 }
130
131 /**
132  * Format a translation line.
133  * @param string $key
134  * @param string $value
135  * @param int $indent
136  * @param int $keyPad
137  * @return string
138  */
139 function formatTranslationLine(string $key, string $value, int $indent = 1, int $keyPad = 1) : string {
140     $escapedValue = str_replace("'", "\\'", $value);
141     return str_repeat(" ", $indent * 4) . str_pad("'{$key}'", $keyPad, ' ') ."=> '{$escapedValue}',";
142 }
143
144 /**
145  * Find the longest key in the array and provide the length
146  * for all keys to be used when printed.
147  * @param array $array
148  * @return int
149  */
150 function calculateKeyPadding(array $array) : int {
151     $top = 0;
152     foreach ($array as $key => $value) {
153         $keyLen = strlen($key);
154         $top = max($top, $keyLen);
155     }
156     return $top + 3;
157 }
158
159 /**
160  * Format an translation array with the given key.
161  * Simply prints as an old-school php array.
162  * Used as a last-resort backup to save unused translations.
163  * @param string $key
164  * @param array $array
165  * @return string
166  */
167 function formatTranslationArray(string $key, array $array) : string {
168     $arrayPHP = var_export($array, true);
169     return "    '{$key}' => {$arrayPHP},";
170 }
171
172 /**
173  * Find a string translation value within a multi-dimensional array
174  * by traversing the given array of keys.
175  * @param array $translations
176  * @param array $keys
177  * @return string|array
178  */
179 function getTranslationByKeys(array $translations, array $keys)  {
180     $val = $translations;
181     foreach ($keys as $key) {
182         $val = $val[$key] ?? '';
183         if ($val === '') return '';
184     }
185     return $val;
186 }
187
188 /**
189  * Unset an inner item of a multi-dimensional array by
190  * traversing the given array of keys.
191  * @param array $input
192  * @param array $keys
193  */
194 function unsetArrayByKeys(array &$input, array $keys) {
195     $val = &$input;
196     $lastIndex = count($keys) - 1;
197     foreach ($keys as $index => &$key) {
198         if ($index === $lastIndex && is_array($val)) {
199             unset($val[$key]);
200         }
201         if (!is_array($val)) return;
202         $val = &$val[$key] ?? [];
203     }
204 }
205
206 /**
207  * Write the given content to a translation file.
208  * @param string $lang
209  * @param string $fileName
210  * @param string $content
211  */
212 function writeLangFile(string $lang, string $fileName, string $content) {
213     $path = __DIR__ . "/{$lang}/{$fileName}.php";
214     if (!file_exists($path)) {
215         errorOut("Expected translation file '{$path}' does not exist");
216     }
217     file_put_contents($path, $content);
218 }
219
220 /**
221  * Load the contents of a language file as an array of text lines.
222  * @param string $lang
223  * @param string $fileName
224  * @return array
225  */
226 function loadLangFileLines(string $lang, string $fileName) : array {
227     $path = __DIR__ . "/{$lang}/{$fileName}.php";
228     if (!file_exists($path)) {
229         errorOut("Expected translation file '{$path}' does not exist");
230     }
231     $lines = explode("\n", file_get_contents($path));
232     return array_map(function($line) {
233         return trim($line, "\r");
234     }, $lines);
235 }
236
237 /**
238  * Load the contents of a language file
239  * @param string $lang
240  * @param string $fileName
241  * @return array
242  */
243 function loadLang(string $lang, string $fileName) : array {
244     $path = __DIR__ . "/{$lang}/{$fileName}.php";
245     if (!file_exists($path)) {
246         errorOut("Expected translation file '{$path}' does not exist");
247     }
248
249     $fileData = include($path);
250     return $fileData;
251 }
252
253 /**
254  * Fetch an array containing the names of all translation files without the extension.
255  * @return array
256  */
257 function getTranslationFileNames() : array {
258     $dir = __DIR__ . "/en";
259     if (!file_exists($dir)) {
260         errorOut("Expected directory '{$dir}' does not exist");
261     }
262     $files = scandir($dir);
263     $fileNames = [];
264     foreach ($files as $file) {
265         if (substr($file, -4) === '.php') {
266             $fileNames[] = substr($file, 0, strlen($file) - 4);
267         }
268     }
269     return $fileNames;
270 }
271
272 /**
273  * Format a locale to follow the lowercase_UPERCASE standard
274  * @param string $lang
275  * @return string
276  */
277 function formatLocale(string $lang) : string {
278     $langParts = explode('_', strtoupper($lang));
279     $langParts[0] = strtolower($langParts[0]);
280     return implode('_', $langParts);
281 }
282
283 /**
284  * Dump a variable then die.
285  * @param $content
286  */
287 function dd($content) {
288     print_r($content);
289     exit(1);
290 }
291
292 /**
293  * Log out some information text in blue
294  * @param $text
295  */
296 function info($text) {
297     echo "\e[34m" . $text . "\e[0m\n";
298 }
299
300 /**
301  * Log out an error in red and exit.
302  * @param $text
303  */
304 function errorOut($text) {
305     echo "\e[31m" . $text . "\e[0m\n";
306     exit(1);
307 }