├── .github ├── issue_template.md └── workflows │ ├── publish.yml │ └── tests.yml ├── .phpunit.result.cache ├── .release-it.json ├── .styleci.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── Makefile ├── README.md ├── babel.config.json ├── composer.json ├── config └── matice.php ├── jest.config.json ├── package-lock.json ├── package.json ├── src ├── BladeTranslationsGenerator.php ├── Commands │ └── TranslationsGeneratorCommand.php ├── Exceptions │ └── LocaleTranslationsFileOrDirNotFoundException.php ├── Facades │ └── Matice.php ├── Helpers.php ├── MaticeServiceProvider.php └── js │ ├── Localization │ ├── MaticeLocalizationConfig.ts │ └── core.ts │ └── matice.ts ├── tsconfig.json └── webpack.config.js /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | ### Expected behavior 2 | 3 | 4 | 5 | ### Current behavior 6 | 7 | 8 | 9 | 10 | 11 | 12 | ### Versions 13 | 14 | - **Laravel**: #.#.# 15 | - **Matice**: #.#.# 16 | 17 | ### Transalations 18 | 19 | 20 | 21 | ```php 22 | ['greet' => 'It greets the wrong way.'] 23 | 24 | trans('greet') // Something strange. 25 | ``` 26 | 27 | ### Contents of `Matice.translations` 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | publish: 9 | name: Publish to NPM 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v2 15 | 16 | - name: Set up NPM 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: '12.x' 20 | registry-url: 'https://registry.npmjs.org' 21 | 22 | - name: Build and publish 23 | env: 24 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 25 | run: | 26 | npm install 27 | npm --no-git-tag-version version $(git describe --tags) --allow-same-version 28 | npm publish --access public 29 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - master 8 | - 1.x 9 | 10 | jobs: 11 | test: 12 | name: PHP ${{ matrix.php }}, Laravel ${{ matrix.laravel }} 13 | runs-on: ubuntu-latest 14 | 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | php: [7.2, 7.3, 7.4, 8.0, 8.1, 8.2] 19 | laravel: [6.*, 7.*, 8.*, 9.*] 20 | exclude: 21 | # Laravel >= 6.0 doesn't support PHP 7.2 or >= 8.1 22 | - laravel: 6.* 23 | php: 8.1 24 | - laravel: 6.* 25 | php: 8.2 26 | 27 | # Laravel >= 7.0 doesn't support PHP 7.2 or >= 8.1 28 | - laravel: 7.* 29 | php: 8.1 30 | - laravel: 7.* 31 | php: 8.2 32 | 33 | # Laravel >= 8.0 doesn't support PHP 7.2 or >= 8.2 34 | - laravel: 8.* 35 | php: 7.2 36 | - laravel: 8.* 37 | php: 8.2 38 | 39 | # Laravel >= 9.0 doesn't support PHP <= 7.4 40 | - laravel: 9.* 41 | php: 7.2 42 | - laravel: 9.* 43 | php: 7.3 44 | - laravel: 9.* 45 | php: 7.4 46 | 47 | include: 48 | - laravel: 6.* 49 | testbench: 4.* 50 | - laravel: 7.* 51 | testbench: 5.* 52 | - laravel: 8.* 53 | testbench: 6.* 54 | - laravel: 9.* 55 | testbench: 7.* 56 | 57 | steps: 58 | - name: Checkout 59 | uses: actions/checkout@v2 60 | 61 | - name: Set up PHP 62 | uses: shivammathur/setup-php@v2 63 | with: 64 | php-version: ${{ matrix.php }} 65 | 66 | - name: Cache Composer dependencies 67 | uses: actions/cache@v1 68 | with: 69 | path: ~/.composer/cache/files 70 | key: php-${{ matrix.php }}-laravel-${{ matrix.laravel }}-composer-${{ hashFiles('**/composer.lock') }} 71 | restore-keys: php-${{ matrix.php }}-laravel-${{ matrix.laravel }}-composer- 72 | 73 | - name: Cache npm dependencies 74 | uses: actions/cache@v1 75 | with: 76 | path: ~/.npm 77 | key: npm-${{ hashFiles('**/package-lock.json') }} 78 | restore-keys: npm- 79 | 80 | - name: Install dependencies 81 | run: | 82 | composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update 83 | composer update --prefer-dist --no-interaction --no-progress --no-suggest 84 | npm install 85 | 86 | - name: Build 87 | run: npm run build 88 | 89 | - name: Run PHPUnit tests 90 | run: vendor/bin/phpunit --testdox --colors=always 91 | 92 | - name: Run Jest tests 93 | run: npm test 94 | -------------------------------------------------------------------------------- /.phpunit.result.cache: -------------------------------------------------------------------------------- 1 | {"version":1,"defects":{"Genl\\Matice\\Tests\\Unit\\ManageTranslationTest::test_load_translations":3,"Genl\\Matice\\Tests\\Unit\\ManageTranslationTest::test_load_translations_from_additional_json_paths":3},"times":{"Genl\\Matice\\Tests\\Unit\\ManageTranslationTest::test_load_translations":0.043,"Genl\\Matice\\Tests\\Unit\\ManageTranslationTest::test_generate_translation_js":0.003,"Genl\\Matice\\Tests\\Unit\\ManageTranslationTest::test_namespaces_can_be_excepted":0.003,"Genl\\Matice\\Tests\\Unit\\ManageTranslationTest::test_only_certain_namespaces_can_be_exported":0.003,"Genl\\Matice\\Tests\\Unit\\ManageTranslationTest::test_only_namespaces_can_be_both_exported_and_excepted":0.003,"Genl\\Matice\\Tests\\Unit\\ManageTranslationTest::test_load_translations_from_additional_json_paths":0.003}} -------------------------------------------------------------------------------- /.release-it.json: -------------------------------------------------------------------------------- 1 | { 2 | "npm": { 3 | "publish": true 4 | }, 5 | "hooks": { 6 | "before:init": ["yarn test", "yarn build"] 7 | }, 8 | "github": { 9 | "release": true 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.styleci.yml: -------------------------------------------------------------------------------- 1 | preset: laravel 2 | 3 | disabled: 4 | - single_class_element_per_statement 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `matice` will be documented in this file 4 | 5 | ## 1.0.0 - 201X-XX-XX 6 | 7 | - initial release 8 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are **welcome** and will be fully **credited**. 4 | 5 | Please read and understand the contribution guide before creating an issue or pull request. 6 | 7 | ## Etiquette 8 | 9 | This project is open source, and as such, the maintainers give their free time to build and maintain the source code 10 | held within. They make the code freely available in the hope that it will be of use to other developers. It would be 11 | extremely unfair for them to suffer abuse or anger for their hard work. 12 | 13 | Please be considerate towards maintainers when raising issues or presenting pull requests. Let's show the 14 | world that developers are civilized and selfless people. 15 | 16 | It's the duty of the maintainer to ensure that all submissions to the project are of sufficient 17 | quality to benefit the project. Many developers have different skillsets, strengths, and weaknesses. Respect the maintainer's decision, and do not be upset or abusive if your submission is not used. 18 | 19 | ## Viability 20 | 21 | When requesting or submitting new features, first consider whether it might be useful to others. Open 22 | source projects are used by many developers, who may have entirely different needs to your own. Think about 23 | whether or not your feature is likely to be used by other users of the project. 24 | 25 | ## Procedure 26 | 27 | Before filing an issue: 28 | 29 | - Attempt to replicate the problem, to ensure that it wasn't a coincidental incident. 30 | - Check to make sure your feature suggestion isn't already present within the project. 31 | - Check the pull requests tab to ensure that the bug doesn't have a fix in progress. 32 | - Check the pull requests tab to ensure that the feature isn't already in progress. 33 | 34 | Before submitting a pull request: 35 | 36 | - Check the codebase to ensure that your feature doesn't already exist. 37 | - Check the pull requests to ensure that another person hasn't already submitted the feature or fix. 38 | 39 | ## Requirements 40 | 41 | If the project maintainer has any additional requirements, you will find them listed here. 42 | 43 | - **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - The easiest way to apply the conventions is to install [PHP Code Sniffer](https://pear.php.net/package/PHP_CodeSniffer). 44 | 45 | - **Add tests!** - Your patch won't be accepted if it doesn't have tests. 46 | 47 | - **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. 48 | 49 | - **Consider our release cycle** - We try to follow [SemVer v2.0.0](https://semver.org/). Randomly breaking public APIs is not an option. 50 | 51 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. 52 | 53 | - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](https://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. 54 | 55 | **Happy coding**! 56 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) GENL 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. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | rnpm: ## Release the package and publish on npm -- DEPRECATED 2 | release-it --dry-run 3 | 4 | publish: # publish on npm publish package on NPM. 5 | npm publish 6 | 7 | test-front: 8 | yarn test 9 | 10 | 11 | test-back: 12 | vendor/bin/phpunit --testdox --colors=always -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Use your Laravel translations in JavaScript 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/genl/matice.svg?style=flat-square)](https://packagist.org/packages/genl/matice) 4 | [![Latest Version on NPM](https://img.shields.io/npm/v/matice.svg?style=flat)](https://npmjs.com/package/matice) 5 | [![Tests](https://github.com/GENL/matice/actions/workflows/tests.yml/badge.svg)](https://github.com/GENL/matice/actions/workflows/tests.yml) 6 | [![Total Downloads on packagist](https://img.shields.io/packagist/dt/genl/matice.svg?style=flat-square)](https://packagist.org/packages/genl/matice/stats) 7 | [![Downloads on NPM](https://img.shields.io/npm/dt/matice.svg?style=flat)](https://www.npmjs.com/package/matice) 8 | 9 | ![Logo](https://banners.beyondco.de/Matice.png?theme=dark&packageManager=composer+require&packageName=genl%2Fmatice&pattern=architect&style=style_1&description=Use+your+Laravel+translations+in+JavaScript&md=1&showWatermark=0&fontSize=100px&images=cube) 10 | 11 | Matice creates a Blade directive that you can include in your views. 12 | It will export a JavaScript object of your Laravel application's translations, 13 | keyed by their names (aliases, lang, filenames, folders name), 14 | as well as globals trans(), __() and transChoice() helper 15 | functions which you can use to access your translations in your JavaScript. 16 | 17 | 18 | - [Installation](#installation) 19 | - [Usage](#usage) 20 | - [Examples](#examples) 21 | - [trans](#trans) 22 | - [Update locale](#update-locale) 23 | - [Pluralization](#pluralization) 24 | - [Trans Choice](#trans-choice) 25 | - [underscore function](#underscore-function) 26 | - [Default Values](#default-values) 27 | - [Retrieve the current locale](#retrieve-the-current-locale) 28 | - [Force locale](#force-locale) 29 | - [Filtering translations](#filtering-translations) 30 | - [Filtering namespaces](#filtering-namespaces) 31 | - [Use with SPA](#use-with-spa) 32 | - [Using with Vue Components](#using-with-vue-components) 33 | - [Dive Deeper](#dive-deeper) 34 | 35 | ## Installation 36 | 37 | You can install the package via composer: 38 | 39 | ```bash 40 | composer require genl/matice 41 | ``` 42 | 43 | 1. ##### Include our Blade directive (`@translations`) somewhere in your template before your main application JavaScript is loaded—likely in the header somewhere. 44 | 1. ##### Publish the vendor if you want to customize config: 45 | ```bash 46 | php artisan vendor:publish --provider="Genl\Matice\MaticeServiceProvider" 47 | ``` 48 | 49 | Matice is available as an NPM `matice` package 50 | which exposes the `trans()` function for use in frontend applications 51 | that do not use Blade. 52 | You can install the NPM package with: 53 | ```bash 54 | // With yarn 55 | yarn add matice 56 | 57 | With npm 58 | npm install matice 59 | ``` 60 | 61 | or load it from a CDN: 62 | ```html 63 | 64 | 65 | ``` 66 | 67 | * Note that the JavaScript package only contains the translations logic. You have to generate your translations file and make 68 | it available to your frontend app. The blade directive `@translations` will do it for you anytime you reload the page. 69 | 70 | **TypeScript support** 71 | 72 | Matice is fully written in TypeScript so, it's compatible with TypeScript projects. 73 | 74 | 75 | 76 | ## Usage 77 | 78 | * ##### Core concepts 79 | 80 | Matice comes with almost the same localization concepts as Laravel. 81 | Read more about [Laravel localization](https://laravel.com/docs/localization) 82 | 83 | This package uses the `@translations` directive to inject a JavaScript object containing all of your application's translations, keyed by their names. This collection is available globally on the client side in the `window.Matice` object. 84 | The `@translations` directive accepts a list of locales to be loaded under th form of an array or a comma seperated string. 85 | If no locales are given, all the translations will be loaded. 86 | 87 | #### Examples 88 | 89 | import the `trans()` function like follow: 90 | ```php 91 | @translations(['en', 'fr']) 92 | 93 | or 94 | 95 | @translations('en, fr') 96 | ``` 97 | 98 | 99 | The package also creates an optional `trans()` JavaScript helper which works like Laravel's PHP `trans()` helper to retrieve translation strings. 100 | 101 | 102 | #### Examples 103 | 104 | import the `trans()` function like follow: 105 | ```javascript 106 | import { trans } from "matice"; 107 | ``` 108 | 109 | Let's assume we have this translations file: 110 | 111 | ```php 112 | // resources/lang/en/custom.php 113 | 114 | return [ 115 | 'greet' => [ 116 | 'me' => 'Hello!', 117 | 'someone' => 'Hello :name!', 118 | 'me_more' => 'Hello Ekcel Henrich!', 119 | 'people' =>'Hello Ekcel!|Hello everyone!', 120 | ], 121 | 'balance' => "{0} You're broke|[1000, 5000] a middle man|[1000000,*] You are awesome :name, :count Million Dollars" 122 | ]; 123 | ``` 124 | 125 | ```php 126 | // resources/lang/fr/custom.php 127 | 128 | return [ 129 | 'greet' => [ 130 | 'me' => 'Bonjour!' 131 | ] 132 | ]; 133 | ``` 134 | 135 | #### trans 136 | 137 | Retrieve a text: 138 | ```javascript 139 | let sentence = '' 140 | 141 | sentence = trans('greet.me') // Hello! 142 | 143 | // With parameters 144 | sentence = trans('greet.someone', {args: {name: "Ekcel"}}) // Hello Ekcel! 145 | ``` 146 | 147 | #### Update locale 148 | 149 | Matice exposes `setLocale` function to change the locale that is used by the `trans` function. 150 | ```javascript 151 | import { setLocale } from "matice" 152 | 153 | // update the locale 154 | setLocale('fr') 155 | const sentence = trans('greet.me') // Bonjour! 156 | ``` 157 | 158 | #### Pluralization 159 | 160 | On pluralization, the choice depends on the `count` argument. To activate pluralization 161 | pass the argument `pluralize` to true. 162 | 163 | ```javascript 164 | // Simple pluralization 165 | sentence = trans('greet.people', {args: {count: 0}, pluralize: true}) // Hello Ekcel! 166 | sentence = trans('greet.people', {args: {count: 2}, pluralize: true}) // Hello everyone! 167 | 168 | // Advanced pluralization with range. Works the same way. 169 | // [balance => '{0} You're broke|[1000, 5000] a middle man|[1000000,*] You are awesome :name, :count Million Dollars'] 170 | sentence = trans('balance', {args: {count: 0}, pluralize: true}) // You're broke 171 | sentence = trans('balance', {args: {count: 3000}, pluralize: true}) // a middle man 172 | ``` 173 | 174 | #### Trans Choice 175 | 176 | Matice provides a helper function for pluralization 177 | 178 | ```javascript 179 | import { transChoice } from "matice" 180 | 181 | let sentence = transChoice('balance', 17433085, {name: 'Ekcel'}) // You are awesome Ekcel, 17433085 Million Dollars 182 | ``` 183 | 184 | 185 | #### Underscore function 186 | * As well of the `trans()` function, Matice provide `__()` that does the same as the `trans()` function but with this particularity 187 | not to throw an error when the key is not found but returns the key itself. 188 | 189 | `transChoice()` always throws an error if the key is not found. To change this behaviour, use `__(key, {pluralize: true})` 190 | 191 | ```js 192 | sentence = trans('greet.unknown') // -> throws an error with a message. 193 | sentence = __('greet.unknown') // greet.unknown 194 | ``` 195 | 196 | #### Default values 197 | 198 | Matice uses your current app locale `app()->getLocale()` as the default locale when generating the translation object with the blade directive `@translations`. 199 | Same applies when generating with command line. 200 | 201 | When Matice does not find a key, it falls back to the default locale and search again. The fallback is the 202 | same you define in your `config.app.fallback_locale`. 203 | 204 | ```php 205 | // config/app.php 206 | 207 | 'locale' => 'fr', 208 | 'fallback_locale' => 'en', 209 | ``` 210 | 211 | #### Retrieve the current locale 212 | Matice exposes the `MaticeLocalizationConfig` class : 213 | ```js 214 | import { MaticeLocalizationConfig } from "matice" 215 | 216 | const locale = MaticeLocalizationConfig.locale // 'en' 217 | 218 | const fallbackLocale = MaticeLocalizationConfig.fallbackLocale // 'en' 219 | 220 | const locales = MaticeLocalizationConfig.locales // ['en', 'fr'] 221 | ``` 222 | 223 | Matice also provides helpers to deal with locales information: 224 | ```js 225 | import { setLocale, getLocale, locales } from "matice" 226 | 227 | // Update the locale 228 | setLocale('fr') // 229 | 230 | const locale = getLocale() // 'fr' 231 | 232 | const locales = locales() // ['en', 'fr'] 233 | ``` 234 | 235 | #### Force locale 236 | With the version 1.1.4, it is possible to force the locale for a specific translation WITHOUT changing the global local. 237 | ```js 238 | setLocale('en') // Set the current locale to English. 239 | 240 | trans('greet.me') // "Hello!" 241 | trans('greet.me', { locale: 'fr' }) // "Bonjour!" 242 | trans('greet.me', { args: {}, locale: 'fr' }) // "Bonjour!" 243 | 244 | __('greet.me', { locale: 'fr' }) // "Bonjour!" 245 | 246 | transChoice('greet.me', 1, undefined, 'fr') // "Bonjour!" 247 | transChoice('greet.me', 1, {}, 'fr') // "Bonjour!" 248 | ``` 249 | 250 | 251 | ## Filtering translations 252 | Matice supports filtering the translations it makes available to your JavaScript, which is great to control the size of your 253 | data your translations become too large with thousands of lines. 254 | 255 | #### Filtering namespaces 256 | To set up namespace translations filtering, update in your config file either an `only` or `except` setting as an array of translations folders or files. 257 | `Note: Setting the same namespace both 'only' and 'except' will result to 'except'. But in the other cases, 'only' takes precedence over 'except'` 258 | 259 | ```php 260 | // config/matice.php 261 | 262 | return [ 263 | // Export only 264 | 'only' => [ 265 | 'fr/', // Take all the 'fr' directory with his children 266 | 'es', // Take all the 'es' directory with his children 267 | 'en/auth', // With or without the file extension 268 | 'en/pagination.php', 269 | 'en/validations', 270 | ], 271 | 272 | // Or... export everything except 273 | 'except' => [ 274 | 'en/passwords', 275 | 'en\\validations', 276 | ], 277 | ]; 278 | ``` 279 | 280 | The base directory is the lang_directory defined in the config file: `config('matice.lang_directory')`. 281 | 282 | ## Use with SPA 283 | Matice registers an Artisan console command to generate a `matice_translations.js` translations file, which can be used (or not) as part of an asset pipeline such as [Laravel Mix](https://laravel.com/docs/mix). 284 | 285 | You can run `php artisan matice:generate --no-export` in your project to generate a static translations file without the export statement in `resources/assets/js/matice_translations.js`. 286 | You can customize the generation path in the `config/matice.php` file. 287 | 288 | ```sh 289 | php artisan matice:generate --no-export 290 | ``` 291 | 292 | An example of `matice_translations.js`, where auth translations exist in `resources/lang/en/auth.php`: 293 | 294 | ```php 295 | // resources/lang/en/auth.php 296 | 297 | return [ 298 | 'failed' => 'These credentials do not match our records.', 299 | 'throttle' => 'Too many login attempts. Please try again in :seconds seconds.', 300 | ]; 301 | ``` 302 | 303 | ```js 304 | // matice_translations.js 305 | 306 | const Matice = { 307 | locale: 'en', 308 | fallbackLocale: 'en', 309 | translations: { 310 | en: { 311 | auth: { 312 | failed: 'These credentials do not match our records.', 313 | throttle: 'Too many login attempts. Please try again in :seconds seconds.' 314 | } 315 | } 316 | } 317 | }; 318 | 319 | ``` 320 | 321 | At this point you can use in javascript this translations file like usual, paste in your html as well. 322 | 323 | This is useful if your laravel and js app are separated like with SPA or PWA. So you can 324 | link the generated translations file with your JS App. If you're not in the case of SPA, WPA... 325 | you might never have to generate the translations manually because `@translations` directive already does 326 | it for you when the app environment is 'production' to improve performance. 327 | 328 | ```html 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 😃 344 | 345 | 346 | ``` 347 | 348 | Whenever your translation messages change, run `php artisan matice:generate` again. 349 | Remember to disable browser cache, it may have cached the old translations file. 350 | 351 | ## Using with Vue Components 352 | Basically, Matice can be integrated to any Javascript projects. Event with some big framework like Vue.js 353 | React.js or Angular. Some frameworks like Vue re-renders the UI dynamically. In this section we show you 354 | how to bind Matice with Vue 2. Based on this example we assume you can take inspiration to do the same with the framework you use for your project. 355 | For example, with React, you should re-render the whole app after `setLocale()` is called for the changes to be visible. 356 | 357 | Add this to your `app.js` file: 358 | 359 | ```javascript 360 | // app.js 361 | 362 | import {__, trans, setLocale, getLocale, transChoice, MaticeLocalizationConfig, locales} from "matice" 363 | 364 | Vue.mixin({ 365 | methods: { 366 | $trans: trans, 367 | $__: __, 368 | $transChoice: transChoice, 369 | $setLocale(locale: string) { 370 | setLocale(locale); 371 | this.$forceUpdate() // Refresh the vue instance(The whole app in case of SPA) after the locale changes. 372 | }, 373 | // The current locale 374 | $locale() { 375 | return getLocale() 376 | }, 377 | // A listing of the available locales 378 | $locales() { 379 | return locales() 380 | } 381 | }, 382 | }) 383 | ``` 384 | 385 | Then you can use the methods in your Vue components like so: 386 | 387 | ```html 388 |

