├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── composer.json ├── package-lock.json ├── package.json ├── phpunit.xml ├── src ├── MicheleAngioni │ └── LaravelJsLangConverter │ │ ├── Commands │ │ └── LangJsCommand.php │ │ ├── Generators │ │ ├── LangJsGenerator.php │ │ └── Templates │ │ │ └── langjs_with_messages.js │ │ └── LaravelJsLangConverterServiceProvider.php ├── config │ └── config.php └── js │ └── lang.js └── tests ├── .gitkeep ├── LangJsCommandTest.php ├── fixtures └── lang │ ├── en │ ├── messages.php │ ├── pagination.php │ ├── reminders.php │ └── validation.php │ ├── es │ └── messages.php │ └── ht │ └── messages.php ├── output └── .gitignore └── spec ├── data └── messages.json ├── lang_choice_spec.js ├── lang_get_spec.js ├── lang_has_spec.js └── lang_locale_spec.js /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | composer.phar 3 | composer.lock 4 | .DS_Store 5 | /tmp 6 | 7 | /.idea 8 | /.settings 9 | 10 | /node_modules 11 | npm-debug* 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 7.0 5 | - 7.1 6 | - 7.2 7 | - nightly 8 | 9 | before_script: 10 | - composer self-update 11 | - composer install --prefer-source --no-interaction --dev 12 | 13 | script: vendor/bin/phpunit 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2018 Michele Angioni 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Laravel JS Lang Converter 2 | ======================= 3 | 4 | > Laravel Localization in JavaScript. 5 | 6 | [![Latest Stable Version](https://poser.pugx.org/michele-angioni/laravel-js-lang-converter/v/stable.svg)](https://packagist.org/packages/michele-angioni/laravel-js-lang-converter) 7 | [![License](https://poser.pugx.org/michele-angioni/laravel-js-lang-converter/license.svg)](https://packagist.org/packages/michele-angioni/laravel-js-lang-converter) 8 | [![Build Status](https://travis-ci.org/micheleangioni/laravel-js-lang-converter.svg?branch=master)](https://travis-ci.org/micheleangioni/laravel-js-lang-converter) 9 | [![SensioLabsInsight](https://insight.sensiolabs.com/projects/b9c37a8d-26aa-458b-8e7e-7ccac6d1e021/small.png)](https://insight.sensiolabs.com/projects/b9c37a8d-26aa-458b-8e7e-7ccac6d1e021) 10 | 11 | Laravel JS Lang Converter converts all your localization messages of your Laravel app to JavaScript, providing a small JavaScript library to interact with those messages in the fron end. 12 | 13 | Most of the work has been developed in [Mariuzzo's package ](https://github.com/rmariuzzo/laravel-js-localization) 14 | 15 | Laravel 5.5+ is supported. PHP 7.0 is required. 16 | For Laravel 5.1 - 5-4 versions, use the v2.x branch. 17 | 18 | Installation 19 | ------------ 20 | 21 | Add the following line to you `composer.json` file under `require`. 22 | 23 | ```json 24 | "michele-angioni/laravel-js-lang-converter": "~3.0" 25 | ``` 26 | 27 | and run `composer update` or `composer install`. 28 | 29 | Then you need to add the Laravel JS Lang Converter service provider in your `app/config/app.php` file 30 | 31 | ```php 32 | 'providers' => [ 33 | // ... 34 | 'MicheleAngioni\LaravelJsLangConverter\LaravelJsLangConverterServiceProvider', 35 | // ... 36 | ], 37 | ``` 38 | 39 | In order to use some package features, you need to publish the config file through the artisan command `php artisan vendor:publish`. It will create the `laravel_js_lang.php` file in your config directory. 40 | 41 | Now you are done! 42 | 43 | Usage 44 | ----- 45 | 46 | This project comes with a command that generate the JavaScript version of all your messages found in `resources/lang` directory. The resulting JavaScript file will have the whole bunch of messages and a thin library similar to Laravel's `Lang` class. 47 | 48 | **Generating JS messages** 49 | 50 | ```shell 51 | php artisan lang:js 52 | ``` 53 | 54 | **Specifying a custom target** 55 | 56 | ```shell 57 | php artisan lang:js public/assets/dist/lang.dist.js 58 | ``` 59 | 60 | **Converting only some files** 61 | 62 | If you don't want to convert ALL your lang files, you can specify the files you want to be converted into your `laravel_js_lang.php` conf file. Under the `files` array, just add the list of your source files, like so: 63 | 64 | ```php 65 | 'files' => [ 66 | 'pagination', 67 | 'validation' 68 | ] 69 | ``` 70 | 71 | **Compressing the JS file** 72 | 73 | ```shell 74 | php artisan lang:js -c 75 | ``` 76 | 77 | **Use [gulp](http://gulpjs.com/) to publish (optional):** 78 | 79 | 1. Install `gulp-shell` from https://github.com/sun-zheng-an/gulp-shell with `npm install --save-dev gulp-shell` . 80 | 81 | 2. Create an extension for elixir in your `gulpfile.js`: 82 | 83 | ```js 84 | var shell = require('gulp-shell'); 85 | 86 | //...... 87 | 88 | var Task = elixir.Task; 89 | 90 | elixir.extend('langjs', function(path, minimize) { 91 | new Task('langjs', function() { 92 | var command = "php artisan lang:js " + (path || "public/js/messages.js"); 93 | if (minimize) { 94 | command += " -c"; 95 | } 96 | return gulp.src("").pipe(shell(command)); 97 | }); 98 | }); 99 | 100 | gulp.task('langJs', shell.task('php artisan lang:js -c public/js/messages.js')); 101 | ``` 102 | 103 | 3. Use the new elixir task: 104 | 105 | ```js 106 | elixir(function(mix) { 107 | var path = "public/js"; 108 | var minimize = true; 109 | mix.langjs(path, minimize); 110 | }); 111 | ``` 112 | 113 | Documentation 114 | ------------- 115 | 116 | This is the documentation regarding the thin JavaScript library. The library is highly inspired on Laravel's `Lang` class. 117 | 118 | **Getting a message** 119 | 120 | ```js 121 | Lang.get('messages.home'); 122 | ``` 123 | 124 | **Getting a message with replacements** 125 | 126 | ```js 127 | Lang.get('messages.welcome', { name: 'Joe' }); 128 | ``` 129 | 130 | **Changing the locale** 131 | 132 | ```js 133 | Lang.setLocale('es'); 134 | ``` 135 | 136 | **Checking if a message key exists** 137 | 138 | ```js 139 | Lang.has('messages.foo'); 140 | ``` 141 | 142 | **Support for singular and plural message based on a count** 143 | 144 | ```js 145 | Lang.choice('messages.apples', 10); 146 | ``` 147 | 148 | **Calling the `choice` method with replacements** 149 | 150 | ```js 151 | Lang.choice('messages.apples', 10, { name: 'Joe' }); 152 | ``` 153 | 154 | For more detailed information, take a look at the source: [Lang.js](https://github.com/michele-angioni/laravel-js-lang-converter/blob/master/js/lang.js). 155 | 156 | How to contribute 157 | =================== 158 | 159 | Pull requests are welcome. 160 | 161 | 1. Fork this repository and clone it. 162 | 2. Create a branch from develop: `git checkout -b feature-foo`. 163 | 3. Push your commits and create a pull request. 164 | 165 | Setting up development environment 166 | ---------------------------------- 167 | 168 | **Prerequisites:** 169 | 170 | You need to have installed the following softwares. 171 | 172 | - Composer 173 | - NodeJS 174 | - NPM 175 | - PHP 7.0+ 176 | 177 | After getting all the required software you may run the following commands to get everything ready: 178 | 179 | 1. Install PHP dependencies: 180 | 181 | ```shell 182 | composer install 183 | ``` 184 | 185 | 2. Install NPM dependences: 186 | 187 | ```shell 188 | npm install -g jasmine-node 189 | 190 | npm install 191 | ``` 192 | 193 | Now you are good to go! Happy coding! 194 | 195 | Unit testing 196 | ------------ 197 | 198 | This project use Jasmine-Node and PHPUnit. All tests are stored at `tests` directory. 199 | 200 | To run all JS tests type in you terminal: 201 | 202 | ```shell 203 | npm test 204 | ``` 205 | 206 | To run all PHP tests type in your terminal: 207 | 208 | ```shell 209 | vendor/bin/phpunit tests/ 210 | ``` 211 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "michele-angioni/laravel-js-lang-converter", 3 | "description": "Laravel Localization in JavaScript", 4 | "type": "library", 5 | "keywords": ["laravel", "laravel 5", "localization", "i18n", "javascript", "js", "lang"], 6 | "homepage": "https://github.com/michele-angioni/laravel-js-lang-converter", 7 | "license": "MIT", 8 | "authors": [{ 9 | "name": "Rubens Mariuzzo", 10 | "email": "rubens@mariuzzo.com", 11 | "homepage": "https://github.com/rmariuzzo", 12 | "role": "Developer" 13 | }, { 14 | "name": "German Popoter", 15 | "email": "me@gpopoteur.com", 16 | "homepage": "https://github.com/gpopoteur", 17 | "role": "Developer" 18 | },{ 19 | "name": "Galievskiy Dmitriy", 20 | "homepage": "https://github.com/xAockd", 21 | "role": "Developer" 22 | },{ 23 | "name": "Ramon Ackermann", 24 | "homepage": "https://github.com/sboo", 25 | "role": "Developer" 26 | },{ 27 | "name": "Michele Angioni", 28 | "email": "michele.angioni@gmail.com", 29 | "homepage": "https://github.com/micheleangioni", 30 | "role": "Developer" 31 | }], 32 | "support": { 33 | "issues": "https://github.com/michele-angioni/laravel-js-lang-converter/issues", 34 | "source": "https://github.com/michele-angioni/laravel-js-lang-converter" 35 | }, 36 | "repositories": [ 37 | { 38 | "type": "git", 39 | "url": "https://github.com/orchestral/testbench" 40 | } 41 | ], 42 | "require": { 43 | "php": ">=7.0", 44 | "laravel/framework": "~5.5", 45 | "tedivm/jshrink": "1.0.*" 46 | }, 47 | "require-dev": { 48 | "phpunit/phpunit": "~6.0|~7.0", 49 | "orchestra/testbench": "~3.5", 50 | "mockery/mockery": "0.9.*" 51 | }, 52 | "autoload": { 53 | "psr-4": { 54 | "MicheleAngioni\\LaravelJsLangConverter\\": "src/MicheleAngioni/LaravelJsLangConverter/" 55 | } 56 | }, 57 | "autoload-dev": { 58 | "psr-4": { 59 | "MicheleAngioni\\LaravelJsLangConverter\\": "tests/" 60 | } 61 | }, 62 | "minimum-stability": "dev", 63 | "prefer-stable": true 64 | } 65 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "laravel-js-lang-converter", 3 | "version": "2.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "coffee-script": { 8 | "version": "1.12.7", 9 | "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.12.7.tgz", 10 | "integrity": "sha512-fLeEhqwymYat/MpTPUjSKHVYYl0ec2mOyALEMLmzr5i1isuG+6jfI2j2d5oBO3VIzgUXgBVIcOT9uH1TFxBckw==", 11 | "dev": true 12 | }, 13 | "fileset": { 14 | "version": "0.1.8", 15 | "resolved": "https://registry.npmjs.org/fileset/-/fileset-0.1.8.tgz", 16 | "integrity": "sha1-UGuRqTluqn4y+0KoQHfHoMc2t0E=", 17 | "dev": true, 18 | "requires": { 19 | "glob": "3.2.11", 20 | "minimatch": "0.2.14" 21 | } 22 | }, 23 | "gaze": { 24 | "version": "0.3.4", 25 | "resolved": "https://registry.npmjs.org/gaze/-/gaze-0.3.4.tgz", 26 | "integrity": "sha1-X5S92gr+U7xxCWm81vKCVI1gwnk=", 27 | "dev": true, 28 | "requires": { 29 | "fileset": "0.1.8", 30 | "minimatch": "0.2.14" 31 | } 32 | }, 33 | "glob": { 34 | "version": "3.2.11", 35 | "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.11.tgz", 36 | "integrity": "sha1-Spc/Y1uRkPcV0QmH1cAP0oFevj0=", 37 | "dev": true, 38 | "requires": { 39 | "inherits": "2.0.3", 40 | "minimatch": "0.3.0" 41 | }, 42 | "dependencies": { 43 | "minimatch": { 44 | "version": "0.3.0", 45 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.3.0.tgz", 46 | "integrity": "sha1-J12O2qxPG7MyZHIInnlJyDlGmd0=", 47 | "dev": true, 48 | "requires": { 49 | "lru-cache": "2.7.3", 50 | "sigmund": "1.0.1" 51 | } 52 | } 53 | } 54 | }, 55 | "growl": { 56 | "version": "1.7.0", 57 | "resolved": "https://registry.npmjs.org/growl/-/growl-1.7.0.tgz", 58 | "integrity": "sha1-3i1mE20ALhErpw8/EMMc98NQsto=", 59 | "dev": true 60 | }, 61 | "inherits": { 62 | "version": "2.0.3", 63 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 64 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", 65 | "dev": true 66 | }, 67 | "jasmine-growl-reporter": { 68 | "version": "0.0.3", 69 | "resolved": "https://registry.npmjs.org/jasmine-growl-reporter/-/jasmine-growl-reporter-0.0.3.tgz", 70 | "integrity": "sha1-uHrlUeNZ0orVIXdl6u9sB7dj9sg=", 71 | "dev": true, 72 | "requires": { 73 | "growl": "1.7.0" 74 | } 75 | }, 76 | "jasmine-node": { 77 | "version": "1.14.5", 78 | "resolved": "https://registry.npmjs.org/jasmine-node/-/jasmine-node-1.14.5.tgz", 79 | "integrity": "sha1-GOg5e4VpJO53ADZmw3MbWupQw50=", 80 | "dev": true, 81 | "requires": { 82 | "coffee-script": "1.12.7", 83 | "gaze": "0.3.4", 84 | "jasmine-growl-reporter": "0.0.3", 85 | "jasmine-reporters": "1.0.2", 86 | "mkdirp": "0.3.5", 87 | "requirejs": "2.3.5", 88 | "underscore": "1.8.3", 89 | "walkdir": "0.0.12" 90 | } 91 | }, 92 | "jasmine-reporters": { 93 | "version": "1.0.2", 94 | "resolved": "https://registry.npmjs.org/jasmine-reporters/-/jasmine-reporters-1.0.2.tgz", 95 | "integrity": "sha1-q2E+1Zd9x0h+hbPBL2qOqNsq3jE=", 96 | "dev": true, 97 | "requires": { 98 | "mkdirp": "0.3.5" 99 | } 100 | }, 101 | "lru-cache": { 102 | "version": "2.7.3", 103 | "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", 104 | "integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI=", 105 | "dev": true 106 | }, 107 | "minimatch": { 108 | "version": "0.2.14", 109 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz", 110 | "integrity": "sha1-x054BXT2PG+aCQ6Q775u9TpqdWo=", 111 | "dev": true, 112 | "requires": { 113 | "lru-cache": "2.7.3", 114 | "sigmund": "1.0.1" 115 | } 116 | }, 117 | "mkdirp": { 118 | "version": "0.3.5", 119 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz", 120 | "integrity": "sha1-3j5fiWHIjHh+4TaN+EmsRBPsqNc=", 121 | "dev": true 122 | }, 123 | "requirejs": { 124 | "version": "2.3.5", 125 | "resolved": "https://registry.npmjs.org/requirejs/-/requirejs-2.3.5.tgz", 126 | "integrity": "sha512-svnO+aNcR/an9Dpi44C7KSAy5fFGLtmPbaaCeQaklUz8BQhS64tWWIIlvEA5jrWICzlO/X9KSzSeXFnZdBu8nw==", 127 | "dev": true 128 | }, 129 | "sigmund": { 130 | "version": "1.0.1", 131 | "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", 132 | "integrity": "sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=", 133 | "dev": true 134 | }, 135 | "underscore": { 136 | "version": "1.8.3", 137 | "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.8.3.tgz", 138 | "integrity": "sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI=", 139 | "dev": true 140 | }, 141 | "walkdir": { 142 | "version": "0.0.12", 143 | "resolved": "https://registry.npmjs.org/walkdir/-/walkdir-0.0.12.tgz", 144 | "integrity": "sha512-HFhaD4mMWPzFSqhpyDG48KDdrjfn409YQuVW7ckZYhW4sE87mYtWifdB/+73RA7+p4s4K18n5Jfx1kHthE1gBw==", 145 | "dev": true 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "laravel-js-lang-converter", 3 | "version": "3.0.0", 4 | "description": "Laravel Localization in JavaScript", 5 | "main": "src/js/lang.js", 6 | "directories": { 7 | "test": "tests" 8 | }, 9 | "scripts": { 10 | "test": "./node_modules/jasmine-node/bin/jasmine-node tests/spec/", 11 | "start": "./node_modules/jasmine-node/bin/jasmine-node tests/spec/ --autotest --watch js/" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git://github.com/michele-angioni/laravel-js-lang-converter.git" 16 | }, 17 | "author": "Michele Angioni", 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/michele-angioni/laravel-js-lang-converter/issues" 21 | }, 22 | "devDependencies": { 23 | "jasmine-node": "~1.14.3" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | ./tests/ 16 | 17 | 18 | 19 | 20 | ./src/ 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/MicheleAngioni/LaravelJsLangConverter/Commands/LangJsCommand.php: -------------------------------------------------------------------------------- 1 | generator = $generator; 33 | parent::__construct(); 34 | } 35 | 36 | /** 37 | * Fire the command. 38 | */ 39 | public function handle() 40 | { 41 | $target = $this->argument('target'); 42 | $options = ['compress' => $this->option('compress')]; 43 | 44 | if ($this->generator->generate($target, $options)) { 45 | return $this->info("Created: {$target}"); 46 | } 47 | 48 | $this->error("Could not create: {$target}"); 49 | } 50 | 51 | /** 52 | * Return all command arguments. 53 | * 54 | * @return array 55 | */ 56 | protected function getArguments() 57 | { 58 | return [ 59 | ['target', InputArgument::OPTIONAL, 'Target path.', $this->getPublicPath() . '/messages.js'], 60 | ]; 61 | } 62 | 63 | /** 64 | * Return all command options. 65 | * 66 | * @return array 67 | */ 68 | protected function getOptions() 69 | { 70 | return [ 71 | ['compress', 'c', InputOption::VALUE_NONE, 'Compress the JavaScript file.', null], 72 | ]; 73 | } 74 | 75 | /** 76 | * Return the public path of the Laravel application. 77 | * 78 | * @return string 79 | */ 80 | public function getPublicPath() 81 | { 82 | return public_path(); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/MicheleAngioni/LaravelJsLangConverter/Generators/LangJsGenerator.php: -------------------------------------------------------------------------------- 1 | file = $file; 27 | $this->sourcePath = $sourcePath; 28 | } 29 | 30 | /** 31 | * Generate a JS lang file from all language files to the $target file. 32 | * 33 | * @param string $target 34 | * @param array $options 35 | * 36 | * @return int 37 | */ 38 | public function generate($target, $options) 39 | { 40 | $messages = $this->getMessages(); 41 | $this->prepareTarget($target); 42 | 43 | $template = $this->file->get(__DIR__ . '/Templates/langjs_with_messages.js'); 44 | $langjs = $this->file->get(__DIR__ . '/../../../js/lang.js'); 45 | 46 | $template = str_replace('\'{ messages }\'', json_encode($messages), $template); 47 | $template = str_replace('\'{ langjs }\';', $langjs, $template); 48 | 49 | if ($options['compress']) { 50 | $template = Minifier::minify($template); 51 | } 52 | 53 | return $this->file->put($target, $template); 54 | } 55 | 56 | /** 57 | * Return all language messages. 58 | * 59 | * @throws \Exception 60 | * @return array 61 | */ 62 | protected function getMessages() 63 | { 64 | $messages = []; 65 | $sourcePath = $this->sourcePath; 66 | 67 | if (!$this->file->exists($sourcePath)) { 68 | throw new \Exception("${sourcePath} doesn't exists!"); 69 | } 70 | 71 | foreach ($this->file->allFiles($sourcePath) as $file) { 72 | $pathName = $file->getRelativePathName(); 73 | 74 | if ($this->file->extension($pathName) !== 'php') { 75 | continue; 76 | } 77 | 78 | if (!$this->fileIsIncludedInFileList(substr($file->getFileName(), 0, -4))) { 79 | continue; 80 | } 81 | 82 | $key = substr($pathName, 0, -4); 83 | $key = str_replace('\\', '.', $key); 84 | $key = str_replace('/', '.', $key); 85 | 86 | $messages[$key] = include "${sourcePath}/${pathName}"; 87 | } 88 | 89 | return $messages; 90 | } 91 | 92 | /** 93 | * Check if input file is included in the file list to be converted. 94 | * If the file list is empty, all files will be included. 95 | * 96 | * @param string $fileName 97 | * @return bool 98 | */ 99 | protected function fileIsIncludedInFileList($fileName) 100 | { 101 | if (!config('laravel_js_lang.files') || count(config('laravel_js_lang.files')) === 0) { 102 | return true; 103 | } 104 | 105 | if (in_array($fileName, config('laravel_js_lang.files'))) { 106 | return true; 107 | } 108 | 109 | return false; 110 | } 111 | 112 | /** 113 | * Prepare the target directory. 114 | * 115 | * @param string $target 116 | */ 117 | protected function prepareTarget($target) 118 | { 119 | $dirname = dirname($target); 120 | 121 | if (!$this->file->exists($dirname)) { 122 | $this->file->makeDirectory($dirname); 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/MicheleAngioni/LaravelJsLangConverter/Generators/Templates/langjs_with_messages.js: -------------------------------------------------------------------------------- 1 | '{ langjs }'; 2 | 3 | (function (root) { 4 | Lang.setMessages('{ messages }'); 5 | })(window); 6 | -------------------------------------------------------------------------------- /src/MicheleAngioni/LaravelJsLangConverter/LaravelJsLangConverterServiceProvider.php: -------------------------------------------------------------------------------- 1 | publishes([ 23 | __DIR__ . '/../../config/config.php' => config_path('laravel_js_lang.php'), 24 | ]); 25 | 26 | $this->mergeConfigFrom( 27 | __DIR__ . '/../../config/config.php', 'laravel_js_lang' 28 | ); 29 | } 30 | 31 | /** 32 | * Register the service provider. 33 | * 34 | * @return void 35 | */ 36 | public function register() 37 | { 38 | $this->app->singleton('localization.js', function ($app) { 39 | $files = $app['files']; 40 | $langs = $app['path.base'] . '/resources/lang'; 41 | $generator = new Generators\LangJsGenerator($files, $langs); 42 | 43 | return new Commands\LangJsCommand($generator); 44 | }); 45 | 46 | $this->commands('localization.js'); 47 | } 48 | 49 | /** 50 | * Get the services provided by the provider. 51 | * 52 | * @return array 53 | */ 54 | public function provides() 55 | { 56 | return ['localization.js']; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/config/config.php: -------------------------------------------------------------------------------- 1 | [ 17 | 18 | ] 19 | 20 | ]; 21 | -------------------------------------------------------------------------------- /src/js/lang.js: -------------------------------------------------------------------------------- 1 | (function(root, factory) { 2 | 'use strict'; 3 | 4 | if (typeof define === 'function' && define.amd) { 5 | // AMD support. 6 | define([], factory); 7 | } else if (typeof exports === 'object') { 8 | // NodeJS support. 9 | module.exports = new(factory())(); 10 | } else { 11 | // Browser global support. 12 | root.Lang = new(factory())(); 13 | } 14 | 15 | }(this, function() { 16 | 'use strict'; 17 | 18 | // Default options // 19 | 20 | var defaults = { 21 | defaultLocale: 'en' /** The default locale if not set. */ 22 | }; 23 | 24 | // Constructor // 25 | 26 | var Lang = function(options) { 27 | options = options || {}; 28 | this.defaultLocale = options.defaultLocale || defaults.defaultLocale; 29 | }; 30 | 31 | // Methods // 32 | 33 | /** 34 | * Set messages source. 35 | * 36 | * @param messages {object} The messages source. 37 | * 38 | * @return void 39 | */ 40 | Lang.prototype.setMessages = function(messages) { 41 | this.messages = messages; 42 | }; 43 | 44 | /** 45 | * Returns a translation message. 46 | * Il no locale is given, the current locale will be used. 47 | * 48 | * @param key {string} The key of the message. 49 | * @param replacements {object} The replacements to be done in the message. 50 | * @param locale {locale} The locale to be used. 51 | * 52 | * @return {string} The translation message. If not found the default locale will be tried and eventually the given key will be returned. 53 | */ 54 | Lang.prototype.get = function(key, replacements, locale) { 55 | if (!locale) { 56 | locale = this.getLocale(); 57 | } 58 | 59 | // Check if the key exists in the required locale. If not found, try the default locale 60 | 61 | if (!this.has(key, locale)) { 62 | 63 | if(locale === this.defaultLocale) 64 | return key; 65 | 66 | locale = this.defaultLocale; 67 | 68 | if (!this.has(key, locale)) 69 | return key; 70 | } 71 | 72 | var message = this._getMessage(key, locale); 73 | if (message === null) { 74 | return key; 75 | } 76 | 77 | if (replacements) { 78 | message = this._applyReplacements(message, replacements); 79 | } 80 | 81 | return message; 82 | }; 83 | 84 | /** 85 | * Returns true if the key is defined on the messages source. 86 | * 87 | * @param key {string} The key of the message. 88 | * @param locale {string} The locale will be used. 89 | * 90 | * @return {boolean} true if the given key is defined on the messages source, otherwise false. 91 | */ 92 | Lang.prototype.has = function(key, locale) { 93 | if (!locale) { 94 | locale = this.getLocale(); 95 | } 96 | 97 | if (typeof key !== 'string' || !this.messages) { 98 | return false; 99 | } 100 | return this._getMessage(key, locale) !== null; 101 | }; 102 | 103 | /** 104 | * Gets the plural or singular form of the message specified based on an integer value. 105 | * 106 | * @param key {string} The key of the message. 107 | * @param count {integer} The number of elements. 108 | * @param replacements {object} The replacements to be done in the message. 109 | * 110 | * @return {string} The translation message according to an integer value. 111 | */ 112 | Lang.prototype.choice = function(key, count, replacements) { 113 | // Set default values for parameters replace and locale 114 | replacements = typeof replacements !== 'undefined' ? replacements : {}; 115 | 116 | // The count must be replaced if found in the message 117 | replacements['count'] = count; 118 | 119 | // Message to get the plural or singular 120 | var message = this.get(key, replacements); 121 | 122 | // Check if message is not null or undefined 123 | if (message === null || message === undefined) { 124 | return message; 125 | } 126 | 127 | // Separate the plural from the singular, if any 128 | var messageParts = message.split('|'); 129 | 130 | // Get the explicit rules, If any 131 | var explicitRules = []; 132 | var regex = /^(\[|\{|\(|\]|\)).*(\[|\(|\]|\)|\})\s/; 133 | 134 | for (var i = 0; i < messageParts.length; i++) { 135 | messageParts[i] = messageParts[i].trim(); 136 | 137 | if (regex.test(messageParts[i])) { 138 | var messageSpaceSplit = messageParts[i].split(/\s/); 139 | explicitRules.push(messageSpaceSplit.shift()); 140 | messageParts[i] = messageSpaceSplit.join(' '); 141 | } 142 | } 143 | 144 | // Check if there's only one message 145 | if (messageParts.length === 1) { 146 | // Nothing to do here 147 | return message; 148 | } 149 | 150 | // Check the explicit rules 151 | for (var i = 0; i < explicitRules.length; i++) { 152 | if (this._testInterval(count, explicitRules[i])) { 153 | return messageParts[i]; 154 | } 155 | } 156 | 157 | var pluralForm = this._getPluralForm(count); 158 | 159 | return messageParts[pluralForm]; 160 | }; 161 | 162 | /** 163 | * Set the current locale. 164 | * 165 | * @param locale {string} The locale to set. 166 | * 167 | * @return void 168 | */ 169 | Lang.prototype.setLocale = function(locale) { 170 | this.locale = locale; 171 | }; 172 | 173 | /** 174 | * Get the current locale. 175 | * 176 | * @return {string} The current locale. 177 | */ 178 | Lang.prototype.getLocale = function() { 179 | return this.locale || this.defaultLocale; 180 | }; 181 | 182 | /** 183 | * Parse a message key into components. 184 | * 185 | * @param key {string} The message key to parse. 186 | * @param locale {string} The locale to be used. 187 | * 188 | * @return {object} A key object with source and entries properties. 189 | */ 190 | Lang.prototype._parseKey = function(key, locale) { 191 | if (typeof key !== 'string') { 192 | return null; 193 | } 194 | var segments = key.split('.'); 195 | return { 196 | source: locale + '.' + segments[0].replace('/', '.'), 197 | entries: segments.slice(1) 198 | }; 199 | }; 200 | 201 | /** 202 | * Returns a translation message. This methods assumes the key exists on input locale. 203 | * 204 | * @param key {string} The key of the message. 205 | * @param locale {string} The locale to be used 206 | * 207 | * @return {string} The translation message for the given key. 208 | */ 209 | Lang.prototype._getMessage = function(key, locale) { 210 | 211 | key = this._parseKey(key, locale); 212 | 213 | // Ensure message source exists. 214 | if (this.messages[key.source] === undefined) { 215 | return null; 216 | } 217 | 218 | // Get message text. 219 | var message = this.messages[key.source]; 220 | while (key.entries.length && (message = message[key.entries.shift()])); 221 | 222 | if (typeof message !== 'string' && typeof message !== 'object') { 223 | return null; 224 | } 225 | 226 | return message; 227 | }; 228 | 229 | /** 230 | * Apply replacements to a string message containing placeholders. 231 | * 232 | * @param message {string} The text message. 233 | * @param replacements {object} The replacements to be done in the message. 234 | * 235 | * @return {string} The string message with replacements applied. 236 | */ 237 | Lang.prototype._applyReplacements = function(message, replacements) { 238 | for (var replace in replacements) { 239 | message = message.split(':' + replace).join(replacements[replace]); 240 | } 241 | return message; 242 | }; 243 | 244 | /** 245 | * Checks if the given `count` is within the interval defined by the {string} `interval` 246 | * 247 | * @param count {int} The amount of items. 248 | * @param interval {string} The interval to be compared with the count. 249 | * @return {boolean} Returns true if count is within interval; false otherwise. 250 | */ 251 | Lang.prototype._testInterval = function(count, interval) { 252 | /** 253 | * From the Symfony\Component\Translation\Interval Docs 254 | * 255 | * Tests if a given number belongs to a given math interval. 256 | * An interval can represent a finite set of numbers: {1,2,3,4} 257 | * An interval can represent numbers between two numbers: [1, +Inf] ]-1,2[ 258 | * The left delimiter can be [ (inclusive) or ] (exclusive). 259 | * The right delimiter can be [ (exclusive) or ] (inclusive). 260 | * Beside numbers, you can use -Inf and +Inf for the infinite. 261 | */ 262 | 263 | var numbers = this._parseNumbersFromInterval(interval); 264 | 265 | var types = { 266 | 'setOfNumbers' : /^\{.*\}$/, 267 | 'bothExclusive': /^(\(|\]|\)).*(\)|\[|\()$/, 268 | 'bothInclusive': /^\[.*\]$/, 269 | 'leftInclusive': /^\[.*(\)|\[|\()$/, 270 | 'rightInclusive': /^(\(|\]|\)).*\]$/ 271 | }; 272 | 273 | if (interval.match(types.setOfNumbers)) { 274 | return numbers.indexOf(count) != -1; 275 | } 276 | 277 | if (interval.match(types.bothInclusive)) { 278 | return count >= numbers[0] && count <= numbers[1]; 279 | } 280 | 281 | if (interval.match(types.bothExclusive)) { 282 | return count > numbers[0] && count < numbers[1]; 283 | } 284 | 285 | if (interval.match(types.rightInclusive)) { 286 | return count > numbers[0] && count <= numbers[1]; 287 | } 288 | 289 | if (interval.match(types.leftInclusive)) { 290 | return count >= numbers[0] && count < numbers[1]; 291 | } 292 | }; 293 | 294 | /** 295 | * Parse a given string (number, +Inf, -Inf, Inf) to Number. 296 | * 297 | * @param {String} str 298 | * @return {Number} 299 | */ 300 | Lang.prototype._parseNumber = function (str){ 301 | str = str.replace(/Inf\s*?$/i, 'Infinity'); 302 | 303 | return Number(str); 304 | }; 305 | 306 | /** 307 | * Parse an interval to array. 308 | * 309 | * @param {String} interval 310 | * @return {Array} 311 | */ 312 | Lang.prototype._parseNumbersFromInterval = function (interval) { 313 | var braces = /\[|\]|\{|\}|\(|\)/g; 314 | var numbers = interval.replace(braces, '').split(/,\s?/); 315 | var newNumbers = []; 316 | 317 | for (var i in numbers) { 318 | newNumbers.push(this._parseNumber(numbers[i])); 319 | } 320 | 321 | return newNumbers; 322 | }; 323 | 324 | /** 325 | * Returns the plural position to use for the given locale and number. 326 | * 327 | * The plural rules are derived from code of the Zend Framework (2010-09-25), 328 | * which is subject to the new BSD license (http://framework.zend.com/license/new-bsd). 329 | * Copyright (c) 2005-2010 Zend Technologies USA Inc. (http://www.zend.com) 330 | * 331 | * @param {Number} count 332 | * @return {Number} 333 | */ 334 | Lang.prototype._getPluralForm = function (count) { 335 | 336 | // If a full locale is found (i.e. en_US), parse the 2-letter lang code from before the underscore 337 | var lang = (this.locale.indexOf('_') == 2) ? this.locale.substr(0, 2) : this.locale; 338 | 339 | switch (lang) { 340 | case 'az': 341 | case 'bo': 342 | case 'dz': 343 | case 'id': 344 | case 'ja': 345 | case 'jv': 346 | case 'ka': 347 | case 'km': 348 | case 'kn': 349 | case 'ko': 350 | case 'ms': 351 | case 'th': 352 | case 'tr': 353 | case 'vi': 354 | case 'zh': 355 | return 0; 356 | 357 | case 'af': 358 | case 'bn': 359 | case 'bg': 360 | case 'ca': 361 | case 'da': 362 | case 'de': 363 | case 'el': 364 | case 'en': 365 | case 'eo': 366 | case 'es': 367 | case 'et': 368 | case 'eu': 369 | case 'fa': 370 | case 'fi': 371 | case 'fo': 372 | case 'fur': 373 | case 'fy': 374 | case 'gl': 375 | case 'gu': 376 | case 'ha': 377 | case 'he': 378 | case 'hu': 379 | case 'is': 380 | case 'it': 381 | case 'ku': 382 | case 'lb': 383 | case 'ml': 384 | case 'mn': 385 | case 'mr': 386 | case 'nah': 387 | case 'nb': 388 | case 'ne': 389 | case 'nl': 390 | case 'nn': 391 | case 'no': 392 | case 'om': 393 | case 'or': 394 | case 'pa': 395 | case 'pap': 396 | case 'ps': 397 | case 'pt': 398 | case 'so': 399 | case 'sq': 400 | case 'sv': 401 | case 'sw': 402 | case 'ta': 403 | case 'te': 404 | case 'tk': 405 | case 'ur': 406 | case 'zu': 407 | return (count == 1) ? 0 : 1; 408 | 409 | case 'am': 410 | case 'bh': 411 | case 'fil': 412 | case 'fr': 413 | case 'gun': 414 | case 'hi': 415 | case 'hy': 416 | case 'ln': 417 | case 'mg': 418 | case 'nso': 419 | case 'xbr': 420 | case 'ti': 421 | case 'wa': 422 | return ((count == 0) || (count == 1)) ? 0 : 1; 423 | 424 | case 'be': 425 | case 'bs': 426 | case 'hr': 427 | case 'ru': 428 | case 'sr': 429 | case 'uk': 430 | return ((count % 10 == 1) && (count % 100 != 11)) ? 0 : (((count % 10 >= 2) && (count % 10 <= 4) && ((count % 100 < 10) || (count % 100 >= 20))) ? 1 : 2); 431 | 432 | case 'cs': 433 | case 'sk': 434 | return (count == 1) ? 0 : (((count >= 2) && (count <= 4)) ? 1 : 2); 435 | 436 | case 'ga': 437 | return (count == 1) ? 0 : ((count == 2) ? 1 : 2); 438 | 439 | case 'lt': 440 | return ((count % 10 == 1) && (count % 100 != 11)) ? 0 : (((count % 10 >= 2) && ((count % 100 < 10) || (count % 100 >= 20))) ? 1 : 2); 441 | 442 | case 'sl': 443 | return (count % 100 == 1) ? 0 : ((count % 100 == 2) ? 1 : (((count % 100 == 3) || (count % 100 == 4)) ? 2 : 3)); 444 | 445 | case 'mk': 446 | return (count % 10 == 1) ? 0 : 1; 447 | 448 | case 'mt': 449 | return (count == 1) ? 0 : (((count == 0) || ((count % 100 > 1) && (count % 100 < 11))) ? 1 : (((count % 100 > 10) && (count % 100 < 20)) ? 2 : 3)); 450 | 451 | case 'lv': 452 | return (count == 0) ? 0 : (((count % 10 == 1) && (count % 100 != 11)) ? 1 : 2); 453 | 454 | case 'pl': 455 | return (count == 1) ? 0 : (((count % 10 >= 2) && (count % 10 <= 4) && ((count % 100 < 12) || (count % 100 > 14))) ? 1 : 2); 456 | 457 | case 'cy': 458 | return (count == 1) ? 0 : ((count == 2) ? 1 : (((count == 8) || (count == 11)) ? 2 : 3)); 459 | 460 | case 'ro': 461 | return (count == 1) ? 0 : (((count == 0) || ((count % 100 > 0) && (count % 100 < 20))) ? 1 : 2); 462 | 463 | case 'ar': 464 | return (count == 0) ? 0 : ((count == 1) ? 1 : ((count == 2) ? 2 : (((count % 100 >= 3) && (count % 100 <= 10)) ? 3 : (((count % 100 >= 11) && (count % 100 <= 99)) ? 4 : 5)))); 465 | 466 | default: 467 | return 0; 468 | } 469 | }; 470 | 471 | return Lang; 472 | 473 | })); 474 | -------------------------------------------------------------------------------- /tests/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/micheleangioni/laravel-js-lang-converter/10a2ac7893c73fc310e8736dc71444520a6d4fcd/tests/.gitkeep -------------------------------------------------------------------------------- /tests/LangJsCommandTest.php: -------------------------------------------------------------------------------- 1 | setLaravel($this->app); 23 | 24 | $code = $this->runCommand($command, ['target' => __DIR__.'/output/lang.js']); 25 | 26 | $this->assertRunsWithSuccess($code); 27 | 28 | $this->assertFileExists(__DIR__.'/output/lang.js'); 29 | 30 | $template = __DIR__.'/../src/MicheleAngioni/LaravelJsLangConverter/Generators/Templates/langjs_with_messages.js'; 31 | 32 | $this->assertFileExists($template); 33 | 34 | $this->assertFileNotEquals($template, __DIR__.'/output/lang.js'); 35 | } 36 | 37 | /** 38 | * @return void 39 | */ 40 | public function testShouldTemplateHasHandlebars() 41 | { 42 | $template = __DIR__.'/../src/MicheleAngioni/LaravelJsLangConverter/Generators/Templates/langjs_with_messages.js'; 43 | 44 | $this->assertFileExists($template); 45 | 46 | $contents = file_get_contents($template); 47 | 48 | $this->assertNotEmpty($contents); 49 | 50 | $this->assertHasHandlebars('messages', $contents); 51 | 52 | $this->assertHasHandlebars('langjs', $contents); 53 | } 54 | 55 | /** 56 | * @return void 57 | */ 58 | public function testShouldOutputHasNotHandlebars() 59 | { 60 | $output = __DIR__.'/output/lang.js'; 61 | 62 | $this->assertFileExists($output); 63 | 64 | $contents = file_get_contents($output); 65 | 66 | $this->assertNotEmpty($contents); 67 | 68 | $this->assertHasNotHandlebars('messages', $contents); 69 | 70 | $this->assertHasNotHandlebars('langjs', $contents); 71 | } 72 | 73 | /** 74 | * @return void 75 | */ 76 | public function testAllFilesShouldbeConverted() 77 | { 78 | $generator = new LangJsGenerator(new File(), __DIR__.'/fixtures/lang'); 79 | 80 | $command = new LangJsCommand($generator); 81 | $command->setLaravel($this->app); 82 | 83 | $code = $this->runCommand($command, ['target' => __DIR__.'/output/lang.js']); 84 | 85 | $this->assertRunsWithSuccess($code); 86 | 87 | $output = __DIR__.'/output/lang.js'; 88 | 89 | $this->assertFileExists($output); 90 | 91 | $contents = file_get_contents($output); 92 | 93 | $this->assertContains('createCongregation', $contents); 94 | } 95 | 96 | /** 97 | * @return void 98 | */ 99 | public function testOnlySelectedFilesShouldbeConverted() 100 | { 101 | $this->app['config']->set('laravel_js_lang.files', [ 102 | 'validation' 103 | ]); 104 | 105 | $generator = new LangJsGenerator(new File(), __DIR__.'/fixtures/lang'); 106 | 107 | $command = new LangJsCommand($generator); 108 | $command->setLaravel($this->app); 109 | 110 | $code = $this->runCommand($command, ['target' => __DIR__.'/output/lang.js']); 111 | 112 | $this->assertRunsWithSuccess($code); 113 | 114 | $output = __DIR__.'/output/lang.js'; 115 | 116 | $this->assertFileExists($output); 117 | 118 | $contents = file_get_contents($output); 119 | 120 | $this->assertNotContains('createCongregation', $contents); 121 | } 122 | 123 | /** 124 | * Run the command. 125 | * @param \Illuminate\Console\Command $command 126 | * @param array $input 127 | * @return int 128 | */ 129 | protected function runCommand($command, $input = []) 130 | { 131 | return $command->run(new ArrayInput($input), new NullOutput()); 132 | } 133 | 134 | /** 135 | * Assert the code return is success. 136 | * @param int $code 137 | * @param null $message 138 | */ 139 | protected function assertRunsWithSuccess($code, $message = '') 140 | { 141 | $this->assertEquals(0, $code, $message); 142 | } 143 | 144 | /** 145 | * @param string $handle 146 | * @param string $contents 147 | */ 148 | protected function assertHasHandlebars($handle, $contents) 149 | { 150 | $this->assertEquals(1, preg_match('/\'\{(\s)'.preg_quote($handle).'(\s)\}\'/', $contents)); 151 | } 152 | 153 | /** 154 | * @param string $handle 155 | * @param string $contents 156 | */ 157 | protected function assertHasNotHandlebars($handle, $contents) 158 | { 159 | $this->assertEquals(0, preg_match('/\'\{(\s)'.preg_quote($handle).'(\s)\}\'/', $contents)); 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /tests/fixtures/lang/en/messages.php: -------------------------------------------------------------------------------- 1 | 'Home', 6 | 'login' => 'Login', 7 | 'pleaseLogin' => 'Please use your username and password to continue:', 8 | 'speakers' => 'Speakers', 9 | 'congregations' => 'Congregations', 10 | 'talks' => 'Talks', 11 | 'loggedAs' => 'Logged as:', 12 | 'badUsernameOrPassword' => 'Bad username or password.', 13 | 'successfullyLoggedOut' => 'You have been successfully logged out.', 14 | 'welcome' => 'Welcome', 15 | 'unexpectedError' => 'Sorry, an unexpected error occurred...', 16 | 'noCongregations' => 'There\'s no congregations.', 17 | 'createOne' => 'Create one', 18 | 'createCongregation' => 'Create Congregation', 19 | 'datetimeFormat' => 'Format: HH:MM', 20 | 'formErrors' => 'Please, double check forms for errors.', 21 | 'congregationCreated' => 'Congregation created successfully!', 22 | 'id' => 'Id', 23 | 'name' => 'Name', 24 | 'address' => 'Address', 25 | 'actions' => 'Actions', 26 | 'edit' => 'Edit', 27 | 'editCongregation' => 'Edit Congregation', 28 | 'update' => 'Update', 29 | 'cancel' => 'Cancel', 30 | 'pmDayOfWeek' => 'Public Meeting Day', 31 | 'pmTime' => 'Public Meeting Time', 32 | 'create' => 'Create', 33 | 'congregationUpdated' => 'Congregation updated!', 34 | 'settings' => 'Settings', 35 | 'save' => 'Save', 36 | 'language' => 'Language', 37 | 'settingsSaved' => 'Settings saved successfully!', 38 | 'family' => array( 39 | 'father' => 'John', 40 | 'mother' => 'Susan', 41 | 'children' => array( 42 | 'son' => 'Jimmy', 43 | ), 44 | ), 45 | 'plural' => 'one apple|a million apples' 46 | ); 47 | -------------------------------------------------------------------------------- /tests/fixtures/lang/en/pagination.php: -------------------------------------------------------------------------------- 1 | '« Previous', 17 | 18 | 'next' => 'Next »', 19 | 20 | ); -------------------------------------------------------------------------------- /tests/fixtures/lang/en/reminders.php: -------------------------------------------------------------------------------- 1 | "Passwords must be at least six characters and match the confirmation.", 17 | 18 | "user" => "We can't find a user with that e-mail address.", 19 | 20 | "token" => "This password reset token is invalid.", 21 | 22 | "sent" => "Password reminder sent!", 23 | 24 | ); 25 | -------------------------------------------------------------------------------- /tests/fixtures/lang/en/validation.php: -------------------------------------------------------------------------------- 1 | "The :attribute must be accepted.", 17 | "active_url" => "The :attribute is not a valid URL.", 18 | "after" => "The :attribute must be a date after :date.", 19 | "alpha" => "The :attribute may only contain letters.", 20 | "alpha_dash" => "The :attribute may only contain letters, numbers, and dashes.", 21 | "alpha_num" => "The :attribute may only contain letters and numbers.", 22 | "array" => "The :attribute must be an array.", 23 | "before" => "The :attribute must be a date before :date.", 24 | "between" => array( 25 | "numeric" => "The :attribute must be between :min and :max.", 26 | "file" => "The :attribute must be between :min and :max kilobytes.", 27 | "string" => "The :attribute must be between :min and :max characters.", 28 | "array" => "The :attribute must have between :min and :max items.", 29 | ), 30 | "confirmed" => "The :attribute confirmation does not match.", 31 | "date" => "The :attribute is not a valid date.", 32 | "date_format" => "The :attribute does not match the format :format.", 33 | "different" => "The :attribute and :other must be different.", 34 | "digits" => "The :attribute must be :digits digits.", 35 | "digits_between" => "The :attribute must be between :min and :max digits.", 36 | "email" => "The :attribute must be a valid email address.", 37 | "exists" => "The selected :attribute is invalid.", 38 | "image" => "The :attribute must be an image.", 39 | "in" => "The selected :attribute is invalid.", 40 | "integer" => "The :attribute must be an integer.", 41 | "ip" => "The :attribute must be a valid IP address.", 42 | "max" => array( 43 | "numeric" => "The :attribute may not be greater than :max.", 44 | "file" => "The :attribute may not be greater than :max kilobytes.", 45 | "string" => "The :attribute may not be greater than :max characters.", 46 | "array" => "The :attribute may not have more than :max items.", 47 | ), 48 | "mimes" => "The :attribute must be a file of type: :values.", 49 | "min" => array( 50 | "numeric" => "The :attribute must be at least :min.", 51 | "file" => "The :attribute must be at least :min kilobytes.", 52 | "string" => "The :attribute must be at least :min characters.", 53 | "array" => "The :attribute must have at least :min items.", 54 | ), 55 | "not_in" => "The selected :attribute is invalid.", 56 | "numeric" => "The :attribute must be a number.", 57 | "regex" => "The :attribute format is invalid.", 58 | "required" => "The :attribute field is required.", 59 | "required_if" => "The :attribute field is required when :other is :value.", 60 | "required_with" => "The :attribute field is required when :values is present.", 61 | "required_with_all" => "The :attribute field is required when :values is present.", 62 | "required_without" => "The :attribute field is required when :values is not present.", 63 | "required_without_all" => "The :attribute field is required when none of :values are present.", 64 | "same" => "The :attribute and :other must match.", 65 | "size" => array( 66 | "numeric" => "The :attribute must be :size.", 67 | "file" => "The :attribute must be :size kilobytes.", 68 | "string" => "The :attribute must be :size characters.", 69 | "array" => "The :attribute must contain :size items.", 70 | ), 71 | "unique" => "The :attribute has already been taken.", 72 | "url" => "The :attribute format is invalid.", 73 | 74 | /* 75 | |-------------------------------------------------------------------------- 76 | | Custom Validation Language Lines 77 | |-------------------------------------------------------------------------- 78 | | 79 | | Here you may specify custom validation messages for attributes using the 80 | | convention "attribute.rule" to name the lines. This makes it quick to 81 | | specify a specific custom language line for a given attribute rule. 82 | | 83 | */ 84 | 85 | 'custom' => array( 86 | 'attribute-name' => array( 87 | 'rule-name' => 'custom-message', 88 | ), 89 | ), 90 | 91 | /* 92 | |-------------------------------------------------------------------------- 93 | | Custom Validation Attributes 94 | |-------------------------------------------------------------------------- 95 | | 96 | | The following language lines are used to swap attribute place-holders 97 | | with something more reader friendly such as E-Mail Address instead 98 | | of "email". This simply helps us make messages a little cleaner. 99 | | 100 | */ 101 | 102 | 'attributes' => array(), 103 | 104 | ); 105 | -------------------------------------------------------------------------------- /tests/fixtures/lang/es/messages.php: -------------------------------------------------------------------------------- 1 | 'Inicio', 6 | 'login' => 'Login', 7 | 'pleaseLogin' => 'Por favor, utilice su nombre de usuario y contraseña para continuar:', 8 | 'speakers' => 'Oradores', 9 | 'congregations' => 'Congregaciones', 10 | 'talks' => 'Discursos', 11 | 'loggedAs' => 'Logueado como:', 12 | 'badUsernameOrPassword' => 'El nombre de usuario y/o contraseñas están incorrectos.', 13 | 'successfullyLoggedOut' => 'Has salido del sistema satisfactoriamente!', 14 | 'welcome' => 'Bienvenido', 15 | 'unexpectedError' => 'Lo sentimos, ha ocurrido un error inesperado', 16 | 'noCongregations' => 'No hay congregaciones', 17 | 'createOne' => 'Crear una', 18 | 'createCongregation' => 'Crear Congrgación', 19 | 'datetimeFormat' => 'Formato: HH:MM', 20 | 'formErrors' => 'Por favor, verique errores en el formulario.', 21 | 'congregationCreated' => 'Congregación creada satisfactoriamente!', 22 | 'id' => 'Id', 23 | 'name' => 'Nombre', 24 | 'address' => 'Dirección', 25 | 'actions' => 'Acciones', 26 | 'edit' => 'Editar', 27 | 'editCongregation' => 'Editar Congregación', 28 | 'update' => 'Actualizar', 29 | 'cancel' => 'Cancelar', 30 | 'pmDayOfWeek' => 'Día del Discurso Público', 31 | 'pmTime' => 'Hora del Discurso Público', 32 | 'create' => 'Crear', 33 | 'congregationUpdated' => 'Congregación actualizada!', 34 | 'settings' => 'Configuración', 35 | 'save' => 'Guardar', 36 | 'language' => 'Idioma', 37 | 'settingsSaved' => 'Configuración guardada satisfactoriamente!', 38 | 'messagePlural' => 'Una manzana|Muchas manzanas' 39 | ); 40 | -------------------------------------------------------------------------------- /tests/fixtures/lang/ht/messages.php: -------------------------------------------------------------------------------- 1 | 'Akèy', 6 | 'login' => 'Aksede', 7 | 'pleaseLogin' => 'Tanpri, itilize non itilizatè ak mo de pas pou ou ka kontinye:', 8 | 'speakers' => 'Oratè', 9 | 'congregations' => 'Kongregasyon', 10 | 'talks' => 'Diskou', 11 | 'loggedAs' => 'Konekte kòm:', 12 | 'badUsernameOrPassword' => 'Non itilizatè a oswa mo de pas yo pa bon.', 13 | 'successfullyLoggedOut' => 'Ou sòti sistèm nan.', 14 | 'welcome' => 'Byenveni', 15 | 'unexpectedError' => 'Malerezman, gen yon pwoblèm ki pase...', 16 | 'noCongregations' => 'Pa gen kongregasyon.', 17 | 'createOne' => 'Kreye youn', 18 | 'createCongregation' => 'Kreye Kongregasyon', 19 | 'datetimeFormat' => 'Fòma: LL:MM', 20 | 'formErrors' => 'Tanpri, fè yon tcheke nan erè yo.', 21 | 'congregationCreated' => 'Kongregasyon kreye!', 22 | 'id' => 'Id', 23 | 'name' => 'Non', 24 | 'address' => 'Adrès', 25 | 'actions' => 'Aksyon', 26 | 'edit' => 'Modifye', 27 | 'editCongregation' => 'Modifye Kongregasyon', 28 | 'update' => 'Mete ajou', 29 | 'cancel' => 'Anile', 30 | 'pmDayOfWeek' => 'Jou Diskou Piblik', 31 | 'pmTime' => 'Lè Diskou Piblik', 32 | 'create' => 'Kreye', 33 | 'congregationUpdated' => 'Kongregasyon ajou!', 34 | 'settings' => 'Paramet', 35 | 'save' => 'Sere', 36 | 'language' => 'Langaj', 37 | 'settingsSaved' => 'Paramet sere!', 38 | 'messagePlural' => 'singular apple|plural apples' // don't know the language 39 | ); 40 | -------------------------------------------------------------------------------- /tests/output/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /tests/spec/data/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "en.validation": { 3 | "accepted": "The :attribute must be accepted.", 4 | "active_url": "The :attribute is not a valid URL.", 5 | "after": "The :attribute must be a date after :date.", 6 | "alpha": "The :attribute may only contain letters.", 7 | "alpha_dash": "The :attribute may only contain letters, numbers, and dashes.", 8 | "alpha_num": "The :attribute may only contain letters and numbers.", 9 | "array": "The :attribute must be an array.", 10 | "before": "The :attribute must be a date before :date.", 11 | "between": { 12 | "numeric": "The :attribute must be between :min and :max.", 13 | "file": "The :attribute must be between :min and :max kilobytes.", 14 | "string": "The :attribute must be between :min and :max characters.", 15 | "array": "The :attribute must have between :min and :max items." 16 | }, 17 | "confirmed": "The :attribute confirmation does not match.", 18 | "date": "The :attribute is not a valid date.", 19 | "date_format": "The :attribute does not match the format :format.", 20 | "different": "The :attribute and :other must be different.", 21 | "digits": "The :attribute must be :digits digits.", 22 | "digits_between": "The :attribute must be between :min and :max digits.", 23 | "email": "The :attribute must be a valid email address.", 24 | "exists": "The selected :attribute is invalid.", 25 | "image": "The :attribute must be an image.", 26 | "in": "The selected :attribute is invalid.", 27 | "integer": "The :attribute must be an integer.", 28 | "ip": "The :attribute must be a valid IP address.", 29 | "max": { 30 | "numeric": "The :attribute may not be greater than :max.", 31 | "file": "The :attribute may not be greater than :max kilobytes.", 32 | "string": "The :attribute may not be greater than :max characters.", 33 | "array": "The :attribute may not have more than :max items." 34 | }, 35 | "mimes": "The :attribute must be a file of type: :values.", 36 | "min": { 37 | "numeric": "The :attribute must be at least :min.", 38 | "file": "The :attribute must be at least :min kilobytes.", 39 | "string": "The :attribute must be at least :min characters.", 40 | "array": "The :attribute must have at least :min items." 41 | }, 42 | "not_in": "The selected :attribute is invalid.", 43 | "numeric": "The :attribute must be a number.", 44 | "regex": "The :attribute format is invalid.", 45 | "required": "The :attribute field is required.", 46 | "required_if": "The :attribute field is required when :other is :value.", 47 | "required_with": "The :attribute field is required when :values is present.", 48 | "required_with_all": "The :attribute field is required when :values is present.", 49 | "required_without": "The :attribute field is required when :values is not present.", 50 | "required_without_all": "The :attribute field is required when none of :values are present.", 51 | "same": "The :attribute and :other must match.", 52 | "size": { 53 | "numeric": "The :attribute must be :size.", 54 | "file": "The :attribute must be :size kilobytes.", 55 | "string": "The :attribute must be :size characters.", 56 | "array": "The :attribute must contain :size items." 57 | }, 58 | "unique": "The :attribute has already been taken.", 59 | "url": "The :attribute format is invalid.", 60 | "custom": { 61 | "attribute-name": { 62 | "rule-name": "custom-message" 63 | } 64 | }, 65 | "attributes": [] 66 | }, 67 | "en.messages": { 68 | "home": "Home", 69 | "login": "Login", 70 | "pleaseLogin": "Please use your username and password to continue:", 71 | "speakers": "Speakers", 72 | "congregations": "Congregations", 73 | "talks": "Talks", 74 | "loggedAs": "Logged as:", 75 | "badUsernameOrPassword": "Bad username or password.", 76 | "successfullyLoggedOut": "You have been successfully logged out.", 77 | "welcome": "Welcome", 78 | "unexpectedError": "Sorry, an unexpected error occurred...", 79 | "noCongregations": "There's no congregations.", 80 | "createOne": "Create one", 81 | "createCongregation": "Create Congregation", 82 | "datetimeFormat": "Format: HH:MM", 83 | "formErrors": "Please, double check forms for errors.", 84 | "congregationCreated": "Congregation created successfully!", 85 | "id": "Id", 86 | "name": "Name", 87 | "address": "Address", 88 | "actions": "Actions", 89 | "edit": "Edit", 90 | "editCongregation": "Edit Congregation", 91 | "update": "Update", 92 | "cancel": "Cancel", 93 | "pmDayOfWeek": "Public Meeting Day", 94 | "pmTime": "Public Meeting Time", 95 | "create": "Create", 96 | "congregationUpdated": "Congregation updated!", 97 | "settings": "Settings", 98 | "save": "Save", 99 | "language": "Language", 100 | "settingsSaved": "Settings saved successfully!", 101 | "family": { 102 | "father": "John", 103 | "mother": "Susan", 104 | "children": { 105 | "son": "Jimmy" 106 | } 107 | }, 108 | "plural": "{1} one apple|(1,+Inf) a million apples", 109 | "count" : "{0} There are none|[1,19] There are some|[20,Inf] There are many" 110 | }, 111 | "en.pagination": { 112 | "previous": "« Previous", 113 | "next": "Next »" 114 | }, 115 | "en.reminders": { 116 | "password": "Passwords must be at least six characters and match the confirmation.", 117 | "user": "We can't find a user with that e-mail address.", 118 | "token": "This password reset token is invalid.", 119 | "sent": "Password reminder sent!" 120 | }, 121 | "ht.messages": { 122 | "home": "Ak\u00e8y", 123 | "login": "Aksede", 124 | "pleaseLogin": "Tanpri, itilize non itilizat\u00e8 ak mo de pas pou ou ka kontinye:", 125 | "speakers": "Orat\u00e8", 126 | "congregations": "Kongregasyon", 127 | "talks": "Diskou", 128 | "loggedAs": "Konekte k\u00f2m:", 129 | "badUsernameOrPassword": "Non itilizat\u00e8 a oswa mo de pas yo pa bon.", 130 | "successfullyLoggedOut": "Ou s\u00f2ti sist\u00e8m nan.", 131 | "welcome": "Byenveni", 132 | "unexpectedError": "Malerezman, gen yon pwobl\u00e8m ki pase...", 133 | "noCongregations": "Pa gen kongregasyon.", 134 | "createOne": "Kreye youn", 135 | "createCongregation": "Kreye Kongregasyon", 136 | "datetimeFormat": "F\u00f2ma: LL:MM", 137 | "formErrors": "Tanpri, f\u00e8 yon tcheke nan er\u00e8 yo.", 138 | "congregationCreated": "Kongregasyon kreye!", 139 | "id": "Id", 140 | "name": "Non", 141 | "address": "Adr\u00e8s", 142 | "actions": "Aksyon", 143 | "edit": "Modifye", 144 | "editCongregation": "Modifye Kongregasyon", 145 | "update": "Mete ajou", 146 | "cancel": "Anile", 147 | "pmDayOfWeek": "Jou Diskou Piblik", 148 | "pmTime": "L\u00e8 Diskou Piblik", 149 | "create": "Kreye", 150 | "congregationUpdated": "Kongregasyon ajou!", 151 | "settings": "Paramet", 152 | "save": "Sere", 153 | "language": "Langaj", 154 | "settingsSaved": "Paramet sere!" 155 | }, 156 | "es.messages": { 157 | "home": "Inicio", 158 | "login": "Login", 159 | "pleaseLogin": "Por favor, utilice su nombre de usuario y contrase\u00f1a para continuar:", 160 | "speakers": "Oradores", 161 | "congregations": "Congregaciones", 162 | "talks": "Discursos", 163 | "loggedAs": "Logueado como:", 164 | "badUsernameOrPassword": "El nombre de usuario y\/o contrase\u00f1as est\u00e1n incorrectos.", 165 | "successfullyLoggedOut": "Has salido del sistema satisfactoriamente!", 166 | "welcome": "Bienvenido", 167 | "unexpectedError": "Lo sentimos, ha ocurrido un error inesperado", 168 | "noCongregations": "No hay congregaciones", 169 | "createOne": "Crear una", 170 | "createCongregation": "Crear Congrgaci\u00f3n", 171 | "datetimeFormat": "Formato: HH:MM", 172 | "formErrors": "Por favor, verique errores en el formulario.", 173 | "congregationCreated": "Congregaci\u00f3n creada satisfactoriamente!", 174 | "id": "Id", 175 | "name": "Nombre", 176 | "address": "Direcci\u00f3n", 177 | "actions": "Acciones", 178 | "edit": "Editar", 179 | "editCongregation": "Editar Congregaci\u00f3n", 180 | "update": "Actualizar", 181 | "cancel": "Cancelar", 182 | "pmDayOfWeek": "D\u00eda del Discurso P\u00fablico", 183 | "pmTime": "Hora del Discurso P\u00fablico", 184 | "create": "Crear", 185 | "congregationUpdated": "Congregaci\u00f3n actualizada!", 186 | "settings": "Configuraci\u00f3n", 187 | "save": "Guardar", 188 | "language": "Idioma", 189 | "settingsSaved": "Configuraci\u00f3n guardada satisfactoriamente!" 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /tests/spec/lang_choice_spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var util = require('util'); 4 | var Lang = require('../../src/js/lang.js'); 5 | var messages = require('./data/messages'); 6 | 7 | Lang.setMessages(messages); 8 | 9 | describe('The Lang.choice() method', function() { 10 | 11 | it('should exists', function() { 12 | expect(Lang.choice).toBeDefined(); 13 | }); 14 | 15 | it('should be a function', function() { 16 | expect(typeof Lang.choice).toBe('function'); 17 | }); 18 | 19 | it('should return the passed key when not found', function() { 20 | expect(Lang.choice('foo.bar', 1)).toBe('foo.bar'); 21 | expect(Lang.choice(null, 1)).toBe(null); 22 | }); 23 | 24 | it('should return the expected message', function() { 25 | expect(Lang.choice('messages.plural', 1)).toBe('one apple'); 26 | expect(Lang.choice('messages.plural', 10)).toBe('a million apples'); 27 | }); 28 | 29 | it('should count correctly', function() { 30 | expect(Lang.choice('messages.count', 0)).toBe('There are none'); 31 | expect(Lang.choice('messages.count', 1)).toBe('There are some'); 32 | expect(Lang.choice('messages.count', 10)).toBe('There are some'); 33 | expect(Lang.choice('messages.count', 19)).toBe('There are some'); 34 | expect(Lang.choice('messages.count', 20)).toBe('There are many'); 35 | expect(Lang.choice('messages.count', 30)).toBe('There are many'); 36 | }); 37 | 38 | it('should return the expected message with replacements', function() { 39 | expect(Lang.choice('validation.accepted', 1)).toBe('The :attribute must be accepted.'); 40 | expect(Lang.choice('validation.accepted', 1, { 41 | 'attribute': 'foo' 42 | })).toBe('The foo must be accepted.'); 43 | }); 44 | 45 | }); 46 | -------------------------------------------------------------------------------- /tests/spec/lang_get_spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var util = require('util'); 4 | var Lang = require('../../src/js/lang.js'); 5 | var messages = require('./data/messages'); 6 | 7 | Lang.setMessages(messages); 8 | 9 | describe('The Lang.get() method', function() { 10 | 11 | it('should exists', function() { 12 | expect(Lang.get).toBeDefined(); 13 | }); 14 | 15 | it('should be a function', function() { 16 | expect(typeof Lang.get).toBe('function'); 17 | }); 18 | 19 | it('should return the passed key when not found', function() { 20 | expect(Lang.get('foo.bar')).toBe('foo.bar'); 21 | expect(Lang.get(null)).toBe(null); 22 | }); 23 | 24 | it('should return the expected message', function() { 25 | expect(Lang.get('messages.home')).toBe('Home'); 26 | }); 27 | 28 | it('should return the expected nested message', function() { 29 | expect(Lang.get('messages.family.father')).toBe('John'); 30 | expect(Lang.get('messages.family.children.son')).toBe('Jimmy'); 31 | }); 32 | 33 | it('should return the expected object', function() { 34 | expect(Lang.get('messages.family.children')).toEqual({ son : 'Jimmy' }); 35 | }); 36 | 37 | it('should return the passed key when nested message does not point to a message', function() { 38 | expect(Lang.get('a.b.c.d.f.g.h.i.j.k')).toBe('a.b.c.d.f.g.h.i.j.k'); 39 | }); 40 | 41 | it('should return the expected message with replacements', function() { 42 | expect(Lang.get('validation.accepted')).toBe('The :attribute must be accepted.'); 43 | expect(Lang.get('validation.accepted', { 44 | 'attribute': 'foo' 45 | })).toBe('The foo must be accepted.'); 46 | }); 47 | 48 | }); 49 | -------------------------------------------------------------------------------- /tests/spec/lang_has_spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var util = require('util'); 4 | var Lang = require('../../src/js/lang.js'); 5 | var messages = require('./data/messages'); 6 | 7 | Lang.setMessages(messages); 8 | 9 | describe('The Lang.has() method', function() { 10 | 11 | it('should exists', function() { 12 | expect(Lang.has).toBeDefined(); 13 | }); 14 | 15 | it('should be a function', function() { 16 | expect(typeof Lang.has).toBe('function'); 17 | }); 18 | 19 | it('should return false when the given key is no defined', function() { 20 | expect(Lang.has('foo.bar')).toBe(false); 21 | expect(Lang.has(null)).toBe(false); 22 | }); 23 | 24 | it('should return true when the given key is defined', function() { 25 | expect(Lang.has('messages.home')).toBe(true); 26 | expect(Lang.has('validation.accepted')).toBe(true); 27 | }); 28 | 29 | }); 30 | -------------------------------------------------------------------------------- /tests/spec/lang_locale_spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var util = require('util'); 4 | var Lang = require('../../src/js/lang.js'); 5 | var messages = require('./data/messages'); 6 | 7 | Lang.setMessages(messages); 8 | 9 | describe('The Lang\'s locale methods', function() { 10 | 11 | it('should have a getLocale', function() { 12 | expect(Lang.getLocale).toBeDefined(); 13 | expect(typeof Lang.getLocale).toBe('function'); 14 | }); 15 | 16 | it('should have a setLocale', function() { 17 | expect(Lang.setLocale).toBeDefined(); 18 | expect(typeof Lang.setLocale).toBe('function'); 19 | }); 20 | 21 | it('should return the default locale', function() { 22 | expect(Lang.getLocale()).toBe('en'); 23 | }); 24 | 25 | it('should return the locale specified', function() { 26 | Lang.setLocale('es'); 27 | expect(Lang.getLocale()).toBe('es'); 28 | }); 29 | 30 | it('should affect messages', function() { 31 | Lang.setLocale('es'); 32 | var es = Lang.get('messages.home'); 33 | Lang.setLocale('en'); 34 | var en = Lang.get('messages.home'); 35 | expect(es).not.toBe(en); 36 | }); 37 | 38 | }); 39 | --------------------------------------------------------------------------------