Manager::files()   B
last analyzed

Complexity

Conditions 3
Paths 2

Size

Total Lines 37
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 21
nc 2
nop 0
dl 0
loc 37
rs 8.8571
c 0
b 0
f 0
1
<?php
2
3
namespace NicolasBeauvais\Transcribe;
4
5
use Illuminate\Contracts\Filesystem\FileNotFoundException;
6
use Illuminate\Filesystem\Filesystem;
7
use Illuminate\Support\Arr;
8
use Illuminate\Support\Collection;
9
use Illuminate\Support\Str;
10
11
class Manager
12
{
13
    /**
14
     * The Filesystem instance.
15
     *
16
     * @var Filesystem
17
     */
18
    private $disk;
19
20
    /**
21
     * The path to the language files.
22
     *
23
     * @var string
24
     */
25
    private $path;
26
27
    /**
28
     * The paths to directories where we look for localised strings to sync.
29
     *
30
     * @var array
31
     */
32
    private $syncPaths;
33
34
    /**
35
     * Manager constructor.
36
     *
37
     * @param Filesystem $disk
38
     * @param string     $path
39
     */
40
    public function __construct(Filesystem $disk, $path, array $syncPaths)
41
    {
42
        $this->disk = $disk;
43
        $this->path = $path;
44
        $this->syncPaths = $syncPaths;
45
    }
46
47
    /**
48
     * Array of language files grouped by file name.
49
     *
50
     * ex: ['user' => ['en' => 'user.php', 'nl' => 'user.php']]
51
     *
52
     * @return array
53
     */
54
    public function files()
55
    {
56
        $files = Collection::make($this->disk->allFiles($this->path))->filter(function ($file) {
57
            return $this->disk->extension($file) == 'php';
58
        });
59
60
        $filesByFile = $files->groupBy(function ($file) {
61
            $filePath = str_replace('.'.$file->getExtension(), '', $file->getRelativePathname());
62
            $filePath = array_reverse(explode('/', $filePath, 2))[0];
63
64
            if (Str::contains($file->getPath(), 'vendor')) {
65
                $filePath = str_replace('.php', '', $file->getFileName());
66
67
                $packageName = basename(dirname($file->getPath()));
68
69
                return "{$packageName}::{$filePath}";
70
            } else {
71
                return $filePath;
72
            }
73
        })->map(function ($files) {
74
            return $files->keyBy(function ($file) {
75
                return explode('/', str_replace($this->path, '', $file->getRelativePathname()))[0];
76
            })->map(function ($file) {
77
                return $file->getRealPath();
78
            });
79
        });
80
81
        // If the path does not contain "vendor" then we're looking at the
82
        // main language files of the application, in this case we will
83
        // neglect all vendor files.
84
        if (!Str::contains($this->path, 'vendor')) {
85
            $filesByFile = $this->neglectVendorFiles($filesByFile);
86
        } else {
87
            $filesByFile = $filesByFile->toArray();
88
        }
89
90
        return $filesByFile;
91
    }
92
93
    /**
94
     * Nelgect all vendor files.
95
     *
96
     * @param $filesByFile Collection
97
     *
98
     * @return array
99
     */
100
    private function neglectVendorFiles($filesByFile)
101
    {
102
        $return = [];
103
104
        foreach ($filesByFile->toArray() as $key => $value) {
105
            if (!Str::contains($key, ':')) {
106
                $return[$key] = $value;
107
            }
108
        }
109
110
        return $return;
111
    }
112
113
    /**
114
     * Array of supported languages.
115
     *
116
     * ex: ['en', 'sp']
117
     *
118
     * @return array
119
     */
120
    public function languages()
121
    {
122
        $languages = array_map(function ($directory) {
123
            return basename($directory);
124
        }, $this->disk->directories($this->path));
125
126
        $languages = array_filter($languages, function ($directory) {
127
            return $directory != 'vendor' && $directory != 'json';
128
        });
129
130
        sort($languages);
131
132
        return Arr::except($languages, ['vendor', 'json']);
133
    }
134
135
    /**
136
     * Create a file for all languages if does not exist already.
137
     *
138
     * @param $fileName
139
     *
140
     * @return void
141
     */
142
    public function createFile($fileName)
143
    {
144
        foreach ($this->languages() as $languageKey) {
145
            $file = $this->path."/{$languageKey}/{$fileName}.php";
146
            if (!$this->disk->exists($file)) {
147
                file_put_contents($file, "<?php \n\n return[];");
148
            }
149
        }
150
    }
151
152
    /**
153
     * Fills translation lines for given keys in different languages.
154
     *
155
     * ex. for $keys = ['name' => ['en' => 'name', 'nl' => 'naam']
156
     *
157
     * @param string $fileName
158
     * @param array  $keys
159
     *
160
     * @return void
161
     */
162
    public function fillKeys($fileName, array $keys)
163
    {
164
        $appends = [];
165
166
        foreach ($keys as $key => $values) {
167
            foreach ($values as $languageKey => $value) {
168
                $filePath = $this->path."/{$languageKey}/{$fileName}.php";
169
170
                Arr::set($appends[$filePath], $key, $value);
171
            }
172
        }
173
174
        foreach ($appends as $filePath => $values) {
175
            $fileContent = $this->getFileContent($filePath, true);
176
177
            $newContent = array_replace_recursive($fileContent, $values);
178
179
            $this->writeFile($filePath, $newContent);
180
        }
181
    }
182
183
    /**
184
     * Remove a key from all language files.
185
     *
186
     * @param string $fileName
187
     * @param string $key
188
     *
189
     * @return void
190
     */
191
    public function removeKey($fileName, $key)
192
    {
193
        foreach ($this->languages() as $language) {
194
            $filePath = $this->path."/{$language}/{$fileName}.php";
195
196
            $fileContent = $this->getFileContent($filePath);
197
198
            Arr::forget($fileContent, $key);
199
200
            $this->writeFile($filePath, $fileContent);
201
        }
202
    }
203
204
    /**
205
     * Write a language file from array.
206
     *
207
     * @param string $filePath
208
     * @param array  $translations
209
     *
210
     * @return void
211
     */
212
    public function writeFile($filePath, array $translations)
213
    {
214
        $content = "<?php\n\nreturn [";
215
216
        $content .= $this->stringLineMaker($translations);
217
218
        $content .= "\n];\n";
219
220
        file_put_contents($filePath, $content);
221
    }
222
223
    /**
224
     * Write the lines of the inner array of the language file.
225
     *
226
     * @param $array
227
     *
228
     * @return string
229
     */
230
    private function stringLineMaker($array, $prepend = '')
231
    {
232
        $output = '';
233
234
        foreach ($array as $key => $value) {
235
            if (is_array($value)) {
236
                $value = $this->stringLineMaker($value, $prepend.'    ');
237
238
                $output .= "\n{$prepend}    '{$key}' => [{$value}\n{$prepend}    ],";
239
            } else {
240
                $value = str_replace('\"', '"', addslashes($value));
241
242
                $output .= "\n{$prepend}    '{$key}' => '{$value}',";
243
            }
244
        }
245
246
        return $output;
247
    }
248
249
    /**
250
     * Get the content in the given file path.
251
     *
252
     * @param string $filePath
253
     * @param bool   $createIfNotExists
254
     *
255
     * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
256
     *
257
     * @return array
258
     */
259
    public function getFileContent($filePath, $createIfNotExists = false)
260
    {
261
        if ($createIfNotExists && !$this->disk->exists($filePath)) {
262
            if (!$this->disk->exists($directory = dirname($filePath))) {
263
                mkdir($directory, 0777, true);
264
            }
265
266
            file_put_contents($filePath, "<?php\n\nreturn [];");
267
268
            return [];
269
        }
270
271
        try {
272
            return (array) include $filePath;
273
        } catch (\ErrorException $e) {
274
            throw new FileNotFoundException('File not found: '.$filePath);
275
        }
276
    }
277
278
    /**
279
     * Collect all translation keys from views files.
280
     *
281
     * e.g. ['users' => ['city', 'name', 'phone']]
282
     *
283
     * @return array
284
     */
285
    public function collectFromFiles()
286
    {
287
        $translationKeys = [];
288
289
        foreach ($this->getAllViewFilesWithTranslations() as $file => $matches) {
290
            foreach ($matches as $key) {
291
                try {
292
                    list($fileName, $keyName) = explode('.', $key, 2);
293
                } catch (\ErrorException $e) {
294
                    continue;
295
                }
296
297
                if (isset($translationKeys[$fileName]) && in_array($keyName, $translationKeys[$fileName])) {
298
                    continue;
299
                }
300
301
                $translationKeys[$fileName][] = $keyName;
302
            }
303
        }
304
305
        return $translationKeys;
306
    }
307
308
    /**
309
     * Get found translation lines found per file.
310
     *
311
     * e.g. ['users.blade.php' => ['users.name'], 'users/index.blade.php' => ['users.phone', 'users.city']]
312
     *
313
     * @return array
314
     */
315
    public function getAllViewFilesWithTranslations()
316
    {
317
        /*
318
         * This pattern is derived from Barryvdh\TranslationManager by Barry vd. Heuvel <[email protected]>
319
         *
320
         * https://github.com/barryvdh/laravel-translation-manager/blob/master/src/Manager.php
321
         */
322
        $functions = ['__', 'trans', 'trans_choice', 'Lang::get', 'Lang::choice', 'Lang::trans', 'Lang::transChoice', '@lang', '@choice'];
323
324
        $pattern =
325
            // See https://regex101.com/r/jS5fX0/4
326
            '[^\w]'. // Must not start with any alphanum or _
327
            '(?<!->)'. // Must not start with ->
328
            '('.implode('|', $functions).')'.// Must start with one of the functions
329
            "\(".// Match opening parentheses
330
            "[\'\"]".// Match " or '
331
            '('.// Start a new group to match:
332
            '[a-zA-Z0-9\/_-]+'.// Must start with group
333
            "([.][^\1)$]+)+".// Be followed by one or more items/keys
334
            ')'.// Close group
335
            "[\'\"]".// Closing quote
336
            "[\),]";  // Close parentheses or new parameter
337
338
        $allMatches = [];
339
340
        foreach ($this->syncPaths as $syncPath) {
341
            /** @var \Symfony\Component\Finder\SplFileInfo $file */
342
            foreach ($this->disk->allFiles($syncPath) as $file) {
343
                if (preg_match_all("/$pattern/siU", $file->getContents(), $matches)) {
344
                    $allMatches[$file->getRelativePathname()] = $matches[2];
345
                }
346
            }
347
        }
348
349
        return $allMatches;
350
    }
351
352
    /**
353
     * Sets the path to a vendor package translation files.
354
     *
355
     * @param string $packageName
356
     *
357
     * @return void
358
     */
359
    public function setPathToVendorPackage($packageName)
360
    {
361
        $this->path = $this->path.'/vendor/'.$packageName;
362
    }
363
364
    /**
365
     * Extract keys that exists in a language but not the other.
366
     *
367
     * Given a dot array of all keys in the format 'file.language.key', this
368
     * method searches for keys that exist in one language but not the
369
     * other and outputs an array consists of those keys.
370
     *
371
     * @param $values
372
     *
373
     * @return array
374
     */
375
    public function getKeysExistingInALanguageButNotTheOther($values)
376
    {
377
        $missing = [];
378
379
        // Array of keys indexed by fileName.key, those are the keys we looked
380
        // at before so we save them in order for us to not look at them
381
        // again in a different language iteration.
382
        $searched = [];
383
384
        // Now we add keys that exist in a language but missing in any of the
385
        // other languages. Those keys combined with ones with values = ''
386
        // will be sent to the console user to fill and save in disk.
387
        foreach ($values as $key => $value) {
388
            list($fileName, $languageKey, $key) = explode('.', $key, 3);
389
390
            if (in_array("{$fileName}.{$key}", $searched)) {
391
                continue;
392
            }
393
394
            foreach ($this->languages() as $languageName) {
395
                if (!Arr::has($values, "{$fileName}.{$languageName}.{$key}") && !array_key_exists("{$fileName}.{$languageName}.{$key}", $values)) {
396
                    $missing[] = "{$fileName}.{$key}:{$languageName}";
397
                }
398
            }
399
400
            $searched[] = "{$fileName}.{$key}";
401
        }
402
403
        return $missing;
404
    }
405
}
406