├── LICENSE ├── README.md ├── composer.json ├── pint.json └── src ├── Concerns ├── About.php ├── Decorator.php ├── Has.php ├── Output.php └── Path.php ├── Console ├── Add.php ├── Base.php ├── Remove.php ├── Reset.php └── Update.php ├── Constants └── Types.php ├── Contracts └── TextDecorator.php ├── Exceptions ├── BaseException.php ├── ProtectedLocaleException.php ├── UnknownLocaleCodeException.php └── UnknownPluginInstanceException.php ├── Helpers ├── Arr.php └── Config.php ├── Plugins ├── Plugin.php └── Provider.php ├── Processors ├── Add.php ├── Processor.php ├── Remove.php ├── Reset.php └── Update.php ├── Resources └── Translation.php ├── ServiceProvider.php └── Services ├── Converters ├── Extensions │ └── SmartPunctExtension.php └── Text │ ├── BaseDecorator.php │ ├── CommonDecorator.php │ └── SmartPunctuationDecorator.php ├── Filesystem ├── Base.php ├── Json.php ├── Manager.php └── Php.php └── Renderer └── ParagraphRenderer.php /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Andrey Helldar, Laravel-Lang Team 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel Lang Publisher 2 | 3 | ![laravel-lang locale publisher](https://preview.dragon-code.pro/laravel-lang/locales-publisher.svg?brand=laravel&mode=dark) 4 | 5 | [![Stable Version][badge_stable]][link_packagist] 6 | [![Total Downloads][badge_downloads]][link_packagist] 7 | [![Github Workflow Status][badge_build]][link_build] 8 | [![License][badge_license]][link_license] 9 | 10 | ## Documentation 11 | 12 | See the [documentation](https://laravel-lang.com/packages-publisher.html) for detailed installation. 13 | 14 | ## Communication 15 | 16 | We also have official [chats](https://t.me/addlist/l0XGtvEIBiljMTMy) in Telegram. 17 | 18 | ## Contributing 19 | 20 | Please see [CONTRIBUTING](https://laravel-lang.com/contributions.html) for details. 21 | 22 | ## Support Us 23 | 24 | ❤️ Laravel Lang? Please consider supporting our collective on [Boosty](https://boosty.to/laravel-lang). 25 | 26 | ## License 27 | 28 | This package is licensed under the [MIT License](https://laravel-lang.com/license.html). 29 | 30 | 31 | [badge_build]: https://img.shields.io/github/actions/workflow/status/laravel-lang/publisher/phpunit.yml?style=flat-square 32 | 33 | [badge_downloads]: https://img.shields.io/packagist/dt/laravel-lang/publisher.svg?style=flat-square 34 | 35 | [badge_license]: https://img.shields.io/packagist/l/laravel-lang/publisher.svg?style=flat-square 36 | 37 | [badge_stable]: https://img.shields.io/github/v/release/laravel-lang/publisher?label=stable&style=flat-square 38 | 39 | [link_build]: https://github.com/laravel-lang/publisher/actions 40 | 41 | [link_license]: LICENSE 42 | 43 | [link_packagist]: https://packagist.org/packages/laravel-lang/publisher 44 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "laravel-lang/publisher", 3 | "description": "Localization publisher for your Laravel application", 4 | "license": "MIT", 5 | "type": "library", 6 | "keywords": [ 7 | "laravel-lang", 8 | "breeze", 9 | "cashier", 10 | "fortify", 11 | "framework", 12 | "i18n", 13 | "jetstream", 14 | "lang", 15 | "languages", 16 | "laravel", 17 | "locale", 18 | "locales", 19 | "localization", 20 | "localizations", 21 | "lpm", 22 | "lumen", 23 | "nova", 24 | "publisher", 25 | "spark", 26 | "trans", 27 | "translation", 28 | "translations", 29 | "validations" 30 | ], 31 | "authors": [ 32 | { 33 | "name": "Andrey Helldar", 34 | "email": "helldar@dragon-code.pro" 35 | }, 36 | { 37 | "name": "Laravel-Lang Team", 38 | "homepage": "https://laravel-lang.com" 39 | } 40 | ], 41 | "support": { 42 | "issues": "https://github.com/Laravel-Lang/publisher/issues", 43 | "source": "https://github.com/Laravel-Lang/publisher" 44 | }, 45 | "require": { 46 | "php": "^8.1", 47 | "ext-json": "*", 48 | "composer/semver": "^3.4", 49 | "dragon-code/pretty-array": "^4.1", 50 | "dragon-code/support": "^6.11.3", 51 | "illuminate/collections": "^10.0 || ^11.0 || ^12.0", 52 | "illuminate/console": "^10.0 || ^11.0 || ^12.0", 53 | "illuminate/support": "^10.0 || ^11.0 || ^12.0", 54 | "laravel-lang/config": "^1.12", 55 | "laravel-lang/locales": "^2.10", 56 | "league/commonmark": "^2.4.1", 57 | "league/config": "^1.2" 58 | }, 59 | "require-dev": { 60 | "laravel-lang/json-fallback": "^2.2", 61 | "orchestra/testbench": "^8.14 || ^9.0 || ^10.0", 62 | "phpunit/phpunit": "^10.4.2 || ^11.0 || ^12.0", 63 | "symfony/var-dumper": "^6.3.6 || ^7.0" 64 | }, 65 | "conflict": { 66 | "laravel-lang/attributes": "<2.0", 67 | "laravel-lang/http-statuses": "<3.0", 68 | "laravel-lang/lang": "<11.0" 69 | }, 70 | "minimum-stability": "stable", 71 | "prefer-stable": true, 72 | "autoload": { 73 | "psr-4": { 74 | "LaravelLang\\Publisher\\": "src/" 75 | } 76 | }, 77 | "autoload-dev": { 78 | "psr-4": { 79 | "Tests\\": "tests/" 80 | }, 81 | "files": [ 82 | "tests/Fixtures/collision_functions.php" 83 | ] 84 | }, 85 | "config": { 86 | "allow-plugins": { 87 | "dragon-code/codestyler": true, 88 | "ergebnis/composer-normalize": true, 89 | "friendsofphp/php-cs-fixer": true, 90 | "symfony/thanks": true 91 | }, 92 | "preferred-install": "dist", 93 | "sort-packages": true 94 | }, 95 | "extra": { 96 | "laravel": { 97 | "providers": [ 98 | "LaravelLang\\Publisher\\ServiceProvider" 99 | ] 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /pint.json: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": [ 3 | "tests/Fixtures" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /src/Concerns/About.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright 2025 Laravel Lang Team 11 | * @license MIT 12 | * 13 | * @see https://laravel-lang.com 14 | */ 15 | 16 | declare(strict_types=1); 17 | 18 | namespace LaravelLang\Publisher\Concerns; 19 | 20 | use Composer\InstalledVersions; 21 | use DragonCode\Support\Facades\Helpers\Arr; 22 | use Illuminate\Foundation\Console\AboutCommand; 23 | use LaravelLang\Config\Facades\Config; 24 | 25 | trait About 26 | { 27 | protected function registerAbout(): void 28 | { 29 | $this->pushInformation(fn () => [ 30 | 'Publisher Version' => $this->getPackageVersion('laravel-lang/publisher'), 31 | ]); 32 | 33 | $this->pushInformation(fn () => $this->getPackages()); 34 | } 35 | 36 | protected function pushInformation(callable $data): void 37 | { 38 | AboutCommand::add('Locales', $data); 39 | } 40 | 41 | protected function getPackages(): array 42 | { 43 | return Arr::of(Config::hidden()->packages->all()) 44 | ->renameKeys(static fn (mixed $key, array $values) => $values['class']) 45 | ->map(fn (array $values) => $this->getPackageVersion($values['name'])) 46 | ->toArray(); 47 | } 48 | 49 | protected function getPackageVersion(string $package): string 50 | { 51 | if (InstalledVersions::isInstalled($package)) { 52 | return InstalledVersions::getPrettyVersion($package); 53 | } 54 | 55 | return 'INCORRECT'; 56 | } 57 | 58 | protected function implodeLocales(array $locales): string 59 | { 60 | return Arr::of($locales)->sort()->implode(', ')->toString(); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Concerns/Decorator.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright 2025 Laravel Lang Team 11 | * @license MIT 12 | * 13 | * @see https://laravel-lang.com 14 | */ 15 | 16 | declare(strict_types=1); 17 | 18 | namespace LaravelLang\Publisher\Concerns; 19 | 20 | trait Decorator 21 | { 22 | protected function decorate(string $locale, array $values): array 23 | { 24 | foreach ($values as &$value) { 25 | if (is_array($value)) { 26 | $value = $this->decorate($locale, $value); 27 | 28 | continue; 29 | } 30 | 31 | $value = $this->decorator->convert($locale, $value); 32 | } 33 | 34 | return $values; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Concerns/Has.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright 2025 Laravel Lang Team 11 | * @license MIT 12 | * 13 | * @see https://laravel-lang.com 14 | */ 15 | 16 | declare(strict_types=1); 17 | 18 | namespace LaravelLang\Publisher\Concerns; 19 | 20 | use DragonCode\Support\Facades\Filesystem\Path; 21 | use Illuminate\Support\Str; 22 | 23 | trait Has 24 | { 25 | protected function hasJson(string $path, bool $extension = true): bool 26 | { 27 | $name = $extension ? Path::extension($path) : Path::filename($path); 28 | 29 | return Str::contains($name, 'json', true); 30 | } 31 | 32 | protected function hasValidation(string $path): bool 33 | { 34 | $name = Path::filename($path); 35 | 36 | return Str::contains($name, 'validation', true); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Concerns/Output.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright 2025 Laravel Lang Team 11 | * @license MIT 12 | * 13 | * @see https://laravel-lang.com 14 | */ 15 | 16 | declare(strict_types=1); 17 | 18 | namespace LaravelLang\Publisher\Concerns; 19 | 20 | use Illuminate\Console\View\Components\Factory; 21 | 22 | trait Output 23 | { 24 | protected ?Factory $components = null; 25 | 26 | protected function info(string $message): void 27 | { 28 | $this->emptyLine(); 29 | 30 | $this->componentFactory()->info($message); 31 | } 32 | 33 | protected function task(string $message, callable $callback): void 34 | { 35 | $this->componentFactory()->task($message, $callback); 36 | } 37 | 38 | protected function emptyLine(): void 39 | { 40 | $this->output->newLine(); 41 | } 42 | 43 | protected function componentFactory(): Factory 44 | { 45 | if (! empty($this->components)) { 46 | return $this->components; 47 | } 48 | 49 | return $this->components = new Factory($this->output); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Concerns/Path.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright 2025 Laravel Lang Team 11 | * @license MIT 12 | * 13 | * @see https://laravel-lang.com 14 | */ 15 | 16 | declare(strict_types=1); 17 | 18 | namespace LaravelLang\Publisher\Concerns; 19 | 20 | use DragonCode\Support\Facades\Filesystem\File; 21 | use DragonCode\Support\Facades\Filesystem\Path as PathHelper; 22 | use DragonCode\Support\Facades\Helpers\Str; 23 | 24 | trait Path 25 | { 26 | protected function localeFilename(string $locale, string $path, bool $has_inline = false): string 27 | { 28 | $path = Str::replaceFormat($path, compact('locale'), '{%s}'); 29 | 30 | $directory = PathHelper::dirname($path); 31 | $filename = PathHelper::filename($path); 32 | $extension = PathHelper::extension($path); 33 | 34 | $main = "$directory/$filename.$extension"; 35 | $inline = "$directory/$filename-inline.$extension"; 36 | 37 | return $has_inline && File::exists($inline) ? $inline : $main; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Console/Add.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright 2025 Laravel Lang Team 11 | * @license MIT 12 | * 13 | * @see https://laravel-lang.com 14 | */ 15 | 16 | declare(strict_types=1); 17 | 18 | namespace LaravelLang\Publisher\Console; 19 | 20 | use LaravelLang\LocaleList\Locale; 21 | use LaravelLang\Locales\Facades\Locales; 22 | use LaravelLang\Publisher\Exceptions\UnknownLocaleCodeException; 23 | use LaravelLang\Publisher\Processors\Add as AddProcessor; 24 | use LaravelLang\Publisher\Processors\Processor; 25 | 26 | class Add extends Base 27 | { 28 | protected $signature = 'lang:add {locales?* : Space-separated list of, eg: de tk it}'; 29 | 30 | protected $description = 'Install new localizations.'; 31 | 32 | protected ?string $question = 'Do you want to install all localizations?'; 33 | 34 | protected Processor|string $processor = AddProcessor::class; 35 | 36 | protected function locales(): array 37 | { 38 | if ($this->confirmAll()) { 39 | return Locales::raw()->available(); 40 | } 41 | 42 | return $this->getLocalesArgument() 43 | ->each(function (Locale|string $locale) { 44 | if (! Locales::isAvailable($locale)) { 45 | throw new UnknownLocaleCodeException($locale); 46 | } 47 | }) 48 | ->all(); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Console/Base.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright 2025 Laravel Lang Team 11 | * @license MIT 12 | * 13 | * @see https://laravel-lang.com 14 | */ 15 | 16 | declare(strict_types=1); 17 | 18 | namespace LaravelLang\Publisher\Console; 19 | 20 | use Illuminate\Console\Command; 21 | use Illuminate\Support\Collection; 22 | use LaravelLang\Locales\Facades\Locales; 23 | use LaravelLang\Publisher\Contracts\TextDecorator; 24 | use LaravelLang\Publisher\Helpers\Config; 25 | use LaravelLang\Publisher\Processors\Processor; 26 | use LaravelLang\Publisher\Services\Converters\Text\CommonDecorator; 27 | use LaravelLang\Publisher\Services\Converters\Text\SmartPunctuationDecorator; 28 | 29 | abstract class Base extends Command 30 | { 31 | protected ?string $question; 32 | 33 | protected Processor|string $processor; 34 | 35 | public function handle(): void 36 | { 37 | $this->resolveProcessor()->prepare()->collect()->store(); 38 | 39 | $this->output->newLine(); 40 | } 41 | 42 | protected function resolveProcessor(): Processor 43 | { 44 | $config = $this->config(); 45 | 46 | return new $this->processor($this->output, $this->locales(), $this->decorator($config), $config); 47 | } 48 | 49 | protected function locales(): array 50 | { 51 | return Locales::raw()->installed(); 52 | } 53 | 54 | protected function decorator(Config $config): TextDecorator 55 | { 56 | return $config->hasSmartPunctuation() ? new SmartPunctuationDecorator($config) : new CommonDecorator($config); 57 | } 58 | 59 | protected function config(): Config 60 | { 61 | return new Config(); 62 | } 63 | 64 | protected function confirmAll(): bool 65 | { 66 | if (empty($this->argument('locales')) && $question = $this->question) { 67 | return $this->confirm($question); 68 | } 69 | 70 | return false; 71 | } 72 | 73 | protected function getLocalesArgument(): Collection 74 | { 75 | return collect($this->argument('locales')); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Console/Remove.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright 2025 Laravel Lang Team 11 | * @license MIT 12 | * 13 | * @see https://laravel-lang.com 14 | */ 15 | 16 | declare(strict_types=1); 17 | 18 | namespace LaravelLang\Publisher\Console; 19 | 20 | use LaravelLang\LocaleList\Locale; 21 | use LaravelLang\Locales\Facades\Locales; 22 | use LaravelLang\Publisher\Exceptions\ProtectedLocaleException; 23 | use LaravelLang\Publisher\Exceptions\UnknownLocaleCodeException; 24 | use LaravelLang\Publisher\Processors\Processor; 25 | use LaravelLang\Publisher\Processors\Remove as RemoveProcessor; 26 | 27 | class Remove extends Base 28 | { 29 | protected $signature = 'lang:rm {locales?* : Space-separated list of, eg: de tk it} {--force : Forced deletion of localization}'; 30 | 31 | protected $description = 'Remove localizations.'; 32 | 33 | protected ?string $question = 'Do you want to remove all localizations?'; 34 | 35 | protected Processor|string $processor = RemoveProcessor::class; 36 | 37 | protected function locales(): array 38 | { 39 | if ($this->confirmAll()) { 40 | return Locales::raw()->installed( 41 | $this->option('force') 42 | ); 43 | } 44 | 45 | return $this->getLocalesArgument() 46 | ->each(function (Locale|string $locale) { 47 | if (! Locales::isAvailable($locale)) { 48 | throw new UnknownLocaleCodeException($locale); 49 | } 50 | 51 | if (Locales::isProtected($locale) && ! $this->option('force')) { 52 | throw new ProtectedLocaleException($locale); 53 | } 54 | }) 55 | ->all(); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Console/Reset.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright 2025 Laravel Lang Team 11 | * @license MIT 12 | * 13 | * @see https://laravel-lang.com 14 | */ 15 | 16 | declare(strict_types=1); 17 | 18 | namespace LaravelLang\Publisher\Console; 19 | 20 | use LaravelLang\Publisher\Processors\Processor; 21 | use LaravelLang\Publisher\Processors\Reset as ResetProcessor; 22 | 23 | class Reset extends Base 24 | { 25 | protected $signature = 'lang:reset'; 26 | 27 | protected $description = 'Resets installed localizations.'; 28 | 29 | protected ?string $question = 'Are you sure you want to reset localization files?'; 30 | 31 | protected Processor|string $processor = ResetProcessor::class; 32 | 33 | public function handle(): void 34 | { 35 | if ($this->confirm($this->question)) { 36 | parent::handle(); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Console/Update.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright 2025 Laravel Lang Team 11 | * @license MIT 12 | * 13 | * @see https://laravel-lang.com 14 | */ 15 | 16 | declare(strict_types=1); 17 | 18 | namespace LaravelLang\Publisher\Console; 19 | 20 | use LaravelLang\Publisher\Processors\Processor; 21 | use LaravelLang\Publisher\Processors\Update as UpdateProcessor; 22 | 23 | class Update extends Base 24 | { 25 | protected $signature = 'lang:update'; 26 | 27 | protected $description = 'Updating installed localizations.'; 28 | 29 | protected Processor|string $processor = UpdateProcessor::class; 30 | } 31 | -------------------------------------------------------------------------------- /src/Constants/Types.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright 2025 Laravel Lang Team 11 | * @license MIT 12 | * 13 | * @see https://laravel-lang.com 14 | */ 15 | 16 | declare(strict_types=1); 17 | 18 | namespace LaravelLang\Publisher\Constants; 19 | 20 | enum Types: string 21 | { 22 | case TypeClass = 'class'; 23 | case TypeName = 'name'; 24 | } 25 | -------------------------------------------------------------------------------- /src/Contracts/TextDecorator.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright 2025 Laravel Lang Team 11 | * @license MIT 12 | * 13 | * @see https://laravel-lang.com 14 | */ 15 | 16 | declare(strict_types=1); 17 | 18 | namespace LaravelLang\Publisher\Contracts; 19 | 20 | interface TextDecorator 21 | { 22 | public function convert(string $locale, string $value): string; 23 | } 24 | -------------------------------------------------------------------------------- /src/Exceptions/BaseException.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright 2025 Laravel Lang Team 11 | * @license MIT 12 | * 13 | * @see https://laravel-lang.com 14 | */ 15 | 16 | declare(strict_types=1); 17 | 18 | namespace LaravelLang\Publisher\Exceptions; 19 | 20 | use LaravelLang\LocaleList\Locale; 21 | use RuntimeException; 22 | 23 | class BaseException extends RuntimeException 24 | { 25 | protected function stringify(array|Locale|string $locales): string 26 | { 27 | return collect($locales) 28 | ->map(static fn (Locale|string $locale) => $locale->value ?? $locale) 29 | ->implode(', '); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Exceptions/ProtectedLocaleException.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright 2025 Laravel Lang Team 11 | * @license MIT 12 | * 13 | * @see https://laravel-lang.com 14 | */ 15 | 16 | declare(strict_types=1); 17 | 18 | namespace LaravelLang\Publisher\Exceptions; 19 | 20 | use LaravelLang\LocaleList\Locale; 21 | 22 | class ProtectedLocaleException extends BaseException 23 | { 24 | public function __construct(array|Locale|string $locales) 25 | { 26 | $locales = $this->stringify($locales); 27 | 28 | parent::__construct("Can't delete protected locales: $locales."); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Exceptions/UnknownLocaleCodeException.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright 2025 Laravel Lang Team 11 | * @license MIT 12 | * 13 | * @see https://laravel-lang.com 14 | */ 15 | 16 | declare(strict_types=1); 17 | 18 | namespace LaravelLang\Publisher\Exceptions; 19 | 20 | use LaravelLang\LocaleList\Locale; 21 | 22 | class UnknownLocaleCodeException extends BaseException 23 | { 24 | public function __construct(array|Locale|string $locale) 25 | { 26 | $locale = $this->stringify($locale); 27 | 28 | parent::__construct("Unknown locale code: $locale."); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Exceptions/UnknownPluginInstanceException.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright 2025 Laravel Lang Team 11 | * @license MIT 12 | * 13 | * @see https://laravel-lang.com 14 | */ 15 | 16 | declare(strict_types=1); 17 | 18 | namespace LaravelLang\Publisher\Exceptions; 19 | 20 | use LaravelLang\Publisher\Plugins\Plugin; 21 | 22 | class UnknownPluginInstanceException extends BaseException 23 | { 24 | public function __construct(string $plugin) 25 | { 26 | parent::__construct($this->message($plugin)); 27 | } 28 | 29 | protected function message(string $plugin): string 30 | { 31 | return sprintf('The %s class is not a %s instance.', $plugin, Plugin::class); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Helpers/Arr.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright 2025 Laravel Lang Team 11 | * @license MIT 12 | * 13 | * @see https://laravel-lang.com 14 | */ 15 | 16 | declare(strict_types=1); 17 | 18 | namespace LaravelLang\Publisher\Helpers; 19 | 20 | use DragonCode\Support\Facades\Helpers\Arr as DragonArr; 21 | 22 | class Arr 23 | { 24 | public function merge(array $source, array $target, bool $filter_keys = false): array 25 | { 26 | foreach ($this->filter($source, $target, $filter_keys) as $key => $value) { 27 | if (! empty($value)) { 28 | $source[$key] = $value; 29 | } 30 | } 31 | 32 | return $source; 33 | } 34 | 35 | protected function filter(array $source, array $target, bool $filter_keys = false): array 36 | { 37 | return $filter_keys ? DragonArr::only($target, DragonArr::keys($source)) : $target; 38 | } 39 | 40 | public function ksort(array $source): array 41 | { 42 | ksort($source); 43 | 44 | foreach ($source as $key => &$value) { 45 | if (is_array($value)) { 46 | $value = $this->ksort($value); 47 | } 48 | } 49 | 50 | return $source; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Helpers/Config.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright 2025 Laravel Lang Team 11 | * @license MIT 12 | * 13 | * @see https://laravel-lang.com 14 | */ 15 | 16 | declare(strict_types=1); 17 | 18 | namespace LaravelLang\Publisher\Helpers; 19 | 20 | use LaravelLang\Config\Facades\Config as BaseConfig; 21 | use LaravelLang\LocaleList\Locale; 22 | use LaravelLang\Locales\Concerns\Aliases; 23 | use LaravelLang\Publisher\Constants\Types; 24 | use Stringable; 25 | 26 | class Config 27 | { 28 | use Aliases; 29 | 30 | public function __construct( 31 | readonly protected Arr $arr = new Arr() 32 | ) {} 33 | 34 | public function getPlugins(): array 35 | { 36 | return BaseConfig::hidden()->plugins->all(); 37 | } 38 | 39 | public function setPlugins(string $path, array $plugins): void 40 | { 41 | BaseConfig::hidden()->plugins->set($path, $plugins); 42 | } 43 | 44 | public function getPackages(): array 45 | { 46 | return BaseConfig::hidden()->packages->all(); 47 | } 48 | 49 | public function getPackageNameByPath(string $path, Types $type = Types::TypeName): string 50 | { 51 | $path = realpath($path); 52 | 53 | return $this->getPackages()[$path][$type->value] ?? $path; 54 | } 55 | 56 | public function setPackage(string $base_path, string $plugin_class, string $package_name): void 57 | { 58 | BaseConfig::hidden()->packages->set($base_path, [ 59 | 'class' => $plugin_class, 60 | 'name' => $package_name, 61 | ]); 62 | } 63 | 64 | public function langPath(Locale|string|null ...$paths): string 65 | { 66 | $path = collect($paths) 67 | ->filter() 68 | ->map(fn (Locale|string $value) => $this->toAlias($value)) 69 | ->implode('/'); 70 | 71 | return $this->path(lang_path(), $path); 72 | } 73 | 74 | public function hasInline(): bool 75 | { 76 | return BaseConfig::shared()->inline; 77 | } 78 | 79 | public function hasAlign(): bool 80 | { 81 | return BaseConfig::shared()->align; 82 | } 83 | 84 | public function hasSmartPunctuation(): bool 85 | { 86 | return BaseConfig::shared()->punctuation->enabled; 87 | } 88 | 89 | public function smartPunctuationConfig(string $locale): array 90 | { 91 | return BaseConfig::shared()->punctuation->locales->get($locale); 92 | } 93 | 94 | protected function path(string $base, string|Stringable|null $suffix = null): string 95 | { 96 | return rtrim($base, '\\/') . '/' . ltrim((string) $suffix, '\\/'); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/Plugins/Plugin.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright 2025 Laravel Lang Team 11 | * @license MIT 12 | * 13 | * @see https://laravel-lang.com 14 | */ 15 | 16 | declare(strict_types=1); 17 | 18 | namespace LaravelLang\Publisher\Plugins; 19 | 20 | use Composer\InstalledVersions; 21 | use Composer\Semver\VersionParser; 22 | 23 | abstract class Plugin 24 | { 25 | protected ?string $vendor = null; 26 | 27 | protected string $version = '*'; 28 | 29 | protected bool $with_project_name = false; 30 | 31 | abstract public function files(): array; 32 | 33 | public function vendor(): ?string 34 | { 35 | return $this->vendor; 36 | } 37 | 38 | public function has(): bool 39 | { 40 | if ($this->hasProjectName()) { 41 | return true; 42 | } 43 | 44 | return $this->hasVendor() && $this->hasVersion(); 45 | } 46 | 47 | private function hasVendor(): bool 48 | { 49 | if ($vendor = $this->vendor()) { 50 | return InstalledVersions::isInstalled($vendor); 51 | } 52 | 53 | return true; 54 | } 55 | 56 | private function hasVersion(): bool 57 | { 58 | if ($vendor = $this->vendor()) { 59 | return InstalledVersions::satisfies(new VersionParser(), $vendor, $this->version); 60 | } 61 | 62 | return true; 63 | } 64 | 65 | private function hasProjectName(): bool 66 | { 67 | if (! $this->with_project_name || ! $this->vendor()) { 68 | return false; 69 | } 70 | 71 | return $this->vendor === (InstalledVersions::getRootPackage()['name'] ?? false); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Plugins/Provider.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright 2025 Laravel Lang Team 11 | * @license MIT 12 | * 13 | * @see https://laravel-lang.com 14 | */ 15 | 16 | declare(strict_types=1); 17 | 18 | namespace LaravelLang\Publisher\Plugins; 19 | 20 | use DragonCode\Support\Facades\Helpers\Arr; 21 | use DragonCode\Support\Facades\Helpers\Str; 22 | use DragonCode\Support\Facades\Instances\Instance; 23 | use Illuminate\Support\ServiceProvider as BaseServiceProvider; 24 | use LaravelLang\Publisher\Exceptions\UnknownPluginInstanceException; 25 | use LaravelLang\Publisher\Helpers\Config; 26 | use RuntimeException; 27 | 28 | abstract class Provider extends BaseServiceProvider 29 | { 30 | protected Config $config; 31 | 32 | protected ?string $package_name = null; 33 | 34 | protected string $base_path; 35 | 36 | protected array $plugins; 37 | 38 | public function register(): void 39 | { 40 | $this->loadConfig(); 41 | 42 | $this->registerPlugins(); 43 | $this->registerPackageName(); 44 | } 45 | 46 | protected function loadConfig(): void 47 | { 48 | $this->config = new Config(); 49 | } 50 | 51 | protected function registerPlugins(): void 52 | { 53 | $this->config->setPlugins( 54 | $this->basePath(), 55 | $this->plugins() 56 | ); 57 | } 58 | 59 | protected function registerPackageName(): void 60 | { 61 | $vendor = Str::of($this->basePath()) 62 | ->after((string) realpath(base_path('vendor'))) 63 | ->ltrim('\\/') 64 | ->replace('\\', '/') 65 | ->toString(); 66 | 67 | if ($name = $this->package_name ?: $vendor) { 68 | if (! is_dir($name) && ! is_dir(realpath('/' . $name) ?: '')) { 69 | $this->config->setPackage($this->basePath(), static::class, $name); 70 | } 71 | } 72 | } 73 | 74 | protected function plugins(): array 75 | { 76 | return Arr::of($this->plugins) 77 | ->tap(static function (string $plugin) { 78 | if (Instance::of($plugin, Plugin::class)) { 79 | return true; 80 | } 81 | 82 | throw new UnknownPluginInstanceException($plugin); 83 | }) 84 | ->unique() 85 | ->sort() 86 | ->values() 87 | ->toArray(); 88 | } 89 | 90 | protected function basePath(): string 91 | { 92 | if ($path = realpath($this->base_path)) { 93 | return $path; 94 | } 95 | 96 | throw new RuntimeException(sprintf('The %s class must contain the definition of the $base_path property. The path must be existing.', static::class)); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/Processors/Add.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright 2025 Laravel Lang Team 11 | * @license MIT 12 | * 13 | * @see https://laravel-lang.com 14 | */ 15 | 16 | declare(strict_types=1); 17 | 18 | namespace LaravelLang\Publisher\Processors; 19 | 20 | use DragonCode\Support\Facades\Filesystem\Directory; 21 | 22 | class Add extends Processor 23 | { 24 | public function prepare(): Processor 25 | { 26 | foreach ($this->locales as $locale) { 27 | Directory::ensureDirectory($this->config->langPath($locale)); 28 | } 29 | 30 | return $this; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Processors/Processor.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright 2025 Laravel Lang Team 11 | * @license MIT 12 | * 13 | * @see https://laravel-lang.com 14 | */ 15 | 16 | declare(strict_types=1); 17 | 18 | namespace LaravelLang\Publisher\Processors; 19 | 20 | use DragonCode\Support\Facades\Filesystem\File; 21 | use Illuminate\Console\OutputStyle; 22 | use LaravelLang\Locales\Concerns\Aliases; 23 | use LaravelLang\Publisher\Concerns\Decorator; 24 | use LaravelLang\Publisher\Concerns\Has; 25 | use LaravelLang\Publisher\Concerns\Output; 26 | use LaravelLang\Publisher\Concerns\Path; 27 | use LaravelLang\Publisher\Constants\Types; 28 | use LaravelLang\Publisher\Contracts\TextDecorator; 29 | use LaravelLang\Publisher\Helpers\Arr as ArrHelper; 30 | use LaravelLang\Publisher\Helpers\Config; 31 | use LaravelLang\Publisher\Plugins\Plugin; 32 | use LaravelLang\Publisher\Resources\Translation; 33 | use LaravelLang\Publisher\Services\Filesystem\Manager; 34 | 35 | abstract class Processor 36 | { 37 | use Aliases; 38 | use Decorator; 39 | use Has; 40 | use Output; 41 | use Path; 42 | 43 | protected bool $reset = false; 44 | 45 | protected array $file_types = ['json', 'php']; 46 | 47 | public function __construct( 48 | readonly protected OutputStyle $output, 49 | readonly protected array $locales, 50 | readonly protected TextDecorator $decorator, 51 | readonly protected Config $config, 52 | protected Manager $filesystem = new Manager(), 53 | protected ArrHelper $arr = new ArrHelper(), 54 | protected Translation $translation = new Translation( 55 | ) 56 | ) {} 57 | 58 | public function prepare(): self 59 | { 60 | return $this; 61 | } 62 | 63 | public function collect(): self 64 | { 65 | $this->info('Collecting translations...'); 66 | 67 | foreach ($this->plugins() as $directory => $plugins) { 68 | $this->task( 69 | $this->config->getPackageNameByPath($directory, Types::TypeClass), 70 | function () use ($directory, $plugins) { 71 | /** @var Plugin $plugin */ 72 | foreach ($plugins as $plugin) { 73 | $this->collectKeys($directory, $plugin->files()); 74 | $this->collectLocalizations($directory, $plugin->files()); 75 | } 76 | } 77 | ); 78 | } 79 | 80 | return $this; 81 | } 82 | 83 | public function store(): void 84 | { 85 | $this->info('Storing changes...'); 86 | 87 | foreach ($this->translation->toArray() as $locale => $items) { 88 | foreach ($items as $filename => $values) { 89 | $this->task($filename, function () use ($filename, $values, $locale) { 90 | $path = $this->config->langPath($filename); 91 | 92 | $values 93 | = $this->reset || ! File::exists($path) 94 | ? $values 95 | : $this->arr->merge( 96 | $this->filesystem->load($path), 97 | $values 98 | ); 99 | 100 | $this->filesystem->store($path, $this->decorate($locale, $values)); 101 | }); 102 | } 103 | } 104 | } 105 | 106 | protected function collectKeys(string $directory, array $files): void 107 | { 108 | foreach ($files as $source => $target) { 109 | $values = $this->filesystem->load($directory . '/source/' . $source); 110 | 111 | $this->translation->setSource($target, $values); 112 | } 113 | } 114 | 115 | protected function collectLocalizations(string $directory, array $files): void 116 | { 117 | foreach ($files as $filename) { 118 | $keys = array_keys($this->translation->getSource($filename)); 119 | 120 | foreach ($this->locales as $locale) { 121 | $locale = $this->fromAlias($locale?->value ?? $locale); 122 | 123 | $locale_alias = $this->toAlias($locale); 124 | 125 | foreach ($this->file_types as $type) { 126 | $main_path = $this->localeFilename($locale_alias, "$directory/locales/$locale/$type.json"); 127 | $inline_path = $this->localeFilename($locale_alias, "$directory/locales/$locale/$type.json", true); 128 | 129 | $values = $this->filesystem->load($main_path); 130 | 131 | if ($main_path !== $inline_path && $this->config->hasInline()) { 132 | $values = $this->arr->merge($values, $this->filesystem->load($inline_path)); 133 | } 134 | 135 | $values = collect($values)->only($keys)->toArray(); 136 | 137 | $this->translation->setTranslations($filename, $locale_alias, $values); 138 | } 139 | } 140 | } 141 | } 142 | 143 | /** 144 | * @return array 145 | */ 146 | protected function plugins(): array 147 | { 148 | return collect($this->config->getPlugins()) 149 | ->map( 150 | fn (array $plugins) => collect($plugins) 151 | ->map(static fn (string $plugin) => new $plugin()) 152 | ->filter(static fn (Plugin $plugin) => $plugin->has()) 153 | ->all() 154 | ) 155 | ->filter() 156 | ->all(); 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/Processors/Remove.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright 2025 Laravel Lang Team 11 | * @license MIT 12 | * 13 | * @see https://laravel-lang.com 14 | */ 15 | 16 | declare(strict_types=1); 17 | 18 | namespace LaravelLang\Publisher\Processors; 19 | 20 | use DragonCode\Support\Facades\Filesystem\Directory; 21 | use DragonCode\Support\Facades\Filesystem\File; 22 | use DragonCode\Support\Facades\Filesystem\Path; 23 | use DragonCode\Support\Facades\Helpers\Arr; 24 | use DragonCode\Support\Facades\Helpers\Str; 25 | use LaravelLang\LocaleList\Locale; 26 | 27 | class Remove extends Processor 28 | { 29 | public function collect(): Processor 30 | { 31 | return $this; 32 | } 33 | 34 | public function store(): void 35 | { 36 | foreach ($this->locales as $locale) { 37 | $this->directories($locale); 38 | $this->files($locale); 39 | } 40 | } 41 | 42 | protected function directories(Locale|string $locale): void 43 | { 44 | Directory::ensureDelete($this->findDirectories($locale)); 45 | } 46 | 47 | protected function files(Locale|string $locale): void 48 | { 49 | File::ensureDelete($this->findFiles($locale)); 50 | } 51 | 52 | protected function findDirectories(Locale|string $locale): array 53 | { 54 | return $this->finder(Directory::class, $locale); 55 | } 56 | 57 | protected function findFiles(Locale|string $locale): array 58 | { 59 | return $this->finder(File::class, $locale); 60 | } 61 | 62 | protected function finder(Directory|File|string $filesystem, Locale|string $locale): array 63 | { 64 | $callback = $this->findCallback($locale); 65 | 66 | $names = []; 67 | 68 | foreach ($this->paths() as $path) { 69 | $items = $filesystem::names($path, $callback, true); 70 | 71 | $names[] = Arr::map($items, static fn (string $name) => Str::prepend($name, $path . '/')); 72 | } 73 | 74 | return Arr::of($names)->flatten()->unique()->toArray(); 75 | } 76 | 77 | protected function findCallback(Locale|string $locale): callable 78 | { 79 | $locale = $locale->value ?? $locale; 80 | 81 | return static fn (string $path) => $locale === Path::filename($path); 82 | } 83 | 84 | protected function paths(): array 85 | { 86 | return Arr::of([ 87 | $this->config->langPath(), 88 | base_path('vendor/laravel/framework/src/Illuminate/Translation/lang'), 89 | base_path('vendor/illuminate/translation/src/Illuminate/Translation/lang'), 90 | __DIR__ . '/../../vendor/laravel/framework/src/Illuminate/Translation/lang', 91 | __DIR__ . '/../../vendor/illuminate/translation/src/Illuminate/Translation/lang', 92 | ]) 93 | ->flatten() 94 | ->filter(static fn (string $path) => is_dir($path) && file_exists($path)) 95 | ->unique() 96 | ->toArray(); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/Processors/Reset.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright 2025 Laravel Lang Team 11 | * @license MIT 12 | * 13 | * @see https://laravel-lang.com 14 | */ 15 | 16 | declare(strict_types=1); 17 | 18 | namespace LaravelLang\Publisher\Processors; 19 | 20 | class Reset extends Processor 21 | { 22 | protected bool $reset = true; 23 | } 24 | -------------------------------------------------------------------------------- /src/Processors/Update.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright 2025 Laravel Lang Team 11 | * @license MIT 12 | * 13 | * @see https://laravel-lang.com 14 | */ 15 | 16 | declare(strict_types=1); 17 | 18 | namespace LaravelLang\Publisher\Processors; 19 | 20 | class Update extends Processor { } 21 | -------------------------------------------------------------------------------- /src/Resources/Translation.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright 2025 Laravel Lang Team 11 | * @license MIT 12 | * 13 | * @see https://laravel-lang.com 14 | */ 15 | 16 | declare(strict_types=1); 17 | 18 | namespace LaravelLang\Publisher\Resources; 19 | 20 | use DragonCode\Contracts\Support\Arrayable; 21 | use DragonCode\Support\Facades\Helpers\Str; 22 | use LaravelLang\Publisher\Helpers\Arr; 23 | 24 | class Translation implements Arrayable 25 | { 26 | protected array $source = []; 27 | 28 | protected array $translations = []; 29 | 30 | public function __construct( 31 | readonly protected Arr $arr = new Arr() 32 | ) {} 33 | 34 | public function getSource(string $filename): array 35 | { 36 | return $this->source[$filename] ?? []; 37 | } 38 | 39 | public function setSource(string $filename, array $values): self 40 | { 41 | $this->source[$filename] = $this->merge($this->source[$filename] ?? [], $values); 42 | 43 | return $this; 44 | } 45 | 46 | public function setTranslations(string $namespace, string $locale, array $values): self 47 | { 48 | $this->translations[$namespace][$locale] = $this->merge( 49 | $this->translations[$namespace][$locale] ?? [], 50 | $values 51 | ); 52 | 53 | return $this; 54 | } 55 | 56 | public function toArray(): array 57 | { 58 | $result = []; 59 | 60 | foreach ($this->source as $filename => $keys) { 61 | foreach ($this->translations[$filename] ?? [] as $locale => $values) { 62 | $name = $this->resolveFilename($filename, $locale); 63 | 64 | $result[$locale][$name] = $this->merge($keys, $values, true); 65 | } 66 | } 67 | 68 | return $this->arr->ksort($result); 69 | } 70 | 71 | protected function resolveFilename(string $path, string $locale): string 72 | { 73 | return Str::replaceFormat($path, compact('locale'), '{%s}'); 74 | } 75 | 76 | protected function merge(array $source, array $target, bool $filter_keys = false): array 77 | { 78 | return $this->arr->merge($source, $target, $filter_keys); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/ServiceProvider.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright 2025 Laravel Lang Team 11 | * @license MIT 12 | * 13 | * @see https://laravel-lang.com 14 | */ 15 | 16 | declare(strict_types=1); 17 | 18 | namespace LaravelLang\Publisher; 19 | 20 | use Illuminate\Support\ServiceProvider as BaseServiceProvider; 21 | use LaravelLang\Publisher\Concerns\About; 22 | use LaravelLang\Publisher\Console\Add; 23 | use LaravelLang\Publisher\Console\Remove; 24 | use LaravelLang\Publisher\Console\Reset; 25 | use LaravelLang\Publisher\Console\Update; 26 | 27 | class ServiceProvider extends BaseServiceProvider 28 | { 29 | use About; 30 | 31 | public function boot(): void 32 | { 33 | $this->bootCommands(); 34 | } 35 | 36 | public function register(): void 37 | { 38 | $this->registerAbout(); 39 | } 40 | 41 | protected function bootCommands(): void 42 | { 43 | $this->commands([ 44 | Add::class, 45 | Remove::class, 46 | Reset::class, 47 | Update::class, 48 | ]); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Services/Converters/Extensions/SmartPunctExtension.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright 2025 Laravel Lang Team 11 | * @license MIT 12 | * 13 | * @see https://laravel-lang.com 14 | */ 15 | 16 | declare(strict_types=1); 17 | 18 | namespace LaravelLang\Publisher\Services\Converters\Extensions; 19 | 20 | use LaravelLang\Publisher\Services\Renderer\ParagraphRenderer; 21 | use League\CommonMark\Environment\EnvironmentBuilderInterface; 22 | use League\CommonMark\Event\DocumentParsedEvent; 23 | use League\CommonMark\Extension\ConfigurableExtensionInterface; 24 | use League\CommonMark\Extension\SmartPunct\DashParser; 25 | use League\CommonMark\Extension\SmartPunct\EllipsesParser; 26 | use League\CommonMark\Extension\SmartPunct\Quote; 27 | use League\CommonMark\Extension\SmartPunct\QuoteParser; 28 | use League\CommonMark\Extension\SmartPunct\QuoteProcessor; 29 | use League\CommonMark\Extension\SmartPunct\ReplaceUnpairedQuotesListener; 30 | use League\CommonMark\Node\Block\Document; 31 | use League\CommonMark\Node\Block\Paragraph; 32 | use League\CommonMark\Node\Inline\Text; 33 | use League\CommonMark\Renderer\Block as CoreBlockRenderer; 34 | use League\CommonMark\Renderer\Inline as CoreInlineRenderer; 35 | use League\Config\ConfigurationBuilderInterface; 36 | use Nette\Schema\Expect; 37 | 38 | class SmartPunctExtension implements ConfigurableExtensionInterface 39 | { 40 | public function configureSchema(ConfigurationBuilderInterface $builder): void 41 | { 42 | $builder->addSchema( 43 | 'smartpunct', 44 | Expect::structure([ 45 | 'double_quote_opener' => Expect::string(Quote::DOUBLE_QUOTE_OPENER), 46 | 'double_quote_closer' => Expect::string(Quote::DOUBLE_QUOTE_CLOSER), 47 | 'single_quote_opener' => Expect::string(Quote::SINGLE_QUOTE_OPENER), 48 | 'single_quote_closer' => Expect::string(Quote::SINGLE_QUOTE_CLOSER), 49 | ]) 50 | ); 51 | } 52 | 53 | public function register(EnvironmentBuilderInterface $environment): void 54 | { 55 | $environment 56 | ->addInlineParser(new QuoteParser(), 10) 57 | ->addInlineParser(new DashParser()) 58 | ->addInlineParser(new EllipsesParser()) 59 | ->addDelimiterProcessor(QuoteProcessor::createDoubleQuoteProcessor( 60 | $environment->getConfiguration()->get('smartpunct/double_quote_opener'), 61 | $environment->getConfiguration()->get('smartpunct/double_quote_closer') 62 | )) 63 | ->addDelimiterProcessor(QuoteProcessor::createSingleQuoteProcessor( 64 | $environment->getConfiguration()->get('smartpunct/single_quote_opener'), 65 | $environment->getConfiguration()->get('smartpunct/single_quote_closer') 66 | )) 67 | ->addEventListener(DocumentParsedEvent::class, new ReplaceUnpairedQuotesListener()) 68 | ->addRenderer(Document::class, new CoreBlockRenderer\DocumentRenderer()) 69 | ->addRenderer(Paragraph::class, new ParagraphRenderer()) 70 | ->addRenderer(Text::class, new CoreInlineRenderer\TextRenderer()); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Services/Converters/Text/BaseDecorator.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright 2025 Laravel Lang Team 11 | * @license MIT 12 | * 13 | * @see https://laravel-lang.com 14 | */ 15 | 16 | declare(strict_types=1); 17 | 18 | namespace LaravelLang\Publisher\Services\Converters\Text; 19 | 20 | use LaravelLang\Publisher\Contracts\TextDecorator; 21 | use LaravelLang\Publisher\Helpers\Config; 22 | 23 | abstract class BaseDecorator implements TextDecorator 24 | { 25 | public function __construct( 26 | protected Config $config 27 | ) {} 28 | } 29 | -------------------------------------------------------------------------------- /src/Services/Converters/Text/CommonDecorator.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright 2025 Laravel Lang Team 11 | * @license MIT 12 | * 13 | * @see https://laravel-lang.com 14 | */ 15 | 16 | declare(strict_types=1); 17 | 18 | namespace LaravelLang\Publisher\Services\Converters\Text; 19 | 20 | class CommonDecorator extends BaseDecorator 21 | { 22 | public function convert(string $locale, string $value): string 23 | { 24 | return $value; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Services/Converters/Text/SmartPunctuationDecorator.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright 2025 Laravel Lang Team 11 | * @license MIT 12 | * 13 | * @see https://laravel-lang.com 14 | */ 15 | 16 | declare(strict_types=1); 17 | 18 | namespace LaravelLang\Publisher\Services\Converters\Text; 19 | 20 | use LaravelLang\Publisher\Services\Converters\Extensions\SmartPunctExtension; 21 | use League\CommonMark\ConverterInterface; 22 | use League\CommonMark\Environment\Environment; 23 | use League\CommonMark\MarkdownConverter; 24 | use League\CommonMark\Util\HtmlFilter; 25 | use Nette\Schema\Expect; 26 | 27 | class SmartPunctuationDecorator extends BaseDecorator 28 | { 29 | protected array $decorators = []; 30 | 31 | public function convert(string $locale, string $value): string 32 | { 33 | return $this->decorator($locale)->convert($value)->getDocument()->firstChild()->firstChild()->getLiteral(); 34 | } 35 | 36 | protected function decorator(string $locale) 37 | { 38 | if (isset($this->decorators[$locale])) { 39 | return $this->decorators[$locale]; 40 | } 41 | 42 | return $this->decorators[$locale] = $this->converter( 43 | $this->config->smartPunctuationConfig($locale) 44 | ); 45 | } 46 | 47 | protected function converter(array $smartpunct): ConverterInterface 48 | { 49 | return new MarkdownConverter( 50 | $this->environment($smartpunct)->addExtension(new SmartPunctExtension()) 51 | ); 52 | } 53 | 54 | protected function environment(array $smartpunct): Environment 55 | { 56 | return new Environment([ 57 | 'smartpunct' => $smartpunct, 58 | 'html_input' => HtmlFilter::ALLOW, 59 | 'renderer' => [ 60 | 'block_separator' => Expect::string(), 61 | 'inner_separator' => Expect::string(), 62 | 'soft_break' => Expect::string(), 63 | ], 64 | ]); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Services/Filesystem/Base.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright 2025 Laravel Lang Team 11 | * @license MIT 12 | * 13 | * @see https://laravel-lang.com 14 | */ 15 | 16 | declare(strict_types=1); 17 | 18 | namespace LaravelLang\Publisher\Services\Filesystem; 19 | 20 | use DragonCode\Contracts\Support\Filesystem; 21 | use DragonCode\PrettyArray\Services\File as Pretty; 22 | use DragonCode\PrettyArray\Services\Formatter; 23 | use DragonCode\Support\Facades\Filesystem\File; 24 | use DragonCode\Support\Facades\Helpers\Arr; 25 | use LaravelLang\Publisher\Concerns\Has; 26 | use LaravelLang\Publisher\Helpers\Config; 27 | 28 | abstract class Base implements Filesystem 29 | { 30 | use Has; 31 | 32 | public function __construct( 33 | protected Pretty $pretty = new Pretty(), 34 | protected Formatter $formatter = new Formatter(), 35 | protected Config $config = new Config( 36 | ) 37 | ) { 38 | $this->formatter->setKeyAsString(); 39 | 40 | if ($this->config->hasAlign()) { 41 | $this->formatter->setEqualsAlign(); 42 | } 43 | } 44 | 45 | public function load(string $path): array 46 | { 47 | if (File::exists($path)) { 48 | return File::load($path); 49 | } 50 | 51 | return []; 52 | } 53 | 54 | protected function sort(array $items): array 55 | { 56 | return Arr::ksort($items); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Services/Filesystem/Json.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright 2025 Laravel Lang Team 11 | * @license MIT 12 | * 13 | * @see https://laravel-lang.com 14 | */ 15 | 16 | declare(strict_types=1); 17 | 18 | namespace LaravelLang\Publisher\Services\Filesystem; 19 | 20 | use DragonCode\Support\Facades\Filesystem\File; 21 | 22 | class Json extends Base 23 | { 24 | public function store(string $path, $content): string 25 | { 26 | $items = $this->sort($content); 27 | 28 | return File::store($path, $this->encode($items)); 29 | } 30 | 31 | protected function encode(array $values): string 32 | { 33 | return json_encode($values, JSON_UNESCAPED_UNICODE ^ JSON_UNESCAPED_SLASHES ^ JSON_PRETTY_PRINT); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Services/Filesystem/Manager.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright 2025 Laravel Lang Team 11 | * @license MIT 12 | * 13 | * @see https://laravel-lang.com 14 | */ 15 | 16 | declare(strict_types=1); 17 | 18 | namespace LaravelLang\Publisher\Services\Filesystem; 19 | 20 | use DragonCode\Contracts\Support\Filesystem; 21 | use DragonCode\Support\Concerns\Resolvable; 22 | use LaravelLang\Publisher\Concerns\Has; 23 | 24 | class Manager implements Filesystem 25 | { 26 | use Has; 27 | use Resolvable; 28 | 29 | public function load(string $path): array 30 | { 31 | return $this->filesystem($path)->load($path); 32 | } 33 | 34 | public function store(string $path, $content): string 35 | { 36 | return $this->filesystem($path)->store($path, $content); 37 | } 38 | 39 | protected function filesystem(string $path): Filesystem 40 | { 41 | return $this->hasJson($path) 42 | ? static::resolveInstance(Json::class) 43 | : static::resolveInstance(Php::class); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Services/Filesystem/Php.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright 2025 Laravel Lang Team 11 | * @license MIT 12 | * 13 | * @see https://laravel-lang.com 14 | */ 15 | 16 | declare(strict_types=1); 17 | 18 | namespace LaravelLang\Publisher\Services\Filesystem; 19 | 20 | use DragonCode\PrettyArray\Services\File; 21 | use DragonCode\Support\Facades\Helpers\Arr; 22 | use DragonCode\Support\Helpers\Ables\Arrayable; 23 | use Illuminate\Support\Arr as IlluminateArr; 24 | 25 | class Php extends Base 26 | { 27 | protected array $except_keys = ['custom.attribute-name.rule-name']; 28 | 29 | public function load(string $path): array 30 | { 31 | return Arr::of(parent::load($path)) 32 | ->flattenKeys() 33 | ->except($this->except_keys) 34 | ->filter() 35 | ->toArray(); 36 | } 37 | 38 | public function store(string $path, $content): string 39 | { 40 | $content = $this->sort($content); 41 | 42 | $content = $this->expand($content); 43 | 44 | if ($this->hasValidation($path)) { 45 | $content = $this->validationSort($content); 46 | } 47 | 48 | $content = $this->format($content); 49 | 50 | File::make($content)->store($path); 51 | 52 | return $path; 53 | } 54 | 55 | protected function format(array $items): string 56 | { 57 | return $this->formatter->raw($items); 58 | } 59 | 60 | protected function expand(array $values): array 61 | { 62 | $result = []; 63 | 64 | foreach ($values as $key => $value) { 65 | IlluminateArr::set($result, $key, $value); 66 | } 67 | 68 | return $result; 69 | } 70 | 71 | protected function validationSort(array $items): array 72 | { 73 | $attributes = Arr::get($items, 'attributes'); 74 | $custom = Arr::get($items, 'custom'); 75 | 76 | return Arr::of($items) 77 | ->except(['attributes', 'custom']) 78 | ->when(! empty($attributes), fn (Arrayable $array) => $array->set('attributes', $this->correctNestedAttributes($attributes))) 79 | ->when(! empty($custom), static fn (Arrayable $array) => $array->set('custom', $custom)) 80 | ->toArray(); 81 | } 82 | 83 | protected function correctNestedAttributes(array $attributes): array 84 | { 85 | return Arr::flattenKeys($attributes); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Services/Renderer/ParagraphRenderer.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright 2025 Laravel Lang Team 11 | * @license MIT 12 | * 13 | * @see https://laravel-lang.com 14 | */ 15 | 16 | declare(strict_types=1); 17 | 18 | namespace LaravelLang\Publisher\Services\Renderer; 19 | 20 | use League\CommonMark\Node\Block\Paragraph; 21 | use League\CommonMark\Node\Node; 22 | use League\CommonMark\Renderer\ChildNodeRendererInterface; 23 | use League\CommonMark\Renderer\NodeRendererInterface; 24 | use League\CommonMark\Xml\XmlNodeRendererInterface; 25 | 26 | class ParagraphRenderer implements NodeRendererInterface, XmlNodeRendererInterface 27 | { 28 | public function render(Node $node, ChildNodeRendererInterface $childRenderer) 29 | { 30 | Paragraph::assertInstanceOf($node); 31 | 32 | return $childRenderer->renderNodes($node->children()); 33 | } 34 | 35 | public function getXmlTagName(Node $node): string 36 | { 37 | return 'paragraph'; 38 | } 39 | 40 | public function getXmlAttributes(Node $node): array 41 | { 42 | return []; 43 | } 44 | } 45 | --------------------------------------------------------------------------------