├── .travis.yml
├── README.md
├── composer.json
├── config
└── lang-generator.php
└── src
├── Commands
└── LangGeneratorCommand.php
├── LangService.php
└── LaravelLangGeneratorServiceProvider.php
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: php
2 |
3 | php:
4 | - 8.2
5 | - 8.3
6 |
7 | install:
8 | - composer update && composer install
9 |
10 | script:
11 | - echo "skipping tests"
12 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
Laravel Lang Generator
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | Searches for multilingual phrases in a Laravel project and automatically generates language files for you. You can search for new translation keys, delete unused keys, and quickly generate new language files.
16 |
17 |
18 | ---
19 |
20 | ## Installation
21 |
22 | You can start the installation through the composer using the command.
23 |
24 | ```
25 | composer require glebsky/laravel-lang-generator
26 | ```
27 | You can also select the required version to support older versions of Laravel
28 |
29 | - v2.0.0 - For Laravel 11+. Using PHP >8.2
30 | - v1.1.0 - For Laravel 8 and 9. Using PHP >7.3.
31 | ```
32 | composer require glebsky/laravel-lang-generator:^1.1.0
33 | ```
34 |
35 | ## Configuration
36 | To create configuration file of this package you can use command:
37 |
38 | ```
39 | php artisan vendor:publish --tag=config
40 | ```
41 | It will create configuration file in `app/config` with name `lang-generator`
42 |
43 | ### About configuration
44 |
45 | file_type: is responsible for the type of the generated file. It is possible to generate both a json and a php array files. Possible values: `array` , `json`
46 |
47 | ---
48 |
49 | file_name: is responsible for the name of the generated files. By default, it is `lang`.
50 |
51 | ---
52 |
53 | languages: is responsible for the generated languages and accepts an array. Language folders with the specified data will be created. By default, it just `en`.
54 |
55 | ---
56 |
57 | ## Usage
58 |
59 | ### Main Command
60 |
61 | This command starts searching for translation keys in the `resource/views` and `app` folders according to the basic settings.
62 | Existing keys will not be removed, only new ones will be added.
63 |
64 | ```
65 | php artisan lang:generate
66 | ```
67 | it will create new language files with found translation keys.
68 | By default, name of lang file is `lang`
69 |
70 | 
71 | 
72 |
73 | ### Parameters
74 |
75 | In addition, the command accepts several parameters that allow you to flexibly manage the package.
76 | ```
77 | php artisan lang:generate --type= --name= --langs= --sync --clear --append --path=
78 | ```
79 | ### About parameters
80 |
81 | `--type=` or `-T`:
82 |
83 | is responsible for the type of the generated file. It is possible to generate both a json and a php array files. Possible values: `array` , `json`.
Example: `php artisan lang:generate --type=json`
84 |
85 | ---
86 |
87 | `--name=` or `-N`:
88 |
89 | is responsible for the name of the generated files. By default, it is `lang`.
90 |
91 | Example: `php artisan lang:generate --name="pagination"`
92 |
93 | ---
94 |
95 | `--langs=` or `-L`:
96 |
97 | is responsible for the generated languages and accepts an array. Language folders with the specified data will be created. By default, it just `en`.
Example: `php artisan lang:generate --langs="en" --langs="es"`
98 |
99 | ---
100 |
101 | `--sync` or `-S`:
102 |
103 | If you specify this flag, then all unused already existing translation keys will be deleted.
Example: `php artisan lang:generate --sync`
104 |
105 | ---
106 |
107 | `--clear` or `-C`:
108 |
109 | If you specify this flag, existing language files are removed and new ones are created. All existing translations will be removed.
110 |
111 | > NOTE! That NOT all language files are deleted, but only with the name specified in the settings.
112 |
113 | Example: `php artisan lang:generate --clear`
114 |
115 | ---
116 |
117 | `--append` or `-A`:
118 |
119 | If you specify this flag, new translations found will be added at the end of the JSON file, which might be useful for automation or version control. Only usable with JSON as type.
120 |
121 | Example: `php artisan lang:generate --type=json --append`
122 |
123 | ## Notes
124 | > `lang:generate` will update your language files by writing them completely, meaning that any comments or special styling will be removed, so I recommend you backup your files.
125 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "glebsky/laravel-lang-generator",
3 | "description": "Searches for multilingual phrases in Laravel project and automatically generates language files for you.",
4 | "keywords": ["laravel", "language", "translation", "automatic translation"],
5 | "type": "library",
6 | "license": "MIT",
7 | "time": "2021-12-20",
8 | "authors": [
9 | {
10 | "name": "glebsky",
11 | "email": "laosdeveloper@gmail.com",
12 | "role": "Developer"
13 | }
14 | ],
15 | "minimum-stability": "stable",
16 | "require": {
17 | "php": "^8.2",
18 | "laravel/framework": "^11.9"
19 | },
20 | "autoload": {
21 | "psr-4": {
22 | "Glebsky\\LaravelLangGenerator\\": "src"
23 | }
24 | },
25 | "extra": {
26 | "laravel": {
27 | "providers": [
28 | "Glebsky\\LaravelLangGenerator\\LaravelLangGeneratorServiceProvider"
29 | ]
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/config/lang-generator.php:
--------------------------------------------------------------------------------
1 | 'array',
14 |
15 | /*
16 | |--------------------------------------------------------------------------
17 | | Translation File Name
18 | |--------------------------------------------------------------------------
19 | |
20 | | A translation file will be created with the specified name if the array mode is selected
21 | |
22 | */
23 | 'file_name' => 'lang',
24 |
25 | /*
26 | |--------------------------------------------------------------------------
27 | | Use short translation keys
28 | |--------------------------------------------------------------------------
29 | |
30 | | If the parser split keys at each dot.
31 | | ex: __('short.key') becomes [ 'short' => [ 'key '=> '' ] ]
32 | |
33 | */
34 | 'short_keys' => true,
35 |
36 | /*
37 | |--------------------------------------------------------------------------
38 | | Supported Languages
39 | |--------------------------------------------------------------------------
40 | |
41 | | Array of supported languages of your application.
42 | | The specified folders will be created in the resource/lang folder
43 | |
44 | */
45 | 'languages' => ['en'],
46 | ];
47 |
--------------------------------------------------------------------------------
/src/Commands/LangGeneratorCommand.php:
--------------------------------------------------------------------------------
1 | manager = $manager;
20 | $this->manager->fileType = config('lang-generator.file_type');
21 | $this->manager->fileName = config('lang-generator.file_name');
22 | $this->manager->languages = config('lang-generator.languages');
23 | }
24 |
25 | /**
26 | * @throws JsonException
27 | */
28 | public function handle(): void
29 | {
30 | $this->manager->output = $this->output;
31 |
32 | //Get user input configs
33 | $this->manager->isSync = $this->option('sync');
34 | $this->manager->isNew = $this->option('clear');
35 | $this->manager->doAppend = $this->option('append');
36 |
37 | $this->manager->fileType = $this->option('type') ?: $this->manager->fileType;
38 | $this->manager->fileName = $this->option('name') ?: $this->manager->fileName;
39 | $this->manager->languages = $this->option('langs') ?: $this->manager->languages;
40 | $this->manager->path = $this->option('path');
41 |
42 | if ($this->manager->doAppend && $this->manager->fileType !== 'json') {
43 | $this->error('The append option is only possible for type json.');
44 |
45 | return;
46 | }
47 |
48 | if ($this->manager->isNew) {
49 | if ($this->confirm('You really want to generate new language files? This will clear all existing files!')) {
50 | $this->manager->parseProject();
51 | } else {
52 | $this->error('Generating translations files aborted.');
53 | }
54 |
55 | return;
56 | }
57 |
58 | $this->manager->parseProject();
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/LangService.php:
--------------------------------------------------------------------------------
1 | line('Start searching for language files...');
42 |
43 | //Parse custom path
44 | if ($this->path) {
45 | $this->info('Parsing custom path...');
46 | $this->line('Path: '.base_path($this->path));
47 |
48 | if (!is_dir(base_path($this->path))) {
49 | $this->error('Can\'t find the specified directory. Please check --path parameter');
50 | exit;
51 | }
52 |
53 | $this->parseDirectory(base_path($this->path));
54 |
55 | $bar = $this->output->createProgressBar(count($this->files));
56 | $bar->start();
57 | foreach ($this->files as $file) {
58 | $this->parseFile($file);
59 | $bar->advance();
60 | }
61 | $bar->finish();
62 |
63 | $this->newLine();
64 | $this->line('Custom path parse finished. Found '.$this->customKeysCount.' keys in '.$this->customFilesCount.' files');
65 | unset($this->files);
66 |
67 | $this->newLine();
68 | $this->line('Total keys found: '.count($this->translationsKeys));
69 |
70 | if (empty($this->translationsKeys)) {
71 | $this->error('Nothing to generate.');
72 | exit;
73 | }
74 |
75 | $this->newLine();
76 | $this->info('Generating translations...');
77 |
78 | $this->generateLangsFiles($this->translationsKeys);
79 |
80 | $this->newLine();
81 | $this->info('Translation files generated.');
82 | exit;
83 | }
84 |
85 | //VIEWS FOLDER
86 | $this->info('Parsing views folder...');
87 | $this->parseDirectory(resource_path('views'));
88 |
89 | $bar = $this->output->createProgressBar(count($this->files));
90 | $bar->start();
91 | foreach ($this->files as $file) {
92 | $this->parseFile($file);
93 | $bar->advance();
94 | }
95 | $bar->finish();
96 |
97 | $this->newLine();
98 | $this->line('Views parse finished. Found '.$this->viewsKeysCount.' keys in '.$this->viewsFilesCount.' files');
99 | unset($this->files);
100 |
101 | //APP FOLDER
102 | $this->newLine();
103 | $this->info('Parsing app folder...');
104 | $this->parseDirectory(app_path());
105 |
106 | $bar = $this->output->createProgressBar(count($this->files));
107 | $bar->start();
108 | foreach ($this->files as $file) {
109 | $this->parseFile($file);
110 | $bar->advance();
111 | }
112 | $bar->finish();
113 |
114 | $this->newLine();
115 | $this->line('App parse finished. Found '.$this->appKeysCount.' keys in '.$this->appFilesCount.' files');
116 |
117 | $this->newLine();
118 | $this->line('Total keys found: '.count($this->translationsKeys));
119 |
120 | $this->newLine();
121 | $this->info('Generating translations...');
122 |
123 | $this->generateLangsFiles($this->translationsKeys);
124 |
125 | $this->newLine();
126 | $this->info('Translation files generated.');
127 | }
128 |
129 | /**
130 | * Parse single folder for the availability of translations files.
131 | *
132 | * @param string $directory
133 | *
134 | * @return void
135 | */
136 | public function parseDirectory(string $directory): void
137 | {
138 | $handle = opendir($directory);
139 | while (false !== ($entry = readdir($handle))) {
140 | if ($entry === '.' || $entry === '..') {
141 | continue;
142 | }
143 | $path = $directory.'/'.$entry;
144 | if (is_dir($path)) {
145 | $this->parseDirectory($path);
146 | continue;
147 | }
148 |
149 | if (is_file($path)) {
150 | $this->files[] = $path;
151 | }
152 | }
153 | closedir($handle);
154 | }
155 |
156 | /**
157 | * Parse translation file for translation keys.
158 | *
159 | * @param string $path
160 | *
161 | * @return void
162 | */
163 | public function parseFile(string $path): void
164 | {
165 | $fileData = file_get_contents($path);
166 |
167 | $re = '/(?:@lang|trans|__)\(\'([^\']*)\'(?:,\s*\[.*?])?\)/m';
168 | preg_match_all($re, $fileData, $matches, PREG_SET_ORDER);
169 |
170 | $data = [];
171 | foreach ($matches as $match) {
172 | if (isset($match[3])) {
173 | $key = str_replace("'", '', $match[3]);
174 | $data[$key] = '';
175 | } elseif (isset($match[2])) {
176 | $key = str_replace("'", '', $match[2]);
177 | $data[$key] = '';
178 | } elseif (isset($match[1])) {
179 | $key = str_replace("'", '', $match[1]);
180 | $data[$key] = '';
181 | }
182 | }
183 |
184 | if (str_contains($path, resource_path('views'))) {
185 | $this->viewsFilesCount++;
186 | $this->viewsKeysCount += count($data);
187 | } elseif (str_contains($path, app_path())) {
188 | $this->appFilesCount++;
189 | $this->appKeysCount += count($data);
190 | } elseif (str_contains($path, $this->path)) {
191 | $this->customFilesCount++;
192 | $this->customKeysCount += count($data);
193 | }
194 |
195 | $this->translationsKeys = array_merge($data, $this->translationsKeys);
196 | }
197 |
198 | /**
199 | * Generate new language files in resource/lang folder.
200 | *
201 | * @param array $dataArr All founded language keys in single file
202 | *
203 | * @throws JsonException
204 | *
205 | * @return void
206 | */
207 | public function generateLangsFiles(array $dataArr): void
208 | {
209 | if ($this->fileType === 'json') {
210 | foreach ($this->languages as $language) {
211 | if (!$this->isNew) {
212 | $dataArr = $this->updateValues(base_path('lang/'.$language.'.json'), $dataArr);
213 | }
214 |
215 | if ($this->isSync) {
216 | $dataArr = $this->syncValues($this->translationsKeys, $dataArr);
217 | }
218 |
219 | file_put_contents(base_path('lang/'.$language.'.json'), json_encode($dataArr, JSON_THROW_ON_ERROR
220 | | JSON_PRETTY_PRINT));
221 | }
222 | } elseif ($this->fileType === 'array') {
223 | $res = [];
224 | $bar = $this->output->createProgressBar(count($dataArr));
225 | $bar->start();
226 | foreach ($dataArr as $key => $value) {
227 | if (config('lang-generator.short_keys', true) && str_contains($key, '.') && !str_contains($key, ' ')) {
228 | data_fill($res, $key, $value);
229 | } else {
230 | $res[$key] = '';
231 | }
232 | $bar->advance();
233 | }
234 | $bar->finish();
235 |
236 | $this->fillKeys($this->fileName, $res);
237 | }
238 | }
239 |
240 | /**
241 | * Assign existing translation keys values to new.
242 | *
243 | * @param string $path
244 | * @param array $dataArr
245 | *
246 | * @throws JsonException
247 | *
248 | * @return array|mixed
249 | */
250 | private function updateValues(string $path, array $dataArr): mixed
251 | {
252 | if ($this->fileType === 'json') {
253 | if (is_file($path)) {
254 | $existingArr = json_decode(file_get_contents($path), true, 512, JSON_THROW_ON_ERROR);
255 |
256 | if ($this->doAppend) {
257 | $tempArray = $existingArr;
258 |
259 | foreach ($dataArr as $key => $value) {
260 | if (!isset($tempArray[$key])) {
261 | $tempArray[$key] = $key;
262 | }
263 | }
264 |
265 | return $tempArray;
266 | }
267 |
268 | foreach ($existingArr as $key => $value) {
269 | $dataArr[$key] = $value;
270 | }
271 |
272 | return $dataArr;
273 | }
274 |
275 | foreach ($dataArr as $key => $value) {
276 | $dataArr[$key] = $key;
277 | }
278 |
279 | return $dataArr;
280 | }
281 |
282 | if (is_file($path)) {
283 | $existingArr = include $path;
284 |
285 | if (is_array($existingArr)) {
286 | foreach ($existingArr as $key => $value) {
287 | if (is_array($value) && isset($dataArr[$key]) && is_array($dataArr[$key])) {
288 | $dataArr[$key] = $this->arrayUpdater($dataArr[$key], $value);
289 | } else {
290 | $dataArr[$key] = $value;
291 | }
292 | }
293 | }
294 | }
295 |
296 | return $dataArr;
297 | }
298 |
299 | /**
300 | * Progressive merge two arrays into one.
301 | *
302 | * @param array $dataArr
303 | * @param array $existingArr
304 | *
305 | * @return array
306 | */
307 | private function arrayUpdater(array $dataArr, array $existingArr): array
308 | {
309 | foreach ($existingArr as $key => $value) {
310 | if (is_array($value)) {
311 | if (isset($dataArr[$key])) {
312 | $dataArr[$key] = $this->arrayUpdater($dataArr[$key], $value);
313 | } else {
314 | $dataArr[$key] = $value;
315 | }
316 | continue;
317 | }
318 | $dataArr[$key] = $value;
319 | }
320 |
321 | return $dataArr;
322 | }
323 |
324 | /**
325 | * Delete unused translation keys.
326 | *
327 | * @param array $parsedArr
328 | * @param array $dataArr
329 | *
330 | * @return array
331 | */
332 | private function syncValues(array $parsedArr, array $dataArr): array
333 | {
334 | foreach ($parsedArr as $key => $value) {
335 | if (str_contains($key, '.') && !str_contains($key, ' ')) {
336 | data_fill($parsedArr, $key, $value);
337 | } else {
338 | $parsedArr[$key] = $value;
339 | }
340 | }
341 |
342 | foreach ($dataArr as $key => $value) {
343 | if (!isset($parsedArr[$key])) {
344 | unset($dataArr[$key]);
345 | continue;
346 | }
347 |
348 | if (is_array($value)) {
349 | if (is_array($parsedArr[$key])) {
350 | $dataArr[$key] = $this->syncValues($parsedArr[$key], $value);
351 | } else {
352 | $dataArr[$key] = $key;
353 | }
354 | }
355 | }
356 |
357 | return $dataArr;
358 | }
359 |
360 | /**
361 | * Fill Array language file.
362 | *
363 | * @param $fileName
364 | * @param array $keys
365 | *
366 | * @throws JsonException
367 | *
368 | * @return void
369 | */
370 | private function fillKeys($fileName, array $keys): void
371 | {
372 | foreach ($this->languages as $language) {
373 | if (!is_dir(base_path('lang'."/$language")) && !mkdir(base_path('lang'."/$language"), 0777, true)
374 | && !is_dir(base_path('lang'."/$language"))) {
375 | throw new RuntimeException(sprintf('Directory "%s" was not created', 'path/to/directory'));
376 | }
377 | $filePath = base_path('lang'."/$language/$fileName.php");
378 |
379 | if (!$this->isNew) {
380 | $keys = $this->updateValues($filePath, $keys);
381 | }
382 |
383 | if ($this->isSync) {
384 | $keys = $this->syncValues($this->translationsKeys, $keys);
385 | }
386 |
387 | file_put_contents($filePath, "writeFile($filePath, $fileContent);
391 | }
392 | }
393 |
394 | /*
395 | |--------------------------------------------------------------------------
396 | | Array Translation methods
397 | |--------------------------------------------------------------------------
398 | */
399 | /**
400 | * Write translation keys to a .php arrays.
401 | *
402 | * @param $filePath
403 | * @param array $translations
404 | *
405 | * @return void
406 | */
407 | private function writeFile($filePath, array $translations): void
408 | {
409 | $content = "stringLineMaker($translations);
412 |
413 | $content .= "\n];";
414 |
415 | file_put_contents($filePath, $content, LOCK_EX);
416 | }
417 |
418 | /**
419 | * Generate a string line for an array translation file.
420 | *
421 | * @param $array
422 | * @param string $prepend
423 | *
424 | * @return string
425 | */
426 | private function stringLineMaker($array, string $prepend = ''): string
427 | {
428 | $output = '';
429 |
430 | foreach ($array as $key => $value) {
431 | if (is_array($value)) {
432 | $value = $this->stringLineMaker($value, $prepend.' ');
433 |
434 | $output .= "\n$prepend '$key' => [$value\n$prepend ],";
435 | } else {
436 | $value = str_replace('\"', '"', addslashes($value));
437 |
438 | $output .= "\n$prepend '$key' => '$value',";
439 | }
440 | }
441 |
442 | return $output;
443 | }
444 | }
445 |
--------------------------------------------------------------------------------
/src/LaravelLangGeneratorServiceProvider.php:
--------------------------------------------------------------------------------
1 | mergeConfigFrom(__DIR__.'/../config/lang-generator.php', 'lang-generator');
13 |
14 | if ($this->app->runningInConsole()) {
15 | $this->commands([
16 | LangGeneratorCommand::class,
17 | ]);
18 | }
19 | }
20 |
21 | public function boot(): void
22 | {
23 | $this->publishes([
24 | __DIR__.'/../config/lang-generator.php' => config_path('lang-generator.php'),
25 | ], 'config');
26 | }
27 | }
28 |
--------------------------------------------------------------------------------