diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..c13c5f6 --- /dev/null +++ b/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["es2015"] +} diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..afbaad8 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,2 @@ +dist/* +gulpfile.js diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..76a177f --- /dev/null +++ b/.eslintrc @@ -0,0 +1,8 @@ +{ + "extends": "airbnb", + "rules": { + "func-names": ["error", "never"], + "no-underscore-dangle": ["error", { "allowAfterThis": true }], + "prefer-arrow-callback": 0 + } +} diff --git a/.gitignore b/.gitignore index 3c3629e..eb03e3e 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ node_modules +*.log diff --git a/.travis.yml b/.travis.yml index baa0031..6b1051a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,4 @@ language: node_js node_js: - - 0.8 + - "4" + - "node" diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..f13e36b --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,9 @@ +The MIT License (MIT) + +Copyright (c) 2017 Florian Lorrain + +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/LICENSE.txt b/LICENSE.txt deleted file mode 100644 index 258283a..0000000 --- a/LICENSE.txt +++ /dev/null @@ -1,20 +0,0 @@ -Copyright (c) 2012 Jed Schmidt, http://jed.is/ - -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. \ No newline at end of file diff --git a/README.md b/README.md index 8f8be92..275fd87 100644 --- a/README.md +++ b/README.md @@ -5,58 +5,56 @@ locale is a [node.js][node] module for negotiating HTTP locales for incoming bro It works like this: you (optionally) tell it the languages you support, and it figures out the best one to use for each incoming request from a browser. So if you support `en`, `en_US`, `ja`, `kr`, and `zh_TW`, and a request comes in that accepts `en_UK` or `en`, locale will figure out that `en` is the best language to use. -**Credits to [jed](https://github.com/jed) who passed the ownership of the package.** +Sources should be compatible with Node >= 0.8 but the dev environment needs Node >= 4. Examples -------- ### For the node.js HTTP module ```javascript -var http = require("http") - , locale = require("locale") - , supported = new locale.Locales(["en", "en_US", "ja"]) +const http = require('http'); +const locale = require('locale'); +const supported = new locale.Locales(['en', 'en_US', 'ja']); -http.createServer(function(req, res) { - var locales = new locale.Locales(req.headers["accept-language"]) - res.writeHeader(200, {"Content-Type": "text/plain"}) +http.createServer(function (req, res) { + const locales = new locale.Locales(req.headers['accept-language']); + res.writeHeader(200, { 'Content-Type': 'text/plain' }); res.end( - "You asked for: " + req.headers["accept-language"] + "\n" + - "We support: " + supported + "\n" + - "Our default is: " + locale.Locale["default"] + "\n" + - "The best match is: " + locales.best(supported) + "\n" - ) -}).listen(8000) + `You asked for: ${req.headers['accept-language']} + We support: ${supported} + Our default is: ${locale.Locale.default} + The best match is: ' + ${locales.best(supported)}` + ); +}).listen(8000); ``` ### For Connect/Express ```javascript -var http = require("http") - , express = require("express") - , locale = require("locale") - , supported = ["en", "en_US", "ja"] - , app = express() +const express = require('express'); +const locale = require('locale'); +const supported = ['en', 'en_US', 'ja']; +const app = express.createServer(locale(supported)); -app.use(locale(supported)) - -app.get("/", function(req, res) { - res.header("Content-Type", "text/plain") +app.get('/', function (req, res) { + res.header('Content-Type', 'text/plain'); res.send( - "You asked for: " + req.headers["accept-language"] + "\n" + - "We support: " + supported + "\n" + - "Our default is: " + locale.Locale["default"] + "\n" + - "The best match is: " + req.locale + "\n" - ) -}) - -app.listen(8000) + `You asked for: ${req.headers['accept-language']} + We support: ${supported} + Our default is: ${locale.Locale.default} + The best match is: ${req.locale}` + ); +}); + +app.listen(8000); ``` Install ------- +```bash +$ npm install locale +``` - $ npm install locale - -(Note that although this repo is CoffeeScript, the actual npm library is pre-compiled to pure JavaScript and has no run-time dependencies.) +Note - the package has no dependencies. API --- @@ -81,19 +79,29 @@ The Locales constructor takes a string compliant with the [`Accept-Language` HTT This method takes the target locale and compares it against the optionally provided list of supported locales, and returns the most appropriate locale based on the quality scores of the target locale. If no exact match exists (i.e. language+country) then it will fallback to `language` if supported, or if the language isn't supported it will return the default locale. - supported = new locale.Locales(['en', 'en_US'], 'en'); - (new locale.Locales('en')).best(supported).toString(); // 'en' - (new locale.Locales('en_GB')).best(supported).toString(); // 'en' - (new locale.Locales('en_US')).best(supported).toString(); // 'en_US' - (new locale.Locales('jp')).best(supported); // supported.default || locale.Locale["default"] +```javascript +supported = new locale.Locales(['en', 'en_US'], 'en'); +(new locale.Locales('en')).best(supported).toString(); // 'en' +(new locale.Locales('en_GB')).best(supported).toString(); // 'en' +(new locale.Locales('en_US')).best(supported).toString(); // 'en_US' +(new locale.Locales('jp')).best(supported); // supported.default || locale.Locale["default"] +``` + +Contributing +--- +1. Fork this repo and clone it locally. +2. Make sure you're using Node >= 4 +3. Run `npm install` to install dev dependencies. +4. Run `npm test` and `npm build` and check no error pops up. +5. Now you're ready to rumble! You can use the default `gulp` task to watch and run tests while you're making changes. +Sources are ES2015 compliant and transpiled via Babel. Copyright --------- +This project is licensed under the MIT license. For more information see LICENSE.md. -Copyright (c) 2012 Jed Schmidt. See LICENSE.txt for details. - -Send any questions or comments [here](http://twitter.com/jedschmidt). +Credits to [jed](https://github.com/jed) who was the initial maintainer of the package. [node]: http://nodejs.org [express]: http://expressjs.com diff --git a/dist/locale.js b/dist/locale.js new file mode 100644 index 0000000..a33df07 --- /dev/null +++ b/dist/locale.js @@ -0,0 +1,243 @@ +'use strict'; + +var Locale = require('./locale'); +var Locales = require('./locales'); + +var app = function app(supported, def) { + var supportedLocales = supported; + + if (!(supportedLocales instanceof Locales)) { + supportedLocales = new Locales(supportedLocales, def); + supportedLocales.index(); + } + + return function (req, res, next) { + var locales = new Locales(req.headers['accept-language']); + + var bestLocale = locales.best(supportedLocales); + req.locale = String(bestLocale); + req.rawLocale = bestLocale; + next(); + }; +}; + +app.Locales = Locales; +app.Locale = Locale; + +module.exports = app; +'use strict'; + +var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }(); + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +var defaultLocaleString = function defaultLocaleString() { + return process.env.LANG || 'en_US'; +}; + +var Locale = function () { + function Locale(str) { + _classCallCheck(this, Locale); + + if (!str) return null; + + var match = str.match(/[a-z]+/gi); + + var _match = _slicedToArray(match, 2), + language = _match[0], + country = _match[1]; + + this.code = str; + this.language = language.toLowerCase(); + var normalized = [this.language]; + + if (country) { + this.country = country.toUpperCase(); + normalized.push(this.country); + } + + this.normalized = normalized.join('_'); + } + + _createClass(Locale, [{ + key: 'serialize', + value: function serialize() { + if (!this.language) return null; + return this.code; + } + }]); + + return Locale; +}(); + +Locale.prototype.toString = Locale.prototype.serialize; +Locale.prototype.toJSON = Locale.prototype.serialize; +Locale.default = new Locale(defaultLocaleString()); + +module.exports = Locale; +'use strict'; + +var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }(); + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +var Locale = require('./locale'); + +var Locales = function () { + function Locales(input, def) { + _classCallCheck(this, Locales); + + var elements = []; + + if (input) { + if (typeof input === 'string' || input instanceof String) { + elements = input.split(','); + } else if (input instanceof Array) { + elements = input.slice(); + } + + elements = elements.map(function (item) { + if (!item) return null; + + var _item$split = item.split(';'), + _item$split2 = _slicedToArray(_item$split, 2), + locale = _item$split2[0], + q = _item$split2[1]; + + var locale2 = new Locale(locale.trim()); + var score = 1; + + if (q) { + score = q.slice(2) || 0; + } + + locale2.score = score; + + return locale2; + }).filter(function (e) { + return e instanceof Locale; + }).sort(function (a, b) { + return b.score - a.score; + }); + } + + this.elements = elements; + this._index = null; + if (def) { + this.default = new Locale(def); + } + } + + _createClass(Locales, [{ + key: 'index', + value: function index() { + if (!this._index) { + this._index = {}; + + this.elements.forEach(function (locale, idx) { + this._index[locale.normalized] = idx; + }, this); + } + return this._index; + } + }, { + key: 'best', + value: function best(locales) { + var setLocale = function setLocale(l) { + var r = l; + r.defaulted = false; + return r; + }; + + var locale = Locale.default; + if (locales && locales.default) { + locale = locales.default; + } + locale.defaulted = true; + + if (!locales) { + if (this.elements[0]) { + locale = this.elements[0]; + locale.defaulted = true; + } + return locale; + } + + var _iteratorNormalCompletion = true; + var _didIteratorError = false; + var _iteratorError = undefined; + + try { + for (var _iterator = this.elements[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { + var item = _step.value; + // eslint-disable-line no-restricted-syntax + var appropriateLocaleIndex = Locales.appropriateIndex(locales, item); + + if (appropriateLocaleIndex !== null) { + return setLocale(locales.elements[appropriateLocaleIndex]); + } + } + } catch (err) { + _didIteratorError = true; + _iteratorError = err; + } finally { + try { + if (!_iteratorNormalCompletion && _iterator.return) { + _iterator.return(); + } + } finally { + if (_didIteratorError) { + throw _iteratorError; + } + } + } + + return locale; + } + }, { + key: 'serialize', + value: function serialize() { + return [].concat(_toConsumableArray(this.elements)); + } + }, { + key: 'toString', + value: function toString() { + return String(this.toJSON()); + } + }], [{ + key: 'appropriateIndex', + value: function appropriateIndex(locales, locale) { + var index = locales.index(); + + var normalizedIndex = index[locale.normalized]; + var languageIndex = index[locale.language]; + + if (normalizedIndex !== undefined) { + return normalizedIndex; + } else if (languageIndex !== undefined) { + return languageIndex; + } + + var sameLanguageLocaleIndex = locales.elements.findIndex(function (l) { + return l.language === locale.language; + }); + if (sameLanguageLocaleIndex > -1) return sameLanguageLocaleIndex; + + return null; + } + }]); + + return Locales; +}(); + +Locales.prototype.toJSON = Locales.prototype.serialize; +Locales.prototype.sort = Array.prototype.sort; +Locales.prototype.push = Array.prototype.push; + +module.exports = Locales; \ No newline at end of file diff --git a/examples/express.js b/examples/express.js index b007bb9..b4e8879 100644 --- a/examples/express.js +++ b/examples/express.js @@ -1,17 +1,18 @@ -var http = require("http") - , express = require("express") - , locale = require("../lib") - , supported = ["en", "en_US", "ja"] - , app = express.createServer(locale(supported)) +const express = require('express'); // eslint-disable-line import/no-extraneous-dependencies -app.get("/", function(req, res) { - res.header("Content-Type", "text/plain") +const locale = require('../src'); + +const supported = ['en', 'en_US', 'ja']; +const app = express.createServer(locale(supported)); + +app.get('/', function (req, res) { + res.header('Content-Type', 'text/plain'); res.send( - "You asked for: " + req.headers["accept-language"] + "\n" + - "We support: " + supported + "\n" + - "Our default is: " + locale.Locale["default"] + "\n" + - "The best match is: " + req.locale + "\n" - ) -}) + `You asked for: ${req.headers['accept-language']} + We support: ${supported} + Our default is: ${locale.Locale.default} + The best match is: ${req.locale}` // eslint-disable-line comma-dangle + ); +}); -app.listen(8000) \ No newline at end of file +app.listen(8000); diff --git a/examples/http.js b/examples/http.js index 2dbe2e4..b66f011 100644 --- a/examples/http.js +++ b/examples/http.js @@ -1,14 +1,16 @@ -var http = require("http") - , locale = require("../lib") - , supported = new locale.Locales(["en", "en_US", "ja"]) +const http = require('http'); -http.createServer(function(req, res) { - var locales = new locale.Locales(req.headers["accept-language"]) - res.writeHeader(200, {"Content-Type": "text/plain"}) +const locale = require('../src'); + +const supported = new locale.Locales(['en', 'en_US', 'ja']); + +http.createServer(function (req, res) { + const locales = new locale.Locales(req.headers['accept-language']); + res.writeHeader(200, { 'Content-Type': 'text/plain' }); res.end( - "You asked for: " + req.headers["accept-language"] + "\n" + - "We support: " + supported + "\n" + - "Our default is: " + locale.Locale["default"] + "\n" + - "The best match is: " + locales.best(supported) + "\n" - ) -}).listen(8000) \ No newline at end of file + `You asked for: ${req.headers['accept-language']} + We support: ${supported} + Our default is: ${locale.Locale.default} + The best match is: ' + ${locales.best(supported)}` // eslint-disable-line comma-dangle + ); +}).listen(8000); diff --git a/gulpfile.js b/gulpfile.js new file mode 100644 index 0000000..69a3c2d --- /dev/null +++ b/gulpfile.js @@ -0,0 +1,52 @@ +var gulp = require('gulp'); +var concat = require('gulp-concat'); +var uglify = require('gulp-uglify'); +var babel = require('gulp-babel'); +var mocha = require('gulp-mocha'); +var gutil = require('gulp-util'); +var eslint = require('gulp-eslint'); +var del = require('del'); + +var paths = { + scripts: ['src/**.js'], + tests: ['test/tests.js'], + dist: { + folder: 'dist', + file: 'locale.js' + } +}; + +gulp.task('clean', function () { + return del([paths.dist.folder]); +}); + +gulp.task('build', ['lint', 'clean'], function () { + return gulp.src(paths.scripts) + .pipe(babel({ + presets: ['es2015'], + })) + .pipe(concat(paths.dist.file)) + .pipe(gulp.dest(paths.dist.folder)); +}); + +gulp.task('lint', function () { + return gulp.src(paths.scripts) + .pipe(eslint()) + .pipe(eslint.format()) + .pipe(eslint.failAfterError()); +}); + +gulp.task('mocha', function () { + return gulp.src(paths.tests, { read: false }) + .pipe(mocha({ + reporter: 'spec', + compilers: 'js:babel-core/register', + })) + .on('error', gutil.log); +}); + +gulp.task('watch', function () { + gulp.watch([paths.scripts, paths.tests], ['lint', 'mocha']); +}); + +gulp.task('default', ['watch']); diff --git a/lib/index.js b/lib/index.js deleted file mode 100644 index e8b194b..0000000 --- a/lib/index.js +++ /dev/null @@ -1,161 +0,0 @@ -// Generated by CoffeeScript 1.6.3 -(function() { - var Locale, Locales, app, _ref, - __slice = [].slice; - - app = function(supported, def) { - if (!(supported instanceof Locales)) { - supported = new Locales(supported, def); - supported.index(); - } - return function(req, res, next) { - var bestLocale, locales; - locales = new Locales(req.headers["accept-language"]); - bestLocale = locales.best(supported); - req.locale = String(bestLocale); - req.rawLocale = bestLocale; - return next(); - }; - }; - - app.Locale = (function() { - var serialize; - - Locale["default"] = new Locale(process.env.LANG || "en_US"); - - function Locale(str) { - var country, language, match, normalized; - if (!(match = str != null ? str.match(/[a-z]+/gi) : void 0)) { - return; - } - language = match[0], country = match[1]; - this.code = str; - this.language = language.toLowerCase(); - if (country) { - this.country = country.toUpperCase(); - } - normalized = [this.language]; - if (this.country) { - normalized.push(this.country); - } - this.normalized = normalized.join("_"); - } - - serialize = function() { - if (this.language) { - return this.code; - } else { - return null; - } - }; - - Locale.prototype.toString = serialize; - - Locale.prototype.toJSON = serialize; - - return Locale; - - })(); - - app.Locales = (function() { - var serialize; - - Locales.prototype.length = 0; - - Locales.prototype._index = null; - - Locales.prototype.sort = Array.prototype.sort; - - Locales.prototype.push = Array.prototype.push; - - function Locales(str, def) { - var item, locale, q, _i, _len, _ref, _ref1; - if (def) { - this["default"] = new Locale(def); - } - if (!str) { - return; - } - _ref = (String(str)).split(","); - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - item = _ref[_i]; - _ref1 = item.split(";"), locale = _ref1[0], q = _ref1[1]; - locale = new Locale(locale.trim()); - locale.score = q ? +q.slice(2) || 0 : 1; - this.push(locale); - } - this.sort(function(a, b) { - return b.score - a.score; - }); - } - - Locales.prototype.index = function() { - var idx, locale, _i, _len; - if (!this._index) { - this._index = {}; - for (idx = _i = 0, _len = this.length; _i < _len; idx = ++_i) { - locale = this[idx]; - this._index[locale.normalized] = idx; - } - } - return this._index; - }; - - Locales.prototype.best = function(locales) { - var index, item, l, languageIndex, locale, normalizedIndex, setLocale, _i, _j, _len, _len1; - setLocale = function(l) { - var r; - r = l; - r.defaulted = false; - return r; - }; - locale = Locale["default"]; - if (locales && locales["default"]) { - locale = locales["default"]; - } - locale.defaulted = true; - if (!locales) { - if (this[0]) { - locale = this[0]; - locale.defaulted = true; - } - return locale; - } - index = locales.index(); - for (_i = 0, _len = this.length; _i < _len; _i++) { - item = this[_i]; - normalizedIndex = index[item.normalized]; - languageIndex = index[item.language]; - if (normalizedIndex != null) { - return setLocale(locales[normalizedIndex]); - } else if (languageIndex != null) { - return setLocale(locales[languageIndex]); - } else { - for (_j = 0, _len1 = locales.length; _j < _len1; _j++) { - l = locales[_j]; - if (l.language === item.language) { - return setLocale(l); - } - } - } - } - return locale; - }; - - serialize = function() { - return __slice.call(this); - }; - - Locales.prototype.toJSON = serialize; - - Locales.prototype.toString = function() { - return String(this.toJSON()); - }; - - return Locales; - - })(); - - _ref = module.exports = app, Locale = _ref.Locale, Locales = _ref.Locales; - -}).call(this); diff --git a/lib/test.js b/lib/test.js deleted file mode 100644 index 981416a..0000000 --- a/lib/test.js +++ /dev/null @@ -1,146 +0,0 @@ -// Generated by CoffeeScript 1.6.3 -(function() { - var assert, defaultLocale, express, http, locale, server; - - http = require("http"); - - assert = require("assert"); - - express = require("express"); - - locale = require("./"); - - server = null; - - defaultLocale = locale.Locale["default"]; - - before(function(callback) { - var app; - app = express(); - app.use(locale(["en-US", "fr", "fr-CA", "en", "ja", "de", "da-DK"])); - app.get("/", function(req, res) { - res.set("content-language", req.locale); - res.set("defaulted", req.rawLocale.defaulted); - res.set("Connection", "close"); - return res.send(200); - }); - return server = app.listen(8001, callback); - }); - - describe("Defaults", function() { - it("should use the environment language as default.", function(callback) { - return http.get({ - port: 8001 - }, function(res) { - assert.equal(res.headers["content-language"], defaultLocale); - assert.equal(true, !!res.headers["defaulted"]); - return callback(); - }); - }); - it("should fallback to the default for unsupported languages.", function(callback) { - return http.get({ - port: 8001, - headers: { - "Accept-Language": "es-ES" - } - }, function(res) { - assert.equal(res.headers["content-language"], defaultLocale); - assert.equal(true, !!res.headers["defaulted"]); - return callback(); - }); - }); - return it("should fallback to the instance default for unsupported languages if instance default is defined.", function(callback) { - var instanceDefault, supportedLocales; - instanceDefault = 'en_GB'; - supportedLocales = new locale.Locales(["da-DK"], instanceDefault); - assert.equal(((new locale.Locales("cs,en-US;q=0.8,en;q=0.6")).best(supportedLocales)).toString(), instanceDefault); - return callback(); - }); - }); - - describe("Priority", function() { - it("should fallback to a more general language if a country specific language isn't available.", function(callback) { - return http.get({ - port: 8001, - headers: { - "Accept-Language": "en-GB" - } - }, function(res) { - assert.equal(res.headers["content-language"], "en", "Unsupported country should fallback to countryless language"); - assert.equal(false, !res.headers["defaulted"]); - return callback(); - }); - }); - it("should use the highest quality language supported, regardless of order.", function(callback) { - http.get({ - port: 8001, - headers: { - "Accept-Language": "en;q=.8, ja" - } - }, function(res) { - assert.equal(res.headers["content-language"], "ja", "Highest quality language supported should be used, regardless of order."); - return assert.equal(false, !res.headers["defaulted"]); - }); - http.get({ - port: 8001, - headers: { - "Accept-Language": "fr-FR, ja-JA;q=0.5" - } - }, function(res) { - assert.equal(res.headers["content-language"], "fr", "Highest quality language supported should be used, regardless of order."); - return assert.equal(false, !res.headers["defaulted"]); - }); - return http.get({ - port: 8001, - headers: { - "Accept-Language": "en-US,en;q=0.93,es-ES;q=0.87,es;q=0.80,it-IT;q=0.73,it;q=0.67,de-DE;q=0.60,de;q=0.53,fr-FR;q=0.47,fr;q=0.40,ja;q=0.33,zh-Hans-CN;q=0.27,zh-Hans;q=0.20,ar-SA;q=0.13,ar;q=0.067" - } - }, function(res) { - assert.equal(res.headers["content-language"], "en-US", "Highest quality language supported should be used, regardless of order."); - assert.equal(false, !res.headers["defaulted"]); - return callback(); - }); - }); - it("should use a country specific language when an unsupported general language is requested", function(callback) { - return http.get({ - port: 8001, - headers: { - "Accept-Language": "da" - } - }, function(res) { - assert.equal(res.headers["content-language"], "da-DK"); - return callback(); - }); - }); - it("should fallback to a country specific language even when there's a lower quality exact match", function(callback) { - return http.get({ - port: 8001, - headers: { - "Accept-Language": "ja;q=.8, da" - } - }, function(res) { - assert.equal(res.headers["content-language"], "da-DK"); - assert.equal(false, !res.headers["defaulted"]); - return callback(); - }); - }); - return it("should match country-specific language codes even when the separator is different", function(callback) { - return http.get({ - port: 8001, - headers: { - "Accept-Language": "fr_CA" - } - }, function(res) { - assert.equal(res.headers["content-language"], "fr-CA"); - assert.equal(false, !res.headers["defaulted"]); - return callback(); - }); - }); - }); - - after(function() { - server.close(); - return process.exit(0)(); - }); - -}).call(this); diff --git a/package.json b/package.json index 23bd589..f3a422b 100644 --- a/package.json +++ b/package.json @@ -4,26 +4,43 @@ "description": "Browser locale negotiation for node.js", "version": "0.1.0", "homepage": "https://github.com/florrain/locale", + "bugs": "https://github.com/florrain/locale/issues", "repository": { "type": "git", "url": "git://github.com/florrain/locale.git" }, - "main": "./lib", + "main": "./dist/locale.js", "scripts": { - "test": "./node_modules/.bin/mocha ./src/test.coffee", - "prepublish": "coffee -o lib/ -c src/" + "test": "./node_modules/.bin/mocha ./test/tests.js", + "prepublish": "gulp build" }, "engines": { "node": ">0.8.x" }, "dependencies": {}, "devDependencies": { - "coffee-script": "~1.6.0", - "express": "~3.0.0", - "mocha": "~1.13.0" + "babel-core": "^6.24.0", + "babel-preset-es2015": "^6.24.0", + "del": "^2.2.2", + "eslint": "^3.18.0", + "eslint-config-airbnb": "^14.1.0", + "eslint-plugin-import": "^2.2.0", + "eslint-plugin-jsx-a11y": "^4.0.0", + "eslint-plugin-react": "^6.10.3", + "express": "^3.0.6", + "gulp": "^3.9.1", + "gulp-babel": "^6.1.2", + "gulp-concat": "^2.6.1", + "gulp-eslint": "^3.0.1", + "gulp-mocha": "^4.1.0", + "gulp-uglify": "^2.1.2", + "gulp-util": "^3.0.8", + "mocha": "^3.2.0" }, "contributors": [ "Jed Smith (https://github.com/jed)", - "D. Stuart Freeman (https://github.com/stuartf)" - ] + "D. Stuart Freeman (https://github.com/stuartf)", + "Florian Lorrain (https://github.com/florrain)" + ], + "license": "MIT" } diff --git a/src/index.coffee b/src/index.coffee deleted file mode 100644 index 7595d8e..0000000 --- a/src/index.coffee +++ /dev/null @@ -1,108 +0,0 @@ -app = (supported, def) -> - unless supported instanceof Locales - supported = new Locales supported, def - do supported.index - - (req, res, next) -> - locales = new Locales req.headers["accept-language"] - - bestLocale = locales.best supported - req.locale = String bestLocale - req.rawLocale = bestLocale - do next - -class app.Locale - @default: new Locale process.env.LANG or "en_US" - - constructor: (str) -> - return unless match = str?.match /[a-z]+/gi - - [language, country] = match - - @code = str - @language = do language.toLowerCase - @country = do country.toUpperCase if country - - normalized = [@language] - normalized.push @country if @country - @normalized = normalized.join "_" - - serialize = -> - if @language - return @code - else - return null - - toString: serialize - toJSON: serialize - -class app.Locales - length: 0 - _index: null - - sort: Array::sort - push: Array::push - - constructor: (str, def) -> - if def - @default = new Locale def - - return unless str - - for item in (String str).split "," - [locale, q] = item.split ";" - - locale = new Locale do locale.trim - locale.score = if q then +q[2..] or 0 else 1 - - @push locale - - @sort (a, b) -> b.score - a.score - - index: -> - unless @_index - @_index = {} - @_index[locale.normalized] = idx for locale, idx in @ - - @_index - - best: (locales) -> - setLocale = (l) -> # When don't return the default - r = l - r.defaulted = false - return r - - locale = Locale.default - if locales and locales.default - locale = locales.default - locale.defaulted = true - - unless locales - if @[0] - locale = @[0] - locale.defaulted = true - return locale - - index = do locales.index - - for item in @ - normalizedIndex = index[item.normalized] - languageIndex = index[item.language] - - if normalizedIndex? then return setLocale(locales[normalizedIndex]) - else if languageIndex? then return setLocale(locales[languageIndex]) - else - for l in locales - if l.language == item.language then return setLocale(l) - - locale - - serialize = -> - [@...] - - toJSON: serialize - - toString: -> - String do @toJSON - -{Locale, Locales} = module.exports = app diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..419c807 --- /dev/null +++ b/src/index.js @@ -0,0 +1,25 @@ +const Locale = require('./locale'); +const Locales = require('./locales'); + +const app = function (supported, def) { + let supportedLocales = supported; + + if (!(supportedLocales instanceof Locales)) { + supportedLocales = new Locales(supportedLocales, def); + supportedLocales.index(); + } + + return (req, res, next) => { + const locales = new Locales(req.headers['accept-language']); + + const bestLocale = locales.best(supportedLocales); + req.locale = String(bestLocale); + req.rawLocale = bestLocale; + next(); + }; +}; + +app.Locales = Locales; +app.Locale = Locale; + +module.exports = app; diff --git a/src/locale.js b/src/locale.js new file mode 100644 index 0000000..ee36bf5 --- /dev/null +++ b/src/locale.js @@ -0,0 +1,31 @@ +const defaultLocaleString = () => process.env.LANG || 'en_US'; + +class Locale { + constructor(str) { + if (!str) return null; + + const match = str.match(/[a-z]+/gi); + const [language, country] = match; + + this.code = str; + this.language = language.toLowerCase(); + const normalized = [this.language]; + + if (country) { + this.country = country.toUpperCase(); + normalized.push(this.country); + } + + this.normalized = normalized.join('_'); + } + + serialize() { + if (!this.language) return null; + return this.code; + } +} +Locale.prototype.toString = Locale.prototype.serialize; +Locale.prototype.toJSON = Locale.prototype.serialize; +Locale.default = new Locale(defaultLocaleString()); + +module.exports = Locale; diff --git a/src/locales.js b/src/locales.js new file mode 100644 index 0000000..7b847ca --- /dev/null +++ b/src/locales.js @@ -0,0 +1,114 @@ +const Locale = require('./locale'); + +class Locales { + constructor(input, def) { + let elements = []; + + if (input) { + if (typeof input === 'string' || input instanceof String) { + elements = input.split(','); + } else if (input instanceof Array) { + elements = input.slice(); + } + + elements = elements + .map(function (item) { + if (!item) return null; + + const [locale, q] = item.split(';'); + const locale2 = new Locale(locale.trim()); + let score = 1; + + if (q) { + score = q.slice(2) || 0; + } + + locale2.score = score; + + return locale2; + }) + .filter(e => e instanceof Locale) + .sort((a, b) => b.score - a.score); + } + + this.elements = elements; + this._index = null; + if (def) { + this.default = new Locale(def); + } + } + + index() { + if (!this._index) { + this._index = {}; + + this.elements.forEach(function (locale, idx) { + this._index[locale.normalized] = idx; + }, this); + } + return this._index; + } + + best(locales) { + const setLocale = function (l) { + const r = l; + r.defaulted = false; + return r; + }; + + let locale = Locale.default; + if (locales && locales.default) { + locale = locales.default; + } + locale.defaulted = true; + + if (!locales) { + if (this.elements[0]) { + locale = this.elements[0]; + locale.defaulted = true; + } + return locale; + } + + for (const item of this.elements) { // eslint-disable-line no-restricted-syntax + const appropriateLocaleIndex = Locales.appropriateIndex(locales, item); + + if (appropriateLocaleIndex !== null) { + return setLocale(locales.elements[appropriateLocaleIndex]); + } + } + + return locale; + } + + static appropriateIndex(locales, locale) { + const index = locales.index(); + + const normalizedIndex = index[locale.normalized]; + const languageIndex = index[locale.language]; + + if (normalizedIndex !== undefined) { + return normalizedIndex; + } else if (languageIndex !== undefined) { + return languageIndex; + } + + const sameLanguageLocaleIndex = locales.elements.findIndex(l => l.language === locale.language); + if (sameLanguageLocaleIndex > -1) return sameLanguageLocaleIndex; + + return null; + } + + serialize() { + return [...this.elements]; + } + + toString() { + return String(this.toJSON()); + } +} +Locales.prototype.toJSON = Locales.prototype.serialize; +Locales.prototype.sort = Array.prototype.sort; +Locales.prototype.push = Array.prototype.push; + +module.exports = Locales; diff --git a/src/test.coffee b/src/test.coffee deleted file mode 100644 index 76dde21..0000000 --- a/src/test.coffee +++ /dev/null @@ -1,114 +0,0 @@ -http = require "http" -assert = require "assert" -express = require "express" -locale = require "./" - -server = null -defaultLocale = locale.Locale.default - -before (callback) -> - app = do express - - app.use locale ["en-US", "fr", "fr-CA", "en", "ja", "de", "da-DK"] - app.get "/", (req, res) -> - res.set "content-language", req.locale - res.set "defaulted", req.rawLocale.defaulted - res.set "Connection", "close" - res.send(200) - server = app.listen 8001, callback - -describe "Defaults", -> - it "should use the environment language as default.", (callback) -> - http.get port: 8001, (res) -> - assert.equal( - res.headers["content-language"] - defaultLocale - ) - assert.equal(true, !!res.headers["defaulted"]) - callback() - - it "should fallback to the default for unsupported languages.", (callback) -> - http.get port: 8001, headers: "Accept-Language": "es-ES", (res) -> - assert.equal( - res.headers["content-language"] - defaultLocale - ) - assert.equal(true, !!res.headers["defaulted"]) - callback() - - it "should fallback to the instance default for unsupported languages if instance default is defined.", (callback) -> - instanceDefault = 'SomeFakeLanguage-NotReal' - supportedLocales = new locale.Locales ["en-US", "fr", "fr-CA", "en", "ja", "de", "da-DK"], instanceDefault - assert.equal( - ((new locale.Locales "es-ES").best supportedLocales).toString() - instanceDefault - ) - callback() - -describe "Priority", -> - it "should fallback to a more general language if a country specific language isn't available.", (callback) -> - http.get port: 8001, headers: "Accept-Language": "en-GB", (res) -> - assert.equal( - res.headers["content-language"] - "en" - "Unsupported country should fallback to countryless language" - ) - assert.equal(false, !res.headers["defaulted"]) - callback() - - it "should use the highest quality language supported, regardless of order.", (callback) -> - http.get port: 8001, headers: "Accept-Language": "en;q=.8, ja", (res) -> - assert.equal( - res.headers["content-language"] - "ja" - "Highest quality language supported should be used, regardless of order." - ) - assert.equal(false, !res.headers["defaulted"]) - - http.get port: 8001, headers: "Accept-Language": "fr-FR, ja-JA;q=0.5", (res) -> - assert.equal( - res.headers["content-language"] - "fr" - "Highest quality language supported should be used, regardless of order." - ) - assert.equal(false, !res.headers["defaulted"]) - - http.get port: 8001, headers: "Accept-Language": "en-US,en;q=0.93,es-ES;q=0.87,es;q=0.80,it-IT;q=0.73,it;q=0.67,de-DE;q=0.60,de;q=0.53,fr-FR;q=0.47,fr;q=0.40,ja;q=0.33,zh-Hans-CN;q=0.27,zh-Hans;q=0.20,ar-SA;q=0.13,ar;q=0.067", (res) -> - assert.equal( - res.headers["content-language"] - "en-US" - "Highest quality language supported should be used, regardless of order." - ) - assert.equal(false, !res.headers["defaulted"]) - - callback() - - it "should use a country specific language when an unsupported general language is requested", (callback) -> - http.get port: 8001, headers: "Accept-Language": "da", (res) -> - assert.equal( - res.headers["content-language"] - "da-DK" - ) - callback() - - it "should fallback to a country specific language even when there's a lower quality exact match", (callback) -> - http.get port: 8001, headers: "Accept-Language": "ja;q=.8, da", (res) -> - assert.equal( - res.headers["content-language"] - "da-DK" - ) - assert.equal(false, !res.headers["defaulted"]) - callback() - - it "should match country-specific language codes even when the separator is different", (callback) -> - http.get port: 8001, headers: "Accept-Language": "fr_CA", (res) -> - assert.equal( - res.headers["content-language"] - "fr-CA" - ) - assert.equal(false, !res.headers["defaulted"]) - callback() - -after -> - do server.close - do process.exit 0 diff --git a/test/.eslintrc b/test/.eslintrc new file mode 100644 index 0000000..7eeefc3 --- /dev/null +++ b/test/.eslintrc @@ -0,0 +1,5 @@ +{ + "env": { + "mocha": true + } +} diff --git a/test/mocha.opts b/test/mocha.opts index 6300284..50afa0d 100644 --- a/test/mocha.opts +++ b/test/mocha.opts @@ -1,2 +1,2 @@ --reporter spec ---compilers coffee:coffee-script +--compilers js:babel-core/register diff --git a/test/tests.js b/test/tests.js new file mode 100644 index 0000000..ad43f40 --- /dev/null +++ b/test/tests.js @@ -0,0 +1,122 @@ +const http = require('http'); +const assert = require('assert'); +const express = require('express'); +const locale = require('../src/'); + +let server = null; +const defaultLocale = locale.Locale.default; + +before(function (callback) { + const app = express(); + + app.use(locale(['en-US', 'fr', 'fr-CA', 'en', 'ja', 'de', 'da-DK'])); + app.get('/', function (req, res) { + res.set('content-language', req.locale); + res.set('defaulted', req.rawLocale.defaulted); + res.set('Connection', 'close'); + res.send(200); + }); + server = app.listen(8001, callback); +}); + +after(function () { + server.close(); +}); + +describe('Defaults', function () { + it('should use the environment language as default.', function (callback) { + http.get({ port: 8001 }, function (res) { + assert.equal( + res.headers['content-language'], + defaultLocale, + ); + assert.equal(true, !!res.headers.defaulted); + callback(); + }); + }); + + it('should fallback to the default for unsupported languages.', function (callback) { + http.get({ port: 8001, headers: { 'Accept-Language': 'es-ES' } }, function (res) { + assert.equal( + res.headers['content-language'], + defaultLocale, + ); + assert.equal(true, !!res.headers.defaulted); + callback(); + }); + }); + + it('should fallback to the instance default for unsupported languages if instance default is defined.', function (callback) { + const instanceDefault = 'SomeFakeLanguage-NotReal'; + const supportedLocales = new locale.Locales(['en-US', 'fr', 'fr-CA', 'en', 'ja', 'de', 'da-DK'], instanceDefault); + assert.equal( + ((new locale.Locales('es-ES')).best(supportedLocales)).toString(), + instanceDefault, + ); + callback(); + }); +}); + +describe('Priority', function () { + it('should fallback to a more general language if a country specific language isn\'t available.', function (callback) { + return http.get({ port: 8001, headers: { 'Accept-Language': 'en-GB' } }, function (res) { + assert.equal(res.headers['content-language'], 'en', 'Unsupported country should fallback to countryless language'); + assert.equal(false, !res.headers.defaulted); + callback(); + }); + }); + + it('should use the highest quality language supported, regardless of order.', function (callback) { + http.get({ port: 8001, headers: { 'Accept-Language': 'en;q=.8, ja' } }, function (res) { + assert.equal(res.headers['content-language'], 'ja', 'Highest quality language supported should be used, regardless of order.'); + assert.equal(false, !res.headers.defaulted); + }); + http.get({ port: 8001, headers: { 'Accept-Language': 'fr-FR, ja-JA;q=0.5' } }, function (res) { + assert.equal(res.headers['content-language'], 'fr', 'Highest quality language supported should be used, regardless of order.'); + assert.equal(false, !res.headers.defaulted); + }); + http.get({ + port: 8001, + headers: { + 'Accept-Language': 'en-US,en;q=0.93,es-ES;q=0.87,es;q=0.80,it-IT;q=0.73,it;q=0.67,de-DE;q=0.60,de;q=0.53,fr-FR;q=0.47,fr;q=0.40,ja;q=0.33,zh-Hans-CN;q=0.27,zh-Hans;q=0.20,ar-SA;q=0.13,ar;q=0.067', + }, + }, function (res) { + assert.equal(res.headers['content-language'], 'en-US', 'Highest quality language supported should be used, regardless of order.'); + assert.equal(false, !res.headers.defaulted); + callback(); + }); + }); + + it('should use a country specific language when an unsupported general language is requested', function (callback) { + return http.get({ + port: 8001, + headers: { + 'Accept-Language': 'da', + }, + }, function (res) { + assert.equal(res.headers['content-language'], 'da-DK'); + callback(); + }); + }); + + it('should fallback to a country specific language even when there\'s a lower quality exact match', function (callback) { + return http.get({ + port: 8001, + headers: { + 'Accept-Language': 'ja;q=.8, da', + }, + }, function (res) { + assert.equal(res.headers['content-language'], 'da-DK'); + assert.equal(false, !res.headers.defaulted); + callback(); + }); + }); + + it('should match country-specific language codes even when the separator is different', function (callback) { + return http.get({ port: 8001, headers: { 'Accept-Language': 'fr_CA' } }, function (res) { + assert.equal(res.headers['content-language'], 'fr-CA'); + assert.equal(false, !res.headers.defaulted); + callback(); + }); + }); +});