├── .gitignore ├── LICENSE.md ├── README.md ├── composer.json └── src ├── Console └── Commands │ ├── MakeTranslationCommand.php │ └── MakeTranslationFileCommand.php ├── Exceptions └── FallbackLanguageFileNotExistsException.php ├── LaravelTranslationGeneratorServiceProvider.php └── Services ├── Finders ├── LanguagesFinder.php └── TranslationFilesFinder.php ├── Generators ├── JsonFileGenerator.php ├── PhpFileGenerator.php └── TranslationGenerator.php ├── MakeTranslationFileService.php ├── PackagesTranslationsService.php └── TranslationsFixer.php /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | composer.lock 3 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright (c) Krystian Zaręba 4 | 5 | > Permission is hereby granted, free of charge, to any person obtaining a copy 6 | > of this software and associated documentation files (the "Software"), to deal 7 | > in the Software without restriction, including without limitation the rights 8 | > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | > copies of the Software, and to permit persons to whom the Software is 10 | > furnished to do so, subject to the following conditions: 11 | > 12 | > The above copyright notice and this permission notice shall be included in 13 | > all copies or substantial portions of the Software. 14 | > 15 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | > THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel Translation Generator 2 | ![license mit](https://badgen.net/github/license/krzar/laravel-translation-generator) 3 | ![release](https://badgen.net/github/release/krzar/laravel-translation-generator/master) 4 | ![last commit](https://badgen.net/github/last-commit/krzar/laravel-translation-generator) 5 | 6 | This package allows you to: 7 | - Generate translation files for the specified language, 8 | - Generate new translation files for each language, 9 | - Completing missing keys for translations 10 | 11 | ## Requirements 12 | 13 | | Laravel | PHP | Package | Supported | 14 | |:-----------:|:-----:|:-------:|:------------------:| 15 | | From 10.x | 8.1+ | 3.x | :white_check_mark: | 16 | | 6.x to 10.x | 8.0+ | 2.x | :white_check_mark: | 17 | 18 | ## Installation 19 | 20 | ```bash 21 | composer require krzar/laravel-translation-generator 22 | ``` 23 | 24 | ## Usage 25 | 26 | > [!NOTE] 27 | > Remember, this package supports Laravel Prompts. So you can skip commands arguments and just answer the questions. 28 | 29 | ### Generate new translation 30 | 31 | Generate new translation files for `es` language. 32 | ```bash 33 | php artisan make:translation es 34 | ``` 35 | 36 | If the file exists, it will be completed with the missing keys. 37 | 38 | Files and keys will be copied based on the fallback locale specified in the app configuration. 39 | 40 | You can change fallback locale. 41 | 42 | **If you have published any translations from other packages the command will ask you if you want to generate 43 | new language translations for them as well.** 44 | 45 | ```bash 46 | php artisan make:translation es --fallback=de 47 | ``` 48 | 49 | You can also overwrite all values if file currently exists. 50 | 51 | ```bash 52 | php artisan make:translation es --overwrite 53 | ``` 54 | 55 | All values will be copied from fallback locale by default. 56 | If you want to clear every translation value you can use clear-values option. 57 | 58 | ```bash 59 | php artisan make:translation es --clear-values 60 | ``` 61 | 62 | This will clear values only for new keys, to clear everything, combine two options. 63 | 64 | ```bash 65 | php artisan make:translation es --clear-values --overwrite 66 | ``` 67 | 68 | ### Generate new translation file 69 | 70 | Generate new php translation file for every language. 71 | 72 | ```bash 73 | php artisan make:translation-file common 74 | ``` 75 | 76 | This will generate new php file `common.php` in every language folder (except packages translations folders). 77 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "krzar/laravel-translation-generator", 3 | "description": "Translations generator for laravel apps.", 4 | "keywords": [ 5 | "laravel", 6 | "translations", 7 | "translations-generator" 8 | ], 9 | "license": "MIT", 10 | "version": "v3.1.0", 11 | "authors": [ 12 | { 13 | "name": "Krystian Zaręba" 14 | } 15 | ], 16 | "autoload": { 17 | "psr-4": { 18 | "Krzar\\LaravelTranslationGenerator\\": "src/" 19 | } 20 | }, 21 | "require": { 22 | "php": "^8.1", 23 | "illuminate/console": "^10.0|^11.0", 24 | "illuminate/support": "^10.0|^11.0", 25 | "illuminate/collections": "^10.0|^11.0", 26 | "illuminate/container": "^10.0|^11.0", 27 | "laravel/prompts": "^0.1.11", 28 | "ext-json": "*" 29 | }, 30 | "extra": { 31 | "laravel": { 32 | "providers": [ 33 | "Krzar\\LaravelTranslationGenerator\\LaravelTranslationGeneratorServiceProvider" 34 | ] 35 | } 36 | }, 37 | "require-dev": { 38 | "laravel/pint": "^1.4" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Console/Commands/MakeTranslationCommand.php: -------------------------------------------------------------------------------- 1 | getLang(); 36 | $fallback = $this->getFallback(); 37 | $overwrite = $this->getOverwrite(); 38 | $clearValues = $this->getClearValues(); 39 | 40 | $generatePackagesTranslations = $this->generatePackagesTranslations(); 41 | 42 | foreach (self::GENERATORS as $generatorClass) { 43 | /** @var TranslationGenerator $generator */ 44 | $generator = new $generatorClass( 45 | $lang, 46 | $fallback, 47 | $overwrite, 48 | $clearValues, 49 | $generatePackagesTranslations 50 | ); 51 | 52 | try { 53 | $generator->generate(); 54 | } catch (FallbackLanguageFileNotExistsException $e) { 55 | $this->error($e->getMessage()); 56 | 57 | return self::FAILURE; 58 | } 59 | } 60 | 61 | info("Translations for '$lang' language has been created."); 62 | 63 | return self::SUCCESS; 64 | } 65 | 66 | private function generatePackagesTranslations(): bool 67 | { 68 | $packages = $this->packagesTranslationsService->findPackages(); 69 | 70 | if ($packages) { 71 | info('Translation files were found for the following packages:'); 72 | 73 | $packages->each(function (string $package) { 74 | $this->line("- $package"); 75 | }); 76 | 77 | return confirm('Do you want to generate files for packages as well?'); 78 | } 79 | 80 | return false; 81 | } 82 | 83 | private function getLang(): string 84 | { 85 | $lang = $this->argument('lang'); 86 | 87 | if (! $lang) { 88 | $lang = text( 89 | label: 'Enter language code', 90 | placeholder: 'Example: en, pl', 91 | required: true 92 | ); 93 | } 94 | 95 | return $lang; 96 | } 97 | 98 | private function getFallback(): string 99 | { 100 | $fallback = $this->option('fallback'); 101 | 102 | if (! $fallback) { 103 | $fallback = text( 104 | label: 'Enter fallback language code', 105 | placeholder: 'Example: en, pl', 106 | default: config('app.fallback_locale') 107 | ); 108 | } 109 | 110 | return $fallback; 111 | } 112 | 113 | private function getOverwrite(): bool 114 | { 115 | $overwrite = $this->option('overwrite'); 116 | 117 | if ($overwrite === false) { 118 | $overwrite = confirm( 119 | label: 'Do you want to overwrite existing translations?', 120 | default: false, 121 | yes: 'Yes, overwrite', 122 | no: 'No, skip' 123 | ); 124 | } 125 | 126 | return $overwrite; 127 | } 128 | 129 | private function getClearValues(): bool 130 | { 131 | $clearValues = $this->option('clear-values'); 132 | 133 | if ($clearValues === false) { 134 | $clearValues = confirm( 135 | label: 'Do you want to clear existing translations?', 136 | default: false, 137 | yes: 'Yes, clear', 138 | no: 'No, skip' 139 | ); 140 | } 141 | 142 | return $clearValues; 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/Console/Commands/MakeTranslationFileCommand.php: -------------------------------------------------------------------------------- 1 | getFileName(); 30 | $languages = $this->getLanguages(); 31 | 32 | $this->makeTranslationFileService->generate($fileName, $languages); 33 | 34 | info("Translation file '$fileName.php' has been created for given languages."); 35 | 36 | return self::SUCCESS; 37 | } 38 | 39 | private function getFileName(): string 40 | { 41 | $fileName = $this->argument('name'); 42 | 43 | if (empty($fileName)) { 44 | $fileName = text('Enter translation file name'); 45 | } 46 | 47 | return $fileName; 48 | } 49 | 50 | private function getLanguages(): Collection 51 | { 52 | $availableLanguages = $this->languagesFinder->getAvailableLanguages(); 53 | 54 | return collect(multiselect( 55 | label: 'Select languages for which you want to create translation file.', 56 | options: $availableLanguages, 57 | hint: 'If you want to select all languages, just click Enter.' 58 | )); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Exceptions/FallbackLanguageFileNotExistsException.php: -------------------------------------------------------------------------------- 1 | app->runningInConsole()) { 14 | $this->commands([ 15 | MakeTranslationCommand::class, 16 | MakeTranslationFileCommand::class, 17 | ]); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Services/Finders/LanguagesFinder.php: -------------------------------------------------------------------------------- 1 | filter( 14 | fn (string $directory) => $this->filterDirectory($directory) 15 | )->mapWithKeys( 16 | fn (string $directory) => [$directory => $directory] 17 | ); 18 | } 19 | 20 | private function filterDirectory(string $directory): bool 21 | { 22 | return ! in_array($directory, self::IGNORED_DIRECTORIES) && is_dir(lang_path($directory)); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Services/Finders/TranslationFilesFinder.php: -------------------------------------------------------------------------------- 1 | filter(fn (string $file) => Str::endsWith($file, self::PHP_EXT)); 24 | } 25 | 26 | return collect(); 27 | } 28 | 29 | public static function jsonFile(string $lang, ?string $package = null): string 30 | { 31 | if ($package) { 32 | return lang_path("vendor/$package/$lang".self::JSON_EXT); 33 | } 34 | 35 | return lang_path($lang.self::JSON_EXT); 36 | } 37 | 38 | public static function jsonFiles(?string $package = null): Collection 39 | { 40 | $path = $package ? lang_path("vendor/$package") : lang_path(); 41 | 42 | if (file_exists($path)) { 43 | return collect(scandir($path))->filter(fn (string $file) => Str::endsWith($file, self::JSON_EXT)); 44 | } 45 | 46 | return collect(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Services/Generators/JsonFileGenerator.php: -------------------------------------------------------------------------------- 1 | filesNotExists()) { 14 | return; 15 | } 16 | 17 | $this->generateSingle(); 18 | 19 | if ($this->generatePackagesTranslations) { 20 | $this->generatePackagesFiles(); 21 | } 22 | } 23 | 24 | protected function getTranslations(string $locale): ?Collection 25 | { 26 | $path = TranslationFilesFinder::jsonFile($locale, $this->currentPackage); 27 | 28 | return file_exists($path) ? collect(json_decode(file_get_contents($path), true)) : null; 29 | } 30 | 31 | protected function putToFile(Collection $translations): void 32 | { 33 | $targetPath = TranslationFilesFinder::jsonFile($this->lang, $this->currentPackage); 34 | $content = json_encode($translations, JSON_PRETTY_PRINT); 35 | 36 | file_put_contents($targetPath, $content); 37 | } 38 | 39 | /** 40 | * @throws FallbackLanguageFileNotExistsException 41 | */ 42 | private function generatePackagesFiles(): void 43 | { 44 | $this->packagesTranslationsService->findPackages()->each(function (string $package) { 45 | $this->currentPackage = $package; 46 | $this->generateSingle(); 47 | }); 48 | } 49 | 50 | private function filesNotExists(): bool 51 | { 52 | return TranslationFilesFinder::jsonFiles($this->currentPackage)->isEmpty(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Services/Generators/PhpFileGenerator.php: -------------------------------------------------------------------------------- 1 | generateFiles(); 17 | 18 | if ($this->generatePackagesTranslations) { 19 | $this->generatePackagesFiles(); 20 | } 21 | } 22 | 23 | public function parseContent(?Collection $translations = null): string 24 | { 25 | return sprintf( 26 | 'translationsToString($translations) : '' 30 | ); 31 | } 32 | 33 | /** 34 | * @throws FallbackLanguageFileNotExistsException 35 | */ 36 | private function generatePackagesFiles(): void 37 | { 38 | $this->packagesTranslationsService->findPackages()->each(function (string $package) { 39 | $this->currentPackage = $package; 40 | $this->generateFiles(); 41 | }); 42 | } 43 | 44 | /** 45 | * @throws FallbackLanguageFileNotExistsException 46 | */ 47 | private function generateFiles(): void 48 | { 49 | $this->setTargetPath(); 50 | 51 | TranslationFilesFinder::phpFiles($this->fallback, $this->currentPackage)->each(function (string $fileName) { 52 | $this->currentFileName = $fileName; 53 | 54 | $this->generateSingle(); 55 | }); 56 | } 57 | 58 | private function setTargetPath(): void 59 | { 60 | $this->targetPath = lang_path( 61 | $this->currentPackage ? "vendor/$this->currentPackage/$this->lang" : $this->lang 62 | ); 63 | 64 | if (! file_exists($this->targetPath)) { 65 | mkdir($this->targetPath); 66 | } 67 | } 68 | 69 | private function translationsToString(Collection $translations, int $level = 1): string 70 | { 71 | $tabs = sprintf("%'\t{$level}s", ''); 72 | 73 | return $translations->reduce(function (string $string, mixed $value, string $key) use ($level, $tabs) { 74 | if (is_string($value)) { 75 | $toAppend = sprintf("$tabs\"%s\" => \"%s\",\n", $key, $value); 76 | } else { 77 | $toAppend = sprintf( 78 | "$tabs\"%s\" => [\n%s$tabs],\n", 79 | $key, 80 | $this->translationsToString(collect($value), $level + 1) 81 | ); 82 | } 83 | 84 | return "{$string}{$toAppend}"; 85 | }, ''); 86 | } 87 | 88 | protected function getTranslations(string $locale): ?Collection 89 | { 90 | $path = $this->currentPackage ? "$this->currentPackage::$this->currentFileName" : $this->currentFileName; 91 | 92 | $translations = Lang::get(str_replace('.php', '', $path), [], $locale); 93 | 94 | return $translations !== '' ? collect($translations) : null; 95 | } 96 | 97 | protected function putToFile(Collection $translations): void 98 | { 99 | $targetPath = "$this->targetPath/$this->currentFileName"; 100 | $content = $this->parseContent($translations); 101 | 102 | file_put_contents($targetPath, $content); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/Services/Generators/TranslationGenerator.php: -------------------------------------------------------------------------------- 1 | packagesTranslationsService = new PackagesTranslationsService; 26 | } 27 | 28 | /** 29 | * @throws FallbackLanguageFileNotExistsException 30 | */ 31 | protected function generateSingle(): void 32 | { 33 | $translations = $this->getTranslations($this->fallback); 34 | 35 | if ($translations === null) { 36 | throw new FallbackLanguageFileNotExistsException( 37 | $this->fallback, 38 | $this->currentFileName ?: "$this->fallback.json" 39 | ); 40 | } 41 | 42 | $currentTranslations = $this->getTranslations($this->lang); 43 | 44 | if (! $this->overwrite && $currentTranslations) { 45 | $translations = TranslationsFixer::fixToOtherTranslations( 46 | $translations, 47 | $currentTranslations, 48 | $this->clearValues 49 | ); 50 | } elseif ($this->clearValues) { 51 | $translations = TranslationsFixer::fixToEmpty($translations); 52 | } 53 | 54 | $this->putToFile($translations); 55 | } 56 | 57 | /** 58 | * @throws FallbackLanguageFileNotExistsException 59 | */ 60 | abstract public function generate(): void; 61 | 62 | abstract protected function putToFile(Collection $translations): void; 63 | 64 | abstract protected function getTranslations(string $locale): ?Collection; 65 | } 66 | -------------------------------------------------------------------------------- /src/Services/MakeTranslationFileService.php: -------------------------------------------------------------------------------- 1 | isEmpty()) { 19 | $languages = $this->languagesFinder->getAvailableLanguages(); 20 | } 21 | 22 | $languages->each( 23 | fn (string $lang) => $this->generateFile($name, $lang) 24 | ); 25 | } 26 | 27 | private function generateFile(string $name, string $lang): void 28 | { 29 | $path = lang_path("$lang/$name.php"); 30 | 31 | file_put_contents($path, $this->phpFileGenerator->parseContent()); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Services/PackagesTranslationsService.php: -------------------------------------------------------------------------------- 1 | filter( 20 | fn (string $fileName) => $fileName !== '.' && $fileName !== '..' 21 | ); 22 | 23 | return $packages->count() > 0 ? $packages : null; 24 | } 25 | 26 | public function getPhpTranslationsFiles(string $fallback): Collection 27 | { 28 | return $this->findPackages()->flatMap( 29 | fn (string $package) => $this->getPackagePhpTranslationsFiles($package, $fallback) 30 | ); 31 | } 32 | 33 | private function getPackagePhpTranslationsFiles(string $package, string $fallback): Collection 34 | { 35 | $path = lang_path(sprintf('%s/%s/%s', self::VENDOR_PATH, $package, $fallback)); 36 | 37 | return collect(scandir($path))->filter( 38 | fn (string $fileName) => $fileName !== '.' && $fileName !== '..' 39 | )->map(fn (string $fileName) => sprintf( 40 | '%s/%s/%s/%s', 41 | self::VENDOR_PATH, 42 | $package, 43 | $fallback, 44 | $fileName 45 | )); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Services/TranslationsFixer.php: -------------------------------------------------------------------------------- 1 | map( 12 | fn (string|array $value) => is_string($value) ? '' : self::fixToEmpty(collect($value)) 13 | ); 14 | } 15 | 16 | public static function fixToOtherTranslations( 17 | Collection $translations, 18 | Collection $otherTranslations, 19 | bool $clearIfNotExists = false 20 | ): Collection { 21 | return $translations->map(fn (string|array $value, string $key) => self::fixToOtherTranslationSingle( 22 | $value, 23 | $otherTranslations->get($key), 24 | $clearIfNotExists 25 | )); 26 | } 27 | 28 | public static function fixToOtherTranslationSingle( 29 | string|array $translation, 30 | string|array|null $otherTranslation, 31 | bool $clearIfNotExists = false 32 | ): string|Collection { 33 | if (is_string($translation)) { 34 | if ($otherTranslation !== null) { 35 | return $otherTranslation; 36 | } 37 | 38 | return $clearIfNotExists ? '' : $translation; 39 | } 40 | 41 | return self::fixToOtherTranslations( 42 | collect($translation), 43 | collect($translation) 44 | ); 45 | } 46 | } 47 | --------------------------------------------------------------------------------