From 3d5dd4dfc17582f47953365b0633aeccd2222499 Mon Sep 17 00:00:00 2001 From: Rickard Falk Date: Mon, 29 Apr 2024 13:34:03 +0200 Subject: [PATCH 1/9] fix: use chosenLocale for cookie language --- lib/language.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/language.js b/lib/language.js index 6e34f9e..e4279d7 100644 --- a/lib/language.js +++ b/lib/language.js @@ -34,8 +34,11 @@ function _init(req, res, newLang) { 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.cookie(cookieName, chosenLocale.language || defaultLanguage, { + secure: true, + sameSite: 'strict', + httpOnly: true, + }) } res.locals.locale = chosenLocale From e08ce20296dd963d580537e162358d62de802b58 Mon Sep 17 00:00:00 2001 From: mthege Date: Mon, 29 Apr 2024 14:55:38 +0200 Subject: [PATCH 2/9] feat: tests for language.js --- lib/language.test.js | 58 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 lib/language.test.js diff --git a/lib/language.test.js b/lib/language.test.js new file mode 100644 index 0000000..5d86691 --- /dev/null +++ b/lib/language.test.js @@ -0,0 +1,58 @@ +// const locale = require('locale') +const { languageHandler } = require('./language') + +describe('languageHandler', () => { + const cookieOptions = { httpOnly: true, sameSite: 'strict', secure: true } + const cookieName = 'language' + + it('should save requested language in cookie', () => { + 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) + }) + + it('should not save cookie if it is already set', () => { + 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 in cookie', () => { + 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) + }) + + 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) + }) + + it('should use accept-language if present', () => { + const requestedLanguage = 'en' + const req = { query: {}, cookies: {}, headers: { 'accept-language': requestedLanguage } } + const res = { locals: {}, cookie: jest.fn() } + const next = jest.fn() + + languageHandler(req, res, next) + expect(res.cookie).toHaveBeenCalledWith(cookieName, requestedLanguage, cookieOptions) + }) +}) From 8f96edc465d32e1c3c195080f892bd86b446ca2b Mon Sep 17 00:00:00 2001 From: Ellen Date: Mon, 29 Apr 2024 15:41:57 +0200 Subject: [PATCH 3/9] Add some more tests --- lib/language.test.js | 45 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/lib/language.test.js b/lib/language.test.js index 5d86691..72a603e 100644 --- a/lib/language.test.js +++ b/lib/language.test.js @@ -55,4 +55,49 @@ describe('languageHandler', () => { languageHandler(req, res, next) expect(res.cookie).toHaveBeenCalledWith(cookieName, requestedLanguage, cookieOptions) }) + + it('should use requestedLanguage even if language cookie is present', () => { + 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) + }) + + it('should use language cookie if requestedLanguage is invalid', () => { + const requestedLanguage = 'fr' + const clientLanguage = 'en' + const req = { + query: { l: requestedLanguage }, + cookies: {}, + headers: { 'accept-language': clientLanguage }, + } + const res = { locals: {}, cookie: jest.fn() } + const next = jest.fn() + + languageHandler(req, res, next) + expect(res.cookie).toHaveBeenCalledWith(cookieName, clientLanguage, cookieOptions) + }) + + it('should use client language if cookie language is invalid', () => { + const presetLanguage = 'fr' + const clientLanguage = 'en' + const req = { + query: {}, + cookies: { [cookieName]: presetLanguage }, + headers: { 'accept-language': clientLanguage }, + } + const res = { locals: {}, cookie: jest.fn() } + const next = jest.fn() + + languageHandler(req, res, next) + expect(res.cookie).toHaveBeenCalledWith(cookieName, clientLanguage, cookieOptions) + }) }) From d46d132c2c31410e30c862a4d60eb1bab93bf563 Mon Sep 17 00:00:00 2001 From: Ellen Date: Mon, 29 Apr 2024 15:42:22 +0200 Subject: [PATCH 4/9] Refactor init method for language --- lib/language.js | 41 +++++++++++++++++------------------------ 1 file changed, 17 insertions(+), 24 deletions(-) diff --git a/lib/language.js b/lib/language.js index e4279d7..0368200 100644 --- a/lib/language.js +++ b/lib/language.js @@ -7,45 +7,38 @@ 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 + } +} + /** * Initialize locale and language for the current user session (respects cookie: language). */ function _init(req, res, newLang) { - let lang = newLang || req.cookies[cookieName] - - // Only allow lang to be one of the valid languages - if (lang && !validLanguages.includes(lang)) lang = defaultLanguage + let lang = + validLanguage(newLang) || + validLanguage(req.cookies[cookieName]) || + validLanguage(req.headers['accept-language']) || + defaultLanguage - let chosenLocale + const locales = new locale.Locales(lang, defaultLanguage) + const chosenLocale = locales.best(supportedLocales) - 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 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]) { - res.cookie(cookieName, chosenLocale.language || defaultLanguage, { - secure: true, - sameSite: 'strict', - httpOnly: true, - }) + // Don't reset the cookie with the same language + if (req.cookies[cookieName] !== lang) { + res.cookie(cookieName, chosenLocale.language, cookieOptions) } res.locals.locale = chosenLocale // Backwards compatibility only in case someone accessed the prop directly: res.locals.language = chosenLocale.language - - return chosenLocale } /** From 9078fe59f4146da070b05eadd724a0f5e5d7bfb7 Mon Sep 17 00:00:00 2001 From: Rickard Falk Date: Tue, 30 Apr 2024 08:43:53 +0200 Subject: [PATCH 5/9] fix: accept-language header contains preferred languages, not a single language --- lib/language.js | 23 ++++++++++++----------- lib/language.test.js | 28 +++++++++++++++++----------- 2 files changed, 29 insertions(+), 22 deletions(-) diff --git a/lib/language.js b/lib/language.js index 0368200..1a1c01d 100644 --- a/lib/language.js +++ b/lib/language.js @@ -18,27 +18,29 @@ const validLanguage = 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 = + const lang = validLanguage(newLang) || validLanguage(req.cookies[cookieName]) || - validLanguage(req.headers['accept-language']) || - defaultLanguage - - const locales = new locale.Locales(lang, defaultLanguage) - const chosenLocale = locales.best(supportedLocales) + useAcceptLanguageOrDefault(req.headers['accept-language']) - // Don't reset the cookie with the same language if (req.cookies[cookieName] !== lang) { - res.cookie(cookieName, chosenLocale.language, cookieOptions) + res.cookie(cookieName, lang, cookieOptions) } - res.locals.locale = chosenLocale + const locales = new locale.Locales(lang) + res.locals.locale = locales // Backwards compatibility only in case someone accessed the prop directly: - res.locals.language = chosenLocale.language + res.locals.language = locales.language } /** @@ -73,7 +75,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 index 72a603e..ff1bc27 100644 --- a/lib/language.test.js +++ b/lib/language.test.js @@ -3,6 +3,9 @@ const { languageHandler } = require('./language') describe('languageHandler', () => { const cookieOptions = { httpOnly: true, sameSite: 'strict', secure: true } + // Example from Chrome on macOS with Preferred languages set to 1. English (United Kingdom), + // 2. English (United States), 3. English, 4. Swedish, 5. Norwegian Bokmål, 6. German, and 7. Latin + const preferredLanguages = 'en-GB,en-US;q=0.9,en;q=0.8,sv;q=0.7,nb;q=0.6,de;q=0.5,la;q=0.4' const cookieName = 'language' it('should save requested language in cookie', () => { @@ -47,13 +50,14 @@ describe('languageHandler', () => { }) it('should use accept-language if present', () => { - const requestedLanguage = 'en' - const req = { query: {}, cookies: {}, headers: { 'accept-language': requestedLanguage } } + const acceptLanguage = preferredLanguages + 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, requestedLanguage, cookieOptions) + expect(res.cookie).toHaveBeenCalledWith(cookieName, expectedLanguage, cookieOptions) }) it('should use requestedLanguage even if language cookie is present', () => { @@ -73,31 +77,33 @@ describe('languageHandler', () => { it('should use language cookie if requestedLanguage is invalid', () => { const requestedLanguage = 'fr' - const clientLanguage = 'en' + const acceptLanguage = preferredLanguages + const expectedLanguage = 'en' const req = { query: { l: requestedLanguage }, cookies: {}, - headers: { 'accept-language': clientLanguage }, + headers: { 'accept-language': acceptLanguage }, } const res = { locals: {}, cookie: jest.fn() } const next = jest.fn() languageHandler(req, res, next) - expect(res.cookie).toHaveBeenCalledWith(cookieName, clientLanguage, cookieOptions) + expect(res.cookie).toHaveBeenCalledWith(cookieName, expectedLanguage, cookieOptions) }) it('should use client language if cookie language is invalid', () => { - const presetLanguage = 'fr' - const clientLanguage = 'en' + const cookieLanguage = 'fr' + const acceptLanguage = preferredLanguages + const expectedLanguage = 'en' const req = { query: {}, - cookies: { [cookieName]: presetLanguage }, - headers: { 'accept-language': clientLanguage }, + 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, clientLanguage, cookieOptions) + expect(res.cookie).toHaveBeenCalledWith(cookieName, expectedLanguage, cookieOptions) }) }) From 6be3590535801c4c414e3257a6c14da1978097d5 Mon Sep 17 00:00:00 2001 From: Rickard Falk Date: Tue, 30 Apr 2024 10:24:51 +0200 Subject: [PATCH 6/9] add tests for res.locals.locale.language --- lib/language.test.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/language.test.js b/lib/language.test.js index ff1bc27..b2170e3 100644 --- a/lib/language.test.js +++ b/lib/language.test.js @@ -16,6 +16,7 @@ describe('languageHandler', () => { languageHandler(req, res, next) expect(res.cookie).toHaveBeenCalledWith(cookieName, requestedLanguage, cookieOptions) + expect(res.locals.locale.language).toEqual(requestedLanguage) }) it('should not save cookie if it is already set', () => { @@ -26,6 +27,7 @@ describe('languageHandler', () => { languageHandler(req, res, next) expect(res.cookie).not.toHaveBeenCalled() + expect(res.locals.locale.language).toEqual(presetLanguage) }) it('should not save invalid language in cookie', () => { @@ -37,6 +39,7 @@ describe('languageHandler', () => { 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', () => { @@ -47,6 +50,7 @@ describe('languageHandler', () => { languageHandler(req, res, next) expect(res.cookie).toHaveBeenCalledWith(cookieName, defaultLanguage, cookieOptions) + expect(res.locals.locale.language).toEqual(defaultLanguage) }) it('should use accept-language if present', () => { @@ -58,6 +62,7 @@ describe('languageHandler', () => { languageHandler(req, res, next) expect(res.cookie).toHaveBeenCalledWith(cookieName, expectedLanguage, cookieOptions) + expect(res.locals.locale.language).toEqual(expectedLanguage) }) it('should use requestedLanguage even if language cookie is present', () => { @@ -73,6 +78,7 @@ describe('languageHandler', () => { languageHandler(req, res, next) expect(res.cookie).toHaveBeenCalledWith(cookieName, requestedLanguage, cookieOptions) + expect(res.locals.locale.language).toEqual(requestedLanguage) }) it('should use language cookie if requestedLanguage is invalid', () => { @@ -89,6 +95,7 @@ describe('languageHandler', () => { languageHandler(req, res, next) expect(res.cookie).toHaveBeenCalledWith(cookieName, expectedLanguage, cookieOptions) + expect(res.locals.locale.language).toEqual(expectedLanguage) }) it('should use client language if cookie language is invalid', () => { @@ -105,5 +112,6 @@ describe('languageHandler', () => { languageHandler(req, res, next) expect(res.cookie).toHaveBeenCalledWith(cookieName, expectedLanguage, cookieOptions) + expect(res.locals.locale.language).toEqual(expectedLanguage) }) }) From da2a120736c8a1f1806d910d6792cd83def4fe15 Mon Sep 17 00:00:00 2001 From: Rickard Falk Date: Thu, 2 May 2024 14:03:10 +0200 Subject: [PATCH 7/9] fix: set res.locals.locale.language --- lib/language.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/language.js b/lib/language.js index 1a1c01d..818653f 100644 --- a/lib/language.js +++ b/lib/language.js @@ -38,9 +38,10 @@ function _init(req, res, newLang) { } const locales = new locale.Locales(lang) - res.locals.locale = locales + const bestLocale = locales.best(supportedLocales) + res.locals.locale = bestLocale // Backwards compatibility only in case someone accessed the prop directly: - res.locals.language = locales.language + res.locals.language = bestLocale.language } /** From cf6172083d528d08ce8bf86d5515551f611c2f39 Mon Sep 17 00:00:00 2001 From: Rickard Falk Date: Thu, 2 May 2024 14:03:39 +0200 Subject: [PATCH 8/9] refactor language tests --- lib/language.test.js | 44 ++++++++++++++++++++------------------------ 1 file changed, 20 insertions(+), 24 deletions(-) diff --git a/lib/language.test.js b/lib/language.test.js index b2170e3..a4b057e 100644 --- a/lib/language.test.js +++ b/lib/language.test.js @@ -3,12 +3,9 @@ const { languageHandler } = require('./language') describe('languageHandler', () => { const cookieOptions = { httpOnly: true, sameSite: 'strict', secure: true } - // Example from Chrome on macOS with Preferred languages set to 1. English (United Kingdom), - // 2. English (United States), 3. English, 4. Swedish, 5. Norwegian Bokmål, 6. German, and 7. Latin - const preferredLanguages = 'en-GB,en-US;q=0.9,en;q=0.8,sv;q=0.7,nb;q=0.6,de;q=0.5,la;q=0.4' const cookieName = 'language' - it('should save requested language in cookie', () => { + it('should save language if requested and valid', () => { const requestedLanguage = 'sv' const req = { query: { l: requestedLanguage }, cookies: {}, headers: {} } const res = { locals: {}, cookie: jest.fn() } @@ -19,7 +16,7 @@ describe('languageHandler', () => { expect(res.locals.locale.language).toEqual(requestedLanguage) }) - it('should not save cookie if it is already set', () => { + 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() } @@ -27,10 +24,9 @@ describe('languageHandler', () => { languageHandler(req, res, next) expect(res.cookie).not.toHaveBeenCalled() - expect(res.locals.locale.language).toEqual(presetLanguage) }) - it('should not save invalid language in cookie', () => { + it('should not save invalid language', () => { const invalidLanguage = 'fr' const defaultLanguage = 'sv' const req = { query: { l: invalidLanguage }, cookies: {}, headers: {} } @@ -53,19 +49,7 @@ describe('languageHandler', () => { expect(res.locals.locale.language).toEqual(defaultLanguage) }) - it('should use accept-language if present', () => { - const acceptLanguage = preferredLanguages - 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 requestedLanguage even if language cookie is present', () => { + it('should update language if requested ', () => { const requestedLanguage = 'en' const presetLanguage = 'sv' const req = { @@ -81,9 +65,21 @@ describe('languageHandler', () => { expect(res.locals.locale.language).toEqual(requestedLanguage) }) - it('should use language cookie if requestedLanguage is invalid', () => { + 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 = preferredLanguages + const acceptLanguage = 'en,sv;q=0.9' const expectedLanguage = 'en' const req = { query: { l: requestedLanguage }, @@ -98,9 +94,9 @@ describe('languageHandler', () => { expect(res.locals.locale.language).toEqual(expectedLanguage) }) - it('should use client language if cookie language is invalid', () => { + it('should use accept-language if cookie language is invalid', () => { const cookieLanguage = 'fr' - const acceptLanguage = preferredLanguages + const acceptLanguage = 'en,sv;q=0.9' const expectedLanguage = 'en' const req = { query: {}, From a7f04a352840ec43f944991dd2dbd98074463566 Mon Sep 17 00:00:00 2001 From: Pipeline Date: Thu, 2 May 2024 12:09:48 +0000 Subject: [PATCH 9/9] 9.3.1-0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) 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",