diff --git a/lib/language.js b/lib/language.js index 6e34f9e..818653f 100644 --- a/lib/language.js +++ b/lib/language.js @@ -7,42 +7,41 @@ const locale = require('locale') const cookieName = 'language' +const cookieOptions = { secure: true, sameSite: 'strict', httpOnly: true } const validLanguages = ['sv', 'en'] const defaultLanguage = validLanguages[0] const supportedLocales = new locale.Locales(validLanguages, defaultLanguage) +const validLanguage = lang => { + if (validLanguages.includes(lang)) { + return lang + } +} + +const useAcceptLanguageOrDefault = preferredLanguages => { + const preferredLocales = new locale.Locales(preferredLanguages, defaultLanguage) + const bestLocale = preferredLocales.best(supportedLocales) + return bestLocale.language +} + /** * Initialize locale and language for the current user session (respects cookie: language). */ function _init(req, res, newLang) { - let lang = newLang || req.cookies[cookieName] + const lang = + validLanguage(newLang) || + validLanguage(req.cookies[cookieName]) || + useAcceptLanguageOrDefault(req.headers['accept-language']) - // Only allow lang to be one of the valid languages - if (lang && !validLanguages.includes(lang)) lang = defaultLanguage - - let chosenLocale - - if (lang) { - const locales = new locale.Locales(lang, defaultLanguage) - chosenLocale = locales.best(supportedLocales) - } else { - const locales = new locale.Locales(req.headers['accept-language'], defaultLanguage) - chosenLocale = locales.best(supportedLocales) + if (req.cookies[cookieName] !== lang) { + res.cookie(cookieName, lang, cookieOptions) } - // If we got an explicit language we set the language cookie - if (newLang) { - res.cookie(cookieName, chosenLocale.language, { secure: true, sameSite: 'strict', httpOnly: true }) - } else if (!req.cookies[cookieName]) { - // Make sure language cookie is set so subsequent requests are guaranteed to use the same language - res.cookie(cookieName, lang || 'sv', { secure: true, sameSite: 'strict', httpOnly: true }) - } - - res.locals.locale = chosenLocale + const locales = new locale.Locales(lang) + const bestLocale = locales.best(supportedLocales) + res.locals.locale = bestLocale // Backwards compatibility only in case someone accessed the prop directly: - res.locals.language = chosenLocale.language - - return chosenLocale + res.locals.language = bestLocale.language } /** @@ -77,7 +76,6 @@ function _cookieLanguage(req) { module.exports = { getLanguage: _getLanguage, - init: _init, validLanguages, defaultLanguage, languageHandler: _languageHandler, diff --git a/lib/language.test.js b/lib/language.test.js new file mode 100644 index 0000000..a4b057e --- /dev/null +++ b/lib/language.test.js @@ -0,0 +1,113 @@ +// const locale = require('locale') +const { languageHandler } = require('./language') + +describe('languageHandler', () => { + const cookieOptions = { httpOnly: true, sameSite: 'strict', secure: true } + const cookieName = 'language' + + it('should save language if requested and valid', () => { + const requestedLanguage = 'sv' + const req = { query: { l: requestedLanguage }, cookies: {}, headers: {} } + const res = { locals: {}, cookie: jest.fn() } + const next = jest.fn() + + languageHandler(req, res, next) + expect(res.cookie).toHaveBeenCalledWith(cookieName, requestedLanguage, cookieOptions) + expect(res.locals.locale.language).toEqual(requestedLanguage) + }) + + it('should not save language in cookie if language has not changed', () => { + const presetLanguage = 'sv' + const req = { cookies: { [cookieName]: presetLanguage }, query: {}, headers: {} } + const res = { locals: {}, cookie: jest.fn() } + const next = jest.fn() + + languageHandler(req, res, next) + expect(res.cookie).not.toHaveBeenCalled() + }) + + it('should not save invalid language', () => { + const invalidLanguage = 'fr' + const defaultLanguage = 'sv' + const req = { query: { l: invalidLanguage }, cookies: {}, headers: {} } + const res = { locals: {}, cookie: jest.fn() } + const next = jest.fn() + + languageHandler(req, res, next) + expect(res.cookie).toHaveBeenCalledWith(cookieName, defaultLanguage, cookieOptions) + expect(res.locals.locale.language).toEqual(defaultLanguage) + }) + + it('should use default language as fallback', () => { + const defaultLanguage = 'sv' + const req = { query: {}, cookies: {}, headers: {} } + const res = { locals: {}, cookie: jest.fn() } + const next = jest.fn() + + languageHandler(req, res, next) + expect(res.cookie).toHaveBeenCalledWith(cookieName, defaultLanguage, cookieOptions) + expect(res.locals.locale.language).toEqual(defaultLanguage) + }) + + it('should update language if requested ', () => { + const requestedLanguage = 'en' + const presetLanguage = 'sv' + const req = { + query: { l: requestedLanguage }, + cookies: { [cookieName]: presetLanguage }, + headers: {}, + } + const res = { locals: {}, cookie: jest.fn() } + const next = jest.fn() + + languageHandler(req, res, next) + expect(res.cookie).toHaveBeenCalledWith(cookieName, requestedLanguage, cookieOptions) + expect(res.locals.locale.language).toEqual(requestedLanguage) + }) + + it('should use accept-language if requested language and cookie are unset', () => { + const acceptLanguage = 'en,sv;q=0.9' + const expectedLanguage = 'en' + const req = { query: {}, cookies: {}, headers: { 'accept-language': acceptLanguage } } + const res = { locals: {}, cookie: jest.fn() } + const next = jest.fn() + + languageHandler(req, res, next) + expect(res.cookie).toHaveBeenCalledWith(cookieName, expectedLanguage, cookieOptions) + expect(res.locals.locale.language).toEqual(expectedLanguage) + }) + + it('should use accept-language if requested language is invalid and cookie is unset', () => { + const requestedLanguage = 'fr' + const acceptLanguage = 'en,sv;q=0.9' + const expectedLanguage = 'en' + const req = { + query: { l: requestedLanguage }, + cookies: {}, + headers: { 'accept-language': acceptLanguage }, + } + const res = { locals: {}, cookie: jest.fn() } + const next = jest.fn() + + languageHandler(req, res, next) + expect(res.cookie).toHaveBeenCalledWith(cookieName, expectedLanguage, cookieOptions) + expect(res.locals.locale.language).toEqual(expectedLanguage) + }) + + it('should use accept-language if cookie language is invalid', () => { + const cookieLanguage = 'fr' + const acceptLanguage = 'en,sv;q=0.9' + const expectedLanguage = 'en' + const req = { + query: {}, + cookies: { [cookieName]: cookieLanguage }, + headers: { 'accept-language': acceptLanguage }, + } + const res = { locals: {}, cookie: jest.fn() } + const next = jest.fn() + + languageHandler(req, res, next) + expect(res.cookie).toHaveBeenCalledWith(cookieName, expectedLanguage, cookieOptions) + expect(res.locals.locale.language).toEqual(expectedLanguage) + }) +}) diff --git a/package-lock.json b/package-lock.json index ee72679..28e0e2d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@kth/kth-node-web-common", - "version": "9.3.0", + "version": "9.3.1-0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@kth/kth-node-web-common", - "version": "9.3.0", + "version": "9.3.1-0", "license": "MIT", "dependencies": { "@kth/cortina-block": "^5.1.1", diff --git a/package.json b/package.json index f0225f4..52f1151 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@kth/kth-node-web-common", - "version": "9.3.0", + "version": "9.3.1-0", "description": "Common components for node-web projects", "scripts": { "test": "jest",