├── src ├── Contracts │ ├── Translator.php │ └── Parser.php ├── Services │ ├── GoogleTranslator.php │ ├── JsonParser.php │ └── PhpParser.php ├── LangRuServiceProvider.php └── Commands │ └── Translate.php ├── lang ├── ru │ ├── pagination.php │ ├── auth.php │ ├── passwords.php │ └── validation.php └── ru.json ├── LICENSE.md ├── composer.json └── README.md /src/Contracts/Translator.php: -------------------------------------------------------------------------------- 1 | 'Вперёд »', 17 | 'previous' => '« Назад', 18 | 19 | ]; 20 | -------------------------------------------------------------------------------- /lang/ru/auth.php: -------------------------------------------------------------------------------- 1 | 'Неверное имя пользователя или пароль.', 18 | 'password' => 'Некорректный пароль.', 19 | 'throttle' => 'Слишком много попыток входа. Пожалуйста, попробуйте еще раз через :seconds секунд.', 20 | 21 | ]; 22 | -------------------------------------------------------------------------------- /lang/ru/passwords.php: -------------------------------------------------------------------------------- 1 | 'Ваш пароль был сброшен.', 18 | 'sent' => 'Ссылка на сброс пароля была отправлена.', 19 | 'throttled' => 'Пожалуйста, подождите перед повторной попыткой.', 20 | 'token' => 'Ошибочный код сброса пароля.', 21 | 'user' => 'Не удалось найти пользователя с указанным электронным адресом.', 22 | 23 | ]; 24 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Vladislav Sidelnikov 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 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kerigard/laravel-lang-ru", 3 | "description": "Ru lang for Laravel", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "Vladislav Sidelnikov", 8 | "email": "kerihoros@gmail.com" 9 | } 10 | ], 11 | "homepage": "https://github.com/kerigard/laravel-lang-ru", 12 | "keywords": [ 13 | "laravel" 14 | ], 15 | "require": { 16 | "php": "^8.2", 17 | "illuminate/translation": "^11.0|^12.0", 18 | "illuminate/console": "^11.0|^12.0", 19 | "illuminate/support": "^11.0|^12.0", 20 | "illuminate/http": "^11.0|^12.0" 21 | }, 22 | "require-dev": { 23 | "phpunit/phpunit": "^10.0|^11.0", 24 | "orchestra/testbench": "^9.0|^10.0" 25 | }, 26 | "autoload": { 27 | "psr-4": { 28 | "Kerigard\\LaravelLangRu\\": "src/" 29 | } 30 | }, 31 | "autoload-dev": { 32 | "psr-4": { 33 | "Kerigard\\LaravelLangRu\\Tests\\": "tests/" 34 | } 35 | }, 36 | "extra": { 37 | "laravel": { 38 | "providers": [ 39 | "Kerigard\\LaravelLangRu\\LangRuServiceProvider" 40 | ] 41 | } 42 | }, 43 | "minimum-stability": "dev", 44 | "prefer-stable": true 45 | } 46 | -------------------------------------------------------------------------------- /src/Services/GoogleTranslator.php: -------------------------------------------------------------------------------- 1 | source = $lang; 17 | } 18 | 19 | public function setTarget(string $lang): void 20 | { 21 | $this->target = $lang; 22 | } 23 | 24 | public function getSource(): string 25 | { 26 | return $this->source; 27 | } 28 | 29 | public function getTarget(): string 30 | { 31 | return $this->target; 32 | } 33 | 34 | public function translate(string $text): string 35 | { 36 | $result = Http::get('https://translate.googleapis.com/translate_a/single', [ 37 | 'client' => 'gtx', 38 | 'sl' => $this->source, 39 | 'tl' => $this->target, 40 | 'dt' => 't', 41 | 'q' => $text, 42 | ]) 43 | ->throw() 44 | ->json(); 45 | 46 | $translated = implode('', array_map(function (array $translation) { 47 | return $translation[0] ?? ''; 48 | }, $result[0] ?? [])); 49 | 50 | return $translated ?: $text; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel Lang Ru 2 | 3 |

4 | Build Status 5 | Total Downloads 6 | Latest Stable Version 7 | License 8 |

