From 39b1367778267db7a9a807d6c0bb1030a0982b2b Mon Sep 17 00:00:00 2001 From: Peter Johnson Date: Mon, 20 Mar 2017 18:22:34 +0100 Subject: [PATCH] language: accept param as well as request headers --- middleware/requestLanguage.js | 9 +- .../language_querystring_invalid.coffee | 37 ++++++ .../language_querystring_valid.coffee | 37 ++++++ .../place/language_querystring_invalid.coffee | 37 ++++++ .../place/language_querystring_valid.coffee | 37 ++++++ .../language_querystring_invalid.coffee | 37 ++++++ .../reverse/language_querystring_valid.coffee | 37 ++++++ .../language_querystring_invalid.coffee | 37 ++++++ .../search/language_querystring_valid.coffee | 37 ++++++ test/unit/middleware/requestLanguage.js | 107 +++++++++++++++++- 10 files changed, 408 insertions(+), 4 deletions(-) create mode 100644 test/ciao/autocomplete/language_querystring_invalid.coffee create mode 100644 test/ciao/autocomplete/language_querystring_valid.coffee create mode 100644 test/ciao/place/language_querystring_invalid.coffee create mode 100644 test/ciao/place/language_querystring_valid.coffee create mode 100644 test/ciao/reverse/language_querystring_invalid.coffee create mode 100644 test/ciao/reverse/language_querystring_valid.coffee create mode 100644 test/ciao/search/language_querystring_invalid.coffee create mode 100644 test/ciao/search/language_querystring_valid.coffee diff --git a/middleware/requestLanguage.js b/middleware/requestLanguage.js index ebe21b9a..c3083d46 100644 --- a/middleware/requestLanguage.js +++ b/middleware/requestLanguage.js @@ -1,10 +1,10 @@ /** this middleware is responsible for negotiating HTTP locales for incoming - browser requests by reading 'Accept-Language' request headers. + browser requests by reading the querystring param 'lang' or 'Accept-Language' request headers. the preferred language will then be available on the $req object: - eg. for 'Accept-Language: fr': + eg. for '?lang=fr' or 'Accept-Language: fr': ``` console.log( req.language ); @@ -53,8 +53,11 @@ const allLocales = new locale.Locales( Object.keys( language ) ); // return the middleware module.exports = function middleware( req, res, next ){ + // input language, either from query param or header + var input = ( req.query && req.query.lang ) || ( req.headers && req.headers['accept-language'] ); + // parse request & choose best locale - var locales = new locale.Locales( req.headers['accept-language'] || '' ); + var locales = new locale.Locales( input || '' ); var best = locales.best( allLocales ); // set $req.language property diff --git a/test/ciao/autocomplete/language_querystring_invalid.coffee b/test/ciao/autocomplete/language_querystring_invalid.coffee new file mode 100644 index 00000000..5fd866fc --- /dev/null +++ b/test/ciao/autocomplete/language_querystring_invalid.coffee @@ -0,0 +1,37 @@ + +#> language +path: '/v1/autocomplete?lang=example&text=example' + +#? 200 ok +response.statusCode.should.be.equal 200 +response.should.have.header 'charset', 'utf8' +response.should.have.header 'content-type', 'application/json; charset=utf-8' + +#? valid geocoding block +should.exist json.geocoding +should.exist json.geocoding.version +should.exist json.geocoding.attribution +should.exist json.geocoding.query +should.exist json.geocoding.engine +should.exist json.geocoding.engine.name +should.exist json.geocoding.engine.author +should.exist json.geocoding.engine.version +should.exist json.geocoding.timestamp + +#? valid geojson +json.type.should.be.equal 'FeatureCollection' +json.features.should.be.instanceof Array + +#? expected errors +should.not.exist json.geocoding.errors + +#? expected warnings +should.not.exist json.geocoding.warnings + +#? language +json.geocoding.query['lang'].should.eql { + name: 'English', + iso6391: 'en', + iso6393: 'eng', + defaulted: true +} diff --git a/test/ciao/autocomplete/language_querystring_valid.coffee b/test/ciao/autocomplete/language_querystring_valid.coffee new file mode 100644 index 00000000..4b267ff2 --- /dev/null +++ b/test/ciao/autocomplete/language_querystring_valid.coffee @@ -0,0 +1,37 @@ + +#> language +path: '/v1/autocomplete?lang=fr&text=example' + +#? 200 ok +response.statusCode.should.be.equal 200 +response.should.have.header 'charset', 'utf8' +response.should.have.header 'content-type', 'application/json; charset=utf-8' + +#? valid geocoding block +should.exist json.geocoding +should.exist json.geocoding.version +should.exist json.geocoding.attribution +should.exist json.geocoding.query +should.exist json.geocoding.engine +should.exist json.geocoding.engine.name +should.exist json.geocoding.engine.author +should.exist json.geocoding.engine.version +should.exist json.geocoding.timestamp + +#? valid geojson +json.type.should.be.equal 'FeatureCollection' +json.features.should.be.instanceof Array + +#? expected errors +should.not.exist json.geocoding.errors + +#? expected warnings +should.not.exist json.geocoding.warnings + +#? language +json.geocoding.query['lang'].should.eql { + defaulted: false, + iso6391: 'fr', + iso6393: 'fra', + name: 'French' +} diff --git a/test/ciao/place/language_querystring_invalid.coffee b/test/ciao/place/language_querystring_invalid.coffee new file mode 100644 index 00000000..89a07045 --- /dev/null +++ b/test/ciao/place/language_querystring_invalid.coffee @@ -0,0 +1,37 @@ + +#> language +path: '/v1/place?lang=example&ids=geonames:venue:1' + +#? 200 ok +response.statusCode.should.be.equal 200 +response.should.have.header 'charset', 'utf8' +response.should.have.header 'content-type', 'application/json; charset=utf-8' + +#? valid geocoding block +should.exist json.geocoding +should.exist json.geocoding.version +should.exist json.geocoding.attribution +should.exist json.geocoding.query +should.exist json.geocoding.engine +should.exist json.geocoding.engine.name +should.exist json.geocoding.engine.author +should.exist json.geocoding.engine.version +should.exist json.geocoding.timestamp + +#? valid geojson +json.type.should.be.equal 'FeatureCollection' +json.features.should.be.instanceof Array + +#? expected errors +should.not.exist json.geocoding.errors + +#? expected warnings +should.not.exist json.geocoding.warnings + +#? language +json.geocoding.query['lang'].should.eql { + name: 'English', + iso6391: 'en', + iso6393: 'eng', + defaulted: true +} diff --git a/test/ciao/place/language_querystring_valid.coffee b/test/ciao/place/language_querystring_valid.coffee new file mode 100644 index 00000000..3192e754 --- /dev/null +++ b/test/ciao/place/language_querystring_valid.coffee @@ -0,0 +1,37 @@ + +#> language +path: '/v1/place?lang=fr&ids=geonames:venue:1' + +#? 200 ok +response.statusCode.should.be.equal 200 +response.should.have.header 'charset', 'utf8' +response.should.have.header 'content-type', 'application/json; charset=utf-8' + +#? valid geocoding block +should.exist json.geocoding +should.exist json.geocoding.version +should.exist json.geocoding.attribution +should.exist json.geocoding.query +should.exist json.geocoding.engine +should.exist json.geocoding.engine.name +should.exist json.geocoding.engine.author +should.exist json.geocoding.engine.version +should.exist json.geocoding.timestamp + +#? valid geojson +json.type.should.be.equal 'FeatureCollection' +json.features.should.be.instanceof Array + +#? expected errors +should.not.exist json.geocoding.errors + +#? expected warnings +should.not.exist json.geocoding.warnings + +#? language +json.geocoding.query['lang'].should.eql { + defaulted: false, + iso6391: 'fr', + iso6393: 'fra', + name: 'French' +} diff --git a/test/ciao/reverse/language_querystring_invalid.coffee b/test/ciao/reverse/language_querystring_invalid.coffee new file mode 100644 index 00000000..6c8130b2 --- /dev/null +++ b/test/ciao/reverse/language_querystring_invalid.coffee @@ -0,0 +1,37 @@ + +#> language +path: '/v1/reverse?lang=example&point.lat=1&point.lon=2' + +#? 200 ok +response.statusCode.should.be.equal 200 +response.should.have.header 'charset', 'utf8' +response.should.have.header 'content-type', 'application/json; charset=utf-8' + +#? valid geocoding block +should.exist json.geocoding +should.exist json.geocoding.version +should.exist json.geocoding.attribution +should.exist json.geocoding.query +should.exist json.geocoding.engine +should.exist json.geocoding.engine.name +should.exist json.geocoding.engine.author +should.exist json.geocoding.engine.version +should.exist json.geocoding.timestamp + +#? valid geojson +json.type.should.be.equal 'FeatureCollection' +json.features.should.be.instanceof Array + +#? expected errors +should.not.exist json.geocoding.errors + +#? expected warnings +should.not.exist json.geocoding.warnings + +#? language +json.geocoding.query['lang'].should.eql { + name: 'English', + iso6391: 'en', + iso6393: 'eng', + defaulted: true +} diff --git a/test/ciao/reverse/language_querystring_valid.coffee b/test/ciao/reverse/language_querystring_valid.coffee new file mode 100644 index 00000000..45f22e1b --- /dev/null +++ b/test/ciao/reverse/language_querystring_valid.coffee @@ -0,0 +1,37 @@ + +#> language +path: '/v1/reverse?lang=fr&point.lat=1&point.lon=2' + +#? 200 ok +response.statusCode.should.be.equal 200 +response.should.have.header 'charset', 'utf8' +response.should.have.header 'content-type', 'application/json; charset=utf-8' + +#? valid geocoding block +should.exist json.geocoding +should.exist json.geocoding.version +should.exist json.geocoding.attribution +should.exist json.geocoding.query +should.exist json.geocoding.engine +should.exist json.geocoding.engine.name +should.exist json.geocoding.engine.author +should.exist json.geocoding.engine.version +should.exist json.geocoding.timestamp + +#? valid geojson +json.type.should.be.equal 'FeatureCollection' +json.features.should.be.instanceof Array + +#? expected errors +should.not.exist json.geocoding.errors + +#? expected warnings +should.not.exist json.geocoding.warnings + +#? language +json.geocoding.query['lang'].should.eql { + defaulted: false, + iso6391: 'fr', + iso6393: 'fra', + name: 'French' +} diff --git a/test/ciao/search/language_querystring_invalid.coffee b/test/ciao/search/language_querystring_invalid.coffee new file mode 100644 index 00000000..c1798bf5 --- /dev/null +++ b/test/ciao/search/language_querystring_invalid.coffee @@ -0,0 +1,37 @@ + +#> language +path: '/v1/search?lang=example&text=example' + +#? 200 ok +response.statusCode.should.be.equal 200 +response.should.have.header 'charset', 'utf8' +response.should.have.header 'content-type', 'application/json; charset=utf-8' + +#? valid geocoding block +should.exist json.geocoding +should.exist json.geocoding.version +should.exist json.geocoding.attribution +should.exist json.geocoding.query +should.exist json.geocoding.engine +should.exist json.geocoding.engine.name +should.exist json.geocoding.engine.author +should.exist json.geocoding.engine.version +should.exist json.geocoding.timestamp + +#? valid geojson +json.type.should.be.equal 'FeatureCollection' +json.features.should.be.instanceof Array + +#? expected errors +should.not.exist json.geocoding.errors + +#? expected warnings +should.not.exist json.geocoding.warnings + +#? language +json.geocoding.query['lang'].should.eql { + name: 'English', + iso6391: 'en', + iso6393: 'eng', + defaulted: true +} diff --git a/test/ciao/search/language_querystring_valid.coffee b/test/ciao/search/language_querystring_valid.coffee new file mode 100644 index 00000000..8b389efd --- /dev/null +++ b/test/ciao/search/language_querystring_valid.coffee @@ -0,0 +1,37 @@ + +#> language +path: '/v1/search?lang=fr&text=example' + +#? 200 ok +response.statusCode.should.be.equal 200 +response.should.have.header 'charset', 'utf8' +response.should.have.header 'content-type', 'application/json; charset=utf-8' + +#? valid geocoding block +should.exist json.geocoding +should.exist json.geocoding.version +should.exist json.geocoding.attribution +should.exist json.geocoding.query +should.exist json.geocoding.engine +should.exist json.geocoding.engine.name +should.exist json.geocoding.engine.author +should.exist json.geocoding.engine.version +should.exist json.geocoding.timestamp + +#? valid geojson +json.type.should.be.equal 'FeatureCollection' +json.features.should.be.instanceof Array + +#? expected errors +should.not.exist json.geocoding.errors + +#? expected warnings +should.not.exist json.geocoding.warnings + +#? language +json.geocoding.query['lang'].should.eql { + defaulted: false, + iso6391: 'fr', + iso6393: 'fra', + name: 'French' +} diff --git a/test/unit/middleware/requestLanguage.js b/test/unit/middleware/requestLanguage.js index 3528ad6c..a8d9e9a7 100644 --- a/test/unit/middleware/requestLanguage.js +++ b/test/unit/middleware/requestLanguage.js @@ -16,7 +16,7 @@ var DEFAULTS = { module.exports.tests.defaults = function(test, common) { test('default language', function(t) { - var req = { headers: {} }; + var req = {}; middleware(req, {}, function () { t.deepEqual( req.language, DEFAULTS, '$req.language set' ); @@ -40,6 +40,25 @@ module.exports.tests.invalid = function(test, common) { 'accept-language': 'invalid language' }}; + middleware(req, {}, function () { + t.deepEqual( req.language, DEFAULTS, '$req.language set' ); + + t.deepEqual( req.clean.lang, { + defaulted: req.language.defaulted, + iso6391: req.language.iso6391, + iso6393: req.language.iso6393, + name: req.language.name + }, '$req.clean.lang set' ); + + t.end(); + }); + }); + test('query: invalid language', function(t) { + + var req = { query: { + lang: 'invalid language' + }}; + middleware(req, {}, function () { t.deepEqual( req.language, DEFAULTS, '$req.language set' ); @@ -86,6 +105,36 @@ module.exports.tests.valid = function(test, common) { t.end(); }); }); + test('query: valid language - french', function(t) { + + var req = { query: { + lang: 'fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5' + }}; + + var expected = { + defaulted: false, + iso6391: 'fr', + iso6392B: 'fre', + iso6392T: 'fra', + iso6393: 'fra', + name: 'French', + scope: 'individual', + type: 'living' + }; + + middleware(req, {}, function () { + t.deepEqual( req.language, expected, '$req.language set' ); + + t.deepEqual( req.clean.lang, { + defaulted: req.language.defaulted, + iso6391: req.language.iso6391, + iso6393: req.language.iso6393, + name: req.language.name + }, '$req.clean.lang set' ); + + t.end(); + }); + }); test('headers: valid language - english', function(t) { @@ -109,6 +158,62 @@ module.exports.tests.valid = function(test, common) { t.end(); }); }); + test('query: valid language - english', function(t) { + + var req = { query: { + lang: 'en' + }}; + + var expected = { + defaulted: false, + iso6391: 'en', + iso6392B: 'eng', + iso6392T: 'eng', + iso6393: 'eng', + name: 'English', + scope: 'individual', + type: 'living' + }; + + middleware(req, {}, function () { + t.deepEqual( req.language, expected, '$req.language set' ); + t.end(); + }); + }); +}; + +module.exports.tests.precedence = function(test, common) { + test('precedence: query has precedence over headers', function(t) { + + var req = { + headers: { 'accept-language': 'fr' }, + query: { 'lang': 'es' } + }; + + var expected = { + defaulted: false, + iso6391: 'es', + iso6392B: 'spa', + iso6392T: 'spa', + iso6393: 'spa', + name: 'Spanish', + scope: 'individual', + type: 'living' + }; + + middleware(req, {}, function () { + t.deepEqual( req.language, expected, '$req.language set' ); + + t.deepEqual( req.clean.lang, { + defaulted: req.language.defaulted, + iso6391: req.language.iso6391, + iso6393: req.language.iso6393, + name: req.language.name + }, '$req.clean.lang set' ); + + t.end(); + }); + }); }; module.exports.all = function (tape, common) {