{{ $trans('greet.me') }}

389 | ``` 390 | 391 | ## Dive Deeper 392 | 393 | Matice extends the Laravel `Translator` class. Use `Translator::list()` to return 394 | an array representation of all of your app translations. 395 | 396 | If you want to load only translations of a specific locale, use the matice facade: 397 | ```php 398 | use GENL\Matice\Facades\Matice; 399 | 400 | // Loads all the translations 401 | $translations = Matice::translations(); 402 | 403 | // Or loads translations for a specific locale. 404 | $translations = Matice::translations($locale); 405 | ``` 406 | 407 | 408 | **Environment-based loading of minified matice helper file** 409 | 410 | When using the `@translations` Blade directive, Matice detects the current environment and minify the output if `APP_ENV` is `production`. 411 | 412 | In development, `@translations` loops into the lang directory to generate the matice object each time the page reloads, and doesn't generate`matice_translations.js` file. 413 | In production, `@translations` generate the `matice_translations.js` file for you when your app is open for the first time then the generated file is used every time the page reloads. 414 | The Matice object is not generated every time, so it has no effect on performances in production. 415 | This behavior can be disabled in the `config/matice.php` file, set `use_generated_translations_file_in_prod` to false. 416 | 417 | ######**Note: Matice supports json translation files as well as php files.**, 418 | 419 | 420 | ### Testing 421 | 422 | ``` bash 423 | // -- laravel test -- 424 | composer test 425 | 426 | // -- js test -- 427 | 428 | // With yarn 429 | yarn test 430 | 431 | // With npm 432 | npm run test 433 | ``` 434 | 435 | ### Changelog 436 | 437 | Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recently. 438 | 439 | ## Contributing 440 | 441 | Please see [CONTRIBUTING](CONTRIBUTING.md) for details. 442 | 443 | ### Security 444 | 445 | If you discover any security related issues, please email bigcodole@gmail.com instead of using the issue tracker. 446 | 447 | ## Credits 448 | 449 | - [GENL](https://github.com/GENL/matice) 450 | - [All Contributors](../../contributors) 451 | - This package was largely inspired by [Ziggy](https://github.com/tighten/ziggy) 452 | 453 | ## License 454 | 455 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 456 | 457 | ## Laravel Package Boilerplate 458 | 459 | This package was generated using the [Laravel Package Boilerplate](https://laravelpackageboilerplate.com). 460 | -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-typescript" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "genl/matice", 3 | "description": "Use your Laravel translations in JavaScript. Generates a Blade directive exporting all of your Laravel translations and provides a nice trans() helper function in JavaScript.", 4 | "keywords": [ 5 | "matice", 6 | "laravel", 7 | "translations" 8 | ], 9 | "homepage": "https://github.com/genl/matice", 10 | "license": "MIT", 11 | "type": "library", 12 | "authors": [ 13 | { 14 | "name": "Ekcel Ekoumelong", 15 | "email": "bigcodole@gmail.com", 16 | "homepage": "https://ekcel-portofolio.firebaseapp.com" 17 | } 18 | ], 19 | "require": { 20 | "laravel/framework": ">=6.0@dev" 21 | }, 22 | "require-dev": { 23 | "orchestra/testbench": "^4.10||^5.0||^6.0||^7.0" 24 | }, 25 | "autoload": { 26 | "psr-4": { 27 | "Genl\\Matice\\": "src" 28 | } 29 | }, 30 | "autoload-dev": { 31 | "psr-4": { 32 | "Genl\\Matice\\Tests\\": "tests" 33 | } 34 | }, 35 | "scripts": { 36 | "test": "vendor/bin/phpunit", 37 | "test-coverage": "vendor/bin/phpunit --coverage-html coverage" 38 | 39 | }, 40 | "config": { 41 | "sort-packages": true 42 | }, 43 | "extra": { 44 | "laravel": { 45 | "providers": [ 46 | "Genl\\Matice\\MaticeServiceProvider" 47 | ], 48 | "aliases": { 49 | "Matice": "Genl\\Matice\\Facades\\Matice" 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /config/matice.php: -------------------------------------------------------------------------------- 1 | function_exists('lang_path') ? lang_path() : resource_path('lang'), 13 | 14 | /* 15 | |-------------------------------------------------------------------------- 16 | | Use existing generated file in prod 17 | |-------------------------------------------------------------------------- 18 | | 19 | | Whether @translations should always use the generated translations in production. 20 | | If false, the @translations directive will always regenerate the translations. 21 | | 22 | */ 23 | 'use_generated_translations_file_in_prod' => true, 24 | 25 | /* 26 | |-------------------------------------------------------------------------- 27 | | generated translations file name 28 | |-------------------------------------------------------------------------- 29 | | 30 | | The place where to generate translations file. 31 | | 32 | */ 33 | 'generate_translations_path' => resource_path('assets/js/matice_translations.js'), 34 | 35 | /* 36 | |-------------------------------------------------------------------------- 37 | | Restrictions 38 | |-------------------------------------------------------------------------- 39 | | 40 | | Specify which translation namespaces must(only) be exported. 41 | | It could be the paths to the folders or files you want to exported to the client. 42 | 43 | | The base directory is the "lang_directory" 44 | | 45 | */ 46 | 'only' => [ 47 | // 48 | ], 49 | 50 | /* 51 | |-------------------------------------------------------------------------- 52 | | Restrictions 53 | |-------------------------------------------------------------------------- 54 | | 55 | | Specify which translation namespaces must NOT be exported. 56 | | It could be the paths to the folders or files you want to exported to the client. 57 | 58 | | The base directory is the "lang_directory" 59 | | 60 | */ 61 | 'except' => [ 62 | // 63 | ], 64 | 65 | ]; 66 | -------------------------------------------------------------------------------- /jest.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "verbose": true, 3 | "rootDir": "tests/js", 4 | "preset": "ts-jest", 5 | "testEnvironment": "node" 6 | } 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "matice", 3 | "version": "1.2.0", 4 | "description": "Matice - Use your Laravel translations in JavaScript", 5 | "main": "dist/matice.js", 6 | "types": "dist/matice.d.ts", 7 | "directories": { 8 | "test": "jest" 9 | }, 10 | "files": [ 11 | "dist" 12 | ], 13 | "scripts": { 14 | "test": "jest", 15 | "build": "tsc && webpack", 16 | "prepare": "npm run build --production" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/GENL/matice.git" 21 | }, 22 | "keywords": [ 23 | "matice", 24 | "laravel", 25 | "translations" 26 | ], 27 | "author": "Ekcel Ekoumelong", 28 | "license": "MIT", 29 | "bugs": { 30 | "url": "https://github.com/GENL/matice/issues" 31 | }, 32 | "homepage": "https://github.com/GENL/matice#readme", 33 | "devDependencies": { 34 | "@babel/preset-typescript": "^7.26.0", 35 | "@types/jest": "^29.5.14", 36 | "@types/node": "^22.10.1", 37 | "jest": "^29.7.0", 38 | "release-it": "^17.10.0", 39 | "ts-jest": "^29.2.5", 40 | "ts-loader": "^9.5.1", 41 | "typescript": "^5.7.2", 42 | "webpack": "^5.97.1", 43 | "webpack-cli": "^5.1.4" 44 | }, 45 | "dependencies": { 46 | "global": "^4.4.0", 47 | "uglify-js": "^3.19.3" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/BladeTranslationsGenerator.php: -------------------------------------------------------------------------------- 1 | unique()->toArray() : 52 | $locales; 53 | // when $useCache is set to true, $wrapInHtml also has to be true. 54 | // TODO: make it possible to return the matice object without wrapping the it 55 | // in the HTML script when $useCache === true. 56 | abort_if( 57 | $useCache && !$wrapInHtml, 58 | 400, 59 | 'Cannot generate translations because when $useCache is true, $wrapInHtml also has to be true.' 60 | ); 61 | $path = config('matice.generate_translations_path'); 62 | // Use the cache if the translation file exists 63 | if ($useCache) { 64 | if (File::exists($path)) { 65 | $generatedTranslationFileContent = File::get($path); 66 | return $this->makeMaticeHtml( 67 | $generatedTranslationFileContent, 68 | true, 69 | "Matice Laravel Translations generated", "Using cached translations" 70 | ); 71 | } 72 | Log::warning("Trying to use the cached matice translations file but the file was not found."); 73 | error_log("Trying to use the cached matice translations file but the file was not found."); 74 | } 75 | if ($wrapInHtml) { 76 | return $this->makeMaticeHtml( 77 | $this->makeMaticeJSObject($locales), 78 | true, 79 | "Matice Laravel Translations generated" 80 | ); 81 | } else { 82 | return $this->makeMaticeJSObject($locales, $hasExport); 83 | } 84 | } 85 | 86 | /** 87 | * @param string[] $locales 88 | * @param bool $hasExport 89 | * @return string 90 | * @throws LocaleTranslationsFileOrDirNotFoundException 91 | */ 92 | private function makeMaticeJSObject(array $locales, bool $hasExport=true): string 93 | { 94 | $translations = json_encode($this->translations($locales)); 95 | $appLocale = $locale ?? app()->getLocale(); 96 | $fallbackLocale = config('app.fallback_locale'); 97 | $exportStatement = $hasExport ? "\n$this->maticeExportStatement" : ''; 98 | return <<maticeExportStatement/",'', $maticeJSObject) : 122 | $maticeJSObject; 123 | $c = ''; 124 | foreach ($comments as $comment) { 125 | $c .= "\n"; 126 | } 127 | return << 129 | 132 | EOT; 133 | } 134 | 135 | 136 | /** 137 | * Load all the translations array. 138 | * 139 | * @param string[] $locales 140 | * @return array 141 | * @throws LocaleTranslationsFileOrDirNotFoundException 142 | */ 143 | public function translations(array $locales = []): array 144 | { 145 | $translations = MaticeServiceProvider::makeFolderFilesTree(config('matice.lang_directory')); 146 | Helpers::applyTranslationRestrictions($translations); 147 | $selectedTranslations = []; 148 | foreach($locales as $l) { 149 | if (isset($translations[$l])) { 150 | // Loads translations of the locale 151 | $selectedTranslations[$l] = $translations[$l]; 152 | } else { 153 | throw new LocaleTranslationsFileOrDirNotFoundException($l); 154 | } 155 | } 156 | return $selectedTranslations ?: $translations; 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/Commands/TranslationsGeneratorCommand.php: -------------------------------------------------------------------------------- 1 | argument('path'); 35 | $generatedTranslations = Matice::generate( 36 | null, false, false, !$this->option('no-export') 37 | ); 38 | $generatedTranslations = ('/* 39 | |-------------------------------------------------------------------------- 40 | | Generated Laravel translations 41 | |-------------------------------------------------------------------------- 42 | | 43 | | This file is autogenerated and should not be modified. 44 | | To load new translations run: php artisan matice:generate 45 | | 46 | */ 47 | ' . $generatedTranslations . 48 | ' 49 | ' 50 | ); 51 | $this->makeDirectory($path); 52 | File::put($path, $generatedTranslations); 53 | $this->info("Matice translations file generated at $path"); 54 | } 55 | 56 | protected function makeDirectory(string $path) 57 | { 58 | if (! File::isDirectory(dirname($path))) { 59 | File::makeDirectory(dirname($path), 0777, true, true); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Exceptions/LocaleTranslationsFileOrDirNotFoundException.php: -------------------------------------------------------------------------------- 1 | getExtension() !== 'json') { 39 | continue; 40 | } 41 | $locale = $file->getFilenameWithoutExtension(); 42 | $tree[$locale] = array_merge( 43 | $tree[$locale] ?? [], 44 | json_decode($file->getContents(), true) 45 | ); 46 | } 47 | } 48 | } 49 | 50 | return $tree; 51 | } 52 | 53 | private static function getJsonPaths($dir): array 54 | { 55 | $jsonPaths = []; 56 | 57 | if (method_exists(Lang::getLoader(), 'jsonPaths')) { 58 | $jsonPaths = Lang::getLoader()->jsonPaths(); 59 | } elseif (method_exists(Lang::getLoader(), 'getJsonPaths')) { 60 | $jsonPaths = Lang::getLoader()->getJsonPaths(); 61 | } 62 | 63 | return array_unique( 64 | array_merge([$dir], $jsonPaths) 65 | ); 66 | } 67 | 68 | private static function readLocaleFolder(string $localeFolder): array 69 | { 70 | $tree = []; 71 | $ffs = scandir($localeFolder); 72 | 73 | foreach ($ffs as $ff) { 74 | // We skip hidden folders or config files in the directory. 75 | if (Str::startsWith($ff, '.')) { 76 | continue; 77 | } 78 | 79 | $extension = '.' . Str::afterLast($ff, '.'); 80 | $ff = basename($ff, $extension); 81 | $tree[$ff] = []; 82 | 83 | if (is_dir($localeFolder . '/' . $ff)) { 84 | $tree[$ff] = self::readLocaleFolder($localeFolder . '/' . $ff); 85 | } 86 | 87 | if (is_file($pathName = $localeFolder . '/' . $ff . $extension)) { 88 | $existingTranslations = $tree[$ff] ?? []; 89 | 90 | if ($extension === '.json') { 91 | $tree[$ff] = array_merge( 92 | $existingTranslations, 93 | json_decode(File::get($pathName), true) 94 | ); 95 | } else if ($extension === '.php') { 96 | $tree[$ff] = array_merge( 97 | $existingTranslations, 98 | require($pathName) 99 | ); 100 | } 101 | } 102 | } 103 | 104 | return $tree; 105 | } 106 | 107 | /** 108 | * This method removes the excepted namespaces from the translations 109 | * and add allows only the exportable translations if defined. 110 | * 111 | * When the same namespace is included and excepted at the same time, it considered excepted. 112 | * 113 | * @param array $translations 114 | */ 115 | public static function applyTranslationRestrictions(array &$translations) 116 | { 117 | // ---------- 118 | // Manage exported namespaces 119 | // ---------- 120 | $exportables = config('matice.only'); 121 | 122 | // When the user ask to export only a certain namespaces, we empty the $translation to fill them later 123 | // with the only ones required. 124 | if (! empty($exportables)) { 125 | $copy = $translations; 126 | $translations = []; 127 | } 128 | 129 | foreach ($exportables as $exportableNamespace) { 130 | // Force "/" as separator 131 | $exportableNamespace = str_replace('\\', '/', trim($exportableNamespace, '/\\')); 132 | // Remove the last dot that might exit when the namespace is a file. 133 | $exportableNamespace = Str::beforeLast($exportableNamespace,'.'); 134 | // Replace the "/" by "." 135 | $exportableNamespace = str_replace('/', '.', $exportableNamespace); 136 | 137 | // Set only the translations for the exportable namespaces 138 | $value = Arr::get($copy, (string)$exportableNamespace); 139 | Arr::set($translations, ($exportableNamespace), $value); 140 | } 141 | 142 | // ---------- 143 | // Manage excepted namespaces 144 | // ---------- 145 | $hidden = config('matice.except'); 146 | 147 | foreach ($hidden as $hiddenNamespace) { 148 | // Force "/" as separator 149 | $hiddenNamespace = str_replace('\\', '/', trim($hiddenNamespace, '/\\')); 150 | // Remove the last dot that might exit when the namespace is a file. 151 | $hiddenNamespace = Str::beforeLast($hiddenNamespace,'.'); 152 | // Replace the "/" by "." 153 | $hiddenNamespace = str_replace('/', '.', $hiddenNamespace); 154 | 155 | // remove the translations in the array 156 | Arr::forget($translations, $hiddenNamespace); 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/MaticeServiceProvider.php: -------------------------------------------------------------------------------- 1 | loadTranslationsFrom(__DIR__.'/../resources/lang', 'matice'); 24 | // $this->loadViewsFrom(__DIR__.'/../resources/views', 'matice'); 25 | // $this->loadMigrationsFrom(__DIR__.'/../database/migrations'); 26 | // $this->loadRoutesFrom(__DIR__.'/routes.php'); 27 | 28 | if ($this->app->runningInConsole()) { 29 | $this->publishes([ 30 | __DIR__ . '/../config/matice.php' => config_path('matice.php'), 31 | ], 'config'); 32 | 33 | // Publishing the views. 34 | /*$this->publishes([ 35 | __DIR__.'/../resources/views' => resource_path('views/vendor/matice'), 36 | ], 'views');*/ 37 | 38 | // Publishing assets. 39 | /*$this->publishes([ 40 | __DIR__.'/../resources/assets' => public_path('vendor/matice'), 41 | ], 'assets');*/ 42 | 43 | // Publishing the translation files. 44 | /*$this->publishes([ 45 | __DIR__.'/../resources/lang' => resource_path('lang/vendor/matice'), 46 | ], 'lang');*/ 47 | } 48 | 49 | // Registering package commands. 50 | $this->commands([ 51 | TranslationsGeneratorCommand::class, 52 | ]); 53 | 54 | Translator::macro('list', function () { 55 | return MaticeServiceProvider::makeFolderFilesTree(config('matice.lang_directory')); 56 | }); 57 | 58 | Blade::directive('translations', function ($locales) { 59 | $locales = $locales ?: 'null'; 60 | $useCache = config('matice.use_generated_translations_file_in_prod') === true 61 | && app()->isProduction() 62 | ? 'true' : 'false'; 63 | if ( 64 | $useCache === 'true' && 65 | !File::exists(config('matice.generate_translations_path')) 66 | ) { 67 | Artisan::call('matice:generate'); 68 | } 69 | return "make('matice')->generate($locales, true, $useCache); ?>"; 70 | }); 71 | } 72 | 73 | /** 74 | * Register the application services. 75 | */ 76 | public function register() 77 | { 78 | // Automatically apply the package configuration 79 | $this->mergeConfigFrom(__DIR__ . '/../config/matice.php', 'matice'); 80 | 81 | // Register the main class to use with the facade 82 | $this->app->singleton('matice', function () { 83 | return new BladeTranslationsGenerator; 84 | }); 85 | } 86 | 87 | public static function makeFolderFilesTree($dir): array 88 | { 89 | return Helpers::makeFolderFilesTree($dir); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/js/Localization/MaticeLocalizationConfig.ts: -------------------------------------------------------------------------------- 1 | export default class MaticeLocalizationConfig { 2 | /** 3 | * The currently used locale 4 | */ 5 | public static locale: string; 6 | 7 | public static fallbackLocale: string; 8 | 9 | /** 10 | * All the available locales 11 | */ 12 | public static locales: string[]; 13 | } 14 | -------------------------------------------------------------------------------- /src/js/Localization/core.ts: -------------------------------------------------------------------------------- 1 | import MaticeLocalizationConfig from "./MaticeLocalizationConfig" 2 | 3 | function assert(value: boolean, message: string) { 4 | if (! value) throw message 5 | } 6 | 7 | export interface TranslationOptions { 8 | args?: { [key: string]: any }, 9 | pluralize?: boolean, 10 | locale?: string, 11 | } 12 | 13 | class Localization { 14 | private static _instance: Localization 15 | 16 | public static get instance(): Localization { 17 | if (Localization._instance === undefined) { 18 | Localization._instance = new Localization() 19 | } 20 | return Localization._instance 21 | } 22 | 23 | private constructor() { 24 | // @ts-ignore 25 | MaticeLocalizationConfig.locale = Matice.locale 26 | 27 | // @ts-ignore 28 | MaticeLocalizationConfig.fallbackLocale = Matice.fallbackLocale 29 | 30 | // @ts-ignore 31 | MaticeLocalizationConfig.locales = Object.keys(Matice.translations) 32 | } 33 | 34 | /** 35 | * Update the locale 36 | * @param locale 37 | */ 38 | public setLocale(locale: string) { 39 | MaticeLocalizationConfig.locale = locale 40 | } 41 | 42 | /** 43 | * Retrieve the current locale 44 | */ 45 | public getLocale() { 46 | return MaticeLocalizationConfig.locale 47 | } 48 | 49 | /** 50 | * Return a listing of the locales. 51 | */ 52 | public locales() { 53 | return MaticeLocalizationConfig.locales 54 | } 55 | 56 | /** 57 | * Get the translations of [locale]. 58 | * @param locale 59 | */ 60 | private translations(locale: string = MaticeLocalizationConfig.locale) { 61 | // Matice is added with the directive "@translation" 62 | // @ts-ignore 63 | let translations = Matice.translations 64 | 65 | if (translations === undefined) { 66 | console.warn('Matice Translation not found. For Matice-js to work, make sure to add @translations' + 67 | ' blade directive in your view. Usually insert the directive in app.layout.') 68 | translations = [] 69 | } else { 70 | translations = translations[locale] 71 | 72 | if (translations === undefined) { 73 | throw `Locale [${locale}] does not exist.` 74 | } 75 | } 76 | 77 | return translations 78 | } 79 | 80 | /** 81 | * Translate the given key. 82 | */ 83 | public trans(key: string, silentNotFoundError: boolean, options: TranslationOptions = {args: {}, pluralize: false}) { 84 | const args = options.args || {} 85 | 86 | let sentence = this.findSentence(key, silentNotFoundError, options.locale) 87 | 88 | if (options.pluralize) { 89 | assert(typeof args.count === 'number', 90 | 'On pluralization, the argument `count` must be a number and non-null.') 91 | sentence = this.pluralize(sentence, args.count) 92 | } 93 | 94 | // Replace the variables in sentence. 95 | Object.keys(args).forEach((key) => { 96 | sentence = sentence.replace(new RegExp(':' + key, 'g'), args[key]) 97 | }) 98 | 99 | return sentence 100 | } 101 | 102 | 103 | // noinspection JSMethodCanBeStatic 104 | /** 105 | * Manage sentence pluralization the sentence. Return the good sentence depending on the `count` argument. 106 | */ 107 | private pluralize(sentence: string, count: number): string { 108 | let parts = sentence.split('|') 109 | // Make sure the pieces are always three for the ease of calculation. 110 | // We fill the empty indexes with the direct preceding index. 111 | // We fill the empty parts by the last part. 112 | if (parts.length >= 3) parts = [parts[0], parts[1], parts[2]] 113 | else if (parts.length === 2) parts = [parts[0], parts[0], parts[1]] 114 | else parts = [parts[0], parts[0], parts[0]] 115 | // Manage multiple number range. 116 | let ranges: { min: number, max: number, part: string }[] = [] 117 | const pattern = /^(\[(\s*\d+\s*)+,(\s*(\d+|\*)\s*)])|({\s*\d+\s*})/ 118 | for (let i = 0; i < parts.length; i++) { 119 | let part = parts[i] 120 | let matched = part.match(pattern) 121 | if (matched === null) { 122 | // If no range is found, use the part index as the range. 123 | parts[i] = `{${i}} ${parts[i]}` 124 | matched = [parts[i]] 125 | } 126 | // Remove unwanted the opening range characters: "[", "{", 127 | const replaced = matched[0].replace(/[\[{]/, '') 128 | // Split the matched to have an array of string number 129 | const rangeNumbers = replaced.split(/(?<=\d),/, 2).map((m: string) => { 130 | // In JS, parseInt() parses the firsts non-empty characters that are parsable. 131 | // So parseInt("2} ABC") will return 2. 132 | const parsed = Number.parseInt(m.trim()) 133 | // If parsed is a star(*) which means infinity, just replace by count + 1 134 | return Number.isInteger(parsed) ? parsed : count + 1 135 | }) 136 | // Let's make sure to remove the range closing symbols in the parts: "]", "}". 137 | parts[i] = part = part.replace(pattern, '') 138 | ranges.push( 139 | rangeNumbers.length == 1 140 | ? {min: rangeNumbers[0], max: rangeNumbers[0], part} 141 | : {min: rangeNumbers[0], max: rangeNumbers[1], part} 142 | ) 143 | } 144 | let foundInRange = false 145 | // Compare the part with the range to choose the pluralization. 146 | // ------- ------ 147 | // Return the first part if count is zero or negative 148 | if (count <= 0) { 149 | sentence = parts[0] 150 | } else { 151 | for (const range of ranges) { 152 | // If count is in the range, return the corresponding text part. 153 | if (count >= range.min && count <= range.max) { 154 | // count is in the range. 155 | sentence = range.part 156 | foundInRange = true 157 | break 158 | } 159 | } 160 | if (! foundInRange) { 161 | // If count is not in the range, we use the last part. 162 | sentence = parts[parts.length - 1] 163 | } 164 | } 165 | return sentence 166 | } 167 | 168 | /** 169 | * Find the sentence using associated with the [key]. 170 | * @param key 171 | * @param silentNotFoundError 172 | * @param locale 173 | * @param splitKey 174 | * @returns {string} 175 | * @private 176 | */ 177 | private findSentence(key: string, silentNotFoundError: boolean, locale: string = MaticeLocalizationConfig.locale, splitKey: boolean = false): string { 178 | const translations: { [key: string]: any } = this.translations(locale) 179 | 180 | // Initially, [link] is a [Map] but at the end, it can be a [String], 181 | // the sentence. 182 | let link = translations 183 | 184 | const parts = splitKey ? key.split('.') : [key] 185 | 186 | for (const part of parts) { 187 | // Get the new json until we fall on the last key of 188 | // the array which should point to a String. 189 | if (typeof link === 'object' && part in link) { 190 | // Make sure the key exist. 191 | link = link[part] 192 | } else { 193 | // If key not found, try to split it using dot. 194 | if (!splitKey) { 195 | return this.findSentence(key, silentNotFoundError, locale, true) 196 | } 197 | 198 | // If key not found, try with the fallback locale. 199 | if (locale !== MaticeLocalizationConfig.fallbackLocale) { 200 | return this.findSentence(key, silentNotFoundError, MaticeLocalizationConfig.fallbackLocale) 201 | } 202 | 203 | // If the key not found and the silent mode is on, return the key, 204 | if (silentNotFoundError) return key 205 | 206 | // If key not found and the silent mode is off, throw error, 207 | throw `Translation key not found : "${key}" -> Exactly "${part}" not found` 208 | } 209 | } 210 | 211 | return link.toString() 212 | } 213 | } 214 | 215 | 216 | 217 | /* 218 | | 219 | | ---------------------------------- 220 | | Exports 221 | | ---------------------------------- 222 | | 223 | */ 224 | 225 | 226 | /** 227 | * Translate the given message. 228 | * @param key 229 | * @param options 230 | */ 231 | export function trans(key: string, options: TranslationOptions = {args: {}, pluralize: false}) { 232 | return Localization.instance.trans(key, false, options) 233 | } 234 | 235 | /** 236 | * Translate the given message with the particularity to return the key if 237 | * the sentence was not found, instead of throwing an exception. 238 | * @param key 239 | * @param options 240 | */ 241 | export function __(key: string, options: TranslationOptions = {args: {}, pluralize: false}) { 242 | return Localization.instance.trans(key, true, options) 243 | } 244 | 245 | /** 246 | * An helper to the trans function but with the pluralization mode activated by default. 247 | * @param key 248 | * @param count 249 | * @param args 250 | * @param locale 251 | */ 252 | export function transChoice(key: string, count: number, args: {} = {}, locale: string = MaticeLocalizationConfig.locale) { 253 | return trans(key, { args: {...args, count}, pluralize: true, locale }) 254 | } 255 | 256 | /** 257 | * Update the locale 258 | * @param locale 259 | */ 260 | export function setLocale(locale: string) { 261 | Localization.instance.setLocale(locale) 262 | 263 | } 264 | 265 | /** 266 | * Retrieve the current locale 267 | */ 268 | export function getLocale() { 269 | return Localization.instance.getLocale() 270 | } 271 | 272 | /** 273 | * Return a listing of the locales. 274 | */ 275 | export function locales() { 276 | return Localization.instance.locales() 277 | } 278 | -------------------------------------------------------------------------------- /src/js/matice.ts: -------------------------------------------------------------------------------- 1 | export {trans, __, setLocale, getLocale, locales, transChoice, TranslationOptions} from './Localization/core' 2 | 3 | export {default as MaticeLocalizationConfig} from './Localization/MaticeLocalizationConfig' 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src"], 3 | "exclude": ["node_modules", "dist"], 4 | "compilerOptions": { 5 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 6 | 7 | /* Basic Options */ 8 | // "incremental": true, /* Enable incremental compilation */ 9 | "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 10 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 11 | "lib": ["es2015", "dom"], /* Specify library files to be included in the compilation. */ 12 | "allowJs": true, /* Allow javascript files to be compiled. */ 13 | // "checkJs": true, /* Report errors in .js files. */ 14 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 15 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 16 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 17 | "sourceMap": true, /* Generates corresponding '.map' file. */ 18 | // "outFile": "./", /* Concatenate and emit output to single file. */ 19 | "outDir": "dist", /* Redirect output structure to the directory. */ 20 | "rootDir": "./src/js", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 21 | // "composite": true, /* Enable project compilation */ 22 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 23 | // "removeComments": true, /* Do not emit comments to output. */ 24 | // "noEmit": true, /* Do not emit outputs. */ 25 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 26 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 27 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 28 | 29 | /* Strict Type-Checking Options */ 30 | "strict": true, /* Enable all strict type-checking options. */ 31 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 32 | "strictNullChecks": true, /* Enable strict null checks. */ 33 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 34 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 35 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 36 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 37 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 38 | 39 | /* Additional Checks */ 40 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 41 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 42 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 43 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 44 | 45 | /* Module Resolution Options */ 46 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 47 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 48 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 49 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 50 | // "typeRoots": [], /* List of folders to include type definitions from. */ 51 | // "types": [], /* Type declaration files to be included in compilation. */ 52 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 53 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 54 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 55 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 56 | 57 | /* Source Map Options */ 58 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 59 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 60 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 61 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 62 | 63 | /* Experimental Options */ 64 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 65 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 66 | 67 | /* Advanced Options */ 68 | "skipLibCheck": true, /* Skip type checking of declaration files. */ 69 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require( 'path' ); 2 | 3 | module.exports = { 4 | // bundling mode 5 | mode: 'production', 6 | target: "web", 7 | // entry files 8 | entry: './src/js/matice.ts', 9 | devtool: 'cheap-source-map', 10 | // output bundles (location) 11 | output: { 12 | path: path.resolve(__dirname, 'dist'), 13 | filename: 'matice.min.js', 14 | }, 15 | // file resolutions 16 | resolve: { 17 | extensions: [ '.ts', '.js' ], 18 | }, 19 | // loaders 20 | module: { 21 | rules: [ 22 | { 23 | test: /\.tsx?$/, 24 | use: 'ts-loader', 25 | exclude: /node_modules/, 26 | }, 27 | ] 28 | }, 29 | }; 30 | --------------------------------------------------------------------------------