9 | 10 | Русские языковые ресурсы для Laravel. 11 | 12 | Перевод основан на репозитории https://github.com/Laravel-Lang/lang. 13 | 14 | ## Установка через composer 15 | 16 | ```bash 17 | composer require kerigard/laravel-lang-ru 18 | ``` 19 | 20 | Опубликуйте языковые ресурсы, используя artisan команду `vendor:publish`, чтобы изменить файлы локализации: 21 | 22 | ```bash 23 | php artisan vendor:publish --provider="Kerigard\LaravelLangRu\LangRuServiceProvider" 24 | ``` 25 | 26 | ### Автоматический перевод языковых ресурсов 27 | 28 | По умолчанию переводит все файлы из папки lang с английского на русский язык. 29 | 30 | ```bash 31 | php artisan lang:translate 32 | ``` 33 | 34 | Можно указать с какого и на какой язык выполнять перевод, а так же конкретные папки и файлы. 35 | 36 | ```bash 37 | php artisan lang:translate --source=en --target=ru --filter=en/validation.php --filter=vendor/my-package 38 | ``` 39 | 40 | ## Ручная установка 41 | 42 | > [!NOTE] 43 | > При данном варианте установки копируются только файлы с языковыми ресурсами 44 | 45 | ### Laravel 9 и выше (папка lang находится в корне проекта) 46 | 47 | ```bash 48 | curl https://codeload.github.com/Kerigard/laravel-lang-ru/tar.gz/master -L -o lang.tgz && tar --strip=1 -xvzf lang.tgz laravel-lang-ru-master/lang && rm lang.tgz 49 | ``` 50 | 51 | ### Laravel <= 8 52 | 53 | ```bash 54 | curl https://codeload.github.com/Kerigard/laravel-lang-ru/tar.gz/master -L -o lang.tgz && tar --strip=1 -xvzf lang.tgz -C resources laravel-lang-ru-master/lang && rm lang.tgz 55 | ``` 56 | -------------------------------------------------------------------------------- /src/LangRuServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->runningInConsole()) { 20 | $this->defineBindings(); 21 | $this->publishFiles(); 22 | $this->registerCommands(); 23 | } 24 | } 25 | 26 | protected function registerLoader(): void 27 | { 28 | $this->app->singleton('translation.loader', function ($app) { 29 | $paths = []; 30 | 31 | if (is_dir($path = "{$app['path.base']}/vendor/laravel/framework/src/Illuminate/Translation/lang")) { 32 | $paths[] = $path; 33 | } 34 | if (is_dir($path = "{$app['path.base']}/vendor/illuminate/translation/lang")) { 35 | $paths[] = $path; 36 | } 37 | 38 | $paths[] = __DIR__.'/../lang'; 39 | $paths[] = $app['path.lang']; 40 | 41 | return new FileLoader($app['files'], $paths); 42 | }); 43 | } 44 | 45 | protected function defineBindings(): void 46 | { 47 | $this->app->bind(Parser::class, function (Application $application, array $parameters) { 48 | return match (@$parameters['extension']) { 49 | 'json' => new JsonParser(@$parameters['code']), 50 | default => new PhpParser(@$parameters['code']), 51 | }; 52 | }); 53 | $this->app->bind(Translator::class, GoogleTranslator::class); 54 | } 55 | 56 | protected function publishFiles(): void 57 | { 58 | $this->publishes([ 59 | __DIR__.'/../lang' => is_dir($this->app->langPath()) 60 | ? $this->app->langPath() 61 | : $this->app->basePath('lang'), 62 | ], 'laravel-lang-ru'); 63 | } 64 | 65 | protected function registerCommands(): void 66 | { 67 | $this->commands([ 68 | Translate::class, 69 | ]); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Services/JsonParser.php: -------------------------------------------------------------------------------- 1 | code = $code; 18 | $this->exists = ! is_null($code); 19 | } 20 | 21 | public function load(string $filename): self 22 | { 23 | if (file_exists($filename)) { 24 | $this->code = file_get_contents($filename); 25 | $this->exists = true; 26 | } 27 | 28 | return $this; 29 | } 30 | 31 | public function exists(): bool 32 | { 33 | return $this->exists; 34 | } 35 | 36 | public function parse(): self 37 | { 38 | $items = json_decode($this->code, true) ?? []; 39 | 40 | foreach ($items as $key => $item) { 41 | if (is_string($item)) { 42 | $this->items[$key] = [ 43 | 'type' => 'item', 44 | 'array_key' => $key, 45 | 'key' => $key, 46 | 'value' => $item, 47 | ]; 48 | } else { 49 | $this->items[$key] = [ 50 | 'type' => 'base', 51 | 'array_key' => $key, 52 | 'key' => $key, 53 | 'data' => $item, 54 | ]; 55 | } 56 | } 57 | 58 | return $this; 59 | } 60 | 61 | public function items(): array 62 | { 63 | return $this->items; 64 | } 65 | 66 | public function has(string $key): bool 67 | { 68 | return array_key_exists($key, $this->items); 69 | } 70 | 71 | public function set(array $newToken, string $value): void 72 | { 73 | $newToken['type'] = 'item'; 74 | $newToken['value'] = $value; 75 | $this->items[$newToken['key']] = $newToken; 76 | } 77 | 78 | public function remove(string $key): void 79 | { 80 | unset($this->items[$key]); 81 | } 82 | 83 | public function raw(): string 84 | { 85 | $array = []; 86 | 87 | ksort($this->items); 88 | 89 | foreach ($this->items as $item) { 90 | if ($item['type'] == 'base') { 91 | $array[$item['array_key']] = $item['data']; 92 | } else { 93 | $array[$item['array_key']] = $item['value']; 94 | } 95 | } 96 | 97 | return json_encode((object) $array, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /lang/ru.json: -------------------------------------------------------------------------------- 1 | { 2 | "(and :count more error)": "(и ещё :count ошибка)", 3 | "(and :count more errors)": "(и ещё :count ошибка)|(и ещё :count ошибки)|(и ещё :count ошибок)", 4 | "A decryption key is required.": "Ключ дешифровки обязателен.", 5 | "All rights reserved.": "Все права защищены.", 6 | "Encrypted environment file already exists.": "Зашифрованный файл настроек окружения уже существует.", 7 | "Encrypted environment file not found.": "Зашифрованный файл настроек окружения не найден.", 8 | "Environment file already exists.": "Файл настроек окружения уже существует.", 9 | "Environment file not found.": "Файл настроек окружения не найден.", 10 | "errors": "ошибки", 11 | "Forbidden": "Запрещено", 12 | "Go to page :page": "Перейти к :page-й странице", 13 | "Hello!": "Здравствуйте!", 14 | "If you did not create an account, no further action is required.": "Если Вы не создавали учетную запись, никаких дополнительных действий не требуется.", 15 | "If you did not request a password reset, no further action is required.": "Если Вы не запрашивали восстановление пароля, никаких дополнительных действий не требуется.", 16 | "If you're having trouble clicking the \":actionText\" button, copy and paste the URL below\ninto your web browser:": "Если у Вас возникли проблемы с нажатием кнопки \":actionText\", скопируйте и вставьте приведенный ниже URL-адрес в свой браузер:", 17 | "Invalid filename.": "Некорректное имя файла.", 18 | "Invalid JSON was returned from the route.": "Маршрут вернул некорректный JSON.", 19 | "length": "длина", 20 | "Location": "Местоположение", 21 | "Login": "Войти", 22 | "Logout": "Выйти", 23 | "Not Found": "Не найдено", 24 | "of": "из", 25 | "Page Expired": "Страница устарела", 26 | "Pagination Navigation": "Навигация", 27 | "Payment Required": "Требуется оплата", 28 | "Please click the button below to verify your email address.": "Пожалуйста, нажмите кнопку ниже, чтобы подтвердить свой адрес электронной почты.", 29 | "Regards": "С уважением", 30 | "Regards,": "С уважением,", 31 | "Register": "Регистрация", 32 | "Reset Password": "Сбросить пароль", 33 | "Reset Password Notification": "Оповещение о сбросе пароля", 34 | "results": "результатов", 35 | "Server Error": "Ошибка сервера", 36 | "Service Unavailable": "Сервис недоступен", 37 | "Showing": "Показано с", 38 | "The given data was invalid.": "Указанные данные недействительны.", 39 | "The response is not a streamed response.": "Ответ не является потоковым.", 40 | "The response is not a view.": "Ответ не является представлением.", 41 | "This action is unauthorized.": "Действие не авторизовано.", 42 | "This password reset link will expire in :count minutes.": "Срок действия ссылки для сброса пароля истекает через :count минут.", 43 | "to": "по", 44 | "Toggle navigation": "Переключить навигацию", 45 | "Too Many Requests": "Слишком много запросов", 46 | "Unauthorized": "Не авторизован", 47 | "Verify Email Address": "Подтвердить адрес электронной почты", 48 | "Whoops!": "Упс!", 49 | "You are receiving this email because we received a password reset request for your account.": "Вы получили это письмо, потому что мы получили запрос на сброс пароля для Вашей учётной записи." 50 | } -------------------------------------------------------------------------------- /src/Commands/Translate.php: -------------------------------------------------------------------------------- 1 | option('source'); 26 | $target = $this->option('target'); 27 | 28 | $files = File::allFiles($this->laravel->langPath($source)); 29 | $vendorDirs = File::isDirectory($this->laravel->langPath('vendor')) 30 | ? File::directories($this->laravel->langPath('vendor')) 31 | : []; 32 | 33 | if (File::isFile($file = $this->laravel->langPath("{$source}.json"))) { 34 | $files[] = new SplFileInfo($file); 35 | } 36 | 37 | foreach ($vendorDirs as $dir) { 38 | array_push($files, ...File::allFiles($this->joinPaths($dir, $source))); 39 | } 40 | 41 | foreach ($files as $key => $file) { 42 | foreach ($this->option('filter') as $path) { 43 | $path = preg_replace('/[\/\\\]/', DIRECTORY_SEPARATOR, $path); 44 | 45 | if (Str::startsWith($file->getRealPath(), $this->laravel->langPath($path))) { 46 | continue 2; 47 | } 48 | } 49 | 50 | if (! empty($this->option('filter')) || ! in_array($file->getExtension(), ['php', 'json'])) { 51 | unset($files[$key]); 52 | } 53 | } 54 | 55 | if (empty($files)) { 56 | $this->components->info('Нет языковых ресурсов для перевода.'); 57 | 58 | return; 59 | } 60 | 61 | $this->components->info("Перевод языковых ресурсов с {$source} на {$target}."); 62 | 63 | $this->translator = $this->laravel->make(Translator::class); 64 | $this->translator->setSource($source); 65 | $this->translator->setTarget($target); 66 | 67 | foreach ($files as $file) { 68 | $path = Str::after($file->getRealPath(), $this->laravel->langPath(DIRECTORY_SEPARATOR)); 69 | $this->components->task($path, fn () => $this->translateFile($file, $target)); 70 | } 71 | 72 | $this->newLine(); 73 | } 74 | 75 | private function translateFile(SplFileInfo $file, string $target): void 76 | { 77 | /** @var \Kerigard\LaravelLangRu\Contracts\Parser */ 78 | $sourceParser = $this->laravel->make(Parser::class, ['extension' => $file->getExtension()]); 79 | $source = $sourceParser->load($file->getRealPath())->parse(); 80 | 81 | $targetPath = $file->getPath() == $this->laravel->langPath() 82 | ? $this->joinPaths($file->getPath(), "{$target}." . $file->getExtension()) 83 | : $this->joinPaths(dirname($file->getPath()), $target, $file->getFilename()); 84 | 85 | /** @var \Kerigard\LaravelLangRu\Contracts\Parser */ 86 | $targetParser = $this->laravel->make(Parser::class, ['extension' => $file->getExtension()]); 87 | $targetParser = $targetParser->load($targetPath); 88 | $target = $targetParser->exists() ? $targetParser->parse() : $source; 89 | 90 | foreach ($source->items() as $key => $token) { 91 | if (array_key_exists('value', $token) && (! $targetParser->exists() || ! $target->has($key))) { 92 | $target->set($token, $this->translate($token['value'])); 93 | } 94 | } 95 | 96 | File::ensureDirectoryExists(pathinfo($targetPath, PATHINFO_DIRNAME)); 97 | File::replace($targetPath, $target->raw()); 98 | } 99 | 100 | private function translate(string $content): string 101 | { 102 | $content = preg_replace('/(:[\w-]+)/iu', '<$1>', $content); 103 | $content = preg_replace('/(&#?\w+;)/iu', '', $content); 104 | 105 | preg_match_all('/<(:[\w-]+)>/iu', $content, $attributes); 106 | preg_match_all('//iu', $content, $symbols); 107 | 108 | $content = $this->translator->translate($content); 109 | 110 | $content = preg_replace_array('/<\s?:.+?>/iu', $attributes[1], $content); 111 | $content = preg_replace_array('//iu', $symbols[1], $content); 112 | 113 | return $content; 114 | } 115 | 116 | private function joinPaths(string $basePath, string ...$paths): string 117 | { 118 | foreach ($paths as $index => $path) { 119 | if (empty($path)) { 120 | unset($paths[$index]); 121 | } else { 122 | $paths[$index] = DIRECTORY_SEPARATOR.ltrim($path, DIRECTORY_SEPARATOR); 123 | } 124 | } 125 | 126 | return $basePath.implode('', $paths); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/Services/PhpParser.php: -------------------------------------------------------------------------------- 1 | code = $code; 23 | $this->exists = ! is_null($code); 24 | } 25 | 26 | public function load(string $filename): self 27 | { 28 | if (file_exists($filename)) { 29 | $this->code = file_get_contents($filename); 30 | $this->exists = true; 31 | } 32 | 33 | return $this; 34 | } 35 | 36 | public function exists(): bool 37 | { 38 | return $this->exists; 39 | } 40 | 41 | public function parse(): self 42 | { 43 | $tokens = PhpToken::tokenize($this->code ?? ''); 44 | $newToken = null; 45 | $brackets = $arrays = $arrayKeys = []; 46 | $hasArrayValue = false; 47 | 48 | foreach ($tokens as $key => $token) { 49 | $type = 'base'; 50 | $options = []; 51 | 52 | if ( 53 | ($token->is(T_RETURN) && empty($arrays)) || 54 | ($token->is(T_WHITESPACE) && @$newToken['type'] == 'return') 55 | ) { 56 | $type = 'return'; 57 | } elseif ($token->is(T_ARRAY) || ($token->is(T_WHITESPACE) && @$newToken['old_array'])) { 58 | $type = 'array_open'; 59 | $options['old_array'] = true; 60 | } elseif ($token->is(['[', '('])) { 61 | $brackets[] = $key; 62 | 63 | if (in_array(@$newToken['type'], ['return', 'array_open'])) { 64 | $type = 'array_open'; 65 | $arrays[] = $key; 66 | } elseif ($newToken) { 67 | $type = $newToken['type']; 68 | 69 | if ($newToken['type'] == 'array_item' && $hasArrayValue) { 70 | $newToken['closed'] = $newToken['open_nested'] = true; 71 | $arrays[] = $key; 72 | $arrayKeys[] = $newToken['array_key']; 73 | $hasArrayValue = false; 74 | } 75 | } 76 | } elseif ($token->is([']', ')'])) { 77 | $lastBracket = array_pop($brackets); 78 | 79 | if ($lastBracket == @$arrays[count($arrays) - 1]) { 80 | if (@$newToken['type'] == 'array_item' && ! @$newToken['closed']) { 81 | $newToken['text'] .= ','; 82 | $newToken['closed'] = true; 83 | } 84 | 85 | if (empty($arrayKeys)) { 86 | $type = 'array_close'; 87 | } else { 88 | $type = 'array_item'; 89 | $options['key'] = implode('.', $arrayKeys); 90 | $options['array_key'] = array_pop($arrayKeys); 91 | $options['indent'] = (count($arrayKeys) + 1) * 4; 92 | $options['close_nested'] = true; 93 | $options['group'] = $this->groupIndex; 94 | 95 | $this->tokens[] = $newToken; 96 | $newToken = null; 97 | } 98 | 99 | array_pop($arrays); 100 | } elseif ($newToken) { 101 | $type = $newToken['type']; 102 | } 103 | } elseif ( 104 | $token->is([T_COMMENT, T_DOC_COMMENT]) && 105 | str_starts_with($token->text, '/*') && 106 | empty($arrayKeys) && 107 | (@$newToken['type'] != 'array_item' || @$newToken['closed']) 108 | ) { 109 | $type = 'comment'; 110 | $options['group'] = ++$this->groupIndex; 111 | } elseif (! empty($arrays)) { 112 | $type = 'array_item'; 113 | 114 | if (@$newToken['closed']) { 115 | if ($token->is(T_COMMENT)) { 116 | if ($token->line == $tokens[$key - 1]?->line) { 117 | $newToken['comment'] = $token->text; 118 | 119 | continue; 120 | } 121 | } elseif ($token->isIgnorable()) { 122 | continue; 123 | } 124 | 125 | $this->tokens[] = $newToken; 126 | $newToken = null; 127 | } elseif ( 128 | $newToken && 129 | $token->is(T_COMMENT) && 130 | @$arrays[count($arrays) - 1] == @$brackets[count($brackets) - 1] 131 | ) { 132 | $newToken['comment'] = $token->text; 133 | 134 | continue; 135 | } 136 | if ($token->is(',') && @$arrays[count($arrays) - 1] == @$brackets[count($brackets) - 1]) { 137 | $options['closed'] = true; 138 | } elseif ($token->is(T_DOUBLE_ARROW) && ! @$newToken['no_value']) { 139 | $hasArrayValue = true; 140 | } elseif ( 141 | ! $hasArrayValue && ! array_key_exists('value', $newToken ?? []) && ! @$newToken['no_value'] 142 | ) { 143 | if (! $token->isIgnorable()) { 144 | $text = $token->is(T_CONSTANT_ENCAPSED_STRING) 145 | ? stripslashes($token->text) 146 | : $token->text; 147 | 148 | if (@$newToken['type'] == 'array_item' && array_key_exists('array_key', $newToken ?? [])) { 149 | $newToken['array_key'] .= $text; 150 | $newToken['key'] .= $text; 151 | } else { 152 | $options['array_key'] = $text; 153 | $options['key'] = empty($arrayKeys) ? $text : (implode('.', $arrayKeys).".{$text}"); 154 | $options['indent'] = (count($arrayKeys) + 1) * 4; 155 | $options['group'] = $this->groupIndex; 156 | } 157 | } 158 | 159 | $hasArrayValue = false; 160 | } elseif ($hasArrayValue && ! $token->isIgnorable()) { 161 | if ($token->is(T_CONSTANT_ENCAPSED_STRING)) { 162 | $options['value'] = trim(stripslashes($token->text), '\'"'); 163 | } else { 164 | $options['no_value'] = true; 165 | } 166 | 167 | $hasArrayValue = false; 168 | } elseif ( 169 | $token->is(T_WHITESPACE) && 170 | @$newToken['type'] == 'array_item' && 171 | array_key_exists('value', $newToken ?? []) 172 | ) { 173 | continue; 174 | } 175 | } 176 | 177 | if ($newToken && $newToken['type'] != $type) { 178 | $this->tokens[] = $newToken; 179 | $newToken = null; 180 | } 181 | 182 | if (is_null($newToken)) { 183 | if ($type == 'array_item' && $token->is(T_WHITESPACE)) { 184 | continue; 185 | } 186 | 187 | $newToken = [ 188 | 'type' => $type, 189 | 'text' => $token->text, 190 | ...$options, 191 | ]; 192 | } elseif ($newToken['type'] == $type) { 193 | $newToken['text'] .= $token->text; 194 | $newToken += $options; 195 | } 196 | } 197 | 198 | if (! is_null($newToken)) { 199 | $this->tokens[] = $newToken; 200 | } 201 | 202 | foreach ($this->tokens as &$token) { 203 | if ($token['type'] == 'array_item' && ! @$token['close_nested']) { 204 | $this->items[$token['key']] = $token; 205 | } 206 | } 207 | 208 | return $this; 209 | } 210 | 211 | public function items(): array 212 | { 213 | return $this->items; 214 | } 215 | 216 | public function has(string $key): bool 217 | { 218 | return array_key_exists($key, $this->items); 219 | } 220 | 221 | public function set(array $newToken, string $value): void 222 | { 223 | $text = "{$newToken['array_key']} => '".str_replace("'", "\\'", $value)."',"; 224 | $newToken['text'] = $text; 225 | $newToken['value'] = $value; 226 | 227 | if ($this->has($newToken['key'])) { 228 | foreach ($this->tokens as &$token) { 229 | if ($token['type'] == 'array_item' && $token['key'] == $newToken['key'] && ! @$token['closed_nested']) { 230 | $token['text'] = $newToken['text']; 231 | $token['value'] = $newToken['value']; 232 | $this->items[$token['key']] = $token; 233 | 234 | break; 235 | } 236 | } 237 | } else { 238 | $index = 0; 239 | $group = $newToken['group'] > $this->groupIndex ? ($this->groupIndex ? 1 : 0) : $newToken['group']; 240 | 241 | if ($this->groupIndex) { 242 | foreach ($this->tokens as $token) { 243 | if ($token['type'] == 'comment' && $token['group'] == $group) { 244 | break; 245 | } 246 | 247 | $index++; 248 | } 249 | 250 | if ($index > array_key_last($this->tokens)) { 251 | $index = 0; 252 | } 253 | } 254 | 255 | if (! $index) { 256 | foreach ($this->tokens as $token) { 257 | if ($token['type'] == 'array_open') { 258 | break; 259 | } 260 | 261 | $index++; 262 | } 263 | 264 | if ($index > array_key_last($this->tokens)) { 265 | $index = 0; 266 | } 267 | } 268 | 269 | if ($index) { 270 | $newToken['group'] = $group; 271 | array_splice($this->tokens, $index + 1, 0, [$newToken]); 272 | $this->items[$newToken['key']] = $newToken; 273 | } 274 | } 275 | } 276 | 277 | public function remove(string $key): void 278 | { 279 | $removedToken = @$this->items[$key]; 280 | 281 | if (! $removedToken) { 282 | return; 283 | } 284 | 285 | foreach ($this->tokens as $i => $token) { 286 | if ($token['type'] == 'array_item' && $token['key'] == $removedToken['key']) { 287 | unset($this->tokens[$i]); 288 | 289 | if (@$token['open_nested']) { 290 | foreach ($this->tokens as &$childToken) { 291 | if ( 292 | $childToken['type'] == 'array_item' && 293 | $childToken['key'] == "{$removedToken['key']}.{$childToken['array_key']}" 294 | ) { 295 | $childToken['indent'] -= 4; 296 | } 297 | } 298 | } else { 299 | break; 300 | } 301 | } 302 | } 303 | 304 | unset($this->items[$key]); 305 | } 306 | 307 | public function raw(): string 308 | { 309 | $code = ''; 310 | 311 | usort($this->tokens, function (array $tokenA, array $tokenB) { 312 | if ($tokenA['type'] != 'array_item' || $tokenB['type'] != 'array_item') { 313 | return 0; 314 | } 315 | 316 | if ($tokenA['group'] != $tokenB['group']) { 317 | return $tokenA['group'] <=> $tokenB['group']; 318 | } 319 | 320 | if ( 321 | $tokenA['key'] == "{$tokenB['key']}.{$tokenA['array_key']}" || 322 | $tokenB['key'] == "{$tokenA['key']}.{$tokenB['array_key']}" 323 | ) { 324 | if (@$tokenA['close_nested'] && @$tokenB['close_nested']) { 325 | return $tokenB['key'] <=> $tokenA['key']; 326 | } 327 | 328 | return @$tokenA['close_nested'] <=> @$tokenB['close_nested'] ?: $tokenA['key'] <=> $tokenB['key']; 329 | } 330 | 331 | return $tokenA['key'] <=> $tokenB['key']; 332 | }); 333 | 334 | foreach ($this->tokens as $key => $token) { 335 | if ($token['type'] == 'array_open') { 336 | $code .= "{$token['text']}\n\n"; 337 | } elseif ($token['type'] == 'comment') { 338 | $code .= " {$token['text']}\n\n"; 339 | } elseif ($token['type'] == 'array_item') { 340 | $next = @$this->tokens[$key + 1]; 341 | 342 | if ( 343 | ! @$token['close_nested'] || 344 | ! @$this->tokens[$key - 1]['open_nested'] || 345 | $token['array_key'] != @$this->tokens[$key - 1]['array_key'] 346 | ) { 347 | $code .= str_pad('', $token['indent']); 348 | } 349 | 350 | $code .= $token['text']; 351 | 352 | if (array_key_exists('comment', $token)) { 353 | $code .= " {$token['comment']}"; 354 | } 355 | 356 | if (@$next['type'] != 'array_item') { 357 | $code .= "\n\n"; 358 | } elseif (! @$next['close_nested'] || $token['array_key'] != @$next['array_key']) { 359 | $code .= "\n"; 360 | } 361 | } else { 362 | $code .= $token['text']; 363 | } 364 | } 365 | 366 | return $code; 367 | } 368 | } 369 | -------------------------------------------------------------------------------- /lang/ru/validation.php: -------------------------------------------------------------------------------- 1 | 'Вы должны принять :attribute.', 18 | 'accepted_if' => 'Вы должны принять :attribute, когда :other содержит :value.', 19 | 'active_url' => 'Значение поля :attribute должно быть действительным URL адресом.', 20 | 'after' => 'Значение поля :attribute должно быть датой после :date.', 21 | 'after_or_equal' => 'Значение поля :attribute должно быть датой после или равной :date.', 22 | 'alpha' => 'Значение поля :attribute может содержать только буквы.', 23 | 'alpha_dash' => 'Значение поля :attribute может содержать только буквы, цифры, дефис и нижнее подчеркивание.', 24 | 'alpha_num' => 'Значение поля :attribute может содержать только буквы и цифры.', 25 | 'any_of' => 'Значение поля :attribute не найдено в списке разрешённых.', 26 | 'array' => 'Значение поля :attribute должно быть массивом.', 27 | 'ascii' => 'Значение поля :attribute должно содержать только однобайтовые цифро-буквенные символы.', 28 | 'before' => 'Значение поля :attribute должно быть датой до :date.', 29 | 'before_or_equal' => 'Значение поля :attribute должно быть датой до или равной :date.', 30 | 'between' => [ 31 | 'array' => 'Количество элементов в поле :attribute должно быть от :min до :max.', 32 | 'file' => 'Размер файла в поле :attribute должен быть от :min до :max Кб.', 33 | 'numeric' => 'Значение поля :attribute должно быть от :min до :max.', 34 | 'string' => 'Количество символов в поле :attribute должно быть от :min до :max.', 35 | ], 36 | 'boolean' => 'Значение поля :attribute должно быть логического типа.', 37 | 'can' => 'Значение поля :attribute должно быть авторизованным.', 38 | 'confirmed' => 'Значение поля :attribute не совпадает с подтверждаемым.', 39 | 'contains' => 'В поле :attribute отсутствует необходимое значение.', 40 | 'current_password' => 'Неверный пароль.', 41 | 'date' => 'Значение поля :attribute должно быть корректной датой.', 42 | 'date_equals' => 'Значение поля :attribute должно быть датой равной :date.', 43 | 'date_format' => 'Значение поля :attribute должно соответствовать формату даты: :format.', 44 | 'decimal' => 'Значение поля :attribute должно содержать :decimal цифр десятичных разрядов.', 45 | 'declined' => 'Значение поля :attribute должно быть отклонено.', 46 | 'declined_if' => 'Значение поля :attribute должно быть отклонено, когда :other содержит :value.', 47 | 'different' => 'Значения полей :attribute и :other должны различаться.', 48 | 'digits' => 'Количество символов в поле :attribute должно быть равным :digits.', 49 | 'digits_between' => 'Количество символов в поле :attribute должно быть от :min до :max.', 50 | 'dimensions' => 'Изображение, указанное в поле :attribute, имеет недопустимые размеры.', 51 | 'distinct' => 'Элементы в значении поля :attribute не должны повторяться.', 52 | 'doesnt_contain' => 'Значение поля :attribute не должно содержать ни одного из следующих элементов: :values.', 53 | 'doesnt_end_with' => 'Значение поля :attribute не должно заканчиваться одним из следующих: :values.', 54 | 'doesnt_start_with' => 'Значение поля :attribute не должно начинаться с одного из следующих: :values.', 55 | 'email' => 'Значение поля :attribute должно быть действительным электронным адресом.', 56 | 'ends_with' => 'Значение поля :attribute должно заканчиваться одним из следующих: :values.', 57 | 'enum' => 'Значение поля :attribute отсутствует в списке разрешённых.', 58 | 'exists' => 'Значение поля :attribute не существует.', 59 | 'extensions' => 'Файл в поле :attribute должен иметь одно из следующих расширений: :values.', 60 | 'file' => 'В поле :attribute должен быть указан файл.', 61 | 'filled' => 'Значение поля :attribute обязательно для заполнения.', 62 | 'gt' => [ 63 | 'array' => 'Количество элементов в поле :attribute должно быть больше :value.', 64 | 'file' => 'Размер файла, указанный в поле :attribute, должен быть больше :value Кб.', 65 | 'numeric' => 'Значение поля :attribute должно быть больше :value.', 66 | 'string' => 'Количество символов в поле :attribute должно быть больше :value.', 67 | ], 68 | 'gte' => [ 69 | 'array' => 'Количество элементов в поле :attribute должно быть :value или больше.', 70 | 'file' => 'Размер файла, указанный в поле :attribute, должен быть :value Кб или больше.', 71 | 'numeric' => 'Значение поля :attribute должно быть :value или больше.', 72 | 'string' => 'Количество символов в поле :attribute должно быть :value или больше.', 73 | ], 74 | 'hex_color' => 'Значение поля :attribute должно быть корректным цветом в HEX формате.', 75 | 'image' => 'Файл, указанный в поле :attribute, должен быть изображением.', 76 | 'in' => 'Значение поля :attribute отсутствует в списке разрешённых.', 77 | 'in_array' => 'Значение поля :attribute должно быть указано в поле :other.', 78 | 'in_array_keys' => 'Массив в значении поля :attribute должен иметь как минимум один из следующих ключей: :values.', 79 | 'integer' => 'Значение поля :attribute должно быть целым числом.', 80 | 'ip' => 'Значение поля :attribute должно быть действительным IP-адресом.', 81 | 'ipv4' => 'Значение поля :attribute должно быть действительным IPv4-адресом.', 82 | 'ipv6' => 'Значение поля :attribute должно быть действительным IPv6-адресом.', 83 | 'json' => 'Значение поля :attribute должно быть JSON строкой.', 84 | 'list' => 'Значение поля :attribute должно быть списком.', 85 | 'lowercase' => 'Значение поля :attribute должно быть в нижнем регистре.', 86 | 'lt' => [ 87 | 'array' => 'Количество элементов в поле :attribute должно быть меньше :value.', 88 | 'file' => 'Размер файла, указанный в поле :attribute, должен быть меньше :value Кб.', 89 | 'numeric' => 'Значение поля :attribute должно быть меньше :value.', 90 | 'string' => 'Количество символов в поле :attribute должно быть меньше :value.', 91 | ], 92 | 'lte' => [ 93 | 'array' => 'Количество элементов в поле :attribute должно быть :value или меньше.', 94 | 'file' => 'Размер файла, указанный в поле :attribute, должен быть :value Кб или меньше.', 95 | 'numeric' => 'Значение поля :attribute должно быть равным или меньше :value.', 96 | 'string' => 'Количество символов в поле :attribute должно быть :value или меньше.', 97 | ], 98 | 'mac_address' => 'Значение поля :attribute должно быть корректным MAC-адресом.', 99 | 'max' => [ 100 | 'array' => 'Количество элементов в поле :attribute не может превышать :max.', 101 | 'file' => 'Размер файла в поле :attribute не может быть больше :max Кб.', 102 | 'numeric' => 'Значение поля :attribute не может быть больше :max.', 103 | 'string' => 'Количество символов в значении поля :attribute не может превышать :max.', 104 | ], 105 | 'max_digits' => 'Значение поля :attribute не должно содержать больше :max цифр.', 106 | 'mimes' => 'Файл, указанный в поле :attribute, должен быть одного из следующих типов: :values.', 107 | 'mimetypes' => 'Файл, указанный в поле :attribute, должен быть одного из следующих типов: :values.', 108 | 'min' => [ 109 | 'array' => 'Количество элементов в поле :attribute должно быть не меньше :min.', 110 | 'file' => 'Размер файла, указанный в поле :attribute, должен быть не меньше :min Кб.', 111 | 'numeric' => 'Значение поля :attribute должно быть не меньше :min.', 112 | 'string' => 'Количество символов в поле :attribute должно быть не меньше :min.', 113 | ], 114 | 'min_digits' => 'Значение поля :attribute должно содержать не меньше :min цифр.', 115 | 'missing' => 'Значение поля :attribute должно отсутствовать.', 116 | 'missing_if' => 'Значение поля :attribute должно отсутствовать, когда :other содержит :value.', 117 | 'missing_unless' => 'Значение поля :attribute должно отсутствовать, когда :other не содержит :value.', 118 | 'missing_with' => 'Значение поля :attribute должно отсутствовать, если :values указано.', 119 | 'missing_with_all' => 'Значение поля :attribute должно отсутствовать, когда указаны все :values.', 120 | 'multiple_of' => 'Значение поля :attribute должно быть кратным :value.', 121 | 'not_in' => 'Значение поля :attribute находится в списке запрета.', 122 | 'not_regex' => 'Значение поля :attribute имеет некорректный формат.', 123 | 'numeric' => 'Значение поля :attribute должно быть числом.', 124 | 'password' => [ 125 | 'letters' => 'Значение поля :attribute должно содержать хотя бы одну букву.', 126 | 'mixed' => 'Значение поля :attribute должно содержать хотя бы одну прописную и одну строчную буквы.', 127 | 'numbers' => 'Значение поля :attribute должно содержать хотя бы одну цифру.', 128 | 'symbols' => 'Значение поля :attribute должно содержать хотя бы один символ.', 129 | 'uncompromised' => 'Значение поля :attribute обнаружено в утёкших данных. Пожалуйста, выберите другое значение для :attribute.', 130 | ], 131 | 'present' => 'Значение поля :attribute должно быть.', 132 | 'present_if' => 'Значение поля :attribute должно быть когда :other содержит :value.', 133 | 'present_unless' => 'Значение поля :attribute должно быть, если только :other не содержит :value.', 134 | 'present_with' => 'Значение поля :attribute должно быть когда одно из :values присутствуют.', 135 | 'present_with_all' => 'Значение поля :attribute должно быть когда все из значений присутствуют: :values.', 136 | 'prohibited' => 'Значение поля :attribute запрещено.', 137 | 'prohibited_if' => 'Значение поля :attribute запрещено, когда :other содержит :value.', 138 | 'prohibited_if_accepted' => 'Значение поля :attribute запрещено, если принято :other.', 139 | 'prohibited_if_declined' => 'Значение поля :attribute запрещено при отказе от :other.', 140 | 'prohibited_unless' => 'Значение поля :attribute запрещено, если :other не состоит в :values.', 141 | 'prohibits' => 'Значение поля :attribute запрещает присутствие :other.', 142 | 'regex' => 'Значение поля :attribute имеет некорректный формат.', 143 | 'required' => 'Поле :attribute обязательно.', 144 | 'required_array_keys' => 'Массив, указанный в поле :attribute, обязательно должен иметь ключи: :values.', 145 | 'required_if' => 'Поле :attribute обязательно для заполнения, когда :other содержит :value.', 146 | 'required_if_accepted' => 'Поле :attribute обязательно, когда :other принято.', 147 | 'required_if_declined' => 'Поле :attribute обязательно, когда :other отклонено.', 148 | 'required_unless' => 'Поле :attribute обязательно для заполнения, когда :other не содержит :values.', 149 | 'required_with' => 'Поле :attribute обязательно для заполнения, когда :values указано.', 150 | 'required_with_all' => 'Поле :attribute обязательно для заполнения, когда :values указано.', 151 | 'required_without' => 'Поле :attribute обязательно для заполнения, когда :values не указано.', 152 | 'required_without_all' => 'Поле :attribute обязательно для заполнения, когда ни одно из :values не указано.', 153 | 'same' => 'Значения полей :attribute и :other должны совпадать.', 154 | 'size' => [ 155 | 'array' => 'Количество элементов в поле :attribute должно быть равным :size.', 156 | 'file' => 'Размер файла, указанный в поле :attribute, должен быть равен :size Кб.', 157 | 'numeric' => 'Значение поля :attribute должно быть равным :size.', 158 | 'string' => 'Количество символов в поле :attribute должно быть равным :size.', 159 | ], 160 | 'starts_with' => 'Поле :attribute должно начинаться с одного из следующих значений: :values.', 161 | 'string' => 'Значение поля :attribute должно быть строкой.', 162 | 'timezone' => 'Значение поля :attribute должно быть действительным часовым поясом.', 163 | 'ulid' => 'Значение поля :attribute должно быть корректным ULID.', 164 | 'unique' => 'Такое значение поля :attribute уже существует.', 165 | 'uploaded' => 'Загрузка файла из поля :attribute не удалась.', 166 | 'uppercase' => 'Значение поля :attribute должно быть в верхнем регистре.', 167 | 'url' => 'Значение поля :attribute не является ссылкой или имеет некорректный формат.', 168 | 'uuid' => 'Значение поля :attribute должно быть корректным UUID.', 169 | 170 | /* 171 | |-------------------------------------------------------------------------- 172 | | Пользовательские Языковые Ресурсы Проверки Значений 173 | |-------------------------------------------------------------------------- 174 | | 175 | | Здесь вы можете указать пользовательские сообщения проверки для 176 | | атрибутов, используя соглашение "attribute.rule" для именования строк. 177 | | Это позволяет быстро указать конкретное сообщение для заданного 178 | | правила атрибута. 179 | | 180 | */ 181 | 182 | 'custom' => [ 183 | 'attribute-name' => [ 184 | 'rule-name' => 'custom-message', 185 | ], 186 | ], 187 | 188 | /* 189 | |-------------------------------------------------------------------------- 190 | | Пользовательские Атрибуты Проверки Значений 191 | |-------------------------------------------------------------------------- 192 | | 193 | | Следующие языковые ресурсы используются для замены нашего заполнителя 194 | | атрибута чем-то более удобным для чтения, например, "E-Mail адрес" 195 | | вместо "email". Это просто помогает нам сделать наше сообщение 196 | | более выразительным. 197 | | 198 | */ 199 | 200 | 'attributes' => [ 201 | 'active' => 'Активно', 202 | 'address' => 'Адрес', 203 | 'age' => 'Возраст', 204 | 'city' => 'Город', 205 | 'code' => 'Код', 206 | 'content' => 'Контент', 207 | 'country' => 'Страна', 208 | 'current_password' => 'Текущий пароль', 209 | 'date' => 'Дата', 210 | 'day' => 'День', 211 | 'default' => 'По умолчанию', 212 | 'description' => 'Описание', 213 | 'email' => 'E-Mail адрес', 214 | 'enabled' => 'Включено', 215 | 'first_name' => 'Имя', 216 | 'gender' => 'Пол', 217 | 'hour' => 'Час', 218 | 'last_name' => 'Фамилия', 219 | 'middle_name' => 'Отчество', 220 | 'minute' => 'Минута', 221 | 'mobile' => 'Моб. номер', 222 | 'month' => 'Месяц', 223 | 'name' => 'Название', 224 | 'password' => 'Пароль', 225 | 'password_confirmation' => 'Подтверждение пароля', 226 | 'phone' => 'Телефон', 227 | 'remember_me' => 'Запомнить меня', 228 | 'second' => 'Секунда', 229 | 'sex' => 'Пол', 230 | 'size' => 'Размер', 231 | 'status' => 'Статус', 232 | 'time' => 'Время', 233 | 'title' => 'Заголовок', 234 | 'username' => 'Никнейм', 235 | 'year' => 'Год', 236 | ], 237 | 238 | ]; 239 | --------------------------------------------------------------------------------