├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE.txt ├── README.md ├── examples ├── express.js └── http.js ├── lib ├── index.js └── test.js ├── package.json ├── src ├── index.coffee └── test.coffee └── test └── mocha.opts /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.8 4 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Jed Schmidt, http://jed.is/ 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | locale [![Build Status](https://travis-ci.org/florrain/locale.svg?branch=master)](https://travis-ci.org/florrain/locale) 2 | ====== 3 | 4 | locale is a [node.js][node] module for negotiating HTTP locales for incoming browser requests. It can be used as a standalone module for HTTP or as [Express][express]/[Connect][connect] middleware, or as the server component for an in-browser gettext implementation like [JED][JED]. 5 | 6 | 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. 7 | 8 | **Credits to [jed](https://github.com/jed) who passed the ownership of the package.** 9 | 10 | Examples 11 | -------- 12 | 13 | ### For the node.js HTTP module 14 | ```javascript 15 | var http = require("http") 16 | , locale = require("locale") 17 | , supported = new locale.Locales(["en", "en_US", "ja"]) 18 | 19 | http.createServer(function(req, res) { 20 | var locales = new locale.Locales(req.headers["accept-language"]) 21 | res.writeHeader(200, {"Content-Type": "text/plain"}) 22 | res.end( 23 | "You asked for: " + req.headers["accept-language"] + "\n" + 24 | "We support: " + supported + "\n" + 25 | "Our default is: " + locale.Locale["default"] + "\n" + 26 | "The best match is: " + locales.best(supported) + "\n" 27 | ) 28 | }).listen(8000) 29 | ``` 30 | 31 | ### For Connect/Express 32 | ```javascript 33 | var http = require("http") 34 | , express = require("express") 35 | , locale = require("locale") 36 | , supported = ["en", "en_US", "ja"] 37 | , default = "en", 38 | , app = express() 39 | 40 | app.use(locale(supported, default)) 41 | 42 | app.get("/", function(req, res) { 43 | res.header("Content-Type", "text/plain") 44 | res.send( 45 | "You asked for: " + req.headers["accept-language"] + "\n" + 46 | "We support: " + supported + "\n" + 47 | "Our default is: " + locale.Locale["default"] + "\n" + 48 | "The best match is: " + req.locale + "\n" 49 | ) 50 | }) 51 | 52 | app.listen(8000) 53 | ``` 54 | 55 | Install 56 | ------- 57 | 58 | $ npm install locale 59 | 60 | (Note that although this repo is CoffeeScript, the actual npm library is pre-compiled to pure JavaScript and has no run-time dependencies.) 61 | 62 | API 63 | --- 64 | 65 | ### locale(supportedLocales, default) 66 | 67 | This module exports a function that can be used as Express/Connect middleware. It takes one argument, a list of supported locales, and adds a `locale` property to incoming HTTP requests, reflecting the most appropriate locale determined using the `best` method described below. 68 | 69 | ### new locale.Locale(languageTag) 70 | 71 | The Locale constructor takes a [language tag][langtag] string consisting of an ISO-639 language abbreviation and optional two-letter ISO-3166 country code, and returns an object with a `language` property containing the former and a `country` property containing the latter. 72 | 73 | ### locale.Locale["default"] 74 | 75 | The default locale for the environment, as parsed from `process.env.LANG`. This is used as the fallback when the best language is calculated from the intersection of requested and supported locales and supported languages has not default. 76 | 77 | ### locales = new locale.Locales(acceptLanguageHeader, default) 78 | 79 | The Locales constructor takes a string compliant with the [`Accept-Language` HTTP header][header], and returns a list of acceptible locales, optionally sorted in descending order by quality score. Second argument is optional default value used as the fallback when the best language is calculated. Otherwise locale.Locale["default"] is used as fallback. 80 | 81 | ### locales.best([supportedLocales]) 82 | 83 | 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. 84 | 85 | supported = new locale.Locales(['en', 'en_US'], 'en'); 86 | (new locale.Locales('en')).best(supported).toString(); // 'en' 87 | (new locale.Locales('en_GB')).best(supported).toString(); // 'en' 88 | (new locale.Locales('en_US')).best(supported).toString(); // 'en_US' 89 | (new locale.Locales('jp')).best(supported); // supported.default || locale.Locale["default"] 90 | 91 | #### Locale object API 92 | 93 | The actual return value of calling `locales.best([supportedLocales])` or `new locale.Locale(languageTag)` is a Locale object. This Locale object has its own interface with the following properties and methods: 94 | 95 | ``` 96 | Locale object 97 | 98 | { 99 | /* properties */ 100 | code: string // returns user-generated input used to construct the Locale. Eg, `en-US` 101 | langauge: string // returns the first 2 letters of the code, lowercased 102 | country: string // returns the second 2 letters of the code if present, uppercased. Returns `undefined` otherwise 103 | normalized: string // returns the language, followed by a `_` and the country, if the country is present 104 | 105 | /* methods */ 106 | toString(): string // returns the code 107 | toJSON(): string // returns the code 108 | } 109 | ``` 110 | 111 | For example: 112 | 113 | ```javascript 114 | var locale = new locale.Locale('pt-BR'); 115 | 116 | console.log(locale); 117 | // { 118 | // code: 'pt-BR', 119 | // language: 'pt', 120 | // country: 'BR', 121 | // normalized: 'pt_BR', 122 | // } 123 | ``` 124 | 125 | Copyright 126 | --------- 127 | 128 | Copyright (c) 2012 Jed Schmidt. See LICENSE.txt for details. 129 | 130 | Send any questions or comments [here](http://twitter.com/jedschmidt). 131 | 132 | [node]: http://nodejs.org 133 | [express]: http://expressjs.com 134 | [JED]: http://slexaxton.github.com/Jed 135 | [connect]: http://senchalabs.github.com/connect 136 | [langtag]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.10 137 | [header]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.4 138 | -------------------------------------------------------------------------------- /examples/express.js: -------------------------------------------------------------------------------- 1 | var http = require("http") 2 | , express = require("express") 3 | , locale = require("../lib") 4 | , supported = ["en", "en_US", "ja"] 5 | , app = express.createServer(locale(supported)) 6 | 7 | app.get("/", function(req, res) { 8 | res.header("Content-Type", "text/plain") 9 | res.send( 10 | "You asked for: " + req.headers["accept-language"] + "\n" + 11 | "We support: " + supported + "\n" + 12 | "Our default is: " + locale.Locale["default"] + "\n" + 13 | "The best match is: " + req.locale + "\n" 14 | ) 15 | }) 16 | 17 | app.listen(8000) -------------------------------------------------------------------------------- /examples/http.js: -------------------------------------------------------------------------------- 1 | var http = require("http") 2 | , locale = require("../lib") 3 | , supported = new locale.Locales(["en", "en_US", "ja"]) 4 | 5 | http.createServer(function(req, res) { 6 | var locales = new locale.Locales(req.headers["accept-language"]) 7 | res.writeHeader(200, {"Content-Type": "text/plain"}) 8 | res.end( 9 | "You asked for: " + req.headers["accept-language"] + "\n" + 10 | "We support: " + supported + "\n" + 11 | "Our default is: " + locale.Locale["default"] + "\n" + 12 | "The best match is: " + locales.best(supported) + "\n" 13 | ) 14 | }).listen(8000) -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.6.3 2 | (function() { 3 | var Locale, Locales, app, _ref, 4 | __slice = [].slice; 5 | 6 | app = function(supported, def) { 7 | if (!(supported instanceof Locales)) { 8 | supported = new Locales(supported, def); 9 | supported.index(); 10 | } 11 | return function(req, res, next) { 12 | var bestLocale, locales; 13 | locales = new Locales(req.headers["accept-language"]); 14 | bestLocale = locales.best(supported); 15 | req.locale = String(bestLocale); 16 | req.rawLocale = bestLocale; 17 | return next(); 18 | }; 19 | }; 20 | 21 | app.Locale = (function() { 22 | var serialize; 23 | 24 | Locale["default"] = new Locale(process.env.LANG || "en_US"); 25 | 26 | function Locale(str) { 27 | var country, language, match, normalized; 28 | if (!(match = str != null ? str.match(/[a-z]+/gi) : void 0)) { 29 | return; 30 | } 31 | language = match[0], country = match[1]; 32 | this.code = str; 33 | this.language = language.toLowerCase(); 34 | if (country) { 35 | this.country = country.toUpperCase(); 36 | } 37 | normalized = [this.language]; 38 | if (this.country) { 39 | normalized.push(this.country); 40 | } 41 | this.normalized = normalized.join("_"); 42 | } 43 | 44 | serialize = function() { 45 | if (this.language) { 46 | return this.code; 47 | } else { 48 | return null; 49 | } 50 | }; 51 | 52 | Locale.prototype.toString = serialize; 53 | 54 | Locale.prototype.toJSON = serialize; 55 | 56 | return Locale; 57 | 58 | })(); 59 | 60 | app.Locales = (function() { 61 | var serialize; 62 | 63 | Locales.prototype.length = 0; 64 | 65 | Locales.prototype._index = null; 66 | 67 | Locales.prototype.sort = Array.prototype.sort; 68 | 69 | Locales.prototype.push = Array.prototype.push; 70 | 71 | function Locales(str, def) { 72 | var item, locale, q, _i, _len, _ref, _ref1; 73 | if (def) { 74 | this["default"] = new Locale(def); 75 | } 76 | if (!str) { 77 | return; 78 | } 79 | _ref = (String(str)).split(","); 80 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 81 | item = _ref[_i]; 82 | _ref1 = item.split(";"), locale = _ref1[0], q = _ref1[1]; 83 | locale = new Locale(locale.trim()); 84 | locale.score = q ? +q.slice(2) || 0 : 1; 85 | this.push(locale); 86 | } 87 | this.sort(function(a, b) { 88 | return b.score - a.score; 89 | }); 90 | } 91 | 92 | Locales.prototype.index = function() { 93 | var idx, locale, _i, _len; 94 | if (!this._index) { 95 | this._index = {}; 96 | for (idx = _i = 0, _len = this.length; _i < _len; idx = ++_i) { 97 | locale = this[idx]; 98 | this._index[locale.normalized] = idx; 99 | } 100 | } 101 | return this._index; 102 | }; 103 | 104 | Locales.prototype.best = function(locales) { 105 | var index, item, l, languageIndex, locale, normalizedIndex, setLocale, _i, _j, _len, _len1; 106 | setLocale = function(l) { 107 | var r; 108 | r = l; 109 | r.defaulted = false; 110 | return r; 111 | }; 112 | locale = Locale["default"]; 113 | if (locales && locales["default"]) { 114 | locale = locales["default"]; 115 | } 116 | locale.defaulted = true; 117 | if (!locales) { 118 | if (this[0]) { 119 | locale = this[0]; 120 | locale.defaulted = true; 121 | } 122 | return locale; 123 | } 124 | index = locales.index(); 125 | for (_i = 0, _len = this.length; _i < _len; _i++) { 126 | item = this[_i]; 127 | normalizedIndex = index[item.normalized]; 128 | languageIndex = index[item.language]; 129 | if (normalizedIndex != null) { 130 | return setLocale(locales[normalizedIndex]); 131 | } else if (languageIndex != null) { 132 | return setLocale(locales[languageIndex]); 133 | } else { 134 | for (_j = 0, _len1 = locales.length; _j < _len1; _j++) { 135 | l = locales[_j]; 136 | if (l.language === item.language) { 137 | return setLocale(l); 138 | } 139 | } 140 | } 141 | } 142 | return locale; 143 | }; 144 | 145 | serialize = function() { 146 | return __slice.call(this); 147 | }; 148 | 149 | Locales.prototype.toJSON = serialize; 150 | 151 | Locales.prototype.toString = function() { 152 | return String(this.toJSON()); 153 | }; 154 | 155 | return Locales; 156 | 157 | })(); 158 | 159 | _ref = module.exports = app, Locale = _ref.Locale, Locales = _ref.Locales; 160 | 161 | }).call(this); 162 | -------------------------------------------------------------------------------- /lib/test.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.6.3 2 | (function() { 3 | var assert, defaultLocale, express, http, locale, server; 4 | 5 | http = require("http"); 6 | 7 | assert = require("assert"); 8 | 9 | express = require("express"); 10 | 11 | locale = require("./"); 12 | 13 | server = null; 14 | 15 | defaultLocale = locale.Locale["default"]; 16 | 17 | before(function(callback) { 18 | var app; 19 | app = express(); 20 | app.use(locale(["en-US", "fr", "fr-CA", "en", "ja", "de", "da-DK"])); 21 | app.get("/", function(req, res) { 22 | res.set("content-language", req.locale); 23 | res.set("defaulted", req.rawLocale.defaulted); 24 | res.set("Connection", "close"); 25 | return res.send(200); 26 | }); 27 | return server = app.listen(8001, callback); 28 | }); 29 | 30 | describe("Defaults", function() { 31 | it("should use the environment language as default.", function(callback) { 32 | return http.get({ 33 | port: 8001 34 | }, function(res) { 35 | assert.equal(res.headers["content-language"], defaultLocale); 36 | assert.equal(true, !!res.headers["defaulted"]); 37 | return callback(); 38 | }); 39 | }); 40 | it("should fallback to the default for unsupported languages.", function(callback) { 41 | return http.get({ 42 | port: 8001, 43 | headers: { 44 | "Accept-Language": "es-ES" 45 | } 46 | }, function(res) { 47 | assert.equal(res.headers["content-language"], defaultLocale); 48 | assert.equal(true, !!res.headers["defaulted"]); 49 | return callback(); 50 | }); 51 | }); 52 | return it("should fallback to the instance default for unsupported languages if instance default is defined.", function(callback) { 53 | var instanceDefault, supportedLocales; 54 | instanceDefault = 'en_GB'; 55 | supportedLocales = new locale.Locales(["da-DK"], instanceDefault); 56 | assert.equal(((new locale.Locales("cs,en-US;q=0.8,en;q=0.6")).best(supportedLocales)).toString(), instanceDefault); 57 | return callback(); 58 | }); 59 | }); 60 | 61 | describe("Priority", function() { 62 | it("should fallback to a more general language if a country specific language isn't available.", function(callback) { 63 | return http.get({ 64 | port: 8001, 65 | headers: { 66 | "Accept-Language": "en-GB" 67 | } 68 | }, function(res) { 69 | assert.equal(res.headers["content-language"], "en", "Unsupported country should fallback to countryless language"); 70 | assert.equal(false, !res.headers["defaulted"]); 71 | return callback(); 72 | }); 73 | }); 74 | it("should use the highest quality language supported, regardless of order.", function(callback) { 75 | http.get({ 76 | port: 8001, 77 | headers: { 78 | "Accept-Language": "en;q=.8, ja" 79 | } 80 | }, function(res) { 81 | assert.equal(res.headers["content-language"], "ja", "Highest quality language supported should be used, regardless of order."); 82 | return assert.equal(false, !res.headers["defaulted"]); 83 | }); 84 | http.get({ 85 | port: 8001, 86 | headers: { 87 | "Accept-Language": "fr-FR, ja-JA;q=0.5" 88 | } 89 | }, function(res) { 90 | assert.equal(res.headers["content-language"], "fr", "Highest quality language supported should be used, regardless of order."); 91 | return assert.equal(false, !res.headers["defaulted"]); 92 | }); 93 | return http.get({ 94 | port: 8001, 95 | headers: { 96 | "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" 97 | } 98 | }, function(res) { 99 | assert.equal(res.headers["content-language"], "en-US", "Highest quality language supported should be used, regardless of order."); 100 | assert.equal(false, !res.headers["defaulted"]); 101 | return callback(); 102 | }); 103 | }); 104 | it("should use a country specific language when an unsupported general language is requested", function(callback) { 105 | return http.get({ 106 | port: 8001, 107 | headers: { 108 | "Accept-Language": "da" 109 | } 110 | }, function(res) { 111 | assert.equal(res.headers["content-language"], "da-DK"); 112 | return callback(); 113 | }); 114 | }); 115 | it("should fallback to a country specific language even when there's a lower quality exact match", function(callback) { 116 | return http.get({ 117 | port: 8001, 118 | headers: { 119 | "Accept-Language": "ja;q=.8, da" 120 | } 121 | }, function(res) { 122 | assert.equal(res.headers["content-language"], "da-DK"); 123 | assert.equal(false, !res.headers["defaulted"]); 124 | return callback(); 125 | }); 126 | }); 127 | return it("should match country-specific language codes even when the separator is different", function(callback) { 128 | return http.get({ 129 | port: 8001, 130 | headers: { 131 | "Accept-Language": "fr_CA" 132 | } 133 | }, function(res) { 134 | assert.equal(res.headers["content-language"], "fr-CA"); 135 | assert.equal(false, !res.headers["defaulted"]); 136 | return callback(); 137 | }); 138 | }); 139 | }); 140 | 141 | after(function() { 142 | server.close(); 143 | return process.exit(0)(); 144 | }); 145 | 146 | }).call(this); 147 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Florian Lorrain", 3 | "name": "locale", 4 | "description": "Browser locale negotiation for node.js", 5 | "version": "0.1.0", 6 | "homepage": "https://github.com/florrain/locale", 7 | "repository": { 8 | "type": "git", 9 | "url": "git://github.com/florrain/locale.git" 10 | }, 11 | "main": "./lib", 12 | "scripts": { 13 | "test": "./node_modules/.bin/mocha ./src/test.coffee", 14 | "prepublish": "coffee -o lib/ -c src/" 15 | }, 16 | "engines": { 17 | "node": ">0.8.x" 18 | }, 19 | "dependencies": {}, 20 | "devDependencies": { 21 | "coffee-script": "~1.6.0", 22 | "express": "~3.0.0", 23 | "mocha": "~1.13.0" 24 | }, 25 | "contributors": [ 26 | "Jed Smith (https://github.com/jed)", 27 | "D. Stuart Freeman (https://github.com/stuartf)" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /src/index.coffee: -------------------------------------------------------------------------------- 1 | app = (supported, def) -> 2 | unless supported instanceof Locales 3 | supported = new Locales supported, def 4 | do supported.index 5 | 6 | (req, res, next) -> 7 | locales = new Locales req.headers["accept-language"] 8 | 9 | bestLocale = locales.best supported 10 | req.locale = String bestLocale 11 | req.rawLocale = bestLocale 12 | do next 13 | 14 | class app.Locale 15 | @default: new Locale process.env.LANG or "en_US" 16 | 17 | constructor: (str) -> 18 | return unless match = str?.match /[a-z]+/gi 19 | 20 | [language, country] = match 21 | 22 | @code = str 23 | @language = do language.toLowerCase 24 | @country = do country.toUpperCase if country 25 | 26 | normalized = [@language] 27 | normalized.push @country if @country 28 | @normalized = normalized.join "_" 29 | 30 | serialize = -> 31 | if @language 32 | return @code 33 | else 34 | return null 35 | 36 | toString: serialize 37 | toJSON: serialize 38 | 39 | class app.Locales 40 | length: 0 41 | _index: null 42 | 43 | sort: Array::sort 44 | push: Array::push 45 | 46 | constructor: (str, def) -> 47 | if def 48 | @default = new Locale def 49 | 50 | return unless str 51 | 52 | for item in (String str).split "," 53 | [locale, q] = item.split ";" 54 | 55 | locale = new Locale do locale.trim 56 | locale.score = if q then +q[2..] or 0 else 1 57 | 58 | @push locale 59 | 60 | @sort (a, b) -> b.score - a.score 61 | 62 | index: -> 63 | unless @_index 64 | @_index = {} 65 | @_index[locale.normalized] = idx for locale, idx in @ 66 | 67 | @_index 68 | 69 | best: (locales) -> 70 | setLocale = (l) -> # When don't return the default 71 | r = l 72 | r.defaulted = false 73 | return r 74 | 75 | locale = Locale.default 76 | if locales and locales.default 77 | locale = locales.default 78 | locale.defaulted = true 79 | 80 | unless locales 81 | if @[0] 82 | locale = @[0] 83 | locale.defaulted = true 84 | return locale 85 | 86 | index = do locales.index 87 | 88 | for item in @ 89 | normalizedIndex = index[item.normalized] 90 | languageIndex = index[item.language] 91 | 92 | if normalizedIndex? then return setLocale(locales[normalizedIndex]) 93 | else if languageIndex? then return setLocale(locales[languageIndex]) 94 | else 95 | for l in locales 96 | if l.language == item.language then return setLocale(l) 97 | 98 | locale 99 | 100 | serialize = -> 101 | [@...] 102 | 103 | toJSON: serialize 104 | 105 | toString: -> 106 | String do @toJSON 107 | 108 | {Locale, Locales} = module.exports = app 109 | -------------------------------------------------------------------------------- /src/test.coffee: -------------------------------------------------------------------------------- 1 | http = require "http" 2 | assert = require "assert" 3 | express = require "express" 4 | locale = require "./" 5 | 6 | server = null 7 | defaultLocale = locale.Locale.default 8 | 9 | before (callback) -> 10 | app = do express 11 | 12 | app.use locale ["en-US", "fr", "fr-CA", "en", "ja", "de", "da-DK"] 13 | app.get "/", (req, res) -> 14 | res.set "content-language", req.locale 15 | res.set "defaulted", req.rawLocale.defaulted 16 | res.set "Connection", "close" 17 | res.send(200) 18 | server = app.listen 8001, callback 19 | 20 | describe "Defaults", -> 21 | it "should use the environment language as default.", (callback) -> 22 | http.get port: 8001, (res) -> 23 | assert.equal( 24 | res.headers["content-language"] 25 | defaultLocale 26 | ) 27 | assert.equal(true, !!res.headers["defaulted"]) 28 | callback() 29 | 30 | it "should fallback to the default for unsupported languages.", (callback) -> 31 | http.get port: 8001, headers: "Accept-Language": "es-ES", (res) -> 32 | assert.equal( 33 | res.headers["content-language"] 34 | defaultLocale 35 | ) 36 | assert.equal(true, !!res.headers["defaulted"]) 37 | callback() 38 | 39 | it "should fallback to the instance default for unsupported languages if instance default is defined.", (callback) -> 40 | instanceDefault = 'SomeFakeLanguage-NotReal' 41 | supportedLocales = new locale.Locales ["en-US", "fr", "fr-CA", "en", "ja", "de", "da-DK"], instanceDefault 42 | assert.equal( 43 | ((new locale.Locales "es-ES").best supportedLocales).toString() 44 | instanceDefault 45 | ) 46 | callback() 47 | 48 | describe "Priority", -> 49 | it "should fallback to a more general language if a country specific language isn't available.", (callback) -> 50 | http.get port: 8001, headers: "Accept-Language": "en-GB", (res) -> 51 | assert.equal( 52 | res.headers["content-language"] 53 | "en" 54 | "Unsupported country should fallback to countryless language" 55 | ) 56 | assert.equal(false, !res.headers["defaulted"]) 57 | callback() 58 | 59 | it "should use the highest quality language supported, regardless of order.", (callback) -> 60 | http.get port: 8001, headers: "Accept-Language": "en;q=.8, ja", (res) -> 61 | assert.equal( 62 | res.headers["content-language"] 63 | "ja" 64 | "Highest quality language supported should be used, regardless of order." 65 | ) 66 | assert.equal(false, !res.headers["defaulted"]) 67 | 68 | http.get port: 8001, headers: "Accept-Language": "fr-FR, ja-JA;q=0.5", (res) -> 69 | assert.equal( 70 | res.headers["content-language"] 71 | "fr" 72 | "Highest quality language supported should be used, regardless of order." 73 | ) 74 | assert.equal(false, !res.headers["defaulted"]) 75 | 76 | 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) -> 77 | assert.equal( 78 | res.headers["content-language"] 79 | "en-US" 80 | "Highest quality language supported should be used, regardless of order." 81 | ) 82 | assert.equal(false, !res.headers["defaulted"]) 83 | 84 | callback() 85 | 86 | it "should use a country specific language when an unsupported general language is requested", (callback) -> 87 | http.get port: 8001, headers: "Accept-Language": "da", (res) -> 88 | assert.equal( 89 | res.headers["content-language"] 90 | "da-DK" 91 | ) 92 | callback() 93 | 94 | it "should fallback to a country specific language even when there's a lower quality exact match", (callback) -> 95 | http.get port: 8001, headers: "Accept-Language": "ja;q=.8, da", (res) -> 96 | assert.equal( 97 | res.headers["content-language"] 98 | "da-DK" 99 | ) 100 | assert.equal(false, !res.headers["defaulted"]) 101 | callback() 102 | 103 | it "should match country-specific language codes even when the separator is different", (callback) -> 104 | http.get port: 8001, headers: "Accept-Language": "fr_CA", (res) -> 105 | assert.equal( 106 | res.headers["content-language"] 107 | "fr-CA" 108 | ) 109 | assert.equal(false, !res.headers["defaulted"]) 110 | callback() 111 | 112 | after -> 113 | do server.close 114 | do process.exit 0 115 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --reporter spec 2 | --compilers coffee:coffee-script 3 | --------------------------------------------------------------------------------