├── .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 | Laravel Lang Generator 4 |

5 | Build Status 6 | StyleCI 7 | Travis 8 | Latest Stable Version 9 | PHP Version 10 | Tags 11 | Last tag 12 | Downloads 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 | ![title](https://i.imgur.com/hvDrlVO.jpeg) 71 | ![title](https://i.imgur.com/GolZehZ.png) 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 | --------------------------------------------------------------------------------