diff --git a/app/component/LangSelect.js b/app/component/LangSelect.js index e4af801785..d058e32dd1 100644 --- a/app/component/LangSelect.js +++ b/app/component/LangSelect.js @@ -1,32 +1,35 @@ import PropTypes from 'prop-types'; import React from 'react'; +import { routerShape } from 'react-router'; import connectToStores from 'fluxible-addons-react/connectToStores'; import moment from 'moment'; import ComponentUsageExample from './ComponentUsageExample'; import { setLanguage } from '../action/userPreferencesActions'; import { isBrowser } from '../util/browser'; +import { replaceQueryParams } from '../util/queryUtils'; -const selectLanguage = (executeAction, lang) => () => { +const selectLanguage = (executeAction, lang, router) => () => { executeAction(setLanguage, lang); if (lang !== 'en') { // eslint-disable-next-line global-require, import/no-dynamic-require require(`moment/locale/${lang}`); } moment.locale(lang); + replaceQueryParams(router, { locale: lang }); }; -const language = (lang, currentLanguage, highlight, executeAction) => ( +const language = (lang, currentLanguage, highlight, executeAction, router) => ( ); -const LangSelect = ({ currentLanguage }, { executeAction, config }) => { +const LangSelect = ({ currentLanguage }, { executeAction, config, router }) => { if (isBrowser) { return (
@@ -36,6 +39,7 @@ const LangSelect = ({ currentLanguage }, { executeAction, config }) => { currentLanguage, lang === currentLanguage, executeAction, + router, ), )}
@@ -64,6 +68,7 @@ LangSelect.propTypes = { LangSelect.contextTypes = { executeAction: PropTypes.func.isRequired, config: PropTypes.object.isRequired, + router: routerShape.isRequired, }; const connected = connectToStores( diff --git a/app/component/SummaryPage.js b/app/component/SummaryPage.js index fee726566c..0dc09ebe3d 100644 --- a/app/component/SummaryPage.js +++ b/app/component/SummaryPage.js @@ -2,6 +2,7 @@ import PropTypes from 'prop-types'; /* eslint-disable react/no-array-index-key */ import React from 'react'; import Relay from 'react-relay/classic'; +import cookie from 'react-cookie'; import moment from 'moment'; import findIndex from 'lodash/findIndex'; import get from 'lodash/get'; @@ -485,7 +486,8 @@ const containerComponent = Relay.createContainer(SummaryPageWithBreakpoint, { preferred: $preferred, unpreferred: $unpreferred, allowedBikeRentalNetworks: $allowedBikeRentalNetworks, - ), + locale: $locale, + ), { ${SummaryPlanContainer.getFragment('plan')} ${ItineraryTab.getFragment('searchTime')} @@ -542,6 +544,7 @@ const containerComponent = Relay.createContainer(SummaryPageWithBreakpoint, { walkSpeed: null, wheelchair: null, allowedBikeRentalNetworks: null, + locale: cookie.load('lang') || 'fi', }, ...defaultRoutingSettings, }, diff --git a/app/component/SummaryPlanContainer.js b/app/component/SummaryPlanContainer.js index 0af4c28167..416621b8f3 100644 --- a/app/component/SummaryPlanContainer.js +++ b/app/component/SummaryPlanContainer.js @@ -352,6 +352,7 @@ class SummaryPlanContainer extends React.Component { $itineraryFiltering: Float!, $modeWeight: InputModeWeight!, $allowedBikeRentalNetworks: [String]!, + $locale: String!, ) { viewer { plan( fromPlace:$fromPlace, @@ -390,6 +391,7 @@ class SummaryPlanContainer extends React.Component { itineraryFiltering: $itineraryFiltering, modeWeight: $modeWeight, allowedBikeRentalNetworks: $allowedBikeRentalNetworks, + locale: $locale, ) {itineraries {startTime,endTime}} } }`; diff --git a/app/util/planParamUtil.js b/app/util/planParamUtil.js index e244d709a5..19eabf3694 100644 --- a/app/util/planParamUtil.js +++ b/app/util/planParamUtil.js @@ -269,6 +269,7 @@ export const preparePlanParams = config => ( walkReluctance, walkSpeed, allowedBikeRentalNetworks, + locale, }, }, }, @@ -401,6 +402,7 @@ export const preparePlanParams = config => ( settings, intermediatePlaceLocations, ), + locale, }, nullOrUndefined, ), diff --git a/server/reittiopasParameterMiddleware.js b/server/reittiopasParameterMiddleware.js index ca026166fa..c63a2eb9a2 100644 --- a/server/reittiopasParameterMiddleware.js +++ b/server/reittiopasParameterMiddleware.js @@ -3,16 +3,25 @@ import isFinite from 'lodash/isFinite'; import oldParamParser from '../app/util/oldParamParser'; import { getConfiguration } from '../app/config'; +function formatQuery(query) { + const params = Object.keys(query) + .map(k => `${k}=${query[k]}`) + .join('&'); + + return `?${params}`; +} + +function formatUrl(req) { + const query = formatQuery(req.query); + return `${req.path}?${query}`; +} + function removeUrlParam(req, param) { if (req.query[param]) { delete req.query[param]; } - const params = Object.keys(req.query) - .map(k => `${k}=${req.query[k]}`) - .join('&'); - const url = `${req.path}?${params}`; - return url; + return formatUrl(req); } export function validateParams(req, config) { @@ -55,14 +64,24 @@ export function validateParams(req, config) { return url; } -export const langParamParser = path => { - if (path.includes('/slangi/')) { - const newPath = path.replace('/slangi/', '/'); - return newPath; - } - const lang = path.substring(0, 4); - const newPath = path.replace(lang, '/'); - return newPath; +const fixLocaleParam = (req, lang) => { + // override locale query param with the selected language + req.query.locale = lang === 'slangi' ? 'fi' : lang; + return formatQuery(req.query); +}; + +export const dropPathLanguageAndFixLocaleParam = (req, lang) => { + return req.path.replace(`/${lang}/`, '/') + fixLocaleParam(req, lang); +}; + +const dropPathLanguageAndRedirect = (req, res, lang) => { + const trimmedUrl = dropPathLanguageAndFixLocaleParam(req, lang); + res.redirect(trimmedUrl); +}; + +const fixLocaleParamAndRedirect = (req, res, lang) => { + const fixedUrl = req.path + fixLocaleParam(req, lang); + res.redirect(fixedUrl); }; export default function reittiopasParameterMiddleware(req, res, next) { @@ -87,15 +106,17 @@ export default function reittiopasParameterMiddleware(req, res, next) { req.query.to_in ) { oldParamParser(req.query, config).then(url => res.redirect(url)); - } else if ( - ['/fi/', '/en/', '/sv/', '/ru/', '/slangi/'].some(param => - req.path.includes(param), - ) - ) { - const redirectPath = langParamParser(req.url); - res.redirect(redirectPath); + } else if (['fi', 'en', 'sv', 'ru', 'slangi'].includes(lang)) { + dropPathLanguageAndRedirect(req, res, lang); } else { - next(); + const { locale } = req.query; + const cookieLang = req.cookies.lang; + + if (cookieLang && locale && cookieLang !== locale) { + fixLocaleParamAndRedirect(req, res, cookieLang); + } else { + next(); + } } } else { next(); diff --git a/test/unit/component/LangSelect.test.js b/test/unit/component/LangSelect.test.js index 7efb474e27..5591388513 100644 --- a/test/unit/component/LangSelect.test.js +++ b/test/unit/component/LangSelect.test.js @@ -46,9 +46,15 @@ describe('LangSelect', () => { 'Europe/Helsinki|EET EEST|-20 -30|01010101010101010101010|1BWp0 1qM0 WM0 1qM0 ' + 'WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00|35e5', }; + + const mockRouter = { + getCurrentLocation: () => '/', + replace: () => true, + }; + configureMoment('sv', configWithMoment); expect(moment.locale()).to.equal('sv'); - selectLanguage(() => true, 'fi')(); + selectLanguage(() => true, 'fi', mockRouter)(); expect(moment.locale()).to.equal('fi'); }); }); diff --git a/test/unit/reittiopasParameterMiddleware.test.js b/test/unit/reittiopasParameterMiddleware.test.js index a27a16c1e3..5d364f86f1 100644 --- a/test/unit/reittiopasParameterMiddleware.test.js +++ b/test/unit/reittiopasParameterMiddleware.test.js @@ -2,27 +2,27 @@ import { expect } from 'chai'; import { describe, it } from 'mocha'; import { validateParams, - langParamParser, + dropPathLanguageAndFixLocaleParam, } from '../../server/reittiopasParameterMiddleware'; import config from '../../app/configurations/config.default'; -const req = { - query: { - minTransferTime: '60', - modes: 'BUS,TRAM,RAIL,SUBWAY,FERRY,WALK,CITYBIKE', - transferPenalty: '0', - walkBoardCost: '540', - walkReluctance: '1.5', - walkSpeed: '1.5', - }, -}; - // validateParams returns an url if it is modified and it removes invalid // parameteres from req.query => two ways to check if it did what it should describe('reittiopasParameterMiddleware', () => { describe('validateParams', () => { + const req = { + query: { + minTransferTime: '60', + modes: 'BUS,TRAM,RAIL,SUBWAY,FERRY,WALK,CITYBIKE', + transferPenalty: '0', + walkBoardCost: '540', + walkReluctance: '1.5', + walkSpeed: '1.5', + }, + }; + it('should not modify valid url', () => { const url = validateParams(req, config); expect(url).to.be.a('undefined'); @@ -44,28 +44,44 @@ describe('reittiopasParameterMiddleware', () => { expect(req.query.modes).to.be.an('undefined'); }); }); - describe('langParamParser', () => { - it('should return empty path', () => { - const path = '/en/'; - const newPath = langParamParser(path); - expect(newPath).to.equal('/'); + + describe('dropLanguageAndSetLocaleParam', () => { + const req = { + path: '/en/', + query: { + locale: 'fi', + }, + }; + + it('should return empty path with "locale" query param', () => { + const relativeUrl = dropPathLanguageAndFixLocaleParam(req, 'en'); + expect(relativeUrl).to.equal('/?locale=en'); }); - it('should return path without language parameter', () => { - const path = + it('should return path without language', () => { + req.path = '/sv/reitti/Rautatientori%2C%20Helsinki%3A%3A60.171283%2C24.942572/Pasila%2C%20Helsinki%3A%3A60.199017%2C24.933973'; - const newPath = langParamParser(path); - expect(newPath).to.equal( - '/reitti/Rautatientori%2C%20Helsinki%3A%3A60.171283%2C24.942572/Pasila%2C%20Helsinki%3A%3A60.199017%2C24.933973', + const relativeUrl = dropPathLanguageAndFixLocaleParam(req, 'sv'); + expect(relativeUrl).to.equal( + '/reitti/Rautatientori%2C%20Helsinki%3A%3A60.171283%2C24.942572/Pasila%2C%20Helsinki%3A%3A60.199017%2C24.933973?locale=sv', ); }); it('should not ignore URL parameters', () => { - const path = - '/en/reitti/Otaniemi,%20Espoo::60.187938,24.83182/Rautatientori,%20Asemanaukio%202,%20Helsinki::60.170384,24.939846?time=1565074800&arriveBy=false&utm_campaign=hsl.fi&utm_source=etusivu-reittihaku&utm_medium=referral'; - const newPath = langParamParser(path); - expect(newPath).to.equal( - '/reitti/Otaniemi,%20Espoo::60.187938,24.83182/Rautatientori,%20Asemanaukio%202,%20Helsinki::60.170384,24.939846?time=1565074800&arriveBy=false&utm_campaign=hsl.fi&utm_source=etusivu-reittihaku&utm_medium=referral', + req.path = + '/en/reitti/Otaniemi,%20Espoo::60.187938,24.83182/Rautatientori,%20Asemanaukio%202,%20Helsinki::60.170384,24.939846'; + req.query = { + time: 1565074800, + arriveBy: false, + utm_campaign: 'hsl.fi', + utm_source: 'etusivu-reittihaku', + utm_medium: 'referral', + locale: 'fi', + }; + + const relativeUrl = dropPathLanguageAndFixLocaleParam(req, 'en'); + expect(relativeUrl).to.equal( + '/reitti/Otaniemi,%20Espoo::60.187938,24.83182/Rautatientori,%20Asemanaukio%202,%20Helsinki::60.170384,24.939846?time=1565074800&arriveBy=false&utm_campaign=hsl.fi&utm_source=etusivu-reittihaku&utm_medium=referral&locale=en', ); }); });