commit 15864d180dcebed7eb33826e28bd5a5d4656b854 Author: fengmk2 Date: Sat May 16 22:06:49 2015 +0800 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8280239 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +*.seed +*.log +*.csv +*.dat +*.out +*.pid +*.gz + +pids +logs +results + +node_modules +npm-debug.log +coverage/ diff --git a/.jshintignore b/.jshintignore new file mode 100644 index 0000000..63c6333 --- /dev/null +++ b/.jshintignore @@ -0,0 +1,4 @@ +node_modules/ +coverage/ +.tmp/ +.git/ diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 0000000..f86f3ed --- /dev/null +++ b/.jshintrc @@ -0,0 +1,95 @@ +{ + // JSHint Default Configuration File (as on JSHint website) + // See http://jshint.com/docs/ for more details + + "maxerr" : 50, // {int} Maximum error before stopping + + // Enforcing + "bitwise" : true, // true: Prohibit bitwise operators (&, |, ^, etc.) + "camelcase" : false, // true: Identifiers must be in camelCase + "curly" : true, // true: Require {} for every new block or scope + "eqeqeq" : true, // true: Require triple equals (===) for comparison + "forin" : false, // true: Require filtering for..in loops with obj.hasOwnProperty() + "immed" : false, // true: Require immediate invocations to be wrapped in parens e.g. `(function () { } ());` + "indent" : false, // {int} Number of spaces to use for indentation + "latedef" : false, // true: Require variables/functions to be defined before being used + "newcap" : false, // true: Require capitalization of all constructor functions e.g. `new F()` + "noarg" : true, // true: Prohibit use of `arguments.caller` and `arguments.callee` + "noempty" : true, // true: Prohibit use of empty blocks + "nonew" : false, // true: Prohibit use of constructors for side-effects (without assignment) + "plusplus" : false, // true: Prohibit use of `++` & `--` + "quotmark" : false, // Quotation mark consistency: + // false : do nothing (default) + // true : ensure whatever is used is consistent + // "single" : require single quotes + // "double" : require double quotes + "undef" : true, // true: Require all non-global variables to be declared (prevents global leaks) + "unused" : true, // true: Require all defined variables be used + "strict" : true, // true: Requires all functions run in ES5 Strict Mode + "trailing" : false, // true: Prohibit trailing whitespaces + "maxparams" : false, // {int} Max number of formal params allowed per function + "maxdepth" : false, // {int} Max depth of nested blocks (within functions) + "maxstatements" : false, // {int} Max number statements per function + "maxcomplexity" : false, // {int} Max cyclomatic complexity per function + "maxlen" : false, // {int} Max number of characters per line + + // Relaxing + "asi" : false, // true: Tolerate Automatic Semicolon Insertion (no semicolons) + "boss" : true, // true: Tolerate assignments where comparisons would be expected + "debug" : false, // true: Allow debugger statements e.g. browser breakpoints. + "eqnull" : false, // true: Tolerate use of `== null` + "es5" : false, // true: Allow ES5 syntax (ex: getters and setters) + "esnext" : true, // true: Allow ES.next (ES6) syntax (ex: `const`) + "moz" : false, // true: Allow Mozilla specific syntax (extends and overrides esnext features) + // (ex: `for each`, multiple try/catch, function expression…) + "evil" : false, // true: Tolerate use of `eval` and `new Function()` + "expr" : true, // true: Tolerate `ExpressionStatement` as Programs + "funcscope" : false, // true: Tolerate defining variables inside control statements" + "globalstrict" : false, // true: Allow global "use strict" (also enables 'strict') + "iterator" : false, // true: Tolerate using the `__iterator__` property + "lastsemic" : false, // true: Tolerate omitting a semicolon for the last statement of a 1-line block + "laxbreak" : true, // true: Tolerate possibly unsafe line breakings + "laxcomma" : false, // true: Tolerate comma-first style coding + "loopfunc" : false, // true: Tolerate functions being defined in loops + "multistr" : true, // true: Tolerate multi-line strings + "proto" : false, // true: Tolerate using the `__proto__` property + "scripturl" : false, // true: Tolerate script-targeted URLs + "smarttabs" : false, // true: Tolerate mixed tabs/spaces when used for alignment + "shadow" : true, // true: Allows re-define variables later in code e.g. `var x=1; x=2;` + "sub" : false, // true: Tolerate using `[]` notation when it can still be expressed in dot notation + "supernew" : false, // true: Tolerate `new function () { ... };` and `new Object;` + "validthis" : false, // true: Tolerate using this in a non-constructor function + + // Environments + "browser" : true, // Web Browser (window, document, etc) + "couch" : false, // CouchDB + "devel" : true, // Development/debugging (alert, confirm, etc) + "dojo" : false, // Dojo Toolkit + "jquery" : false, // jQuery + "mootools" : false, // MooTools + "node" : true, // Node.js + "nonstandard" : false, // Widely adopted globals (escape, unescape, etc) + "prototypejs" : false, // Prototype and Scriptaculous + "rhino" : false, // Rhino + "worker" : false, // Web Workers + "wsh" : false, // Windows Scripting Host + "yui" : false, // Yahoo User Interface + "noyield" : true, // allow generators without a yield + + // Legacy + "nomen" : false, // true: Prohibit dangling `_` in variables + "onevar" : false, // true: Allow only one `var` statement per function + "passfail" : false, // true: Stop on first error + "white" : false, // true: Check against strict whitespace and indentation rules + + // Custom Globals + "globals" : { // additional predefined global variables + // mocha + "describe": true, + "it": true, + "before": true, + "afterEach": true, + "beforeEach": true, + "after": true + } +} diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..e1e45c2 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,7 @@ +language: node_js +node_js: + - 'iojs-1' + - '0.12' + - '0.10' +script: "npm run test-travis" +after_script: "npm install coveralls@2 && cat ./coverage/lcov.info | coveralls" diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..5e17c6d --- /dev/null +++ b/AUTHORS @@ -0,0 +1 @@ +fengmk2 (https://fengmk2.com) diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..70c3765 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +This software is licensed under the MIT License. + +Copyright (c) 2015 koajs and other contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..3198a0d --- /dev/null +++ b/README.md @@ -0,0 +1,118 @@ +koa-locales +======= + +[![NPM version][npm-image]][npm-url] +[![build status][travis-image]][travis-url] +[![Test coverage][coveralls-image]][coveralls-url] +[![Gittip][gittip-image]][gittip-url] +[![David deps][david-image]][david-url] +[![iojs version][iojs-image]][iojs-url] +[![node version][node-image]][node-url] +[![npm download][download-image]][download-url] + +[npm-image]: https://img.shields.io/npm/v/koa-locales.svg?style=flat-square +[npm-url]: https://npmjs.org/package/koa-locales +[travis-image]: https://img.shields.io/travis/koajs/locales.svg?style=flat-square +[travis-url]: https://travis-ci.org/koajs/locales +[coveralls-image]: https://img.shields.io/coveralls/koajs/locales.svg?style=flat-square +[coveralls-url]: https://coveralls.io/r/koajs/locales?branch=master +[gittip-image]: https://img.shields.io/gittip/fengmk2.svg?style=flat-square +[gittip-url]: https://www.gittip.com/fengmk2/ +[david-image]: https://img.shields.io/david/koajs/locales.svg?style=flat-square +[david-url]: https://david-dm.org/koajs/locales +[iojs-image]: https://img.shields.io/badge/io.js-%3E=_1.0-yellow.svg?style=flat-square +[iojs-url]: http://iojs.org/ +[node-image]: https://img.shields.io/badge/node.js-%3E=_0.12-green.svg?style=flat-square +[node-url]: http://nodejs.org/download/ +[download-image]: https://img.shields.io/npm/dm/koa-locales.svg?style=flat-square +[download-url]: https://npmjs.org/package/koa-locales + +koa locales, i18n solution for koa: + +1. All locales resources location on `options.dir`. +2. One api: `__(key[, value, ...])` +3. Auto detect request locale from `query`, `cookie` and `header: Accept-Language` + +## Installation + +```bash +$ npm install koa-locales --save +``` + +## Quick start + +```js +var koa = require('koa'); +var locales = require('koa-locales'); + +var app = koa(); +app.use(locales({ + dir: __dirname + '/locales' +})); +``` + +## API Reference + +### `locales(app, options)` + +Patch locales functions to koa app. + +- {Application} app: koa app instance. +- {Object} options: optional params + - {String} functionName: locale function name patch on koa context. Optional, default is `__`. + - {String} dir: locales resources store directory. Optional, default is `$PWD/locales`. + - {String} defaultLocale: default locale. Optional, default is `en-US`. + - {String} queryField: locale field name on query. Optional, default is `locale`. + - {String} cookieField: locale field name on cookie. Optional, default is `locale`. + - {String|Number} cookieMaxAge: set locale cookie value max age. Optional, default is `1y`, expired after one year. + +```js +locales({ + app: app, + dir: __dirname + '/app/locales', + defaultLocale: 'zh-CN' +})); +``` + +### `context.__(key[, value1[, value2, ...]])` + +Get current request locale text. + +```js +function* home() { + this.body = { + message: this.__('Hello, %s', 'fengmk2') + }; +} +``` + +Examples: + +```js +__('Hello, %s. %s', 'fengmk2', 'koa rock!') +=> +'Hello fengmk2. koa rock!' + +__('{0} {0} {1} {1} {1}', ['foo', 'bar']) +=> +'foo foo bar bar bar' +``` + +## Usage on template + +```js +this.locales.__ = this.__.bind(this); +``` + +[nunjucks] example: + +```html +{{ __('Hello, %s', user.name) }} +``` + +## License + +[MIT](LICENSE) + + +[nunjucks]: https://www.npmjs.com/package/nunjucks diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 0000000..0b2cba7 --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,23 @@ +environment: + matrix: + - nodejs_version: "1.3" + - nodejs_version: "0.12" + - nodejs_version: "0.10" + +# Install scripts. (runs after repo cloning) +install: + # Get the latest stable version of Node.js or io.js + - ps: Install-Product node $env:nodejs_version + # install modules + - npm install + +# Post-install test scripts. +test_script: + # Output useful info for debugging. + - node --version + - npm --version + # run tests + - npm test + +# Don't actually build. +build: off diff --git a/index.js b/index.js new file mode 100644 index 0000000..af669ee --- /dev/null +++ b/index.js @@ -0,0 +1,177 @@ +/**! + * koa-locales - index.js + * + * Copyright(c) koajs and other contributors. + * MIT Licensed + * + * Authors: + * fengmk2 (http://fengmk2.com) + */ + +"use strict"; + +/** + * Module dependencies. + */ + +var debug = require('debug')('koa-locales'); +var util = require('util'); +var fs = require('fs'); +var path = require('path'); +var ms = require('humanize-ms'); + +module.exports = function (app, options) { + options = options || {}; + var defaultLocale = formatLocale(options.defaultLocale || 'en-US'); + var queryField = options.queryField || 'locale'; + var cookieField = options.cookieField || 'locale'; + var cookieMaxAge = ms(options.cookieMaxAge || '1y'); + var localeDir = options.dir || path.join(process.cwd(), 'locales'); + var functionName = options.functionName || '__'; + var resources = {}; + + if (fs.existsSync(localeDir)) { + var names = fs.readdirSync(localeDir); + for (var i = 0; i < names.length; i++) { + var name = names[i]; + if (!/\.(js|json)$/.test(name)) { + continue; + } + var filepath = path.join(localeDir, name); + // support en_US.js => en-US.js + var locale = formatLocale(name.split('.')[0]); + resources[locale] = require(filepath); + } + } + + debug('init locales with %j, got %j resources', options, Object.keys(resources)); + + var ARRAY_INDEX_RE = /\{(\d+)\}/g; + function formatWithArray(text, values) { + return text.replace(ARRAY_INDEX_RE, function (orignal, matched) { + var index = parseInt(matched); + if (index < values.length) { + return values[index]; + } + // not match index, return orignal text + return orignal; + }); + } + + app.context[functionName] = function (key, value, value2, value3, value4) { + if (arguments.length === 0) { + // __() + return ''; + } + + var locale = this.__getLocale(); + var resource = resources[locale] || {}; + var text = resource[key] || key; + debug('%s: %j => %j', locale, key, text); + if (!text) { + return ''; + } + + // for performance reason + if (arguments.length === 1) { + // __(key) + return text; + } + if (arguments.length === 2) { + if (Array.isArray(value)) { + // __(key, array) + // __('{0} {1} {1} {0}', ['foo', 'bar']) + // => + // foo bar bar foo + return formatWithArray(text, value); + } + + // __(key, value) + return util.format(text, value); + } + if (arguments.length === 3) { + // __(key, value1, value2) + return util.format(text, value, value2); + } + if (arguments.length === 4) { + // __(key, value1, value2, value3) + return util.format(text, value, value2, value3); + } + if (arguments.length === 5) { + // __(key, value1, value2, value3, value4) + return util.format(text, value, value2, value3, value4); + } + + // __(key, value1, value2, value3, value4, value5, ...) + var args = Array.prototype.slice.call(arguments, 1); + args.unshift(text); + return util.format.apply(util, args); + }; + + // 1. query: /?locale=en-US + // 2. cookie: locale=zh-TW + // 3. header: Accept-Language: zh-CN,zh;q=0.5 + app.context.__getLocale = function () { + if (this.__locale) { + return this.__locale; + } + + var cookieLocale = this.cookies.get(cookieField); + var locale = this.query[queryField] || cookieLocale; + if (!locale) { + // Accept-Language: zh-CN,zh;q=0.5 + // Accept-Language: zh-CN + var languages = this.acceptsLanguages(); + if (languages) { + if (Array.isArray(languages)) { + if (languages[0] === '*') { + languages = languages.slice(1); + } + if (languages.length > 0) { + for (var i = 0; i < languages.length; i++) { + var lang = formatLocale(languages[i]); + if (resources[lang]) { + locale = lang; + break; + } + } + if (!locale) { + // set the first one + locale = languages[0]; + } + } + } else { + locale = languages; + } + } + + // all missing, set it to defaultLocale + if (!locale) { + locale = defaultLocale; + } + } + + locale = formatLocale(locale); + + // validate locale + if (!resources[locale]) { + locale = defaultLocale; + } + + if (cookieLocale !== locale) { + // locale change, need to set cookie + this.cookies.set(cookieField, locale, { + // make sure brower javascript can read the cookie + httpOnly: false, + maxAge: cookieMaxAge, + }); + } + this.__locale = locale; + return locale; + }; + + function formatLocale(locale) { + // support zh_CN, en_US => zh-CN, en-US + return locale.replace('_', '-').toLowerCase(); + } +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..8f53923 --- /dev/null +++ b/package.json @@ -0,0 +1,56 @@ +{ + "name": "koa-locales", + "version": "1.0.0", + "description": "koa locales, i18n solution for koa", + "main": "index.js", + "files": [ + "index.js" + ], + "scripts": { + "test": "mocha --check-leaks -R spec -t 5000 test/*.test.js", + "test-cov": "node node_modules/.bin/istanbul cover node_modules/.bin/_mocha -- --check-leaks -t 5000 test/*.test.js", + "test-travis": "node node_modules/.bin/istanbul cover node_modules/.bin/_mocha --report lcovonly -- --check-leaks -t 5000 test/*.test.js", + "jshint": "jshint .", + "autod": "autod -w --prefix '~'", + "cnpm": "npm install --registry=https://registry.npm.taobao.org", + "contributors": "contributors -f plain -o AUTHORS" + }, + "dependencies": { + "debug": "~2.2.0", + "humanize-ms": "~1.0.1" + }, + "devDependencies": { + "autod": "*", + "contributors": "*", + "istanbul-harmony": "*", + "jshint": "*", + "koa": "~0.20.0", + "mm": "~1.1.0", + "mocha": "*", + "pedding": "~1.0.0", + "supertest": "~1.0.1" + }, + "homepage": "https://github.com/koajs/locales", + "repository": { + "type": "git", + "url": "git://github.com/koajs/locales.git", + "web": "https://github.com/koajs/locales" + }, + "bugs": { + "url": "https://github.com/koajs/locales/issues", + "email": "m@fengmk2.com" + }, + "keywords": [ + "koa-locales", + "i18n", + "locales", + "koa-i18n", + "koa" + ], + "engines": { + "node": ">=0.12.0", + "iojs": ">=1.0.0" + }, + "author": "fengmk2 (http://fengmk2.com)", + "license": "MIT" +} diff --git a/test/index.test.js b/test/index.test.js new file mode 100644 index 0000000..a70d122 --- /dev/null +++ b/test/index.test.js @@ -0,0 +1,354 @@ +/**! + * koa-locales - test/index.test.js + * + * Copyright(c) koajs and other contributors. + * MIT Licensed + * + * Authors: + * fengmk2 (http://fengmk2.com) + */ + +"use strict"; + +/** + * Module dependencies. + */ + +var assert = require('assert'); +var koa = require('koa'); +var request = require('supertest'); +var pedding = require('pedding'); +var mm = require('mm'); +var locales = require('../'); + +describe('koa-locales.test.js', function () { + + afterEach(mm.restore); + + describe('default options', function () { + var app = createApp(); + + it('should use default locale: en-US', function (done) { + request(app.callback()) + .get('/') + .expect({ + email: 'Email', + hello: 'Hello fengmk2, how are you today?', + message: 'Hello fengmk2, how are you today? How was your 18.', + empty: '', + notexists_key: 'key not exists', + empty_string: '', + novalue: 'key %s ok', + arguments3: '1 2 3', + arguments4: '1 2 3 4', + arguments5: '1 2 3 4 5', + arguments6: '1 2 3 4 5. 6', + values: 'foo bar foo bar {2} {100}', + }) + .expect('Set-Cookie', /^locale=en\-us; path=\/; expires=\w+/) + .expect(200, done); + }); + }); + + describe('custom options', function () { + var app = createApp({ + dir: __dirname + '/locales' + }); + + it('should use default locale: en-US', function (done) { + request(app.callback()) + .get('/') + .expect({ + email: 'Email', + hello: 'Hello fengmk2, how are you today?', + message: 'Hello fengmk2, how are you today? How was your 18.', + empty: '', + notexists_key: 'key not exists', + empty_string: '', + novalue: 'key %s ok', + arguments3: '1 2 3', + arguments4: '1 2 3 4', + arguments5: '1 2 3 4 5', + arguments6: '1 2 3 4 5. 6', + values: 'foo bar foo bar {2} {100}', + }) + .expect('Set-Cookie', /^locale=en\-us; path=\/; expires=\w+/) + .expect(200, done); + }); + + describe('query.locale', function () { + it('should use query locale: zh-CN', function (done) { + request(app.callback()) + .get('/?locale=zh-CN') + .expect({ + email: '邮箱', + hello: 'fengmk2,今天过得如何?', + message: 'Hello fengmk2, how are you today? How was your 18.', + empty: '', + notexists_key: 'key not exists', + empty_string: '', + novalue: 'key %s ok', + arguments3: '1 2 3', + arguments4: '1 2 3 4', + arguments5: '1 2 3 4 5', + arguments6: '1 2 3 4 5. 6', + values: 'foo bar foo bar {2} {100}', + }) + .expect('Set-Cookie', /^locale=zh\-cn; path=\/; expires=\w+/) + .expect(200, done); + }); + + it('should use query locale and change cookie locale', function (done) { + request(app.callback()) + .get('/?locale=zh-CN') + .set('cookie', 'locale=zh-TW') + .expect({ + email: '邮箱', + hello: 'fengmk2,今天过得如何?', + message: 'Hello fengmk2, how are you today? How was your 18.', + empty: '', + notexists_key: 'key not exists', + empty_string: '', + novalue: 'key %s ok', + arguments3: '1 2 3', + arguments4: '1 2 3 4', + arguments5: '1 2 3 4 5', + arguments6: '1 2 3 4 5. 6', + values: 'foo bar foo bar {2} {100}', + }) + .expect('Set-Cookie', /^locale=zh\-cn; path=\/; expires=\w+/) + .expect(200, done); + }); + + it('should ignore invalid locale value', function (done) { + request(app.callback()) + .get('/?locale=xss') + .expect({ + email: 'Email', + hello: 'Hello fengmk2, how are you today?', + message: 'Hello fengmk2, how are you today? How was your 18.', + empty: '', + notexists_key: 'key not exists', + empty_string: '', + novalue: 'key %s ok', + arguments3: '1 2 3', + arguments4: '1 2 3 4', + arguments5: '1 2 3 4 5', + arguments6: '1 2 3 4 5. 6', + values: 'foo bar foo bar {2} {100}', + }) + .expect('Set-Cookie', /^locale=en\-us; path=\/; expires=\w+/) + .expect(200, done); + }); + }); + + describe('cookie.locale', function () { + it('should use cookie locale: zh-CN', function (done) { + request(app.callback()) + .get('/?locale=') + .set('cookie', 'locale=zh-cn') + .expect({ + email: '邮箱', + hello: 'fengmk2,今天过得如何?', + message: 'Hello fengmk2, how are you today? How was your 18.', + empty: '', + notexists_key: 'key not exists', + empty_string: '', + novalue: 'key %s ok', + arguments3: '1 2 3', + arguments4: '1 2 3 4', + arguments5: '1 2 3 4 5', + arguments6: '1 2 3 4 5. 6', + values: 'foo bar foo bar {2} {100}', + }) + .expect(function (res) { + assert(!res.headers['set-cookie']); + }) + .expect(200, done); + }); + }); + + describe('Accept-Language', function () { + it('should use Accept-Language: zh-CN', function (done) { + done = pedding(3, done); + + request(app.callback()) + .get('/?locale=') + .set('Accept-Language', 'zh-CN') + .expect({ + email: '邮箱', + hello: 'fengmk2,今天过得如何?', + message: 'Hello fengmk2, how are you today? How was your 18.', + empty: '', + notexists_key: 'key not exists', + empty_string: '', + novalue: 'key %s ok', + arguments3: '1 2 3', + arguments4: '1 2 3 4', + arguments5: '1 2 3 4 5', + arguments6: '1 2 3 4 5. 6', + values: 'foo bar foo bar {2} {100}', + }) + .expect('Set-Cookie', /^locale=zh\-cn; path=\/; expires=\w+/) + .expect(200, done); + + request(app.callback()) + .get('/?locale=') + .set('Accept-Language', 'zh-CN,zh;q=0.8') + .expect({ + email: '邮箱', + hello: 'fengmk2,今天过得如何?', + message: 'Hello fengmk2, how are you today? How was your 18.', + empty: '', + notexists_key: 'key not exists', + empty_string: '', + novalue: 'key %s ok', + arguments3: '1 2 3', + arguments4: '1 2 3 4', + arguments5: '1 2 3 4 5', + arguments6: '1 2 3 4 5. 6', + values: 'foo bar foo bar {2} {100}', + }) + .expect('Set-Cookie', /^locale=zh\-cn; path=\/; expires=\w+/) + .expect(200, done); + + request(app.callback()) + .get('/?locale=') + .set('Accept-Language', 'en;q=0.8, es, zh_CN') + .expect({ + email: '邮箱', + hello: 'fengmk2,今天过得如何?', + message: 'Hello fengmk2, how are you today? How was your 18.', + empty: '', + notexists_key: 'key not exists', + empty_string: '', + novalue: 'key %s ok', + arguments3: '1 2 3', + arguments4: '1 2 3 4', + arguments5: '1 2 3 4 5', + arguments6: '1 2 3 4 5. 6', + values: 'foo bar foo bar {2} {100}', + }) + .expect('Set-Cookie', /^locale=zh\-cn; path=\/; expires=\w+/) + .expect(200, done); + }); + + it('should work with "Accept-Language: " header', function (done) { + request(app.callback()) + .get('/?locale=') + .set('Accept-Language', '') + .expect({ + email: 'Email', + hello: 'Hello fengmk2, how are you today?', + message: 'Hello fengmk2, how are you today? How was your 18.', + empty: '', + notexists_key: 'key not exists', + empty_string: '', + novalue: 'key %s ok', + arguments3: '1 2 3', + arguments4: '1 2 3 4', + arguments5: '1 2 3 4 5', + arguments6: '1 2 3 4 5. 6', + values: 'foo bar foo bar {2} {100}', + }) + .expect('Set-Cookie', /^locale=en\-us; path=\/; expires=\w+/) + .expect(200, done); + }); + + it('should work with "Accept-Language: en"', function (done) { + request(app.callback()) + .get('/') + .set('Accept-Language', 'en') + .expect({ + email: 'Email', + hello: 'Hello fengmk2, how are you today?', + message: 'Hello fengmk2, how are you today? How was your 18.', + empty: '', + notexists_key: 'key not exists', + empty_string: '', + novalue: 'key %s ok', + arguments3: '1 2 3', + arguments4: '1 2 3 4', + arguments5: '1 2 3 4 5', + arguments6: '1 2 3 4 5. 6', + values: 'foo bar foo bar {2} {100}', + }) + .expect('Set-Cookie', /^locale=en\-us; path=\/; expires=\w+/) + .expect(200, done); + }); + + it('should mock acceptsLanguages return string', function (done) { + mm(app.request, 'acceptsLanguages', function () { + return 'zh-TW'; + }); + request(app.callback()) + .get('/?locale=') + .expect({ + email: 'Email', + hello: 'Hello fengmk2, how are you today?', + message: 'Hello fengmk2, how are you today? How was your 18.', + empty: '', + notexists_key: 'key not exists', + empty_string: '', + novalue: 'key %s ok', + arguments3: '1 2 3', + arguments4: '1 2 3 4', + arguments5: '1 2 3 4 5', + arguments6: '1 2 3 4 5. 6', + values: 'foo bar foo bar {2} {100}', + }) + .expect('Set-Cookie', /^locale=zh\-tw; path=\/; expires=\w+/) + .expect(200, done); + }); + + it('should mock acceptsLanguages return null', function (done) { + mm(app.request, 'acceptsLanguages', function () { + return null; + }); + request(app.callback()) + .get('/?locale=') + .expect({ + email: 'Email', + hello: 'Hello fengmk2, how are you today?', + message: 'Hello fengmk2, how are you today? How was your 18.', + empty: '', + notexists_key: 'key not exists', + empty_string: '', + novalue: 'key %s ok', + arguments3: '1 2 3', + arguments4: '1 2 3 4', + arguments5: '1 2 3 4 5', + arguments6: '1 2 3 4 5. 6', + values: 'foo bar foo bar {2} {100}', + }) + .expect('Set-Cookie', /^locale=en\-us; path=\/; expires=\w+/) + .expect(200, done); + }); + }); + }); +}); + +function createApp(options) { + var app = koa(); + locales(app, options); + var fname = options && options.functionName || '__'; + + app.use(function* () { + this.body = { + email: this[fname]('Email'), + hello: this[fname]('Hello %s, how are you today?', 'fengmk2'), + message: this[fname]('Hello %s, how are you today? How was your %s.', 'fengmk2', 18), + empty: this[fname](), + notexists_key: this[fname]('key not exists'), + empty_string: this[fname](''), + novalue: this[fname]('key %s ok'), + arguments3: this[fname]('%s %s %s', 1, 2, 3), + arguments4: this[fname]('%s %s %s %s', 1, 2, 3, 4), + arguments5: this[fname]('%s %s %s %s %s', 1, 2, 3, 4, 5), + arguments6: this[fname]('%s %s %s %s %s.', 1, 2, 3, 4, 5, 6), + values: this[fname]('{0} {1} {0} {1} {2} {100}', ['foo', 'bar']), + }; + }); + + return app; +} diff --git a/test/locales/foo.txt b/test/locales/foo.txt new file mode 100644 index 0000000..7bc937e --- /dev/null +++ b/test/locales/foo.txt @@ -0,0 +1 @@ +should not load this file diff --git a/test/locales/zh-CN.js b/test/locales/zh-CN.js new file mode 100644 index 0000000..e3d836e --- /dev/null +++ b/test/locales/zh-CN.js @@ -0,0 +1,4 @@ +module.exports = { + 'Email': '邮箱', + 'Hello %s, how are you today?': '%s,今天过得如何?', +}; diff --git a/test/locales/zh_TW.json b/test/locales/zh_TW.json new file mode 100644 index 0000000..1797133 --- /dev/null +++ b/test/locales/zh_TW.json @@ -0,0 +1,3 @@ +{ + +}