├── .gitignore ├── LICENSE ├── README.md ├── composer.json ├── composer.lock ├── phpunit.xml.dist ├── src ├── Bootstrap.php ├── Module.php ├── Request.php ├── UrlManager.php ├── controllers │ └── DefaultController.php ├── models │ └── LanguageKsl.php └── widgets │ ├── ListWidget.php │ └── views │ └── list.php └── tests ├── RequestTest.php ├── TestCase.php ├── UrlManagerTest.php ├── bootstrap.php ├── models └── LanguageKslTest.php └── widgets └── ListWidgetTest.php /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | /vendor -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 klisl 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 | yii2-languages 2 | ================= 3 | 4 | Пакет для создания мультиязычного сайта или WEB-приложения на php-фреймворке Yii-2. Текущий язык отображается в URL. Есть возможность 5 | убрать основной язык из отображаемых. 6 | Пример (русский использован в качестве основного языка и выбрана опция не выводить основной язык): 7 | 8 | * http://site.com 9 | * http://site.com/en 10 | * http://site.com/uk 11 | 12 | * http://site.com/contact 13 | * http://site.com/en/contact 14 | * http://site.com/uk/contact 15 | 16 | 17 | Смена языка осуществляется при нажатии на соответствующие ссылки которые выводятся виджетом. Так же, язык можно менять прямо в адресной строке. 18 | Не используются сессии, куки и база данных для работы расширения. Код рассчитан на максимальное быстродействие. 19 | Использование данного модуля мультиязычности не требует внесения изменений в правила маршрутизации компонента urlManager. 20 | 21 | Расширение устанавливает текущую локализацию приложения в зависимости от выбранного языка. 22 | 23 | 24 | 25 | Установка 26 | ------------------ 27 | * Установка расширения с помощью Composer. 28 | 29 | ``` 30 | composer require klisl/yii2-languages 31 | ``` 32 | 33 | 34 | * Внести изменения в файл **frontend\config\main.php** (для версии advanced) или в 35 | файл **config/web.php** (для версии basic): 36 | 37 | 38 | (1) в массив "return" вставить: 39 | ```php 40 | 'sourceLanguage' => 'ru', // использовать в качестве ключей переводов 41 | ``` 42 | 43 | (2) ниже, так же в массив "return" вставить регистрацию и параметры модуля: 44 | ```php 45 | 'modules' => [ 46 | 'languages' => [ 47 | 'class' => 'klisl\languages\Module', 48 | //Языки используемые в приложении 49 | 'languages' => [ 50 | 'English' => 'en', 51 | 'Русский' => 'ru', 52 | 'Українська' => 'uk', 53 | ], 54 | 'default_language' => 'ru', //основной язык (по-умолчанию) 55 | 'show_default' => false, //true - показывать в URL основной язык, false - нет 56 | ], 57 | ], 58 | ``` 59 | По-умолчанию модуль использует английский, русский и украинский языки. Удалить или добавить нужные в параметрах модуля. 60 | 61 | 62 | (3) в массиве "components" есть вложенный массив "request", вставить в него: 63 | ```php 64 | 'baseUrl' => '', //убрать frontend/web 65 | 'class' => 'klisl\languages\Request' 66 | ``` 67 | 68 | (4) в компоненте приложения "urlManager" включаем ЧПУ для ссылок, подключаем класс UrlManager переопределенный данным расширением: 69 | ```php 70 | 'urlManager' => [ 71 | 'enablePrettyUrl' => true, 72 | 'showScriptName' => false, 73 | 'enableStrictParsing' => true, 74 | 'class' => 'klisl\languages\UrlManager', 75 | 'rules' => [ 76 | 'languages' => 'languages/default/index', //для модуля мультиязычности 77 | //далее создаем обычные правила 78 | '/' => 'site/index', 79 | '' => 'site/', 80 | ], 81 | ], 82 | ``` 83 | В начале списка правил указываем правило для работы модуля мультиязычности. Остальные правила формируются обычным образом. 84 | 85 | (5) в шаблон **frontend\views\layouts\main.php** или нужный вид вставить вывод виджета отображающего ссылки для переключения языков: 86 | ```php 87 | 88 | 89 | 90 | ``` 91 | 92 | 93 | 94 | Использование 95 | ------------- 96 | 97 | #### Перевод фраз. 98 | Для перевода отдельных слов и фраз (пунктов меню например), нужно создать языковые файлы в папке common/messages. 99 | Если используется версия Yii2 Basic, то папка common будет отсутствовать в корне проекта, в таком случае ее нужно создать. 100 | Количество языковых файлов будет столько, сколько у вас дополнительных языков для перевода, не считая основного. 101 | Например, если используется русский, украинский и английский, то создаем папки “en” и “uk” при условии, что русский является основным языком. Метка основного языка не отображается в URL. 102 | 103 | Напоминаю, что основной язык задается в файле frontend\config\main.php, в массиве «return» строкой 104 | **'sourceLanguage' => 'ru',** 105 | 106 | Пример языкового файла common\messages\en\app.php: 107 | ```php 108 | 'Blog', 111 | 'О нас' => 'About me', 112 | 'Контакты' => 'Contact', 113 | ]; 114 | ``` 115 | то есть в массив "return" нужно вписать все слова и фразы которые нужно переводить. 116 | Аналогично нужно создать файл common\messages\uk\app.php для украинского языка. 117 | 118 | В коде (обычно в шаблонах и файлах представлений), фразы которые требуют перевода заключать в вызов метода **Yii::t()**. 119 | Согласно нашей конфигурации так: 120 | ```php 121 | Yii::t('app', 'Блог') 122 | ``` 123 | Русский у нас указан в качестве языка по-умолчанию, поэтому если текущий язык – русский, выведется слово «Блог», а если английский - 'Blog'. 124 | 125 | 126 | #### Перевод статичных страниц. 127 | Статичные страницы - это страницы, которые хранят текст в самом файле (в коде), а не берут контент из базы данных. Целые страницы содержат слишком много текста, в связи с чем нецелесообразно использовать метод Yii::t(). 128 | 129 | В нужном контроллере создаем действие для каждой такой страницы: 130 | ```php 131 | public function actionStat() 132 | { 133 | $language = Yii::$app->language; //текущий язык 134 | //выводим вид соответствующий текущему языку 135 | return $this->render('statPages/stat-'.$language); 136 | } 137 | ``` 138 | то есть вторая часть название файла вида берется из названия языка. 139 | В данном случае в папке с видами создаем отдельную папку для статичных файлов statPages (это не обязательно), а в ней файлы с контентом соответствующего языка: 140 | - stat-ru.php 141 | - stat-uk.php 142 | - stat-en.php 143 | 144 | 145 | #### Перевод статей хранящих контент в базе данных. 146 | 147 | Для настройки базы данных и моделей выполнить действия указанные в данной статье статье: . 148 | 149 | 150 | Подробное описание расширения (с небольшими отличиями т.к. рассмотрен ручной вариант создания модуля): . 151 | 152 | Мой блог: [klisl.com](https://klisl.com) -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "klisl/yii2-languages", 3 | "description": "Package", 4 | "type": "yii2-extension", 5 | "keywords": [ 6 | "yii2", 7 | "languages", 8 | "multilanguage" 9 | ], 10 | "license": "MIT", 11 | "authors": [ 12 | { 13 | "name": "Sergey Klimenchuk", 14 | "email": "ksl80@ukr.net", 15 | "homepage": "https://github.com/klisl/yii2-languages", 16 | "role": "Developer" 17 | } 18 | ], 19 | "minimum-stability": "stable", 20 | "require": { 21 | "yiisoft/yii2": "~2.0" 22 | }, 23 | "extra": { 24 | "bootstrap": "klisl\\languages\\Bootstrap" 25 | }, 26 | "autoload": { 27 | "psr-4": { 28 | "klisl\\languages\\": "src" 29 | } 30 | }, 31 | "autoload-dev": { 32 | "psr-4": { 33 | "klisl\\languages\\tests\\": "tests/" 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", 5 | "This file is @generated automatically" 6 | ], 7 | "content-hash": "8e978864c9cca07fa3ff294b962790e6", 8 | "packages": [ 9 | { 10 | "name": "bower-asset/inputmask", 11 | "version": "3.3.10", 12 | "source": { 13 | "type": "git", 14 | "url": "https://github.com/RobinHerbots/Inputmask.git", 15 | "reference": "14873e5775964275d13621cbe2b52e4448af3707" 16 | }, 17 | "dist": { 18 | "type": "zip", 19 | "url": "https://api.github.com/repos/RobinHerbots/Inputmask/zipball/14873e5775964275d13621cbe2b52e4448af3707", 20 | "reference": "14873e5775964275d13621cbe2b52e4448af3707", 21 | "shasum": "" 22 | }, 23 | "require": { 24 | "bower-asset/jquery": ">=1.7" 25 | }, 26 | "type": "bower-asset-library", 27 | "extra": { 28 | "bower-asset-main": [ 29 | "./dist/inputmask/inputmask.js", 30 | "./dist/inputmask/inputmask.extensions.js", 31 | "./dist/inputmask/inputmask.date.extensions.js", 32 | "./dist/inputmask/inputmask.numeric.extensions.js", 33 | "./dist/inputmask/inputmask.phone.extensions.js", 34 | "./dist/inputmask/jquery.inputmask.js", 35 | "./dist/inputmask/global/document.js", 36 | "./dist/inputmask/global/window.js", 37 | "./dist/inputmask/phone-codes/phone.js", 38 | "./dist/inputmask/phone-codes/phone-be.js", 39 | "./dist/inputmask/phone-codes/phone-nl.js", 40 | "./dist/inputmask/phone-codes/phone-ru.js", 41 | "./dist/inputmask/phone-codes/phone-uk.js", 42 | "./dist/inputmask/dependencyLibs/inputmask.dependencyLib.jqlite.js", 43 | "./dist/inputmask/dependencyLibs/inputmask.dependencyLib.jquery.js", 44 | "./dist/inputmask/dependencyLibs/inputmask.dependencyLib.js", 45 | "./dist/inputmask/bindings/inputmask.binding.js" 46 | ], 47 | "bower-asset-ignore": [ 48 | "**/*", 49 | "!dist/*", 50 | "!dist/inputmask/*", 51 | "!dist/min/*", 52 | "!dist/min/inputmask/*" 53 | ] 54 | }, 55 | "license": [ 56 | "http://opensource.org/licenses/mit-license.php" 57 | ], 58 | "description": "Inputmask is a javascript library which creates an input mask. Inputmask can run against vanilla javascript, jQuery and jqlite.", 59 | "keywords": [ 60 | "form", 61 | "input", 62 | "inputmask", 63 | "jquery", 64 | "mask", 65 | "plugins" 66 | ], 67 | "time": "2017-10-16T09:33:03+00:00" 68 | }, 69 | { 70 | "name": "bower-asset/jquery", 71 | "version": "3.2.1", 72 | "source": { 73 | "type": "git", 74 | "url": "https://github.com/jquery/jquery-dist.git", 75 | "reference": "77d2a51d0520d2ee44173afdf4e40a9201f5964e" 76 | }, 77 | "dist": { 78 | "type": "zip", 79 | "url": "https://api.github.com/repos/jquery/jquery-dist/zipball/77d2a51d0520d2ee44173afdf4e40a9201f5964e", 80 | "reference": "77d2a51d0520d2ee44173afdf4e40a9201f5964e", 81 | "shasum": "" 82 | }, 83 | "type": "bower-asset-library", 84 | "extra": { 85 | "bower-asset-main": "dist/jquery.js", 86 | "bower-asset-ignore": [ 87 | "package.json" 88 | ] 89 | }, 90 | "license": [ 91 | "MIT" 92 | ], 93 | "keywords": [ 94 | "browser", 95 | "javascript", 96 | "jquery", 97 | "library" 98 | ], 99 | "time": "2017-03-20T19:02:00+00:00" 100 | }, 101 | { 102 | "name": "bower-asset/punycode", 103 | "version": "v1.3.2", 104 | "source": { 105 | "type": "git", 106 | "url": "https://github.com/bestiejs/punycode.js.git", 107 | "reference": "38c8d3131a82567bfef18da09f7f4db68c84f8a3" 108 | }, 109 | "dist": { 110 | "type": "zip", 111 | "url": "https://api.github.com/repos/bestiejs/punycode.js/zipball/38c8d3131a82567bfef18da09f7f4db68c84f8a3", 112 | "reference": "38c8d3131a82567bfef18da09f7f4db68c84f8a3", 113 | "shasum": "" 114 | }, 115 | "type": "bower-asset-library", 116 | "extra": { 117 | "bower-asset-main": "punycode.js", 118 | "bower-asset-ignore": [ 119 | "coverage", 120 | "tests", 121 | ".*", 122 | "component.json", 123 | "Gruntfile.js", 124 | "node_modules", 125 | "package.json" 126 | ] 127 | }, 128 | "time": "2014-10-22T12:02:42+00:00" 129 | }, 130 | { 131 | "name": "bower-asset/yii2-pjax", 132 | "version": "2.0.7.1", 133 | "source": { 134 | "type": "git", 135 | "url": "https://github.com/yiisoft/jquery-pjax.git", 136 | "reference": "aef7b953107264f00234902a3880eb50dafc48be" 137 | }, 138 | "dist": { 139 | "type": "zip", 140 | "url": "https://api.github.com/repos/yiisoft/jquery-pjax/zipball/aef7b953107264f00234902a3880eb50dafc48be", 141 | "reference": "aef7b953107264f00234902a3880eb50dafc48be", 142 | "shasum": "" 143 | }, 144 | "require": { 145 | "bower-asset/jquery": ">=1.8" 146 | }, 147 | "type": "bower-asset-library", 148 | "extra": { 149 | "bower-asset-main": "./jquery.pjax.js", 150 | "bower-asset-ignore": [ 151 | ".travis.yml", 152 | "Gemfile", 153 | "Gemfile.lock", 154 | "CONTRIBUTING.md", 155 | "vendor/", 156 | "script/", 157 | "test/" 158 | ] 159 | }, 160 | "license": [ 161 | "MIT" 162 | ], 163 | "time": "2017-10-12T10:11:14+00:00" 164 | }, 165 | { 166 | "name": "cebe/markdown", 167 | "version": "1.1.2", 168 | "source": { 169 | "type": "git", 170 | "url": "https://github.com/cebe/markdown.git", 171 | "reference": "25b28bae8a6f185b5030673af77b32e1163d5c6e" 172 | }, 173 | "dist": { 174 | "type": "zip", 175 | "url": "https://api.github.com/repos/cebe/markdown/zipball/25b28bae8a6f185b5030673af77b32e1163d5c6e", 176 | "reference": "25b28bae8a6f185b5030673af77b32e1163d5c6e", 177 | "shasum": "" 178 | }, 179 | "require": { 180 | "lib-pcre": "*", 181 | "php": ">=5.4.0" 182 | }, 183 | "require-dev": { 184 | "cebe/indent": "*", 185 | "facebook/xhprof": "*@dev", 186 | "phpunit/phpunit": "4.1.*" 187 | }, 188 | "bin": [ 189 | "bin/markdown" 190 | ], 191 | "type": "library", 192 | "extra": { 193 | "branch-alias": { 194 | "dev-master": "1.1.x-dev" 195 | } 196 | }, 197 | "autoload": { 198 | "psr-4": { 199 | "cebe\\markdown\\": "" 200 | } 201 | }, 202 | "notification-url": "https://packagist.org/downloads/", 203 | "license": [ 204 | "MIT" 205 | ], 206 | "authors": [ 207 | { 208 | "name": "Carsten Brandt", 209 | "email": "mail@cebe.cc", 210 | "homepage": "http://cebe.cc/", 211 | "role": "Creator" 212 | } 213 | ], 214 | "description": "A super fast, highly extensible markdown parser for PHP", 215 | "homepage": "https://github.com/cebe/markdown#readme", 216 | "keywords": [ 217 | "extensible", 218 | "fast", 219 | "gfm", 220 | "markdown", 221 | "markdown-extra" 222 | ], 223 | "time": "2017-07-16T21:13:23+00:00" 224 | }, 225 | { 226 | "name": "ezyang/htmlpurifier", 227 | "version": "v4.9.3", 228 | "source": { 229 | "type": "git", 230 | "url": "https://github.com/ezyang/htmlpurifier.git", 231 | "reference": "95e1bae3182efc0f3422896a3236e991049dac69" 232 | }, 233 | "dist": { 234 | "type": "zip", 235 | "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/95e1bae3182efc0f3422896a3236e991049dac69", 236 | "reference": "95e1bae3182efc0f3422896a3236e991049dac69", 237 | "shasum": "" 238 | }, 239 | "require": { 240 | "php": ">=5.2" 241 | }, 242 | "require-dev": { 243 | "simpletest/simpletest": "^1.1" 244 | }, 245 | "type": "library", 246 | "autoload": { 247 | "psr-0": { 248 | "HTMLPurifier": "library/" 249 | }, 250 | "files": [ 251 | "library/HTMLPurifier.composer.php" 252 | ] 253 | }, 254 | "notification-url": "https://packagist.org/downloads/", 255 | "license": [ 256 | "LGPL" 257 | ], 258 | "authors": [ 259 | { 260 | "name": "Edward Z. Yang", 261 | "email": "admin@htmlpurifier.org", 262 | "homepage": "http://ezyang.com" 263 | } 264 | ], 265 | "description": "Standards compliant HTML filter written in PHP", 266 | "homepage": "http://htmlpurifier.org/", 267 | "keywords": [ 268 | "html" 269 | ], 270 | "time": "2017-06-03T02:28:16+00:00" 271 | }, 272 | { 273 | "name": "yiisoft/yii2", 274 | "version": "2.0.13.1", 275 | "source": { 276 | "type": "git", 277 | "url": "https://github.com/yiisoft/yii2-framework.git", 278 | "reference": "7af96d8da5ea3e9a5dd05d0e734b21c5726a6ddf" 279 | }, 280 | "dist": { 281 | "type": "zip", 282 | "url": "https://api.github.com/repos/yiisoft/yii2-framework/zipball/7af96d8da5ea3e9a5dd05d0e734b21c5726a6ddf", 283 | "reference": "7af96d8da5ea3e9a5dd05d0e734b21c5726a6ddf", 284 | "shasum": "" 285 | }, 286 | "require": { 287 | "bower-asset/inputmask": "~3.2.2 | ~3.3.5", 288 | "bower-asset/jquery": "3.2.*@stable | 3.1.*@stable | 2.2.*@stable | 2.1.*@stable | 1.11.*@stable | 1.12.*@stable", 289 | "bower-asset/punycode": "1.3.*", 290 | "bower-asset/yii2-pjax": "~2.0.1", 291 | "cebe/markdown": "~1.0.0 | ~1.1.0", 292 | "ext-ctype": "*", 293 | "ext-mbstring": "*", 294 | "ezyang/htmlpurifier": "~4.6", 295 | "lib-pcre": "*", 296 | "php": ">=5.4.0", 297 | "yiisoft/yii2-composer": "~2.0.4" 298 | }, 299 | "bin": [ 300 | "yii" 301 | ], 302 | "type": "library", 303 | "extra": { 304 | "branch-alias": { 305 | "dev-master": "2.0.x-dev" 306 | } 307 | }, 308 | "autoload": { 309 | "psr-4": { 310 | "yii\\": "" 311 | } 312 | }, 313 | "notification-url": "https://packagist.org/downloads/", 314 | "license": [ 315 | "BSD-3-Clause" 316 | ], 317 | "authors": [ 318 | { 319 | "name": "Qiang Xue", 320 | "email": "qiang.xue@gmail.com", 321 | "homepage": "http://www.yiiframework.com/", 322 | "role": "Founder and project lead" 323 | }, 324 | { 325 | "name": "Alexander Makarov", 326 | "email": "sam@rmcreative.ru", 327 | "homepage": "http://rmcreative.ru/", 328 | "role": "Core framework development" 329 | }, 330 | { 331 | "name": "Maurizio Domba", 332 | "homepage": "http://mdomba.info/", 333 | "role": "Core framework development" 334 | }, 335 | { 336 | "name": "Carsten Brandt", 337 | "email": "mail@cebe.cc", 338 | "homepage": "http://cebe.cc/", 339 | "role": "Core framework development" 340 | }, 341 | { 342 | "name": "Timur Ruziev", 343 | "email": "resurtm@gmail.com", 344 | "homepage": "http://resurtm.com/", 345 | "role": "Core framework development" 346 | }, 347 | { 348 | "name": "Paul Klimov", 349 | "email": "klimov.paul@gmail.com", 350 | "role": "Core framework development" 351 | }, 352 | { 353 | "name": "Dmitry Naumenko", 354 | "email": "d.naumenko.a@gmail.com", 355 | "role": "Core framework development" 356 | }, 357 | { 358 | "name": "Boudewijn Vahrmeijer", 359 | "email": "info@dynasource.eu", 360 | "homepage": "http://dynasource.eu", 361 | "role": "Core framework development" 362 | } 363 | ], 364 | "description": "Yii PHP Framework Version 2", 365 | "homepage": "http://www.yiiframework.com/", 366 | "keywords": [ 367 | "framework", 368 | "yii2" 369 | ], 370 | "time": "2017-11-14T11:08:21+00:00" 371 | }, 372 | { 373 | "name": "yiisoft/yii2-composer", 374 | "version": "2.0.5", 375 | "source": { 376 | "type": "git", 377 | "url": "https://github.com/yiisoft/yii2-composer.git", 378 | "reference": "3f4923c2bde6caf3f5b88cc22fdd5770f52f8df2" 379 | }, 380 | "dist": { 381 | "type": "zip", 382 | "url": "https://api.github.com/repos/yiisoft/yii2-composer/zipball/3f4923c2bde6caf3f5b88cc22fdd5770f52f8df2", 383 | "reference": "3f4923c2bde6caf3f5b88cc22fdd5770f52f8df2", 384 | "shasum": "" 385 | }, 386 | "require": { 387 | "composer-plugin-api": "^1.0" 388 | }, 389 | "require-dev": { 390 | "composer/composer": "^1.0" 391 | }, 392 | "type": "composer-plugin", 393 | "extra": { 394 | "class": "yii\\composer\\Plugin", 395 | "branch-alias": { 396 | "dev-master": "2.0.x-dev" 397 | } 398 | }, 399 | "autoload": { 400 | "psr-4": { 401 | "yii\\composer\\": "" 402 | } 403 | }, 404 | "notification-url": "https://packagist.org/downloads/", 405 | "license": [ 406 | "BSD-3-Clause" 407 | ], 408 | "authors": [ 409 | { 410 | "name": "Qiang Xue", 411 | "email": "qiang.xue@gmail.com" 412 | } 413 | ], 414 | "description": "The composer plugin for Yii extension installer", 415 | "keywords": [ 416 | "composer", 417 | "extension installer", 418 | "yii2" 419 | ], 420 | "time": "2016-12-20T13:26:02+00:00" 421 | } 422 | ], 423 | "packages-dev": [], 424 | "aliases": [], 425 | "minimum-stability": "stable", 426 | "stability-flags": [], 427 | "prefer-stable": false, 428 | "prefer-lowest": false, 429 | "platform": [], 430 | "platform-dev": [] 431 | } 432 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | ./tests 10 | 11 | 12 | 13 | 14 | ./src 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/Bootstrap.php: -------------------------------------------------------------------------------- 1 | getModule('languages') || 25 | YII_ENV == 'test' || 26 | Yii::$app->controllerNamespace == 'console\controllers' || 27 | Yii::$app->controllerNamespace == 'app\commands' 28 | ) return; 29 | 30 | //Включаем перевод сообщений 31 | $app->i18n->translations['app'] = [ 32 | 'class' => 'yii\i18n\PhpMessageSource', 33 | 'basePath' => '@common/messages', 34 | ]; 35 | 36 | $this->run($app); 37 | 38 | } 39 | 40 | /** 41 | * @param \yii\base\Application $app 42 | * 43 | * @return void 44 | */ 45 | public function run($app){ 46 | 47 | $module = Yii::$app->getModule('languages'); 48 | 49 | $url = $app->request->url; 50 | 51 | //Получаем список языков в виде строки 52 | $list_languages = LanguageKsl::list_languages(); 53 | 54 | preg_match("#^/($list_languages)(.*)#", $url, $match_arr); 55 | 56 | //Если URL содержит указатель языка - сохраняем его в параметрах приложения и используем 57 | if (isset($match_arr[1]) && $match_arr[1] != '/' && $match_arr[1] != ''){ 58 | 59 | /* 60 | * Если в настройках выбрано не показывать язык используемый по-умолчанию 61 | * убираем метку текущего языка из URL и перенаправляем на ту же страницу 62 | */ 63 | if( !$module->show_default && $match_arr[1] == $module->default_language) { 64 | $url = $app->request->absoluteUrl; //Возвращает абсолютную ссылку 65 | $lang = $module->default_language; //язык используемый по-умолчанию 66 | $app->response->redirect(['languages/default/index', 'lang' => $lang, 'url' => $url]); 67 | } 68 | 69 | $app->language = $match_arr[1]; 70 | $app->formatter->locale = $match_arr[1]; 71 | $app->homeUrl = '/'.$match_arr[1]; 72 | 73 | 74 | } elseif(!$module->show_default){ 75 | 76 | $lang = $module->default_language; //язык используемый по-умолчанию 77 | 78 | $app->language = $lang; 79 | $app->formatter->locale = $lang; 80 | 81 | /* 82 | * Если URL не содержит указатель языка, а в настройках включен показ основного языка 83 | */ 84 | } else { 85 | $url = $app->request->absoluteUrl; 86 | 87 | $lang = $module->default_language; 88 | 89 | $app->response->redirect(['languages/default/index', 'lang' => $lang, 'url' => $url], 301); 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Module.php: -------------------------------------------------------------------------------- 1 | _lang_url = $this->getUrl(); //полный URL 23 | 24 | $url_list = explode('/', $this->getUrl()); 25 | 26 | $lang_url = isset($url_list[1]) ? $url_list[1] : null; 27 | 28 | //Удалить метку языка из URL 29 | if( $lang_url !== null && $lang_url === Yii::$app->language ) 30 | { 31 | $url = preg_replace("/^\/$lang_url/", '', $this->_lang_url); 32 | return $url; 33 | } 34 | 35 | return $this->_lang_url; 36 | } 37 | 38 | /* 39 | * Переопределяем метод для того, чтобы он использовал URL без метки языка. 40 | * Это позволит использовать обычные правила в UrlManager. 41 | * 42 | * @return string part of the request URL that is after the entry script and before the question mark. 43 | * Note, the returned path info is decoded. 44 | * @throws InvalidConfigException if the path info cannot be determined due to unexpected server configuration 45 | */ 46 | protected function resolvePathInfo() 47 | { 48 | $pathInfo = $this->getLangUrl(); 49 | 50 | if (($pos = strpos($pathInfo, '?')) !== false) { 51 | $pathInfo = substr($pathInfo, 0, $pos); 52 | } 53 | 54 | $pathInfo = urldecode($pathInfo); 55 | 56 | // try to encode in UTF8 if not so 57 | // http://w3.org/International/questions/qa-forms-utf-8.html 58 | if (!preg_match('%^(?: 59 | [\x09\x0A\x0D\x20-\x7E] # ASCII 60 | | [\xC2-\xDF][\x80-\xBF] # non-overlong 2-byte 61 | | \xE0[\xA0-\xBF][\x80-\xBF] # excluding overlongs 62 | | [\xE1-\xEC\xEE\xEF][\x80-\xBF]{2} # straight 3-byte 63 | | \xED[\x80-\x9F][\x80-\xBF] # excluding surrogates 64 | | \xF0[\x90-\xBF][\x80-\xBF]{2} # planes 1-3 65 | | [\xF1-\xF3][\x80-\xBF]{3} # planes 4-15 66 | | \xF4[\x80-\x8F][\x80-\xBF]{2} # plane 16 67 | )*$%xs', $pathInfo) 68 | ) { 69 | $pathInfo = utf8_encode($pathInfo); 70 | } 71 | 72 | $scriptUrl = $this->getScriptUrl(); 73 | $baseUrl = $this->getBaseUrl(); 74 | if (strpos($pathInfo, $scriptUrl) === 0) { 75 | $pathInfo = substr($pathInfo, strlen($scriptUrl)); 76 | } elseif ($baseUrl === '' || strpos($pathInfo, $baseUrl) === 0) { 77 | $pathInfo = substr($pathInfo, strlen($baseUrl)); 78 | } elseif (isset($_SERVER['PHP_SELF']) && strpos($_SERVER['PHP_SELF'], $scriptUrl) === 0) { 79 | $pathInfo = substr($_SERVER['PHP_SELF'], strlen($scriptUrl)); 80 | } else { 81 | throw new InvalidConfigException('Unable to determine the path info of the current request.'); 82 | } 83 | 84 | if (isset($pathInfo[0]) && $pathInfo[0] === '/') { 85 | $pathInfo = substr($pathInfo, 1); 86 | } 87 | 88 | return (string) $pathInfo; 89 | } 90 | } -------------------------------------------------------------------------------- /src/UrlManager.php: -------------------------------------------------------------------------------- 1 | getModule('languages'); 21 | //Сссылка(без идентификатора языка) 22 | $url = parent::createUrl($params); 23 | 24 | $curentLang = Yii::$app->language; 25 | 26 | if (empty($params['lang'])) { 27 | if($curentLang != $module->default_language || $module->show_default === true){ 28 | if ($url == '/') { 29 | return '/' . $curentLang; 30 | } else { 31 | return '/' . $curentLang . $url; 32 | } 33 | } 34 | }; 35 | 36 | return $url; 37 | } 38 | } -------------------------------------------------------------------------------- /src/controllers/DefaultController.php: -------------------------------------------------------------------------------- 1 | request->get('lang'); //язык на который будем менять 27 | 28 | 29 | $url_referrer = Yii::$app->request->get('url'); 30 | 31 | if(!$url_referrer) $url_referrer = Yii::$app->request->referrer; //предыдущая страница 32 | 33 | /* 34 | * Если все же предыдущая страница не получена - возвращаем на главную. 35 | */ 36 | if (!$url_referrer) $url_referrer = Yii::$app->request->hostInfo . '/'. $language; 37 | 38 | //устанавливает/меняет метку языка 39 | $url = LanguageKsl::parsingUrl($language, $url_referrer); 40 | 41 | Yii::$app->response->redirect($url); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/models/LanguageKsl.php: -------------------------------------------------------------------------------- 1 | getModule('languages')->languages; 29 | $list = ''; 30 | 31 | array_walk($languages, function ($value) use (&$list){ 32 | $list .= $value . '|'; 33 | }); 34 | self::$list = $list; 35 | } 36 | 37 | return self::$list; 38 | } 39 | 40 | 41 | /** 42 | * Создает URL с меткой языка 43 | * Разбивает URL на подмассив $match_arr 44 | * 0. http://site.loc/ru/contact 45 | * 1. http://site.loc 46 | * 2. ru или uk или en 47 | * 3. остальная часть 48 | * 49 | * @param string $language 50 | * @param string $url_referrer 51 | * @return string 52 | */ 53 | public static function parsingUrl($language, $url_referrer){ 54 | 55 | $list_languages = self::list_languages(); //список языков 56 | $host = Yii::$app->request->hostInfo; 57 | 58 | $match_arr = []; 59 | 60 | preg_match("#^($host)/($list_languages)(.*)#", $url_referrer, $match_arr); 61 | 62 | //добавляем разделитель 63 | if (isset($match_arr[3]) && !empty($match_arr[3]) && !preg_match('#^\/#', $match_arr[3])){ 64 | $separator = '/'; 65 | } else { 66 | $separator = ''; 67 | } 68 | 69 | 70 | $default_language = Yii::$app->getModule('languages')->default_language; 71 | $show_default = Yii::$app->getModule('languages')->show_default; 72 | 73 | //Удаляем основной язык из URL, если в настройках выбрано "не показывать" 74 | if($language == $default_language && !$show_default){ 75 | $match_arr[2] = null; 76 | } else { 77 | $match_arr[2] = '/'.$language.$separator; 78 | } 79 | 80 | if(isset($match_arr[3])){ 81 | // создание нового URL 82 | $url = $match_arr[1].$match_arr[2].$match_arr[3]; 83 | } elseif (isset($match_arr[2])){ 84 | $url = $match_arr[1].$match_arr[2]; 85 | } else { 86 | $url = $match_arr[1]; 87 | } 88 | 89 | return $url; 90 | } 91 | } -------------------------------------------------------------------------------- /src/widgets/ListWidget.php: -------------------------------------------------------------------------------- 1 | language; //текущий язык 25 | 26 | //Создаем массив ссылок всех языков с соответствующими GET параметрами 27 | $array_lang = []; 28 | foreach (Yii::$app->getModule('languages')->languages as $key => $value){ 29 | 30 | $link = $this->createLink($key, $value); 31 | $array_lang += [$value => $link]; 32 | } 33 | 34 | //ссылку на текущий язык не выводим 35 | if(isset($array_lang[$language])) unset($array_lang[$language]); 36 | $this->array_languages = $array_lang; 37 | 38 | } 39 | 40 | /** 41 | * @param string $key 42 | * @param string $value 43 | * @return string 44 | */ 45 | protected function createLink($key, $value){ 46 | return Html::a($key, ['languages/default/index', 'lang' => $value], ['class' => 'language '.$value] ); 47 | } 48 | 49 | 50 | /** 51 | * @return string 52 | */ 53 | public function run() { 54 | 55 | return $this->render('list',[ 56 | 'array_lang' => $this->array_languages 57 | ]); 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /src/widgets/views/list.php: -------------------------------------------------------------------------------- 1 |
2 | 5 |
-------------------------------------------------------------------------------- /tests/RequestTest.php: -------------------------------------------------------------------------------- 1 | language = 'en'; //метка языка для тестирования 18 | 19 | $request = Yii::$app->request; 20 | 21 | $this->request = $request; 22 | } 23 | 24 | 25 | public function testGetLangUrlWithoutLang() 26 | { 27 | $_SERVER['REQUEST_URI'] = ''; //без метки языка 28 | $clearRequest = $this->request->getLangUrl(); 29 | $this->assertEquals($clearRequest, ''); 30 | } 31 | 32 | public function testGetLangUrlWithLang() 33 | { 34 | $_SERVER['REQUEST_URI'] = '/en'; //с меткой языка 35 | $clearRequest = $this->request->getLangUrl(); 36 | $this->assertEquals($clearRequest, ''); 37 | } 38 | 39 | } -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | mockApplication(); 24 | } 25 | 26 | protected function tearDown() 27 | { 28 | $this->destroyApplication(); 29 | parent::tearDown(); 30 | } 31 | 32 | protected function mockApplication() 33 | { 34 | 35 | new Application([ 36 | 'id' => 'app-test', 37 | 'basePath' => __DIR__, 38 | 'modules' => [ 39 | 'languages' => [ 40 | 'class' => Module::class, 41 | //Языки используемые в приложении 42 | 'languages' => [ 43 | 'English' => 'en', 44 | 'Русский' => 'ru', 45 | 'Українська' => 'uk', 46 | ], 47 | 'default_language' => 'ru', //основной язык (по-умолчанию) 48 | ], 49 | ], 50 | 'components' => [ 51 | 'request' => [ 52 | 'csrfParam' => '_csrf-test', 53 | 'baseUrl' => '', //чтобы убрать frontend/web 54 | 'hostInfo' => 'http://site.com', //т.к. недоступна $_SERVER['SERVER_NAME'] 55 | 'class' => Request::class 56 | ], 57 | 'urlManager' => [ 58 | 'enablePrettyUrl' => true, //не отображать index.php?r= 59 | 'class' => UrlManager::class, 60 | ], 61 | ], 62 | ]); 63 | 64 | } 65 | 66 | protected function destroyApplication() 67 | { 68 | \Yii::$app = null; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /tests/UrlManagerTest.php: -------------------------------------------------------------------------------- 1 | language = 'en'; //метка языка для тестирования 18 | 19 | $urlManager = Yii::$app->urlManager; 20 | /* 21 | * Используется для работы компонента UrlManager 22 | * (часть после домена) 23 | */ 24 | $urlManager->setScriptUrl(''); 25 | 26 | $this->urlManager = $urlManager; 27 | } 28 | 29 | 30 | public function testCreateUrlParamNull() 31 | { 32 | $res = $this->urlManager->createUrl(null); 33 | $this->assertEquals($res, '/en'); 34 | } 35 | 36 | public function testCreateUrlParamNotNull() 37 | { 38 | $res = $this->urlManager->createUrl('/params'); 39 | $this->assertEquals($res, '/en/params'); 40 | } 41 | 42 | public function testCreateUrlParamLang() 43 | { 44 | $res = $this->urlManager->createUrl(['lang' =>'ru']); 45 | 46 | $this->assertEquals($res, '/?lang=ru'); 47 | 48 | } 49 | 50 | } -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | assertEquals($res, 'en|ru|uk|'); 16 | } 17 | 18 | public function testparsingUrlWithDefaultLang() 19 | { 20 | $res = LanguageKsl::parsingUrl('ru', 'http://site.com/en'); 21 | $this->assertEquals($res, 'http://site.com'); 22 | } 23 | 24 | public function testparsingUrlWithAnotherLang() 25 | { 26 | $res = LanguageKsl::parsingUrl('uk', 'http://site.com/en'); 27 | $this->assertEquals($res, 'http://site.com/uk'); 28 | } 29 | } -------------------------------------------------------------------------------- /tests/widgets/ListWidgetTest.php: -------------------------------------------------------------------------------- 1 | language = 'uk'; //метка языка для тестирования 16 | 17 | $mockListWidget = $this->getMockBuilder(ListWidget::class) 18 | ->disableOriginalConstructor() 19 | //метод будет возвращать Null если не переопределить 20 | ->setMethods(['createLink']) 21 | ->getMock(); 22 | 23 | $mockListWidget 24 | //метод отработает минимум 1 раз 25 | ->expects($this->atLeastOnce()) 26 | //переопределяем возвращаемое значение этого метода 27 | ->method('createLink') 28 | ->will($this->returnValue('http://site.com/en')); 29 | 30 | 31 | $mockListWidget->init(); 32 | 33 | $result = $mockListWidget->array_languages; //результат выполнения метода сохраняется в свойство 34 | 35 | $this->assertCount( 2, $result); 36 | $this->assertArrayNotHasKey('uk', $result); 37 | } 38 | 39 | 40 | } 41 | --------------------------------------------------------------------------------