diff --git a/middleware/requestLanguage.js b/middleware/requestLanguage.js new file mode 100644 index 00000000..ebe21b9a --- /dev/null +++ b/middleware/requestLanguage.js @@ -0,0 +1,74 @@ + +/** + this middleware is responsible for negotiating HTTP locales for incoming + browser requests by reading 'Accept-Language' request headers. + + the preferred language will then be available on the $req object: + eg. for 'Accept-Language: fr': + ``` + console.log( req.language ); + + { + name: 'French', + type: 'living', + scope: 'individual', + iso6393: 'fra', + iso6392B: 'fre', + iso6392T: 'fra', + iso6391: 'fr', + defaulted: false + } + ``` + + for configuration options see: + https://github.com/florrain/locale +**/ + +const locale = require('locale'); + +/** + BCP47 language tags can contain three parts: + 1. A language subtag (en, zh). + 2. A script subtag (Hant, Latn). + 3. A region subtag (US, CN). + + at time of writing we will only be concerned with 1. (the language subtag) with + the intention of being compatible with the language standard of whosonfirst data. + + whosonfirst data is in ISO 639-3 format so we will need to configure the library + to support all ISO 639-1 (2 char) codes and convert them to 639-1 (3-char) codes. + + see: https://github.com/whosonfirst/whosonfirst-names +**/ +const iso6393 = require('iso-639-3'); + +// create a dictionary which maps the ISO 639-1 language subtags to a map +// of it's represenation in several different standards. +const language = {}; +iso6393.filter( i => !!i.iso6391 ).forEach( i => language[ i.iso6391 ] = i ); + +// a pre-processed locale list of language subtags we support (all of them). +const allLocales = new locale.Locales( Object.keys( language ) ); + +// return the middleware +module.exports = function middleware( req, res, next ){ + + // parse request & choose best locale + var locales = new locale.Locales( req.headers['accept-language'] || '' ); + var best = locales.best( allLocales ); + + // set $req.language property + req.language = language[ best.language ] || language.en; + req.language.defaulted = best.defaulted; + + // set $req.clean property in order to print language info in response header + req.clean = req.clean || {}; + req.clean.lang = { + name: req.language.name, + iso6391: req.language.iso6391, + iso6393: req.language.iso6393, + defaulted: req.language.defaulted + }; + + next(); +}; diff --git a/package.json b/package.json index e92c0018..9b0b32ea 100644 --- a/package.json +++ b/package.json @@ -47,8 +47,10 @@ "geojson": "^0.4.0", "geojson-extent": "^0.3.1", "geolib": "^2.0.18", + "iso-639-3": "^1.0.0", "iso3166-1": "^0.3.0", "joi": "^10.1.0", + "locale": "^0.1.0", "lodash": "^4.5.0", "markdown": "0.5.0", "morgan": "1.8.1", diff --git a/routes/v1.js b/routes/v1.js index 60e48172..8ee4173f 100644 --- a/routes/v1.js +++ b/routes/v1.js @@ -18,7 +18,8 @@ var sanitizers = { /** ----------------------- middleware ------------------------ **/ var middleware = { - calcSize: require('../middleware/sizeCalculator') + calcSize: require('../middleware/sizeCalculator'), + requestLanguage: require('../middleware/requestLanguage') }; /** ----------------------- controllers ----------------------- **/ @@ -108,6 +109,7 @@ function addRoutes(app, peliasConfig) { ]), search: createRouter([ sanitizers.search.middleware, + middleware.requestLanguage, middleware.calcSize(), // 3rd parameter is which query module to use, use fallback/geodisambiguation // first, then use original search strategy if first query didn't return anything @@ -131,6 +133,7 @@ function addRoutes(app, peliasConfig) { ]), structured: createRouter([ sanitizers.structured_geocoding.middleware, + middleware.requestLanguage, middleware.calcSize(), controllers.search(peliasConfig.api, esclient, queries.structured_geocoding, not(hasResponseDataOrRequestErrors)), postProc.trimByGranularityStructured(), @@ -150,6 +153,7 @@ function addRoutes(app, peliasConfig) { ]), autocomplete: createRouter([ sanitizers.autocomplete.middleware, + middleware.requestLanguage, controllers.search(peliasConfig.api, esclient, queries.autocomplete, not(hasResponseDataOrRequestErrors)), postProc.distances('focus.point.'), postProc.confidenceScores(peliasConfig.api), @@ -165,6 +169,7 @@ function addRoutes(app, peliasConfig) { ]), reverse: createRouter([ sanitizers.reverse.middleware, + middleware.requestLanguage, middleware.calcSize(), controllers.coarse_reverse(pipService, coarse_reverse_should_execute), controllers.search(peliasConfig.api, esclient, queries.reverse, original_reverse_should_execute), @@ -184,6 +189,7 @@ function addRoutes(app, peliasConfig) { ]), nearby: createRouter([ sanitizers.nearby.middleware, + middleware.requestLanguage, middleware.calcSize(), controllers.search(peliasConfig.api, esclient, queries.reverse, not(hasResponseDataOrRequestErrors)), postProc.distances('point.'), @@ -202,6 +208,7 @@ function addRoutes(app, peliasConfig) { ]), place: createRouter([ sanitizers.place.middleware, + middleware.requestLanguage, controllers.place(peliasConfig.api, esclient), postProc.accuracy(), postProc.localNamingConventions(),