├── test ├── locales │ ├── foo.txt │ ├── fr.yml │ ├── de.properties │ ├── zh_TW.json │ └── zh-CN.js ├── other-locales │ └── zh-CN.js └── index.test.js ├── .eslintignore ├── .gitignore ├── AUTHORS ├── .travis.yml ├── appveyor.yml ├── LICENSE ├── package.json ├── benchmark ├── ends-with.js ├── arguments-to-args.js ├── apply.js ├── nested-value.js └── flattening.js ├── History.md ├── .eslintrc ├── README.md └── index.js /test/locales/foo.txt: -------------------------------------------------------------------------------- 1 | should not load this file 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | coverage/ 3 | .tmp/ 4 | .git/ 5 | -------------------------------------------------------------------------------- /test/other-locales/zh-CN.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | Email: '邮箱1', 5 | }; 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.seed 2 | *.log 3 | *.csv 4 | *.dat 5 | *.out 6 | *.pid 7 | *.gz 8 | 9 | pids 10 | logs 11 | results 12 | 13 | node_modules 14 | npm-debug.log 15 | coverage 16 | .idea 17 | -------------------------------------------------------------------------------- /test/locales/fr.yml: -------------------------------------------------------------------------------- 1 | Email: "le email" 2 | emptyValue: "" 3 | Hello %s, how are you today?: "%s, Comment allez-vous" 4 | model: 5 | user: 6 | fields: 7 | name: "prénom" 8 | gender: "le sexe" -------------------------------------------------------------------------------- /test/locales/de.properties: -------------------------------------------------------------------------------- 1 | Email = Emailde 2 | "Hello %s, how are you today?" = "Hallo %s, wie geht es dir heute?" 3 | Hello %s, how are you today? How was your %s. = Hallo %s, wie geht es dir heute? Wie war dein %s. 4 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | fengmk2 (https://fengmk2.com) 2 | Haoliang Gao (https://github.com/popomore) 3 | 闲耘™ (https://github.com/hotoo) 4 | ermin.zem 5 | 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - '4' 5 | - '6' 6 | - '8' 7 | - '10' 8 | - '12' 9 | install: 10 | - npm i npminstall && npminstall 11 | script: 12 | - npm run ci 13 | after_script: 14 | - npminstall codecov && codecov 15 | -------------------------------------------------------------------------------- /test/locales/zh_TW.json: -------------------------------------------------------------------------------- 1 | { 2 | "Email": "郵箱", 3 | "emptyValue": "", 4 | "Hello %s, how are you today?": "%s,今天過得如何?", 5 | "model": { 6 | "user": { 7 | "fields": { 8 | "name": "姓名", 9 | "gender": "性別" 10 | } 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/locales/zh-CN.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | Email: '邮箱', 5 | emptyValue: '', 6 | 'Hello %s, how are you today?': '%s,今天过得如何?', 7 | model: { 8 | user: { 9 | fields: { 10 | name: '姓名', 11 | gender: '性别', 12 | }, 13 | }, 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | matrix: 3 | - nodejs_version: '4' 4 | - nodejs_version: '6' 5 | - nodejs_version: '8' 6 | - nodejs_version: '10' 7 | - nodejs_version: '12' 8 | 9 | install: 10 | - ps: Install-Product node $env:nodejs_version 11 | - npm i npminstall && node_modules\.bin\npminstall 12 | 13 | test_script: 14 | - node --version 15 | - npm --version 16 | - npm run test 17 | 18 | build: off 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This software is licensed under the MIT License. 2 | 3 | Copyright (c) 2015 - 2017 koajs and other contributors 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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "koa-locales", 3 | "version": "1.12.0", 4 | "description": "koa locales, i18n solution for koa", 5 | "main": "index.js", 6 | "files": [ 7 | "index.js" 8 | ], 9 | "scripts": { 10 | "test": "eslint . && mocha -R spec -t 5000 test/*.test.js", 11 | "cov": "istanbul cover _mocha -- -t 5000 test/*.test.js", 12 | "lint": "eslint .", 13 | "ci": "npm run lint && npm run cov", 14 | "autod": "autod -w --prefix '^'", 15 | "contributors": "contributors -f plain -o AUTHORS" 16 | }, 17 | "dependencies": { 18 | "debug": "^2.6.0", 19 | "humanize-ms": "^1.2.0", 20 | "ini": "^1.3.4", 21 | "js-yaml": "^3.13.1", 22 | "npminstall": "^3.23.0", 23 | "object-assign": "^4.1.0" 24 | }, 25 | "devDependencies": { 26 | "autod": "2", 27 | "beautify-benchmark": "^0.2.4", 28 | "benchmark": "^2.1.3", 29 | "contributors": "*", 30 | "egg-ci": "^1.1.0", 31 | "eslint": "1", 32 | "istanbul": "*", 33 | "koa": "^1.2.4", 34 | "mm": "^2.0.0", 35 | "mocha": "4", 36 | "pedding": "^1.1.0", 37 | "supertest": "^2.0.1" 38 | }, 39 | "homepage": "https://github.com/koajs/locales", 40 | "repository": { 41 | "type": "git", 42 | "url": "git://github.com/koajs/locales.git", 43 | "web": "https://github.com/koajs/locales" 44 | }, 45 | "bugs": { 46 | "url": "https://github.com/koajs/locales/issues" 47 | }, 48 | "keywords": [ 49 | "koa-locales", 50 | "i18n", 51 | "locales", 52 | "koa-i18n", 53 | "koa" 54 | ], 55 | "engines": { 56 | "node": ">=4.0.0" 57 | }, 58 | "ci": { 59 | "version": "4, 6, 8, 10, 12" 60 | }, 61 | "author": "fengmk2 (https://fengmk2.com)", 62 | "license": "MIT" 63 | } 64 | -------------------------------------------------------------------------------- /benchmark/ends-with.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Benchmark = require('benchmark'); 4 | const benchmarks = require('beautify-benchmark'); 5 | 6 | const suite = new Benchmark.Suite(); 7 | 8 | function endsWith(str) { 9 | return str.endsWith('.properties'); 10 | } 11 | 12 | function indexOf(str) { 13 | return str.indexOf('.properties') === str.length - 11; 14 | } 15 | 16 | function regexp(str) { 17 | return /\.properties$/.test(str); 18 | } 19 | 20 | console.log('true:'); 21 | console.log(' endsWith: %j', endsWith('filename.properties')); 22 | console.log(' indexOf: %j', indexOf('filename.properties')); 23 | console.log(' regexp: %j', regexp('filename.properties')); 24 | console.log('false:'); 25 | console.log(' endsWith: %j', endsWith('filename')); 26 | console.log(' indexOf: %j', indexOf('filename')); 27 | console.log(' regexp: %j', regexp('filename')); 28 | 29 | suite 30 | 31 | .add('endsWith', function() { 32 | endsWith('filename.properties'); 33 | endsWith('filename'); 34 | }) 35 | .add('indexOf', function() { 36 | indexOf('filename.properties'); 37 | indexOf('filename'); 38 | }) 39 | .add('regexp', function() { 40 | regexp('filename.properties'); 41 | regexp('filename'); 42 | }) 43 | 44 | .on('cycle', function(event) { 45 | benchmarks.add(event.target); 46 | }) 47 | .on('start', function() { 48 | console.log('\n endsWith Benchmark\n node version: %s, date: %s\n Starting...', 49 | process.version, Date()); 50 | }) 51 | .on('complete', function done() { 52 | benchmarks.log(); 53 | }) 54 | .run({ async: false }); 55 | 56 | // true: 57 | // endsWith: true 58 | // indexOf: true 59 | // regexp: true 60 | // false: 61 | // endsWith: false 62 | // indexOf: false 63 | // regexp: false 64 | // 65 | // endsWith Benchmark 66 | // node version: v2.2.1, date: Sun Aug 30 2015 14:39:14 GMT+0800 (CST) 67 | // Starting... 68 | // 3 tests completed. 69 | // 70 | // endsWith x 13,491,036 ops/sec ±0.55% (101 runs sampled) 71 | // indexOf x 13,796,553 ops/sec ±0.57% (99 runs sampled) 72 | // regexp x 4,772,744 ops/sec ±0.57% (98 runs sampled) 73 | -------------------------------------------------------------------------------- /benchmark/arguments-to-args.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Benchmark = require('benchmark'); 4 | const benchmarks = require('beautify-benchmark'); 5 | 6 | const suite = new Benchmark.Suite(); 7 | 8 | function slice() { 9 | return Array.prototype.slice.call(arguments); 10 | } 11 | 12 | function slice0() { 13 | return Array.prototype.slice.call(arguments, 0); 14 | } 15 | 16 | function forLoop() { 17 | const args = new Array(arguments.length); 18 | for(let i = 0; i < args.length; i++) { 19 | args[i] = arguments[i]; 20 | } 21 | return args; 22 | } 23 | 24 | console.log('slice(0, 1, 2, 3, 4, 5, 6, 7): %j', slice(0, 1, 2, 3, 4, 5, 6, 7)); 25 | console.log('slice0(0, 1, 2, 3, 4, 5, 6, 7): %j', slice0(0, 1, 2, 3, 4, 5, 6, 7)); 26 | console.log('forLoop(0, 1, 2, 3, 4, 5, 6, 7): %j', forLoop(0, 1, 2, 3, 4, 5, 6, 7)); 27 | 28 | suite 29 | 30 | .add('Array.prototype.slice.call(arguments)', function() { 31 | slice(0, 1, 2, 3, 4, 5, 6, 7); 32 | }) 33 | .add('Array.prototype.slice.call(arguments, 0)', function() { 34 | slice0(0, 1, 2, 3, 4, 5, 6, 7); 35 | }) 36 | .add('for(let i = 0; i < args.length; i++) {}', function() { 37 | forLoop(0, 1, 2, 3, 4, 5, 6, 7); 38 | }) 39 | 40 | .on('cycle', function(event) { 41 | benchmarks.add(event.target); 42 | }) 43 | .on('start', function() { 44 | console.log('\n arguments to args Benchmark\n node version: %s, date: %s\n Starting...', 45 | process.version, Date()); 46 | }) 47 | .on('complete', function done() { 48 | benchmarks.log(); 49 | }) 50 | .run({ async: false }); 51 | 52 | // slice(0, 1, 2, 3, 4, 5, 6, 7): [0,1,2,3,4,5,6,7] 53 | // slice0(0, 1, 2, 3, 4, 5, 6, 7): [0,1,2,3,4,5,6,7] 54 | // forLoop(0, 1, 2, 3, 4, 5, 6, 7): [0,1,2,3,4,5,6,7] 55 | // 56 | // arguments to args Benchmark 57 | // node version: v2.4.0, date: Tue Jul 21 2015 01:39:54 GMT+0800 (CST) 58 | // Starting... 59 | // 3 tests completed. 60 | // 61 | // Array.prototype.slice.call(arguments) x 4,537,649 ops/sec ±1.18% (94 runs sampled) 62 | // Array.prototype.slice.call(arguments, 0) x 4,605,132 ops/sec ±0.87% (96 runs sampled) 63 | // for(let i = 0; i < args.length; i++) {} x 30,435,436 ops/sec ±0.91% (93 runs sampled) 64 | -------------------------------------------------------------------------------- /benchmark/apply.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Benchmark = require('benchmark'); 4 | const benchmarks = require('beautify-benchmark'); 5 | const util = require('util'); 6 | 7 | const suite = new Benchmark.Suite(); 8 | 9 | function normal(text) { 10 | if (arguments.length === 2) { 11 | return util.format(text, arguments[1]); 12 | } else if (arguments.length === 3) { 13 | return util.format(text, arguments[1], arguments[2]); 14 | } else if (arguments.length === 4) { 15 | return util.format(text, arguments[1], arguments[2], arguments[3]); 16 | } else if (arguments.length === 5) { 17 | return util.format(text, arguments[1], arguments[2], arguments[3], arguments[4]); 18 | } 19 | } 20 | 21 | function apply() { 22 | const args = Array.prototype.slice.call(arguments); 23 | return util.format.apply(util, args); 24 | } 25 | 26 | function apply2() { 27 | const args = new Array(arguments.length); 28 | for (let i = 0, l = arguments.length; i < l; i++) { 29 | args[i] = arguments[i]; 30 | } 31 | return util.format.apply(util, args); 32 | } 33 | 34 | console.log('normal(): %s', normal('this is %s.', 'string')); 35 | console.log('apply(): %s', apply('this is %s.', 'string')); 36 | console.log('apply2(): %s', apply2('this is %s.', 'string')); 37 | 38 | suite 39 | 40 | .add('normal(arg0, arg1, ...)', function() { 41 | normal('this is %s.', 'string'); 42 | normal('this is %s and %s.', 'string', 'string2'); 43 | normal('this is %s and %s and %s.', 'string', 'string2', 'string3'); 44 | normal('this is %s and %s and %s and %s.', 'string', 'string2', 'string3', 'string4'); 45 | }) 46 | .add('function.apply(arg0, arg1, ...)', function() { 47 | apply('this is %s.', 'string'); 48 | apply('this is %s and %s.', 'string', 'string2'); 49 | apply('this is %s and %s and %s.', 'string', 'string2', 'string3'); 50 | apply('this is %s and %s and %s and %s.', 'string', 'string2', 'string3', 'string4'); 51 | }) 52 | .add('function.apply2(arg0, arg1, ...)', function() { 53 | apply2('this is %s.', 'string'); 54 | apply2('this is %s and %s.', 'string', 'string2'); 55 | apply2('this is %s and %s and %s.', 'string', 'string2', 'string3'); 56 | apply2('this is %s and %s and %s and %s.', 'string', 'string2', 'string3', 'string4'); 57 | }) 58 | 59 | .on('cycle', function(event) { 60 | benchmarks.add(event.target); 61 | }) 62 | .on('start', function() { 63 | console.log('\n dynamic arguments Benchmark\n node version: %s, date: %s\n Starting...', 64 | process.version, Date()); 65 | }) 66 | .on('complete', function done() { 67 | benchmarks.log(); 68 | }) 69 | .run({ async: false }); 70 | 71 | // normal(): this is string. 72 | // apply(): this is string. 73 | // apply2(): this is string. 74 | // 75 | // dynamic arguments Benchmark 76 | // node version: v2.2.1, date: Sun Aug 30 2015 20:45:42 GMT+0800 (CST) 77 | // Starting... 78 | // 3 tests completed. 79 | // 80 | // normal(arg0, arg1, ...) x 273,983 ops/sec ±0.80% (101 runs sampled) 81 | // function.apply(arg0, arg1, ...) x 222,616 ops/sec ±0.70% (98 runs sampled) 82 | // function.apply2(arg0, arg1, ...) x 265,349 83 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | 2 | 1.12.0 / 2019-06-16 3 | ================== 4 | 5 | **features** 6 | * [[`47162d3`](http://github.com/koajs/locales/commit/47162d3230427957bfb16644f4db6e3c5490b830)] - feat: gettext from app (#38) (fengmk2 <>) 7 | 8 | 1.11.0 / 2019-04-30 9 | ================== 10 | 11 | **features** 12 | * [[`0767037`](http://github.com/koajs/locales/commit/0767037b3cd27ddf1b82a93f03ace79e76c0e400)] - feat: ctx.__setLocale (#36) (Yiyu He <>) 13 | 14 | 1.10.0 / 2019-04-29 15 | ================== 16 | 17 | **features** 18 | * [[`3043365`](http://github.com/koajs/locales/commit/3043365e09cfd76bf6ef54a4cd7347a97d08fdc5)] - feat: add __getLocaleOrigin (#35) (Yiyu He <>) 19 | 20 | 1.9.0 / 2019-04-17 21 | ================== 22 | 23 | **features** 24 | * [[`08037ee`](http://github.com/koajs/locales/commit/08037ee0ae0a1d74a63d1d7112c79fa43ddf6cd0)] - feat: allow custom locale store cookie domain (#33) (fengmk2 <>) 25 | 26 | 1.8.0 / 2018-01-12 27 | ================== 28 | 29 | **features** 30 | * [[`f89c675`](http://github.com/koajs/locales/commit/f89c6755c1b16a78617ceb071d1c9a8137c8c7a6)] - feat: add writeCookie option (#28) (Tao Xu <>) 31 | 32 | 1.7.0 / 2017-04-27 33 | ================== 34 | 35 | * feat: Add more debug information on a 'silly' level 36 | 37 | 1.6.0 / 2017-04-27 38 | ================== 39 | 40 | * feat: support header lang from localeAlias (#27) 41 | 42 | 1.5.2 / 2017-01-13 43 | ================== 44 | 45 | * fix: make sure signed=false on set/get cookie (#24) 46 | 47 | 1.5.1 / 2016-05-21 48 | ================== 49 | 50 | * deps: humanize-ms@1.2.0, use ^ (#22) 51 | 52 | 1.5.0 / 2016-03-16 53 | ================== 54 | 55 | * feat: add localeAlias options 56 | * chore(package): update benchmark to version 2.0.0 57 | 58 | 1.4.4 / 2015-12-23 59 | ================== 60 | 61 | * fix: return empty if the key-value is empty value 62 | 63 | 1.4.3 / 2015-12-09 64 | ================== 65 | 66 | * fix: if header sent, don't set the cookie 67 | * doc(options.dirs): fix api doc 68 | 69 | 1.4.2 / 2015-09-20 70 | ================== 71 | 72 | * refact(es6): use es6 syntax. 73 | 74 | 1.4.1 / 2015-09-18 75 | ================== 76 | 77 | * refact(nested-data): transform nested data on load localization data. 78 | 79 | 1.4.0 / 2015-09-17 80 | ================== 81 | 82 | * feat: Support nested locale keys. 83 | 84 | 1.3.1 / 2015-09-15 85 | ================== 86 | 87 | * fix: merge-descriptors should be in dependencies 88 | 89 | 1.3.0 / 2015-09-14 90 | ================== 91 | 92 | * feat: Multiple locale paths support. 93 | * refact(apply): apply is not too slow than direct call. fixed #2 94 | 95 | 1.2.0 / 2015-08-31 96 | ================== 97 | 98 | * refact(endsWith): endsWith is fast than regexp 99 | * revert slice arguments to args 100 | 101 | 1.1.0 / 2015-08-31 102 | ================== 103 | 104 | * feat: paramter support object 105 | * refact: arguments to array. 106 | * optimize use regexp without caught group 107 | 108 | 1.0.2 / 2015-05-17 109 | ================== 110 | 111 | * feat: support *.properties resource files 112 | 113 | 1.0.1 / 2015-05-16 114 | ================== 115 | 116 | * Optimization killers 117 | 118 | 1.0.0 / 2015-05-16 119 | ================== 120 | 121 | * first release 122 | -------------------------------------------------------------------------------- /benchmark/nested-value.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Benchmark = require('benchmark'); 4 | const benchmarks = require('beautify-benchmark'); 5 | 6 | const suite = new Benchmark.Suite(); 7 | 8 | function getNestedValue(data, key) { 9 | const keys = key.split('.'); 10 | for (let i = 0; typeof data === 'object' && i < keys.length; i++) { 11 | data = data[keys[i]]; 12 | } 13 | return data; 14 | } 15 | 16 | const resource = { 17 | 'model.user.foo.bar.aa': 'Hello', 18 | model: { 19 | user: { 20 | fields: { 21 | name: 'Real Name', 22 | age: 'Age', 23 | a: { 24 | b: { 25 | c: { 26 | d: { 27 | e: { 28 | f: 'fff', 29 | }, 30 | }, 31 | model: { 32 | user: { 33 | fields: { 34 | name: 'Real Name', 35 | age: 'Age', 36 | a: { 37 | b: { 38 | c: { 39 | d: { 40 | e: { 41 | f: 'fff', 42 | }, 43 | }, 44 | }, 45 | }, 46 | }, 47 | }, 48 | }, 49 | post: { 50 | fields: { 51 | title: 'Subject', 52 | }, 53 | }, 54 | }, 55 | }, 56 | }, 57 | }, 58 | }, 59 | model: { 60 | user: { 61 | fields: { 62 | name: 'Real Name', 63 | age: 'Age', 64 | a: { 65 | b: { 66 | c: { 67 | d: { 68 | e: { 69 | f: 'fff', 70 | }, 71 | }, 72 | }, 73 | }, 74 | }, 75 | }, 76 | }, 77 | post: { 78 | fields: { 79 | title: 'Subject', 80 | }, 81 | }, 82 | }, 83 | }, 84 | post: { 85 | fields: { 86 | title: 'Subject', 87 | }, 88 | }, 89 | model: { 90 | user: { 91 | fields: { 92 | name: 'Real Name', 93 | age: 'Age', 94 | a: { 95 | b: { 96 | c: { 97 | d: { 98 | e: { 99 | f: 'fff', 100 | }, 101 | }, 102 | }, 103 | }, 104 | }, 105 | }, 106 | }, 107 | post: { 108 | fields: { 109 | title: 'Subject', 110 | }, 111 | }, 112 | }, 113 | }, 114 | }; 115 | 116 | const fullKey = 'model.user.fields.a.b.c.d.e.f'; 117 | 118 | console.log('Deeps: ', fullKey.split('.').length); 119 | 120 | // console.log('getNestedValue:', getNestedValue(resource, fullKey)); 121 | 122 | suite 123 | 124 | .add('direct read a key', function() { 125 | resource['model.user.foo.bar.aa']; 126 | }) 127 | .add('by nested', function() { 128 | getNestedValue(resource, fullKey); 129 | }) 130 | .on('cycle', function(event) { 131 | benchmarks.add(event.target); 132 | }) 133 | .on('complete', function done() { 134 | benchmarks.log(); 135 | }) 136 | .run({ async: false }); 137 | -------------------------------------------------------------------------------- /benchmark/flattening.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Benchmark = require('benchmark'); 4 | const benchmarks = require('beautify-benchmark'); 5 | 6 | const suite = new Benchmark.Suite(); 7 | 8 | function isObject(obj) { 9 | return Object.prototype.toString.call(obj) === '[object Object]'; 10 | } 11 | 12 | function flattening(data) { 13 | 14 | const result = {}; 15 | 16 | function deepFlat (data, keys) { 17 | Object.keys(data).forEach(function(key) { 18 | const value = data[key]; 19 | const k = keys ? key : keys + '.' + key; 20 | if (!isObject(value)) { 21 | return result[k] = String(value); 22 | } 23 | deepFlat(value, k); 24 | }); 25 | } 26 | 27 | deepFlat(data, ''); 28 | 29 | return result; 30 | } 31 | 32 | function flattening_1(data) { 33 | 34 | const result = {}; 35 | 36 | function deepFlat (data, keys) { 37 | Object.keys(data).forEach(function(key) { 38 | const value = data[key]; 39 | const k = keys.concat(key); 40 | if (isObject(value)) { 41 | deepFlat(value, k); 42 | } else { 43 | result[k.join('.')] = String(value); 44 | } 45 | }); 46 | } 47 | 48 | deepFlat(data, []); 49 | 50 | return result; 51 | } 52 | 53 | function flattening_2(data) { 54 | 55 | const result = {}; 56 | 57 | function deepFlat (data, flatKey, key) { 58 | const value = data[key]; 59 | if (isObject(value)) { 60 | Object.keys(value).forEach(function(k) { 61 | deepFlat(value, flatKey + '.' + k, k); 62 | }); 63 | } else { 64 | result[flatKey] = String(value); 65 | } 66 | } 67 | 68 | Object.keys(data).forEach(function(key) { 69 | deepFlat(data, key, key); 70 | }); 71 | return result; 72 | } 73 | 74 | const resource = { 75 | 'model.user.foo.bar.aa': 'Hello', 76 | model: { 77 | user: { 78 | fields: { 79 | name: 'Real Name', 80 | age: 'Age', 81 | a: { 82 | b: { 83 | c: { 84 | d: { 85 | e: { 86 | f: 'fff', 87 | }, 88 | }, 89 | model: { 90 | user: { 91 | fields: { 92 | name: 'Real Name', 93 | age: 'Age', 94 | a: { 95 | b: { 96 | c: { 97 | d: { 98 | e: { 99 | f: 'fff', 100 | }, 101 | }, 102 | }, 103 | }, 104 | }, 105 | }, 106 | }, 107 | post: { 108 | fields: { 109 | title: 'Subject', 110 | }, 111 | }, 112 | }, 113 | }, 114 | }, 115 | }, 116 | }, 117 | model: { 118 | user: { 119 | fields: { 120 | name: 'Real Name', 121 | age: 'Age', 122 | a: { 123 | b: { 124 | c: { 125 | d: { 126 | e: { 127 | f: 'fff', 128 | }, 129 | }, 130 | }, 131 | }, 132 | }, 133 | }, 134 | }, 135 | post: { 136 | fields: { 137 | title: 'Subject', 138 | }, 139 | }, 140 | }, 141 | }, 142 | post: { 143 | fields: { 144 | title: 'Subject', 145 | }, 146 | }, 147 | model: { 148 | user: { 149 | fields: { 150 | name: 'Real Name', 151 | age: 'Age', 152 | a: { 153 | b: { 154 | c: { 155 | d: { 156 | e: { 157 | f: 'fff', 158 | }, 159 | }, 160 | }, 161 | }, 162 | }, 163 | }, 164 | }, 165 | post: { 166 | fields: { 167 | title: 'Subject', 168 | }, 169 | }, 170 | }, 171 | }, 172 | }; 173 | 174 | //console.log('flattening:', flattening(resource)); 175 | //console.log('flattening_1:', flattening_1(resource)); 176 | //console.log('flattening_2:', flattening_2(resource)); 177 | 178 | suite 179 | 180 | .add('flattening', function() { 181 | flattening(resource); 182 | }) 183 | .add('flattening_1', function() { 184 | flattening_1(resource); 185 | }) 186 | .add('flattening_2', function() { 187 | flattening_2(resource); 188 | }) 189 | .on('cycle', function(event) { 190 | benchmarks.add(event.target); 191 | }) 192 | .on('complete', function done() { 193 | benchmarks.log(); 194 | }) 195 | .run({ async: false }); 196 | 197 | //$ node benchmark/flattening.js 198 | // 199 | // 3 tests completed. 200 | // 201 | // flattening x 32,863 ops/sec ±0.83% (98 runs sampled) 202 | // flattening_1 x 10,434 ops/sec ±0.73% (96 runs sampled) 203 | // flattening_2 x 21,734 ops/sec ±1.04% (95 runs sampled) 204 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "ecmaFeatures": { 3 | "jsx": true 4 | }, 5 | "parser": "espree", 6 | "env": { 7 | "amd": false, 8 | "jasmine": false, 9 | "node": true, 10 | "mocha": true, 11 | "browser": true, 12 | "builtin": true, 13 | "es6": true 14 | }, 15 | "rules": { 16 | "no-alert": 2, 17 | "no-array-constructor": 2, 18 | "no-bitwise": 2, 19 | "no-caller": 2, 20 | "no-catch-shadow": 2, 21 | "no-cond-assign": [2, "except-parens"], 22 | "no-constant-condition": 2, 23 | "no-continue": 0, 24 | "no-control-regex": 2, 25 | "no-debugger": 2, 26 | "no-delete-var": 2, 27 | "no-div-regex": 0, 28 | "no-dupe-keys": 2, 29 | "no-dupe-args": 2, 30 | "no-duplicate-case": 2, 31 | "no-else-return": 0, 32 | "no-empty": 2, 33 | "no-empty-character-class": 2, 34 | "no-empty-label": 2, 35 | "no-eq-null": 0, 36 | "no-eval": 2, 37 | "no-ex-assign": 2, 38 | "no-extend-native": 2, 39 | "no-extra-bind": 2, 40 | "no-extra-boolean-cast": 2, 41 | "no-extra-parens": 2, 42 | "no-extra-semi": 2, 43 | "no-fallthrough": 2, 44 | "no-floating-decimal": 0, 45 | "no-func-assign": 2, 46 | "no-implied-eval": 2, 47 | "no-inline-comments": 0, 48 | "no-inner-declarations": [2, "functions"], 49 | "no-invalid-regexp": 2, 50 | "no-irregular-whitespace": 2, 51 | "no-iterator": 0, 52 | "no-label-var": 2, 53 | "no-labels": 2, 54 | "no-lone-blocks": 2, 55 | "no-lonely-if": 0, 56 | "no-loop-func": 2, 57 | "no-mixed-requires": [0, false], 58 | "no-mixed-spaces-and-tabs": [2, false], 59 | "linebreak-style": [0, "unix"], 60 | "no-multi-spaces": 2, 61 | "no-multi-str": 2, 62 | "no-multiple-empty-lines": [0, { 63 | "max": 2 64 | }], 65 | "no-native-reassign": 2, 66 | "no-negated-in-lhs": 2, 67 | "no-nested-ternary": 0, 68 | "no-new": 0, 69 | "no-new-func": 2, 70 | "no-new-object": 2, 71 | "no-new-require": 0, 72 | "no-new-wrappers": 2, 73 | "no-obj-calls": 2, 74 | "no-octal": 2, 75 | "no-octal-escape": 2, 76 | "no-param-reassign": 0, 77 | "no-path-concat": 0, 78 | "no-plusplus": 0, 79 | "no-process-env": 0, 80 | "no-process-exit": 0, 81 | "no-proto": 2, 82 | "no-redeclare": 2, 83 | "no-regex-spaces": 2, 84 | "no-restricted-modules": 0, 85 | "no-return-assign": 0, 86 | "no-script-url": 2, 87 | "no-self-compare": 0, 88 | "no-sequences": 2, 89 | "no-shadow": 0, 90 | "no-shadow-restricted-names": 2, 91 | "no-spaced-func": 2, 92 | "no-sparse-arrays": 2, 93 | "no-sync": 0, 94 | "no-ternary": 0, 95 | "no-trailing-spaces": 2, 96 | "no-this-before-super": 0, 97 | "no-throw-literal": 0, 98 | "no-undef": 2, 99 | "no-undef-init": 2, 100 | "no-undefined": 0, 101 | "no-unexpected-multiline": 0, 102 | "no-underscore-dangle": 0, 103 | "no-unneeded-ternary": 0, 104 | "no-unreachable": 2, 105 | "no-unused-expressions": 0, 106 | "no-unused-vars": 2, 107 | "no-use-before-define": [2, "nofunc"], 108 | "no-void": 0, 109 | "no-var": 2, 110 | "no-const-assign": 2, 111 | "prefer-const": 2, 112 | "no-warning-comments": [0, { 113 | "terms": ["todo", "fixme", "xxx"], 114 | "location": "start" 115 | }], 116 | "no-with": 2, 117 | "array-bracket-spacing": [0, "never"], 118 | "accessor-pairs": 0, 119 | "block-scoped-var": 0, 120 | "brace-style": [0, "1tbs"], 121 | "camelcase": 0, 122 | "comma-dangle": [2, "always-multiline"], 123 | "comma-spacing": 2, 124 | "comma-style": 0, 125 | "complexity": [0, 11], 126 | "computed-property-spacing": [0, "never"], 127 | "consistent-return": 0, 128 | "consistent-this": [0, "that"], 129 | "constructor-super": 0, 130 | "curly": [2, "multi-line"], 131 | "default-case": 0, 132 | "dot-location": 0, 133 | "dot-notation": 0, 134 | "eol-last": 2, 135 | "eqeqeq": 2, 136 | "func-names": 0, 137 | "func-style": [0, "declaration"], 138 | "generator-star-spacing": 0, 139 | "guard-for-in": 0, 140 | "handle-callback-err": 0, 141 | "indent": [2, 2], 142 | "key-spacing": [2, { 143 | "beforeColon": false, 144 | "afterColon": true 145 | }], 146 | "lines-around-comment": 0, 147 | "max-depth": [0, 4], 148 | "max-len": [0, 80, 4], 149 | "max-nested-callbacks": [0, 2], 150 | "max-params": [0, 3], 151 | "max-statements": [0, 10], 152 | "new-cap": 0, 153 | "new-parens": 2, 154 | "newline-after-var": 0, 155 | "object-curly-spacing": [0, "never"], 156 | "object-shorthand": 0, 157 | "one-var": 0, 158 | "operator-assignment": [0, "always"], 159 | "operator-linebreak": 0, 160 | "padded-blocks": 0, 161 | "quote-props": 0, 162 | "quotes": [2, "single"], 163 | "radix": 0, 164 | "semi": 2, 165 | "semi-spacing": [2, { 166 | "before": false, 167 | "after": true 168 | }], 169 | "sort-vars": 0, 170 | "space-after-keywords": [0, "always"], 171 | "space-before-blocks": [0, "always"], 172 | "space-before-function-paren": [0, "always"], 173 | "space-in-parens": [0, "never"], 174 | "space-infix-ops": 2, 175 | "space-return-throw-case": 2, 176 | "space-unary-ops": [2, { 177 | "words": true, 178 | "nonwords": false 179 | }], 180 | "spaced-comment": 0, 181 | "strict": [2, "global"], 182 | "use-isnan": 2, 183 | "valid-jsdoc": 0, 184 | "valid-typeof": 0, 185 | "vars-on-top": 0, 186 | "wrap-iife": 0, 187 | "wrap-regex": 0, 188 | "yoda": [2, "never"] 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | koa-locales 2 | ======= 3 | 4 | [![NPM version][npm-image]][npm-url] 5 | [![build status][travis-image]][travis-url] 6 | [![Test coverage][cov-image]][cov-url] 7 | [![David deps][david-image]][david-url] 8 | [![npm download][download-image]][download-url] 9 | 10 | koa locales, i18n solution for koa: 11 | 12 | 1. All locales resources location on `options.dirs`. 13 | 2. resources file supports: `*.js`, `*.json`, `*.yml`, `*.yaml` and `*.properties`, see [examples](test/locales/). 14 | 3. One api: `__(key[, value, ...])`. 15 | 4. Auto detect request locale from `query`, `cookie` and `header: Accept-Language`. 16 | 17 | ## Installation 18 | 19 | ```bash 20 | $ npm install koa-locales --save 21 | ``` 22 | 23 | ## Quick start 24 | 25 | ```js 26 | const koa = require('koa'); 27 | const locales = require('koa-locales'); 28 | 29 | const app = koa(); 30 | const options = { 31 | dirs: [__dirname + '/locales', __dirname + '/foo/locales'], 32 | }; 33 | locales(app, options); 34 | ``` 35 | 36 | ## API Reference 37 | 38 | ### `locales(app, options)` 39 | 40 | Patch locales functions to koa app. 41 | 42 | - {Application} app: koa app instance. 43 | - {Object} options: optional params. 44 | - {String} functionName: locale function name patch on koa context. Optional, default is `__`. 45 | - {String} dirs: locales resources store directories. Optional, default is `['$PWD/locales']`. 46 | - {String} defaultLocale: default locale. Optional, default is `en-US`. 47 | - {String} queryField: locale field name on query. Optional, default is `locale`. 48 | - {String} cookieField: locale field name on cookie. Optional, default is `locale`. 49 | - {String} cookieDomain: domain on cookie. Optional, default is `''`. 50 | - {Object} localeAlias: locale value map. Optional, default is `{}`. 51 | - {Boolean} writeCookie: set cookie if header not sent. Optional, default is `true`. 52 | - {String|Number} cookieMaxAge: set locale cookie value max age. Optional, default is `1y`, expired after one year. 53 | 54 | ```js 55 | locales({ 56 | app: app, 57 | dirs: [__dirname + '/app/locales'], 58 | defaultLocale: 'zh-CN', 59 | }); 60 | ``` 61 | 62 | #### Aliases 63 | 64 | The key `options.localeAlias` allows to not repeat dictionary files, as you can configure to use the same file for *es_ES* for *es*, or *en_UK* for *en*. 65 | 66 | ```js 67 | locales({ 68 | localeAlias: { 69 | es: es_ES, 70 | en: en_UK, 71 | }, 72 | }); 73 | ``` 74 | 75 | ### `context.__(key[, value1[, value2, ...]])` 76 | 77 | Get current request locale text. 78 | 79 | ```js 80 | async function home(ctx) { 81 | ctx.body = { 82 | message: ctx.__('Hello, %s', 'fengmk2'), 83 | }; 84 | } 85 | ``` 86 | 87 | Examples: 88 | 89 | ```js 90 | __('Hello, %s. %s', 'fengmk2', 'koa rock!') 91 | => 92 | 'Hello fengmk2. koa rock!' 93 | 94 | __('{0} {0} {1} {1} {1}', ['foo', 'bar']) 95 | => 96 | 'foo foo bar bar bar' 97 | 98 | __('{a} {a} {b} {b} {b}', {a: 'foo', b: 'bar'}) 99 | => 100 | 'foo foo bar bar bar' 101 | ``` 102 | 103 | ### `context.__getLocale()` 104 | 105 | Get locale from query / cookie and header. 106 | 107 | ### `context.__setLocale()` 108 | 109 | Set locale and cookie. 110 | 111 | ### `context.__getLocaleOrigin()` 112 | 113 | Where does locale come from, could be `query`, `cookie`, `header` and `default`. 114 | 115 | ### `app.__(locale, key[, value1[, value2, ...]])` 116 | 117 | Get the given locale text on application level. 118 | 119 | ```js 120 | console.log(app.__('zh', 'Hello')); 121 | // stdout '你好' for Chinese 122 | ``` 123 | 124 | ## Usage on template 125 | 126 | ```js 127 | this.state.__ = this.__.bind(this); 128 | ``` 129 | 130 | [Nunjucks] example: 131 | 132 | ```html 133 | {{ __('Hello, %s', user.name) }} 134 | ``` 135 | 136 | [Pug] example: 137 | 138 | ```pug 139 | p= __('Hello, %s', user.name) 140 | ``` 141 | 142 | [Koa-pug] integration: 143 | 144 | You can set the property *locals* on the KoaPug instance, where the default locals are stored. 145 | 146 | ```js 147 | app.use(async (ctx, next) => { 148 | koaPug.locals.__ = ctx.__.bind(ctx); 149 | await next(); 150 | }); 151 | ``` 152 | 153 | ## Debugging 154 | 155 | If you are interested on knowing what locale was chosen and why you can enable the debug messages from [debug]. 156 | 157 | There is two level of verbosity: 158 | 159 | ```sh 160 | $ DEBUG=koa-locales node . 161 | ``` 162 | With this line it only will show one line per request, with the chosen language and the origin where the locale come from (queryString, header or cookie). 163 | 164 | ```sh 165 | $ DEBUG=koa-locales:silly node . 166 | ``` 167 | Use this level if something doesn't work as you expect. This is going to debug everything, including each translated line of text. 168 | 169 | ## License 170 | 171 | [MIT](LICENSE) 172 | 173 | 174 | [nunjucks]: https://www.npmjs.com/package/nunjucks 175 | [debug]: https://www.npmjs.com/package/debug 176 | [pug]: https://www.npmjs.com/package/pug 177 | [koa-pug]: https://www.npmjs.com/package/koa-pug 178 | 179 | [npm-image]: https://img.shields.io/npm/v/koa-locales.svg?style=flat-square 180 | [npm-url]: https://npmjs.org/package/koa-locales 181 | [travis-image]: https://img.shields.io/travis/koajs/locales.svg?style=flat-square 182 | [travis-url]: https://travis-ci.org/koajs/locales 183 | [cov-image]: https://codecov.io/github/koajs/locales/coverage.svg?branch=master 184 | [cov-url]: https://codecov.io/github/koajs/locales?branch=master 185 | [david-image]: https://img.shields.io/david/koajs/locales.svg?style=flat-square 186 | [david-url]: https://david-dm.org/koajs/locales 187 | [download-image]: https://img.shields.io/npm/dm/koa-locales.svg?style=flat-square 188 | [download-url]: https://npmjs.org/package/koa-locales 189 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Debug = require('debug'); 4 | const debug = Debug('koa-locales'); 5 | const debugSilly = Debug('koa-locales:silly'); 6 | const ini = require('ini'); 7 | const util = require('util'); 8 | const fs = require('fs'); 9 | const path = require('path'); 10 | const ms = require('humanize-ms'); 11 | const assign = require('object-assign'); 12 | const yaml = require('js-yaml'); 13 | 14 | const DEFAULT_OPTIONS = { 15 | defaultLocale: 'en-US', 16 | queryField: 'locale', 17 | cookieField: 'locale', 18 | localeAlias: {}, 19 | writeCookie: true, 20 | cookieMaxAge: '1y', 21 | dir: undefined, 22 | dirs: [path.join(process.cwd(), 'locales')], 23 | functionName: '__', 24 | }; 25 | 26 | module.exports = function (app, options) { 27 | options = assign({}, DEFAULT_OPTIONS, options); 28 | const defaultLocale = formatLocale(options.defaultLocale); 29 | const queryField = options.queryField; 30 | const cookieField = options.cookieField; 31 | const cookieDomain = options.cookieDomain; 32 | const localeAlias = options.localeAlias; 33 | const writeCookie = options.writeCookie; 34 | const cookieMaxAge = ms(options.cookieMaxAge); 35 | const localeDir = options.dir; 36 | const localeDirs = options.dirs; 37 | const functionName = options.functionName; 38 | const resources = {}; 39 | 40 | /** 41 | * @Deprecated Use options.dirs instead. 42 | */ 43 | if (localeDir && localeDirs.indexOf(localeDir) === -1) { 44 | localeDirs.push(localeDir); 45 | } 46 | 47 | for (let i = 0; i < localeDirs.length; i++) { 48 | const dir = localeDirs[i]; 49 | 50 | if (!fs.existsSync(dir)) { 51 | continue; 52 | } 53 | 54 | const names = fs.readdirSync(dir); 55 | for (let j = 0; j < names.length; j++) { 56 | const name = names[j]; 57 | const filepath = path.join(dir, name); 58 | // support en_US.js => en-US.js 59 | const locale = formatLocale(name.split('.')[0]); 60 | let resource = {}; 61 | 62 | if (name.endsWith('.js') || name.endsWith('.json')) { 63 | resource = flattening(require(filepath)); 64 | } else if (name.endsWith('.properties')) { 65 | resource = ini.parse(fs.readFileSync(filepath, 'utf8')); 66 | } else if (name.endsWith('.yml') || name.endsWith('.yaml')) { 67 | resource = flattening(yaml.safeLoad(fs.readFileSync(filepath, 'utf8'))); 68 | } 69 | 70 | resources[locale] = resources[locale] || {}; 71 | assign(resources[locale], resource); 72 | } 73 | } 74 | 75 | debug('Init locales with %j, got %j resources', options, Object.keys(resources)); 76 | 77 | if (typeof app[functionName] !== 'undefined') { 78 | console.warn('[koa-locales] will override exists "%s" function on app', functionName); 79 | } 80 | 81 | function gettext(locale, key, value) { 82 | if (arguments.length === 0 || arguments.length === 1) { 83 | // __() 84 | // --('en') 85 | return ''; 86 | } 87 | 88 | const resource = resources[locale] || {}; 89 | 90 | let text = resource[key]; 91 | if (text === undefined) { 92 | text = key; 93 | } 94 | 95 | debugSilly('%s: %j => %j', locale, key, text); 96 | if (!text) { 97 | return ''; 98 | } 99 | 100 | if (arguments.length === 2) { 101 | // __(locale, key) 102 | return text; 103 | } 104 | if (arguments.length === 3) { 105 | if (isObject(value)) { 106 | // __(locale, key, object) 107 | // __('zh', '{a} {b} {b} {a}', {a: 'foo', b: 'bar'}) 108 | // => 109 | // foo bar bar foo 110 | return formatWithObject(text, value); 111 | } 112 | 113 | if (Array.isArray(value)) { 114 | // __(locale, key, array) 115 | // __('zh', '{0} {1} {1} {0}', ['foo', 'bar']) 116 | // => 117 | // foo bar bar foo 118 | return formatWithArray(text, value); 119 | } 120 | 121 | // __(locale, key, value) 122 | return util.format(text, value); 123 | } 124 | 125 | // __(locale, key, value1, ...) 126 | const args = new Array(arguments.length - 1); 127 | args[0] = text; 128 | for (let i = 2; i < arguments.length; i++) { 129 | args[i - 1] = arguments[i]; 130 | } 131 | return util.format.apply(util, args); 132 | } 133 | 134 | app[functionName] = gettext; 135 | 136 | app.context[functionName] = function (key, value) { 137 | if (arguments.length === 0) { 138 | // __() 139 | return ''; 140 | } 141 | 142 | const locale = this.__getLocale(); 143 | if (arguments.length === 1) { 144 | return gettext(locale, key); 145 | } 146 | if (arguments.length === 2) { 147 | return gettext(locale, key, value); 148 | } 149 | const args = new Array(arguments.length + 1); 150 | args[0] = locale; 151 | for (let i = 0; i < arguments.length; i++) { 152 | args[i + 1] = arguments[i]; 153 | } 154 | return gettext.apply(this, args); 155 | }; 156 | 157 | // 1. query: /?locale=en-US 158 | // 2. cookie: locale=zh-TW 159 | // 3. header: Accept-Language: zh-CN,zh;q=0.5 160 | app.context.__getLocale = function () { 161 | if (this.__locale) { 162 | return this.__locale; 163 | } 164 | 165 | const cookieLocale = this.cookies.get(cookieField, { signed: false }); 166 | 167 | // 1. Query 168 | let locale = this.query[queryField]; 169 | let localeOrigin = 'query'; 170 | 171 | // 2. Cookie 172 | if (!locale) { 173 | locale = cookieLocale; 174 | localeOrigin = 'cookie'; 175 | } 176 | 177 | // 3. Header 178 | if (!locale) { 179 | // Accept-Language: zh-CN,zh;q=0.5 180 | // Accept-Language: zh-CN 181 | let languages = this.acceptsLanguages(); 182 | if (languages) { 183 | if (Array.isArray(languages)) { 184 | if (languages[0] === '*') { 185 | languages = languages.slice(1); 186 | } 187 | if (languages.length > 0) { 188 | for (let i = 0; i < languages.length; i++) { 189 | const lang = formatLocale(languages[i]); 190 | if (resources[lang] || localeAlias[lang]) { 191 | locale = lang; 192 | localeOrigin = 'header'; 193 | break; 194 | } 195 | } 196 | } 197 | } else { 198 | locale = languages; 199 | localeOrigin = 'header'; 200 | } 201 | } 202 | 203 | // all missing, set it to defaultLocale 204 | if (!locale) { 205 | locale = defaultLocale; 206 | localeOrigin = 'default'; 207 | } 208 | } 209 | 210 | // cookie alias 211 | if (locale in localeAlias) { 212 | const originalLocale = locale; 213 | locale = localeAlias[locale]; 214 | debugSilly('Used alias, received %s but using %s', originalLocale, locale); 215 | } 216 | 217 | locale = formatLocale(locale); 218 | 219 | // validate locale 220 | if (!resources[locale]) { 221 | debugSilly('Locale %s is not supported. Using default (%s)', locale, defaultLocale); 222 | locale = defaultLocale; 223 | } 224 | 225 | // if header not send, set the locale cookie 226 | if (writeCookie && cookieLocale !== locale && !this.headerSent) { 227 | updateCookie(this, locale); 228 | } 229 | debug('Locale: %s from %s', locale, localeOrigin); 230 | debugSilly('Locale: %s from %s', locale, localeOrigin); 231 | this.__locale = locale; 232 | this.__localeOrigin = localeOrigin; 233 | return locale; 234 | }; 235 | 236 | app.context.__getLocaleOrigin = function () { 237 | if (this.__localeOrigin) return this.__localeOrigin; 238 | this.__getLocale(); 239 | return this.__localeOrigin; 240 | }; 241 | 242 | app.context.__setLocale = function (locale) { 243 | this.__locale = locale; 244 | this.__localeOrigin = 'set'; 245 | updateCookie(this, locale); 246 | }; 247 | 248 | function updateCookie(ctx, locale) { 249 | const cookieOptions = { 250 | // make sure brower javascript can read the cookie 251 | httpOnly: false, 252 | maxAge: cookieMaxAge, 253 | signed: false, 254 | domain: cookieDomain, 255 | overwrite: true, 256 | }; 257 | ctx.cookies.set(cookieField, locale, cookieOptions); 258 | debugSilly('Saved cookie with locale %s', locale); 259 | } 260 | }; 261 | 262 | function isObject(obj) { 263 | return Object.prototype.toString.call(obj) === '[object Object]'; 264 | } 265 | 266 | const ARRAY_INDEX_RE = /\{(\d+)\}/g; 267 | function formatWithArray(text, values) { 268 | return text.replace(ARRAY_INDEX_RE, function (orignal, matched) { 269 | const index = parseInt(matched); 270 | if (index < values.length) { 271 | return values[index]; 272 | } 273 | // not match index, return orignal text 274 | return orignal; 275 | }); 276 | } 277 | 278 | const Object_INDEX_RE = /\{(.+?)\}/g; 279 | function formatWithObject(text, values) { 280 | return text.replace(Object_INDEX_RE, function (orignal, matched) { 281 | const value = values[matched]; 282 | if (value) { 283 | return value; 284 | } 285 | // not match index, return orignal text 286 | return orignal; 287 | }); 288 | } 289 | 290 | function formatLocale(locale) { 291 | // support zh_CN, en_US => zh-CN, en-US 292 | return locale.replace('_', '-').toLowerCase(); 293 | } 294 | 295 | function flattening(data) { 296 | 297 | const result = {}; 298 | 299 | function deepFlat(data, keys) { 300 | Object.keys(data).forEach(function (key) { 301 | const value = data[key]; 302 | const k = keys ? keys + '.' + key : key; 303 | if (isObject(value)) { 304 | deepFlat(value, k); 305 | } else { 306 | result[k] = String(value); 307 | } 308 | }); 309 | } 310 | 311 | deepFlat(data, ''); 312 | 313 | return result; 314 | } 315 | -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'); 4 | const koa = require('koa'); 5 | const request = require('supertest'); 6 | const pedding = require('pedding'); 7 | const mm = require('mm'); 8 | const locales = require('..'); 9 | 10 | describe('koa-locales.test.js', function () { 11 | afterEach(mm.restore); 12 | 13 | describe('default options', function () { 14 | const app = createApp(); 15 | 16 | it('should use default locale: en-US', function (done) { 17 | request(app.callback()) 18 | .get('/') 19 | .expect({ 20 | email: 'Email', 21 | hello: 'Hello fengmk2, how are you today?', 22 | message: 'Hello fengmk2, how are you today? How was your 18.', 23 | empty: '', 24 | notexists_key: 'key not exists', 25 | empty_string: '', 26 | empty_value: 'emptyValue', 27 | novalue: 'key %s ok', 28 | arguments3: '1 2 3', 29 | arguments4: '1 2 3 4', 30 | arguments5: '1 2 3 4 5', 31 | arguments6: '1 2 3 4 5. 6', 32 | values: 'foo bar foo bar {2} {100}', 33 | object: 'foo bar foo bar {z}', 34 | 'gender': 'model.user.fields.gender', 35 | 'name': 'model.user.fields.name', 36 | }) 37 | .expect('Set-Cookie', /^locale=en\-us; path=\/; expires=[^;]+ GMT$/) 38 | .expect(200, done); 39 | }); 40 | 41 | it('should not set locale cookie after header sent', function (done) { 42 | request(app.callback()) 43 | .get('/headerSent') 44 | .expect('foo') 45 | .expect(200, function (err) { 46 | assert(!err, err && err.message); 47 | setTimeout(done, 50); 48 | }); 49 | }); 50 | }); 51 | 52 | describe('options.cookieDomain', function () { 53 | const app = createApp({ 54 | cookieDomain: '.foo.com', 55 | }); 56 | 57 | it('should use default locale: en-US', function (done) { 58 | request(app.callback()) 59 | .get('/') 60 | .expect({ 61 | email: 'Email', 62 | hello: 'Hello fengmk2, how are you today?', 63 | message: 'Hello fengmk2, how are you today? How was your 18.', 64 | empty: '', 65 | notexists_key: 'key not exists', 66 | empty_string: '', 67 | empty_value: 'emptyValue', 68 | novalue: 'key %s ok', 69 | arguments3: '1 2 3', 70 | arguments4: '1 2 3 4', 71 | arguments5: '1 2 3 4 5', 72 | arguments6: '1 2 3 4 5. 6', 73 | values: 'foo bar foo bar {2} {100}', 74 | object: 'foo bar foo bar {z}', 75 | 'gender': 'model.user.fields.gender', 76 | 'name': 'model.user.fields.name', 77 | }) 78 | .expect('Set-Cookie', /^locale=en\-us; path=\/; expires=[^;]+; domain=.foo.com$/) 79 | .expect(200, done); 80 | }); 81 | }); 82 | 83 | describe('custom options', function () { 84 | const app = createApp({ 85 | dirs: [__dirname + '/locales', __dirname + '/other-locales'], 86 | }); 87 | const cookieFieldMapApp = createApp({ 88 | dirs: [__dirname + '/locales', __dirname + '/other-locales'], 89 | localeAlias: { 90 | 'en': 'en-US', 91 | 'de-de': 'de', 92 | }, 93 | }); 94 | const appNotWriteCookie = createApp({ 95 | dirs: [__dirname + '/locales', __dirname + '/other-locales'], 96 | writeCookie: false, 97 | }); 98 | 99 | it('should use default locale: en-US', function (done) { 100 | request(app.callback()) 101 | .get('/') 102 | .expect({ 103 | email: 'Email', 104 | hello: 'Hello fengmk2, how are you today?', 105 | message: 'Hello fengmk2, how are you today? How was your 18.', 106 | empty: '', 107 | notexists_key: 'key not exists', 108 | empty_string: '', 109 | empty_value: 'emptyValue', 110 | novalue: 'key %s ok', 111 | arguments3: '1 2 3', 112 | arguments4: '1 2 3 4', 113 | arguments5: '1 2 3 4 5', 114 | arguments6: '1 2 3 4 5. 6', 115 | values: 'foo bar foo bar {2} {100}', 116 | object: 'foo bar foo bar {z}', 117 | 'gender': 'model.user.fields.gender', 118 | 'name': 'model.user.fields.name', 119 | }) 120 | .expect('Set-Cookie', /^locale=en\-us; path=\/; expires=\w+/) 121 | .expect(200, done); 122 | }); 123 | 124 | it('should gettext work on app.__(locale, key, value)', function (done) { 125 | request(app.callback()) 126 | .get('/app_locale_zh') 127 | .expect({ 128 | email: '邮箱1', 129 | }) 130 | .expect(200, done); 131 | }); 132 | 133 | describe('query.locale', function () { 134 | it('should use query locale: zh-CN', function (done) { 135 | request(app.callback()) 136 | .get('/?locale=zh-CN') 137 | .expect({ 138 | email: '邮箱1', 139 | hello: 'fengmk2,今天过得如何?', 140 | message: 'Hello fengmk2, how are you today? How was your 18.', 141 | empty: '', 142 | notexists_key: 'key not exists', 143 | empty_string: '', 144 | empty_value: '', 145 | novalue: 'key %s ok', 146 | arguments3: '1 2 3', 147 | arguments4: '1 2 3 4', 148 | arguments5: '1 2 3 4 5', 149 | arguments6: '1 2 3 4 5. 6', 150 | values: 'foo bar foo bar {2} {100}', 151 | object: 'foo bar foo bar {z}', 152 | 'gender': '性别', 153 | 'name': '姓名', 154 | }) 155 | .expect('Set-Cookie', /^locale=zh\-cn; path=\/; expires=\w+/) 156 | .expect(200, done); 157 | }); 158 | 159 | it('should use query locale: de on *.properties format', function (done) { 160 | request(app.callback()) 161 | .get('/?locale=de') 162 | .expect({ 163 | email: 'Emailde', 164 | hello: 'Hallo fengmk2, wie geht es dir heute?', 165 | message: 'Hallo fengmk2, wie geht es dir heute? Wie war dein 18.', 166 | empty: '', 167 | notexists_key: 'key not exists', 168 | empty_string: '', 169 | empty_value: 'emptyValue', 170 | novalue: 'key %s ok', 171 | arguments3: '1 2 3', 172 | arguments4: '1 2 3 4', 173 | arguments5: '1 2 3 4 5', 174 | arguments6: '1 2 3 4 5. 6', 175 | values: 'foo bar foo bar {2} {100}', 176 | object: 'foo bar foo bar {z}', 177 | 'gender': 'model.user.fields.gender', 178 | 'name': 'model.user.fields.name', 179 | }) 180 | .expect('Set-Cookie', /^locale=de; path=\/; expires=\w+/) 181 | .expect(200, done); 182 | }); 183 | 184 | it('should use query locale and change cookie locale', function (done) { 185 | request(app.callback()) 186 | .get('/?locale=zh-CN') 187 | .set('cookie', 'locale=zh-TW') 188 | .expect({ 189 | email: '邮箱1', 190 | hello: 'fengmk2,今天过得如何?', 191 | message: 'Hello fengmk2, how are you today? How was your 18.', 192 | empty: '', 193 | notexists_key: 'key not exists', 194 | empty_string: '', 195 | empty_value: '', 196 | novalue: 'key %s ok', 197 | arguments3: '1 2 3', 198 | arguments4: '1 2 3 4', 199 | arguments5: '1 2 3 4 5', 200 | arguments6: '1 2 3 4 5. 6', 201 | values: 'foo bar foo bar {2} {100}', 202 | object: 'foo bar foo bar {z}', 203 | 'gender': '性别', 204 | 'name': '姓名', 205 | }) 206 | .expect('Set-Cookie', /^locale=zh\-cn; path=\/; expires=\w+/) 207 | .expect(200, done); 208 | }); 209 | 210 | it('should ignore invalid locale value', function (done) { 211 | request(app.callback()) 212 | .get('/?locale=xss') 213 | .expect({ 214 | email: 'Email', 215 | hello: 'Hello fengmk2, how are you today?', 216 | message: 'Hello fengmk2, how are you today? How was your 18.', 217 | empty: '', 218 | notexists_key: 'key not exists', 219 | empty_string: '', 220 | empty_value: 'emptyValue', 221 | novalue: 'key %s ok', 222 | arguments3: '1 2 3', 223 | arguments4: '1 2 3 4', 224 | arguments5: '1 2 3 4 5', 225 | arguments6: '1 2 3 4 5. 6', 226 | values: 'foo bar foo bar {2} {100}', 227 | object: 'foo bar foo bar {z}', 228 | 'gender': 'model.user.fields.gender', 229 | 'name': 'model.user.fields.name', 230 | }) 231 | .expect('Set-Cookie', /^locale=en\-us; path=\/; expires=\w+/) 232 | .expect(200, done); 233 | }); 234 | 235 | it('should use localeAlias', function (done) { 236 | request(cookieFieldMapApp.callback()) 237 | .get('/?locale=de-de') 238 | .expect({ 239 | email: 'Emailde', 240 | hello: 'Hallo fengmk2, wie geht es dir heute?', 241 | message: 'Hallo fengmk2, wie geht es dir heute? Wie war dein 18.', 242 | empty: '', 243 | notexists_key: 'key not exists', 244 | empty_string: '', 245 | empty_value: 'emptyValue', 246 | novalue: 'key %s ok', 247 | arguments3: '1 2 3', 248 | arguments4: '1 2 3 4', 249 | arguments5: '1 2 3 4 5', 250 | arguments6: '1 2 3 4 5. 6', 251 | values: 'foo bar foo bar {2} {100}', 252 | object: 'foo bar foo bar {z}', 253 | 'gender': 'model.user.fields.gender', 254 | 'name': 'model.user.fields.name', 255 | }) 256 | .expect('Set-Cookie', /^locale=de; path=\/; expires=\w+/) 257 | .expect(200, done); 258 | }); 259 | 260 | it('should use query locale and response without set-cookie', function (done) { 261 | request(appNotWriteCookie.callback()) 262 | .get('/?locale=zh-CN') 263 | .expect({ 264 | email: '邮箱1', 265 | hello: 'fengmk2,今天过得如何?', 266 | message: 'Hello fengmk2, how are you today? How was your 18.', 267 | empty: '', 268 | notexists_key: 'key not exists', 269 | empty_string: '', 270 | empty_value: '', 271 | novalue: 'key %s ok', 272 | arguments3: '1 2 3', 273 | arguments4: '1 2 3 4', 274 | arguments5: '1 2 3 4 5', 275 | arguments6: '1 2 3 4 5. 6', 276 | values: 'foo bar foo bar {2} {100}', 277 | object: 'foo bar foo bar {z}', 278 | 'gender': '性别', 279 | 'name': '姓名', 280 | }) 281 | .expect(function (res) { 282 | if (res.headers['set-cookie'] || res.headers['Set-Cookie']) { 283 | throw new Error('should not write cookie'); 284 | } 285 | }) 286 | .expect(200, done); 287 | }); 288 | 289 | }); 290 | 291 | describe('cookie.locale', function () { 292 | it('should use cookie locale: zh-CN', function (done) { 293 | request(app.callback()) 294 | .get('/?locale=') 295 | .set('cookie', 'locale=zh-cn') 296 | .expect({ 297 | email: '邮箱1', 298 | hello: 'fengmk2,今天过得如何?', 299 | message: 'Hello fengmk2, how are you today? How was your 18.', 300 | empty: '', 301 | notexists_key: 'key not exists', 302 | empty_string: '', 303 | empty_value: '', 304 | novalue: 'key %s ok', 305 | arguments3: '1 2 3', 306 | arguments4: '1 2 3 4', 307 | arguments5: '1 2 3 4 5', 308 | arguments6: '1 2 3 4 5. 6', 309 | values: 'foo bar foo bar {2} {100}', 310 | object: 'foo bar foo bar {z}', 311 | 'gender': '性别', 312 | 'name': '姓名', 313 | }) 314 | .expect(function (res) { 315 | assert(!res.headers['set-cookie']); 316 | }) 317 | .expect(200, done); 318 | }); 319 | }); 320 | 321 | describe('Accept-Language', function () { 322 | it('should use Accept-Language: zh-CN', function (done) { 323 | done = pedding(3, done); 324 | 325 | request(app.callback()) 326 | .get('/?locale=') 327 | .set('Accept-Language', 'zh-CN') 328 | .expect({ 329 | email: '邮箱1', 330 | hello: 'fengmk2,今天过得如何?', 331 | message: 'Hello fengmk2, how are you today? How was your 18.', 332 | empty: '', 333 | notexists_key: 'key not exists', 334 | empty_string: '', 335 | empty_value: '', 336 | novalue: 'key %s ok', 337 | arguments3: '1 2 3', 338 | arguments4: '1 2 3 4', 339 | arguments5: '1 2 3 4 5', 340 | arguments6: '1 2 3 4 5. 6', 341 | values: 'foo bar foo bar {2} {100}', 342 | object: 'foo bar foo bar {z}', 343 | 'gender': '性别', 344 | 'name': '姓名', 345 | }) 346 | .expect('Set-Cookie', /^locale=zh\-cn; path=\/; expires=\w+/) 347 | .expect(200, done); 348 | 349 | request(app.callback()) 350 | .get('/?locale=') 351 | .set('Accept-Language', 'zh-CN,zh;q=0.8') 352 | .expect({ 353 | email: '邮箱1', 354 | hello: 'fengmk2,今天过得如何?', 355 | message: 'Hello fengmk2, how are you today? How was your 18.', 356 | empty: '', 357 | notexists_key: 'key not exists', 358 | empty_string: '', 359 | empty_value: '', 360 | novalue: 'key %s ok', 361 | arguments3: '1 2 3', 362 | arguments4: '1 2 3 4', 363 | arguments5: '1 2 3 4 5', 364 | arguments6: '1 2 3 4 5. 6', 365 | values: 'foo bar foo bar {2} {100}', 366 | object: 'foo bar foo bar {z}', 367 | 'gender': '性别', 368 | 'name': '姓名', 369 | }) 370 | .expect('Set-Cookie', /^locale=zh\-cn; path=\/; expires=\w+/) 371 | .expect(200, done); 372 | 373 | request(app.callback()) 374 | .get('/?locale=') 375 | .set('Accept-Language', 'en;q=0.8, es, zh_CN') 376 | .expect({ 377 | email: '邮箱1', 378 | hello: 'fengmk2,今天过得如何?', 379 | message: 'Hello fengmk2, how are you today? How was your 18.', 380 | empty: '', 381 | notexists_key: 'key not exists', 382 | empty_string: '', 383 | empty_value: '', 384 | novalue: 'key %s ok', 385 | arguments3: '1 2 3', 386 | arguments4: '1 2 3 4', 387 | arguments5: '1 2 3 4 5', 388 | arguments6: '1 2 3 4 5. 6', 389 | values: 'foo bar foo bar {2} {100}', 390 | object: 'foo bar foo bar {z}', 391 | 'gender': '性别', 392 | 'name': '姓名', 393 | }) 394 | .expect('Set-Cookie', /^locale=zh\-cn; path=\/; expires=\w+/) 395 | .expect(200, done); 396 | }); 397 | 398 | it('should work with "Accept-Language: " header', function (done) { 399 | request(app.callback()) 400 | .get('/?locale=') 401 | .set('Accept-Language', '') 402 | .expect({ 403 | email: 'Email', 404 | hello: 'Hello fengmk2, how are you today?', 405 | message: 'Hello fengmk2, how are you today? How was your 18.', 406 | empty: '', 407 | notexists_key: 'key not exists', 408 | empty_string: '', 409 | empty_value: 'emptyValue', 410 | novalue: 'key %s ok', 411 | arguments3: '1 2 3', 412 | arguments4: '1 2 3 4', 413 | arguments5: '1 2 3 4 5', 414 | arguments6: '1 2 3 4 5. 6', 415 | values: 'foo bar foo bar {2} {100}', 416 | object: 'foo bar foo bar {z}', 417 | 'gender': 'model.user.fields.gender', 418 | 'name': 'model.user.fields.name', 419 | }) 420 | .expect('Set-Cookie', /^locale=en\-us; path=\/; expires=\w+/) 421 | .expect(200, done); 422 | }); 423 | 424 | it('should work with "Accept-Language: en"', function (done) { 425 | request(app.callback()) 426 | .get('/') 427 | .set('Accept-Language', 'en') 428 | .expect({ 429 | email: 'Email', 430 | hello: 'Hello fengmk2, how are you today?', 431 | message: 'Hello fengmk2, how are you today? How was your 18.', 432 | empty: '', 433 | notexists_key: 'key not exists', 434 | empty_string: '', 435 | empty_value: 'emptyValue', 436 | novalue: 'key %s ok', 437 | arguments3: '1 2 3', 438 | arguments4: '1 2 3 4', 439 | arguments5: '1 2 3 4 5', 440 | arguments6: '1 2 3 4 5. 6', 441 | values: 'foo bar foo bar {2} {100}', 442 | object: 'foo bar foo bar {z}', 443 | 'gender': 'model.user.fields.gender', 444 | 'name': 'model.user.fields.name', 445 | }) 446 | .expect('Set-Cookie', /^locale=en\-us; path=\/; expires=\w+/) 447 | .expect(200, done); 448 | }); 449 | 450 | it('should work with "Accept-Language: de-de" by localeAlias', function (done) { 451 | request(cookieFieldMapApp.callback()) 452 | .get('/') 453 | .set('Accept-Language', 'ja,de-de;q=0.8') 454 | .expect({ 455 | email: 'Emailde', 456 | hello: 'Hallo fengmk2, wie geht es dir heute?', 457 | message: 'Hallo fengmk2, wie geht es dir heute? Wie war dein 18.', 458 | empty: '', 459 | notexists_key: 'key not exists', 460 | empty_string: '', 461 | empty_value: 'emptyValue', 462 | novalue: 'key %s ok', 463 | arguments3: '1 2 3', 464 | arguments4: '1 2 3 4', 465 | arguments5: '1 2 3 4 5', 466 | arguments6: '1 2 3 4 5. 6', 467 | values: 'foo bar foo bar {2} {100}', 468 | object: 'foo bar foo bar {z}', 469 | 'gender': 'model.user.fields.gender', 470 | 'name': 'model.user.fields.name', 471 | }) 472 | .expect('Set-Cookie', /^locale=de; path=\/; expires=\w+/) 473 | .expect(200, done); 474 | }); 475 | 476 | it('should mock acceptsLanguages return string', function (done) { 477 | mm(app.request, 'acceptsLanguages', function () { 478 | return 'zh-TW'; 479 | }); 480 | request(app.callback()) 481 | .get('/?locale=') 482 | .expect({ 483 | email: '郵箱', 484 | hello: 'fengmk2,今天過得如何?', 485 | message: 'Hello fengmk2, how are you today? How was your 18.', 486 | empty: '', 487 | notexists_key: 'key not exists', 488 | empty_string: '', 489 | empty_value: '', 490 | novalue: 'key %s ok', 491 | arguments3: '1 2 3', 492 | arguments4: '1 2 3 4', 493 | arguments5: '1 2 3 4 5', 494 | arguments6: '1 2 3 4 5. 6', 495 | values: 'foo bar foo bar {2} {100}', 496 | object: 'foo bar foo bar {z}', 497 | 'gender': '性別', 498 | 'name': '姓名', 499 | }) 500 | .expect('Set-Cookie', /^locale=zh\-tw; path=\/; expires=\w+/) 501 | .expect(200, done); 502 | }); 503 | 504 | it('should mock acceptsLanguages return string', function (done) { 505 | mm(app.request, 'acceptsLanguages', function () { 506 | return 'fr'; 507 | }); 508 | request(app.callback()) 509 | .get('/?locale=fr') 510 | .set('Accept-Language', 'fr;q=0.8, fr, fr') 511 | .expect({ 512 | email: 'le email', 513 | hello: 'fengmk2, Comment allez-vous', 514 | message: 'Hello fengmk2, how are you today? How was your 18.', 515 | empty: '', 516 | notexists_key: 'key not exists', 517 | empty_string: '', 518 | empty_value: '', 519 | novalue: 'key %s ok', 520 | arguments3: '1 2 3', 521 | arguments4: '1 2 3 4', 522 | arguments5: '1 2 3 4 5', 523 | arguments6: '1 2 3 4 5. 6', 524 | values: 'foo bar foo bar {2} {100}', 525 | object: 'foo bar foo bar {z}', 526 | gender: 'le sexe', 527 | name: 'prénom', 528 | }) 529 | .expect('Set-Cookie', /^locale=fr; path=\/; expires=\w+/) 530 | .expect(200, done); 531 | }); 532 | 533 | it('should mock acceptsLanguages return null', function (done) { 534 | mm(app.request, 'acceptsLanguages', function () { 535 | return null; 536 | }); 537 | request(app.callback()) 538 | .get('/?locale=') 539 | .expect({ 540 | email: 'Email', 541 | hello: 'Hello fengmk2, how are you today?', 542 | message: 'Hello fengmk2, how are you today? How was your 18.', 543 | empty: '', 544 | notexists_key: 'key not exists', 545 | empty_string: '', 546 | empty_value: 'emptyValue', 547 | novalue: 'key %s ok', 548 | arguments3: '1 2 3', 549 | arguments4: '1 2 3 4', 550 | arguments5: '1 2 3 4 5', 551 | arguments6: '1 2 3 4 5. 6', 552 | values: 'foo bar foo bar {2} {100}', 553 | object: 'foo bar foo bar {z}', 554 | 'gender': 'model.user.fields.gender', 555 | 'name': 'model.user.fields.name', 556 | }) 557 | .expect('Set-Cookie', /^locale=en\-us; path=\/; expires=\w+/) 558 | .expect(200, done); 559 | }); 560 | }); 561 | 562 | describe('__getLocale and __getLocaleOrigin', function () { 563 | it('should __getLocale and __getLocaleOrigin from cookie', function () { 564 | return request(app.callback()) 565 | .get('/origin') 566 | .set('cookie', 'locale=de') 567 | .expect(200, { locale: 'de', localeOrigin: 'cookie' }); 568 | }); 569 | 570 | it('should __getLocale and __getLocaleOrigin from query', function () { 571 | return request(app.callback()) 572 | .get('/origin?locale=de') 573 | .expect(200, { locale: 'de', localeOrigin: 'query' }); 574 | }); 575 | 576 | it('should __getLocale and __getLocaleOrigin from header', function () { 577 | return request(app.callback()) 578 | .get('/origin') 579 | .set('Accept-Language', 'zh-cn') 580 | .expect(200, { locale: 'zh-cn', localeOrigin: 'header' }); 581 | }); 582 | 583 | it('should __getLocale and __getLocaleOrigin from default', function () { 584 | return request(app.callback()) 585 | .get('/origin') 586 | .expect(200, { locale: 'en-us', localeOrigin: 'default' }); 587 | }); 588 | }); 589 | 590 | describe('__setLocale', function () { 591 | it('should set locale and cookie', function () { 592 | return request(app.callback()) 593 | .get('/set') 594 | .set('cookie', 'locale=de') 595 | .expect(200, { locale: 'zh-hk', localeOrigin: 'set' }) 596 | .expect('Set-Cookie', /^locale=zh\-hk; path=\/; expires=[^;]+ GMT$/); 597 | }); 598 | }); 599 | }); 600 | }); 601 | 602 | function createApp(options) { 603 | const app = koa(); 604 | locales(app, options); 605 | const fname = options && options.functionName || '__'; 606 | 607 | app.use(function* () { 608 | if (this.url === '/app_locale_zh') { 609 | this.body = { 610 | email: this.app[fname]('zh-cn', 'Email'), 611 | }; 612 | return; 613 | } 614 | 615 | if (this.path === '/origin') { 616 | assert(this.__getLocaleOrigin() === this.__getLocaleOrigin()); 617 | this.body = { 618 | locale: this.__getLocale(), 619 | localeOrigin: this.__getLocaleOrigin(), 620 | }; 621 | return; 622 | } 623 | 624 | if (this.path === '/set') { 625 | this.__getLocale(); 626 | this.__setLocale('zh-tw'); 627 | this.__setLocale('zh-hk'); 628 | this.body = { 629 | locale: this.__getLocale(), 630 | localeOrigin: this.__getLocaleOrigin(), 631 | }; 632 | return; 633 | } 634 | 635 | if (this.url === '/headerSent') { 636 | this.body = 'foo'; 637 | const that = this; 638 | setTimeout(function () { 639 | that[fname]('Email'); 640 | }, 10); 641 | return; 642 | } 643 | 644 | this.body = { 645 | email: this[fname]('Email'), 646 | name: this[fname]('model.user.fields.name'), 647 | gender: this[fname]('model.user.fields.gender'), 648 | hello: this[fname]('Hello %s, how are you today?', 'fengmk2'), 649 | message: this[fname]('Hello %s, how are you today? How was your %s.', 'fengmk2', 18), 650 | empty: this[fname](), 651 | notexists_key: this[fname]('key not exists'), 652 | empty_string: this[fname](''), 653 | empty_value: this[fname]('emptyValue'), 654 | novalue: this[fname]('key %s ok'), 655 | arguments3: this[fname]('%s %s %s', 1, 2, 3), 656 | arguments4: this[fname]('%s %s %s %s', 1, 2, 3, 4), 657 | arguments5: this[fname]('%s %s %s %s %s', 1, 2, 3, 4, 5), 658 | arguments6: this[fname]('%s %s %s %s %s.', 1, 2, 3, 4, 5, 6), 659 | values: this[fname]('{0} {1} {0} {1} {2} {100}', ['foo', 'bar']), 660 | object: this[fname]('{foo} {bar} {foo} {bar} {z}', { foo: 'foo', bar: 'bar' }), 661 | }; 662 | }); 663 | 664 | return app; 665 | } 666 | --------------------------------------------------------------------------------