]> BookStack Code Mirror - bookstack/blob - resources/lang/format.php
Update auth.php
[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 $fileNames = [$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     $skipArray = false;
47     $arrayKeys = [];
48
49     foreach ($enLines as $index => $line) {
50         $trimLine = trim($line);
51         if ($mode === 'header') {
52             $formatted[$index] = $line;
53             if (str_replace(' ', '', $trimLine) === 'return[') $mode = 'body';
54         }
55
56         if ($mode === 'body') {
57             $matches = [];
58             $arrayEndMatch = preg_match('/]\s*,\s*$/', $trimLine);
59
60             if ($skipArray) {
61                 if ($arrayEndMatch) $skipArray = false;
62                 continue;
63             }
64
65             // Comment to ignore
66             if (strpos($trimLine, '//!') === 0) {
67                 $formatted[$index] = "";
68                 continue;
69             }
70
71             // Comment
72             if (strpos($trimLine, '//') === 0) {
73                 $formatted[$index] = "\t" . $trimLine;
74                 continue;
75             }
76
77             // Arrays
78             $arrayStartMatch = preg_match('/^\'(.*)\'\s+?=>\s+?\[(\],)?\s*?$/', $trimLine, $matches);
79
80             $indent = count($arrayKeys) + 1;
81             if ($arrayStartMatch === 1) {
82                 if ($fileName === 'settings' && $matches[1] === 'language_select') {
83                     $skipArray = true;
84                     continue;
85                 }
86                 $arrayKeys[] = $matches[1];
87                 $formatted[$index] = str_repeat(" ", $indent * 4) . str_pad("'{$matches[1]}'", $longestKeyLength) . "=> [";
88                 if ($arrayEndMatch !== 1) continue;
89             }
90             if ($arrayEndMatch === 1) {
91                 unsetArrayByKeys($langContent, $arrayKeys);
92                 array_pop($arrayKeys);
93                 if (isset($formatted[$index])) {
94                     $formatted[$index] .= '],';
95                 } else {
96                     $formatted[$index] = str_repeat(" ", ($indent-1) * 4) . "],";
97                 }
98                 continue;
99             }
100
101             // Translation
102             $translationMatch = preg_match('/^\'(.*)\'\s+?=>\s+?[\'"](.*)?[\'"].+?$/', $trimLine, $matches);
103             if ($translationMatch === 1) {
104                 $key = $matches[1];
105                 $keys = array_merge($arrayKeys, [$key]);
106                 $langVal = getTranslationByKeys($langContent, $keys);
107                 if (empty($langVal)) continue;
108
109                 $keyPad = $longestKeyLength;
110                 if (count($arrayKeys) === 0) {
111                     unset($langContent[$key]);
112                 } else {
113                     $keyPad = calculateKeyPadding(getTranslationByKeys($enContent, $arrayKeys));
114                 }
115
116                 $formatted[$index] = formatTranslationLine($key, $langVal, $indent, $keyPad);
117                 continue;
118             }
119         }
120
121     }
122
123     // Fill missing lines
124     $arraySize = max(array_keys($formatted));
125     $formatted = array_replace(array_fill(0, $arraySize, ''), $formatted);
126
127     // Add remaining translations
128     $langContent = array_filter($langContent, function($item) {
129         return !is_null($item) && !empty($item);
130     });
131     if (count($langContent) > 0) {
132         $formatted[] = '';
133         $formatted[] = "\t// Unmatched";
134     }
135     foreach ($langContent as $key => $value) {
136         if (is_array($value)) {
137             $formatted[] = formatTranslationArray($key, $value);
138         } else {
139             $formatted[] = formatTranslationLine($key, $value);
140         }
141     }
142
143     // Add end line
144     $formatted[] = '];';
145     return implode("\n", $formatted);
146 }
147
148 /**
149  * Format a translation line.
150  * @param string $key
151  * @param string $value
152  * @param int $indent
153  * @param int $keyPad
154  * @return string
155  */
156 function formatTranslationLine(string $key, string $value, int $indent = 1, int $keyPad = 1) : string {
157     $start = str_repeat(" ", $indent * 4) . str_pad("'{$key}'", $keyPad, ' ');
158     if (strpos($value, "\n") !== false) {
159         $escapedValue = '"' .  str_replace("\n", '\n', $value)  . '"';
160         $escapedValue = '"' .  str_replace('"', '\"', $escapedValue)  . '"';
161     } else {
162         $escapedValue = "'" . str_replace("'", "\\'", $value) . "'";
163     }
164     return "{$start} => {$escapedValue},";
165 }
166
167 /**
168  * Find the longest key in the array and provide the length
169  * for all keys to be used when printed.
170  * @param array $array
171  * @return int
172  */
173 function calculateKeyPadding(array $array) : int {
174     $top = 0;
175     foreach ($array as $key => $value) {
176         $keyLen = strlen($key);
177         $top = max($top, $keyLen);
178     }
179     return min(35, $top + 2);
180 }
181
182 /**
183  * Format an translation array with the given key.
184  * Simply prints as an old-school php array.
185  * Used as a last-resort backup to save unused translations.
186  * @param string $key
187  * @param array $array
188  * @return string
189  */
190 function formatTranslationArray(string $key, array $array) : string {
191     $arrayPHP = var_export($array, true);
192     return "    '{$key}' => {$arrayPHP},";
193 }
194
195 /**
196  * Find a string translation value within a multi-dimensional array
197  * by traversing the given array of keys.
198  * @param array $translations
199  * @param array $keys
200  * @return string|array
201  */
202 function getTranslationByKeys(array $translations, array $keys)  {
203     $val = $translations;
204     foreach ($keys as $key) {
205         $val = $val[$key] ?? '';
206         if ($val === '') return '';
207     }
208     return $val;
209 }
210
211 /**
212  * Unset an inner item of a multi-dimensional array by
213  * traversing the given array of keys.
214  * @param array $input
215  * @param array $keys
216  */
217 function unsetArrayByKeys(array &$input, array $keys) {
218     $val = &$input;
219     $lastIndex = count($keys) - 1;
220     foreach ($keys as $index => &$key) {
221         if ($index === $lastIndex && is_array($val)) {
222             unset($val[$key]);
223         }
224         if (!is_array($val)) return;
225         $val = &$val[$key] ?? [];
226     }
227 }
228
229 /**
230  * Write the given content to a translation file.
231  * @param string $lang
232  * @param string $fileName
233  * @param string $content
234  */
235 function writeLangFile(string $lang, string $fileName, string $content) {
236     $path = __DIR__ . "/{$lang}/{$fileName}.php";
237     if (!file_exists($path)) {
238         errorOut("Expected translation file '{$path}' does not exist");
239     }
240     file_put_contents($path, $content);
241 }
242
243 /**
244  * Load the contents of a language file as an array of text lines.
245  * @param string $lang
246  * @param string $fileName
247  * @return array
248  */
249 function loadLangFileLines(string $lang, string $fileName) : array {
250     $path = __DIR__ . "/{$lang}/{$fileName}.php";
251     if (!file_exists($path)) {
252         errorOut("Expected translation file '{$path}' does not exist");
253     }
254     $lines = explode("\n", file_get_contents($path));
255     return array_map(function($line) {
256         return trim($line, "\r");
257     }, $lines);
258 }
259
260 /**
261  * Load the contents of a language file
262  * @param string $lang
263  * @param string $fileName
264  * @return array
265  */
266 function loadLang(string $lang, string $fileName) : array {
267     $path = __DIR__ . "/{$lang}/{$fileName}.php";
268     if (!file_exists($path)) {
269         errorOut("Expected translation file '{$path}' does not exist");
270     }
271
272     $fileData = include($path);
273     return $fileData;
274 }
275
276 /**
277  * Fetch an array containing the names of all translation files without the extension.
278  * @return array
279  */
280 function getTranslationFileNames() : array {
281     $dir = __DIR__ . "/en";
282     if (!file_exists($dir)) {
283         errorOut("Expected directory '{$dir}' does not exist");
284     }
285     $files = scandir($dir);
286     $fileNames = [];
287     foreach ($files as $file) {
288         if (substr($file, -4) === '.php') {
289             $fileNames[] = substr($file, 0, strlen($file) - 4);
290         }
291     }
292     return $fileNames;
293 }
294
295 /**
296  * Format a locale to follow the lowercase_UPERCASE standard
297  * @param string $lang
298  * @return string
299  */
300 function formatLocale(string $lang) : string {
301     $langParts = explode('_', strtoupper($lang));
302     $langParts[0] = strtolower($langParts[0]);
303     return implode('_', $langParts);
304 }
305
306 /**
307  * Dump a variable then die.
308  * @param $content
309  */
310 function dd($content) {
311     print_r($content);
312     exit(1);
313 }
314
315 /**
316  * Log out some information text in blue
317  * @param $text
318  */
319 function info($text) {
320     echo "\e[34m" . $text . "\e[0m\n";
321 }
322
323 /**
324  * Log out an error in red and exit.
325  * @param $text
326  */
327 function errorOut($text) {
328     echo "\e[31m" . $text . "\e[0m\n";
329     exit(1);
330 }
Morty Proxy This is a proxified and sanitized view of the page, visit original site.