From 82bd1a5d96c116f62f524d6c6b8d8d9d8bbfd1e6 Mon Sep 17 00:00:00 2001 From: philip-cline Date: Thu, 13 May 2021 13:12:43 -0400 Subject: [PATCH 01/22] (Default itinerary) try react-intl --- .../narrative/default/default-itinerary.js | 160 +++++++++++------- package.json | 1 + yarn.lock | 121 ++++++++++++- 3 files changed, 218 insertions(+), 64 deletions(-) diff --git a/lib/components/narrative/default/default-itinerary.js b/lib/components/narrative/default/default-itinerary.js index abd1eac24..8f455b36f 100644 --- a/lib/components/narrative/default/default-itinerary.js +++ b/lib/components/narrative/default/default-itinerary.js @@ -1,5 +1,6 @@ import coreUtils from '@opentripplanner/core-utils' import React from 'react' +import { defineMessages, FormattedDate, FormattedMessage, FormattedNumber, FormattedTime, IntlProvider } from 'react-intl' import NarrativeItinerary from '../narrative-itinerary' import ItineraryBody from '../line-itin/connected-itinerary-body' @@ -7,13 +8,35 @@ import ItinerarySummary from './itinerary-summary' import SimpleRealtimeAnnotation from '../simple-realtime-annotation' import { getTotalFareAsString } from '../../../util/state' -const { isBicycle, isTransit } = coreUtils.itinerary +const { calculateFares, isBicycle, isTransit } = coreUtils.itinerary const { formatDuration, formatTime } = coreUtils.time +const locale = 'fr' + +const translatedMessages = { + "en-US": { + bike: 'Bike', + bikeshare: 'Bikeshare', + clickDetails: "Click to view details", + drive: 'Drive', + modeSeparator: 'to', + walk: 'Walk', + }, + "fr": { + bike: 'Vélo', + bikeshare: 'Vélo en libre-service', + clickDetails: "Cliquez pour afficher les détails", + drive: 'Voiture', + modeSeparator: '+', + walk: 'Marche', + } +} + + // FIXME move to core utils function getItineraryDescription (itinerary) { let primaryTransitDuration = 0 - let mainMode = 'Walk' + let mainMode = translatedMessages[locale].walk let transitMode itinerary.legs.forEach((leg, i) => { const {duration, mode, rentedBike} = leg @@ -22,12 +45,14 @@ function getItineraryDescription (itinerary) { primaryTransitDuration = duration transitMode = mode.toLowerCase() } - if (isBicycle(mode)) mainMode = 'Bike' - if (rentedBike) mainMode = 'Bikeshare' - if (mode === 'CAR') mainMode = 'Drive' + if (isBicycle(mode)) mainMode = translatedMessages[locale].bike + if (rentedBike) mainMode = translatedMessages[locale].bikeshare + if (mode === 'CAR') mainMode = translatedMessages[locale].drive }) let description = mainMode - if (transitMode) description += ` to ${transitMode}` + // Here we are building a "sequence" of modes e.g. "Bike to Train", + // so it is ok to just concatenate individual mode strings even in other languages. + if (transitMode) description += ` ${translatedMessages[locale].modeSeparator} ${transitMode}` return description } @@ -59,7 +84,13 @@ const ITINERARY_ATTRIBUTES = [ { id: 'cost', order: 2, - render: (itinerary, options) => getTotalFareAsString(itinerary) + render: (itinerary, options) => { + // Get unformatted transit fare portion only (in cents). + const { transitFare } = calculateFares(itinerary) + return ( + + ) + } }, { id: 'walkTime', @@ -130,65 +161,68 @@ export default class DefaultItinerary extends NarrativeItinerary { offset: coreUtils.itinerary.getTimeZoneOffset(itinerary) } return ( -
- + {(active && expanded) && + <> + {showRealtimeAnnotation && } + + } - - {(active && expanded) && - <> - {showRealtimeAnnotation && } - - - } -
+ + ) } } diff --git a/package.json b/package.json index 231e168a8..6033ed616 100644 --- a/package.json +++ b/package.json @@ -81,6 +81,7 @@ "react-dom": "^16.9.0", "react-draggable": "^4.4.3", "react-fontawesome": "^1.5.0", + "react-intl": "^5.17.5", "react-loading-skeleton": "^2.1.1", "react-phone-number-input": "^3.1.0", "react-redux": "^7.1.0", diff --git a/yarn.lock b/yarn.lock index 383ad16bf..494506576 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1207,6 +1207,64 @@ resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz#8eed982e2ee6f7f4e44c253e12962980791efd46" integrity sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA== +"@formatjs/ecma402-abstract@1.7.1": + version "1.7.1" + resolved "https://registry.yarnpkg.com/@formatjs/ecma402-abstract/-/ecma402-abstract-1.7.1.tgz#1459a9dad654d5d5ec34765965b8e4f22ad6ff81" + integrity sha512-FjewVLB2DVEVCvvC7IMffzXVhysvi442i6ed0H7qcrT6xtUpO4vr0oZgpOmsv6D9I4Io0GVebIuySwteS/k3gg== + dependencies: + tslib "^2.1.0" + +"@formatjs/fast-memoize@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@formatjs/fast-memoize/-/fast-memoize-1.1.1.tgz#3006b58aca1e39a98aca213356b42da5d173f26b" + integrity sha512-mIqBr5uigIlx13eZTOPSEh2buDiy3BCdMYUtewICREQjbb4xarDiVWoXSnrERM7NanZ+0TAHNXSqDe6HpEFQUg== + +"@formatjs/icu-messageformat-parser@2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.0.1.tgz#a3b542542b92958f1cdd090f1cb475cb7cb4e21a" + integrity sha512-GXHsATo6/9OMgrfAuyX86fYPMLeQXDN93TOKXQeW7A7ULCy9eEOp3beNwhrVFxaGIjVy/haLLqHMT36iyhwvCA== + dependencies: + "@formatjs/ecma402-abstract" "1.7.1" + "@formatjs/icu-skeleton-parser" "1.2.2" + tslib "^2.1.0" + +"@formatjs/icu-skeleton-parser@1.2.2": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.2.2.tgz#6e9a6eff16e3c7b69d67d40b292d4b499e37228e" + integrity sha512-peBBPIiNzJdPsvEzFGCicD7ARvlcaUYOVZ5dljvzzcHqc5OHlH58OrUNWwYgxFS6Dnb3Ncy4qFwlTdPGTTvw1g== + dependencies: + "@formatjs/ecma402-abstract" "1.7.1" + tslib "^2.1.0" + +"@formatjs/intl-displaynames@4.0.16": + version "4.0.16" + resolved "https://registry.yarnpkg.com/@formatjs/intl-displaynames/-/intl-displaynames-4.0.16.tgz#f310b9a43313c6c3da736a912fc27bce395742b4" + integrity sha512-2vmY2yKu7VvtLmuZhPAGAZ6AtQMQpbUGEW/5IEwFub0AGR5iF/ayC7nbrQvg2NWVRVdVBt/HlTws/QWTZaRCyg== + dependencies: + "@formatjs/ecma402-abstract" "1.7.1" + tslib "^2.1.0" + +"@formatjs/intl-listformat@5.0.17": + version "5.0.17" + resolved "https://registry.yarnpkg.com/@formatjs/intl-listformat/-/intl-listformat-5.0.17.tgz#baa4474a95bab26d5dd0c1a4aad246ed193e5505" + integrity sha512-3WU1cmcoGk1zgNR7BoD/GIOaIJSjWa6GXArEOPiE+ocM6ZgYIYsDcjfQOso0z2NNN1k8CPOlfzWY4olwdKAtxQ== + dependencies: + "@formatjs/ecma402-abstract" "1.7.1" + tslib "^2.1.0" + +"@formatjs/intl@1.10.6": + version "1.10.6" + resolved "https://registry.yarnpkg.com/@formatjs/intl/-/intl-1.10.6.tgz#fc3810ccfdc0b81a735c7fe8298e6ff335843b7a" + integrity sha512-Szxu18bG2LSXWlMHQyNap9AibOaCYgK4/1JlBy2o2UB89fjqJfXaotF7rNGD9dJc+JGzcUYhhBH0ofyN9uSXLg== + dependencies: + "@formatjs/ecma402-abstract" "1.7.1" + "@formatjs/fast-memoize" "1.1.1" + "@formatjs/icu-messageformat-parser" "2.0.1" + "@formatjs/intl-displaynames" "4.0.16" + "@formatjs/intl-listformat" "5.0.17" + intl-messageformat "9.6.13" + tslib "^2.1.0" + "@iarna/cli@^1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@iarna/cli/-/cli-1.2.0.tgz#0f7af5e851afe895104583c4ca07377a8094d641" @@ -2038,6 +2096,14 @@ resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d" integrity sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag== +"@types/hoist-non-react-statics@^3.3.1": + version "3.3.1" + resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f" + integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA== + dependencies: + "@types/react" "*" + hoist-non-react-statics "^3.3.0" + "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0": version "2.0.1" resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz#42995b446db9a48a11a07ec083499a860e9138ff" @@ -2083,16 +2149,35 @@ resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== +"@types/prop-types@*": + version "15.7.3" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7" + integrity sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw== + "@types/q@^1.5.1": version "1.5.2" resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.2.tgz#690a1475b84f2a884fd07cd797c00f5f31356ea8" integrity sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw== +"@types/react@*": + version "17.0.5" + resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.5.tgz#3d887570c4489011f75a3fc8f965bf87d09a1bea" + integrity sha512-bj4biDB9ZJmGAYTWSKJly6bMr4BLUiBrx9ujiJEoP9XIDY9CTaPGxE5QWN/1WjpPLzYF7/jRNnV2nNxNe970sw== + dependencies: + "@types/prop-types" "*" + "@types/scheduler" "*" + csstype "^3.0.2" + "@types/retry@^0.12.0": version "0.12.0" resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d" integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA== +"@types/scheduler@*": + version "0.16.1" + resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.1.tgz#18845205e86ff0038517aab7a18a62a6b9f71275" + integrity sha512-EaCxbanVeyxDRTQBkdLb3Bvl/HK7PBK6UJjsSixB0iHKoWxE5uu2Q/DgtpOhPIojN0Zl1whvOd7PoHs2P0s5eA== + "@types/stack-utils@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e" @@ -5459,6 +5544,11 @@ csstype@^2.5.7, csstype@^2.6.7: resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.10.tgz#e63af50e66d7c266edb6b32909cfd0aabe03928b" integrity sha512-D34BqZU4cIlMCY93rZHbrq9pjTAQJ3U8S8rfBqjwHxkGPThWFjzZDQpgMJY0QViLxth6ZKYiwFBo14RdN44U/w== +csstype@^3.0.2: + version "3.0.8" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.8.tgz#d2266a792729fb227cd216fb572f43728e1ad340" + integrity sha512-jXKhWqXPmlUeoQnF/EhTtTl4C9SnrxSH/jZUih3jmO6lBKr99rP3/+FmrMj4EFpOXzMtXHAZkd3x0E6h6Fgflw== + currency-formatter@^1.4.2, currency-formatter@^1.5.5: version "1.5.6" resolved "https://registry.yarnpkg.com/currency-formatter/-/currency-formatter-1.5.6.tgz#efe6eea7881c3ac7aaa6b24f307c71197a077987" @@ -7843,7 +7933,7 @@ hmac-drbg@^1.0.1: minimalistic-assert "^1.0.0" minimalistic-crypto-utils "^1.0.1" -hoist-non-react-statics@^3.0.0, hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1: +hoist-non-react-statics@^3.0.0, hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== @@ -8275,6 +8365,15 @@ interpret@^1.0.0: resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.2.0.tgz#d5061a6224be58e8083985f5014d844359576296" integrity sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw== +intl-messageformat@9.6.13: + version "9.6.13" + resolved "https://registry.yarnpkg.com/intl-messageformat/-/intl-messageformat-9.6.13.tgz#7c4ace385b3b8cc5010bfd774451ed0c50a73a9b" + integrity sha512-F8OHdgdZYdY3O7TSkQtIGY1qBL7ttbbfIb6g9sgjLw1SQ9SlN3rlaUa1tv9RK3sX0qVkqNLqlPVuOfHlhXpm2Q== + dependencies: + "@formatjs/fast-memoize" "1.1.1" + "@formatjs/icu-messageformat-parser" "2.0.1" + tslib "^2.1.0" + into-stream@^5.0.0: version "5.1.0" resolved "https://registry.yarnpkg.com/into-stream/-/into-stream-5.1.0.tgz#b05f37d8fed05c06a0b43b556d74e53e5af23878" @@ -13570,6 +13669,21 @@ react-input-autosize@^2.2.2: dependencies: prop-types "^15.5.8" +react-intl@^5.17.5: + version "5.17.5" + resolved "https://registry.yarnpkg.com/react-intl/-/react-intl-5.17.5.tgz#8e70fcb6c9a70d20c066f514cb34ad48620c6b75" + integrity sha512-CCGZnb69TUXlDm/GL6R6VAD4RFTqPcIlotRs8RA5SjZX5gxJVgVokvlM9T3sA5BWInGNZUXXNOiZE7yROVUDIg== + dependencies: + "@formatjs/ecma402-abstract" "1.7.1" + "@formatjs/icu-messageformat-parser" "2.0.1" + "@formatjs/intl" "1.10.6" + "@formatjs/intl-displaynames" "4.0.16" + "@formatjs/intl-listformat" "5.0.17" + "@types/hoist-non-react-statics" "^3.3.1" + hoist-non-react-statics "^3.3.2" + intl-messageformat "9.6.13" + tslib "^2.1.0" + react-is@^16.3.2, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.4, react-is@^16.8.6, react-is@^16.9.0: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" @@ -16147,6 +16261,11 @@ tslib@^1.10.0, tslib@^1.9.0, tslib@^1.9.3: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== +tslib@^2.1.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.2.0.tgz#fb2c475977e35e241311ede2693cee1ec6698f5c" + integrity sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w== + tty-browserify@0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6" From 5f61f66df536bdaacdb7f9c2fb422b09efcf9066 Mon Sep 17 00:00:00 2001 From: philip-cline Date: Fri, 21 May 2021 16:39:07 -0400 Subject: [PATCH 02/22] feat(i18n): Add user language setting, translate default itinerary --- lib/actions/user.js | 6 +- .../narrative/default/default-itinerary.js | 131 ++++++++++++++---- package.json | 2 +- yarn.lock | 9 +- 4 files changed, 119 insertions(+), 29 deletions(-) diff --git a/lib/actions/user.js b/lib/actions/user.js index 67fe1911b..ab790be3d 100644 --- a/lib/actions/user.js +++ b/lib/actions/user.js @@ -31,10 +31,12 @@ function createNewUser (auth0User) { auth0UserId: auth0User.sub, email: auth0User.email, hasConsentedToTerms: false, // User must agree to terms. + language: 'fr', notificationChannel: 'email', phoneNumber: '', savedLocations: [], - storeTripHistory: false // User must opt in. + storeTripHistory: false, // User must opt in. + use24HourFormat: true } } @@ -134,6 +136,8 @@ export function fetchOrInitializeUser (auth0User) { const isNewAccount = status === 'error' || (user && user.result === 'ERR') const userData = isNewAccount ? createNewUser(auth0User) : user + userData.language = 'fr' + userData.use24HourFormat = true // Set user in redux state. // (This sorts saved places, and, for existing users, fetches trips.) diff --git a/lib/components/narrative/default/default-itinerary.js b/lib/components/narrative/default/default-itinerary.js index 8f455b36f..940b5892d 100644 --- a/lib/components/narrative/default/default-itinerary.js +++ b/lib/components/narrative/default/default-itinerary.js @@ -1,6 +1,8 @@ +import moment from 'moment-timezone'; import coreUtils from '@opentripplanner/core-utils' import React from 'react' import { defineMessages, FormattedDate, FormattedMessage, FormattedNumber, FormattedTime, IntlProvider } from 'react-intl' +import { connect } from 'react-redux' import NarrativeItinerary from '../narrative-itinerary' import ItineraryBody from '../line-itin/connected-itinerary-body' @@ -9,32 +11,70 @@ import SimpleRealtimeAnnotation from '../simple-realtime-annotation' import { getTotalFareAsString } from '../../../util/state' const { calculateFares, isBicycle, isTransit } = coreUtils.itinerary -const { formatDuration, formatTime } = coreUtils.time +//const { formatTime } = coreUtils.time -const locale = 'fr' + + + +const translatedModes = { + 'en-US': { + TRAM: 'Streetcar', + SUBWAY: 'Subway', + RAIL: 'Rail', + BUS: 'Bus', + FERRY: 'Ferry', + CABLE_CAR: 'Cable Car', + GONDOLA: 'Gondola', + FUNICULAR: 'Funicular' + }, + 'fr': { + TRAM: 'Tram', + SUBWAY: 'Métro', + RAIL: 'Train', + BUS: 'Bus', + FERRY: 'Ferry', + CABLE_CAR: 'Tram tiré par câble', + GONDOLA: 'Téléphérique', + FUNICULAR: 'Funiculaire' + } +} const translatedMessages = { - "en-US": { + 'en-US': { bike: 'Bike', bikeshare: 'Bikeshare', - clickDetails: "Click to view details", + clickDetails: 'Click to view details', + // Use ordered placeholders for the departure-arrival string + // (this will accommodate right-to-left languages by swapping the order in this string). + departureArrivalTimes: `{startTime}—{endTime}`, drive: 'Drive', - modeSeparator: 'to', + // Use ordered placeholders when multiple modes are involved + // (this will accommodate right-to-left languages by swapping the order/separator in this string). + multiModeSummary: '{accessMode} to {transitMode}', walk: 'Walk', + // If trip is less than one hour, only display the minutes. + tripDurationFormatZeroHours: '{minutes, number} min', + // TODO: Distinguish between one hour (singular) and 2 hours or more? + tripDurationFormat: '{hours, number} hr, {minutes, number} min' }, - "fr": { + 'fr': { bike: 'Vélo', bikeshare: 'Vélo en libre-service', - clickDetails: "Cliquez pour afficher les détails", + clickDetails: 'Cliquez pour afficher les détails', + departureArrivalTimes: `{startTime}—{endTime}`, drive: 'Voiture', - modeSeparator: '+', + multiModeSummary: '{accessMode} + {transitMode}', walk: 'Marche', + tripDurationFormatZeroHours: '{minutes, number} mn', + tripDurationFormat: '{hours, number} h, {minutes, number} mn' } } - +/** + * Obtains the description of an itinerary in the given locale. + */ // FIXME move to core utils -function getItineraryDescription (itinerary) { +function getItineraryDescription (itinerary, locale) { let primaryTransitDuration = 0 let mainMode = translatedMessages[locale].walk let transitMode @@ -43,17 +83,49 @@ function getItineraryDescription (itinerary) { if (isTransit(mode) && duration > primaryTransitDuration) { // TODO: convert OTP's TRAM mode to the correct wording for Portland primaryTransitDuration = duration - transitMode = mode.toLowerCase() + transitMode = translatedModes[locale][mode].toLowerCase() } if (isBicycle(mode)) mainMode = translatedMessages[locale].bike if (rentedBike) mainMode = translatedMessages[locale].bikeshare if (mode === 'CAR') mainMode = translatedMessages[locale].drive }) - let description = mainMode - // Here we are building a "sequence" of modes e.g. "Bike to Train", - // so it is ok to just concatenate individual mode strings even in other languages. - if (transitMode) description += ` ${translatedMessages[locale].modeSeparator} ${transitMode}` - return description + + return transitMode + ? + : mainMode +} + +/** + * Formats the given duration according to the selected locale. + */ +function formatDuration (duration) { + const dur = moment.duration(duration, 'seconds') + const hours = dur.hours() + const minutes = dur.minutes() + if (hours === 0) { + return ( + + ) + } else { + return ( + + ) + } +} + +function formatTime( startTime, endTime, timeFormat ) { + return( + + ) } const ITINERARY_ATTRIBUTES = [ @@ -72,12 +144,8 @@ const ITINERARY_ATTRIBUTES = [ if (options.selection === 'ARRIVALTIME') return formatTime(itinerary.endTime, options) else return formatTime(itinerary.startTime, options) } - return ( - - {formatTime(itinerary.startTime, options)} - — - {formatTime(itinerary.endTime, options)} - + return( + formatTime( itinerary.startTime, itinerary.endTime, options.timeFormat) ) } }, @@ -88,7 +156,7 @@ const ITINERARY_ATTRIBUTES = [ // Get unformatted transit fare portion only (in cents). const { transitFare } = calculateFares(itinerary) return ( - + ) } }, @@ -117,7 +185,7 @@ const ITINERARY_ATTRIBUTES = [ } ] -export default class DefaultItinerary extends NarrativeItinerary { +class DefaultItinerary extends NarrativeItinerary { _onMouseEnter = () => { const {active, index, setVisibleItinerary, visibleItinerary} = this.props // Set this itinerary as visible if not already visible. @@ -146,12 +214,13 @@ export default class DefaultItinerary extends NarrativeItinerary { return false } - render () { + render () { const { active, expanded, itinerary, LegIcon, + userLanguage: locale, setActiveLeg, showRealtimeAnnotation, timeFormat @@ -180,7 +249,7 @@ export default class DefaultItinerary extends NarrativeItinerary { >
- {getItineraryDescription(itinerary)} + {getItineraryDescription(itinerary, locale)}
    {ITINERARY_ATTRIBUTES @@ -199,6 +268,7 @@ export default class DefaultItinerary extends NarrativeItinerary { options.selection = this.props.sort.type } options.LegIcon = LegIcon + options.timeFormat = this.props.use24HourFormat ? 'H:mm' : 'h:mm a' return (
  • {attribute.render(itinerary, options)} @@ -226,3 +296,12 @@ export default class DefaultItinerary extends NarrativeItinerary { ) } } + +const mapStateToProps = (state, ownProps) => { + return { + userLanguage: state.user.loggedInUser == null ? 'en-US' : state.user.loggedInUser.language, + use24HourFormat: state.user.loggedInUser == null ? false : state.user.loggedInUser.use24HourFormat + } +} + +export default connect(mapStateToProps)(DefaultItinerary) diff --git a/package.json b/package.json index 6033ed616..99339bec0 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "lodash.isequal": "^4.5.0", "lodash.memoize": "^4.1.2", "moment": "^2.17.1", - "moment-timezone": "^0.5.23", + "moment-timezone": "^0.5.33", "object-hash": "^1.3.1", "object-path": "^0.11.5", "object-to-formdata": "^4.1.0", diff --git a/yarn.lock b/yarn.lock index 494506576..17fec2754 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10928,13 +10928,20 @@ mold-source-map@~0.4.0: convert-source-map "^1.1.0" through "~2.2.7" -moment-timezone@^0.5.23, moment-timezone@^0.5.27: +moment-timezone@^0.5.27: version "0.5.31" resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.31.tgz#9c40d8c5026f0c7ab46eda3d63e49c155148de05" integrity sha512-+GgHNg8xRhMXfEbv81iDtrVeTcWt0kWmTEY1XQK14dICTXnWJnT0dxdlPspwqF3keKMVPXwayEsk1DI0AA/jdA== dependencies: moment ">= 2.9.0" +moment-timezone@^0.5.33: + version "0.5.33" + resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.33.tgz#b252fd6bb57f341c9b59a5ab61a8e51a73bbd22c" + integrity sha512-PTc2vcT8K9J5/9rDEPe5czSIKgLoGsH8UNpA4qZTVw0Vd/Uz19geE9abbIOQKaAQFcnQ3v5YEXrbSc5BpshH+w== + dependencies: + moment ">= 2.9.0" + "moment@>= 2.9.0", moment@^2.17.1, moment@^2.24.0: version "2.29.1" resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3" From 28eb425d6fd1cba9e918f17835543b7eb51982ca Mon Sep 17 00:00:00 2001 From: philip-cline Date: Thu, 27 May 2021 14:15:20 -0400 Subject: [PATCH 03/22] feat(default-itinerary): Add mini react-intl i18n implementation. Add a mini internationalization implementation to the default itinerary changing the mode names, mode separator, the time format and the duration format. At the moment only english (en-US) and french (fr-FR) translations are provided and more languages will be added in later pull requests. --- i18n/en-US.yml | 30 +++++ i18n/fr-FR.yml | 24 ++++ .../narrative/default/default-itinerary.js | 107 +++++------------- lib/util/i18n.js | 21 ++++ 4 files changed, 104 insertions(+), 78 deletions(-) create mode 100644 i18n/en-US.yml create mode 100644 i18n/fr-FR.yml create mode 100644 lib/util/i18n.js diff --git a/i18n/en-US.yml b/i18n/en-US.yml new file mode 100644 index 000000000..93ac81ef0 --- /dev/null +++ b/i18n/en-US.yml @@ -0,0 +1,30 @@ +_id: en-US +_name: English +components: + DefaultItinerary: + bike: Bike + bikeshare: Bikeshare + clickDetails: Click to view details + # Use ordered placeholders for the departure-arrival string + # (this will accommodate right-to-left languages by swapping the order in this string). + departureArrivalTimes: "{startTime}—{endTime}" + drive: Drive + # Use ordered placeholders when multiple modes are involved + # (this will accommodate right-to-left languages by swapping the order/separator in this string). + multiModeSummary: "{accessMode} to {transitMode}" + walk: Walk + # If trip is less than one hour only display the minutes. + tripDurationFormatZeroHours: "{minutes, number} min" + # TODO: Distinguish between one hour (singular) and 2 hours or more? + tripDurationFormat: "{hours, number} hr {minutes, number} min" + +common: + otpModes: + TRAM: Streetcar + SUBWAY: Subway + RAIL: Rail + BUS: Bus + FERRY: Ferry + CABLE_CAR: Cable Car + GONDOLA: Gondola + FUNICULAR: Funicular \ No newline at end of file diff --git a/i18n/fr-FR.yml b/i18n/fr-FR.yml new file mode 100644 index 000000000..ae90359c9 --- /dev/null +++ b/i18n/fr-FR.yml @@ -0,0 +1,24 @@ +_id: fr-FR +_name: French +components: + DefaultItinerary: + bike: Vélo + bikeshare: Vélo en libre-service + clickDetails: Cliquez pour afficher les détails + departureArrivalTimes: "{startTime}—{endTime}" + drive: Voiture + multiModeSummary: "{accessMode} + {transitMode}" + walk: Marche + tripDurationFormatZeroHours: "{minutes, number} mn" + tripDurationFormat: "{hours, number} h, {minutes, number} mn" + +common: + otpModes: + TRAM: Tram + SUBWAY: Métro + RAIL: Train + BUS: Bus + FERRY: Ferry + CABLE_CAR: Tram tiré par câble + GONDOLA: Téléphérique + FUNICULAR: Funiculaire diff --git a/lib/components/narrative/default/default-itinerary.js b/lib/components/narrative/default/default-itinerary.js index 940b5892d..1d414d0e0 100644 --- a/lib/components/narrative/default/default-itinerary.js +++ b/lib/components/narrative/default/default-itinerary.js @@ -1,95 +1,40 @@ -import moment from 'moment-timezone'; +import moment from 'moment-timezone' import coreUtils from '@opentripplanner/core-utils' import React from 'react' -import { defineMessages, FormattedDate, FormattedMessage, FormattedNumber, FormattedTime, IntlProvider } from 'react-intl' +import { FormattedMessage, FormattedNumber, IntlProvider } from 'react-intl' import { connect } from 'react-redux' +import { getUrlParams } from '@opentripplanner/core-utils/lib/query' import NarrativeItinerary from '../narrative-itinerary' import ItineraryBody from '../line-itin/connected-itinerary-body' -import ItinerarySummary from './itinerary-summary' import SimpleRealtimeAnnotation from '../simple-realtime-annotation' -import { getTotalFareAsString } from '../../../util/state' - -const { calculateFares, isBicycle, isTransit } = coreUtils.itinerary -//const { formatTime } = coreUtils.time - +import { getMessages } from '../../../util/i18n' +import ItinerarySummary from './itinerary-summary' - -const translatedModes = { - 'en-US': { - TRAM: 'Streetcar', - SUBWAY: 'Subway', - RAIL: 'Rail', - BUS: 'Bus', - FERRY: 'Ferry', - CABLE_CAR: 'Cable Car', - GONDOLA: 'Gondola', - FUNICULAR: 'Funicular' - }, - 'fr': { - TRAM: 'Tram', - SUBWAY: 'Métro', - RAIL: 'Train', - BUS: 'Bus', - FERRY: 'Ferry', - CABLE_CAR: 'Tram tiré par câble', - GONDOLA: 'Téléphérique', - FUNICULAR: 'Funiculaire' - } -} - -const translatedMessages = { - 'en-US': { - bike: 'Bike', - bikeshare: 'Bikeshare', - clickDetails: 'Click to view details', - // Use ordered placeholders for the departure-arrival string - // (this will accommodate right-to-left languages by swapping the order in this string). - departureArrivalTimes: `{startTime}—{endTime}`, - drive: 'Drive', - // Use ordered placeholders when multiple modes are involved - // (this will accommodate right-to-left languages by swapping the order/separator in this string). - multiModeSummary: '{accessMode} to {transitMode}', - walk: 'Walk', - // If trip is less than one hour, only display the minutes. - tripDurationFormatZeroHours: '{minutes, number} min', - // TODO: Distinguish between one hour (singular) and 2 hours or more? - tripDurationFormat: '{hours, number} hr, {minutes, number} min' - }, - 'fr': { - bike: 'Vélo', - bikeshare: 'Vélo en libre-service', - clickDetails: 'Cliquez pour afficher les détails', - departureArrivalTimes: `{startTime}—{endTime}`, - drive: 'Voiture', - multiModeSummary: '{accessMode} + {transitMode}', - walk: 'Marche', - tripDurationFormatZeroHours: '{minutes, number} mn', - tripDurationFormat: '{hours, number} h, {minutes, number} mn' - } -} +const { calculateFares, isBicycle, isTransit } = coreUtils.itinerary /** * Obtains the description of an itinerary in the given locale. */ // FIXME move to core utils -function getItineraryDescription (itinerary, locale) { +function getItineraryDescription (itinerary) { let primaryTransitDuration = 0 - let mainMode = translatedMessages[locale].walk + let mainModeStringId = 'walk' let transitMode itinerary.legs.forEach((leg, i) => { const {duration, mode, rentedBike} = leg if (isTransit(mode) && duration > primaryTransitDuration) { // TODO: convert OTP's TRAM mode to the correct wording for Portland primaryTransitDuration = duration - transitMode = translatedModes[locale][mode].toLowerCase() + transitMode = } - if (isBicycle(mode)) mainMode = translatedMessages[locale].bike - if (rentedBike) mainMode = translatedMessages[locale].bikeshare - if (mode === 'CAR') mainMode = translatedMessages[locale].drive + if (isBicycle(mode)) mainModeStringId = 'bike' + if (rentedBike) mainModeStringId = 'bikeshare' + if (mode === 'CAR') mainModeStringId = 'drive' }) + const mainMode = return transitMode ? : mainMode @@ -119,8 +64,8 @@ function formatDuration (duration) { } } -function formatTime( startTime, endTime, timeFormat ) { - return( +function formatTime (startTime, endTime, timeFormat) { + return ( + ) } }, @@ -214,13 +159,14 @@ class DefaultItinerary extends NarrativeItinerary { return false } - render () { + render () { const { active, expanded, itinerary, LegIcon, - userLanguage: locale, + // userLanguage: locale, + locale, setActiveLeg, showRealtimeAnnotation, timeFormat @@ -229,8 +175,13 @@ class DefaultItinerary extends NarrativeItinerary { format: timeFormat, offset: coreUtils.itinerary.getTimeZoneOffset(itinerary) } + // const locale = getUrlParams().locale || 'en-US' + const componentMessages = getMessages(['components', 'DefaultItinerary'], locale) + const otpModes = getMessages(['common', 'otpModes'], locale) + const allMessages = {...componentMessages, ...otpModes} + return ( - +
    - {getItineraryDescription(itinerary, locale)} + {getItineraryDescription(itinerary)}
      {ITINERARY_ATTRIBUTES @@ -280,7 +231,6 @@ class DefaultItinerary extends NarrativeItinerary { {(active && !expanded) && - {/**/} } @@ -299,6 +249,7 @@ class DefaultItinerary extends NarrativeItinerary { const mapStateToProps = (state, ownProps) => { return { + locale: getUrlParams().locale || 'en-US', userLanguage: state.user.loggedInUser == null ? 'en-US' : state.user.loggedInUser.language, use24HourFormat: state.user.loggedInUser == null ? false : state.user.loggedInUser.use24HourFormat } diff --git a/lib/util/i18n.js b/lib/util/i18n.js new file mode 100644 index 000000000..65711c800 --- /dev/null +++ b/lib/util/i18n.js @@ -0,0 +1,21 @@ +import objectPath from 'object-path' + +// Load the localized strings for all languages here. +// (We cannot really load languages on demand using fetch... that would require tinkering with mastarm build, +// so here we build an index of translated messages.) +const translatedMessages = { + 'en-US': require('../../i18n/en-US.yml'), + 'fr-FR': require('../../i18n/fr-FR.yml') + // TODO: Add and load other language files here. +} + +/** + * Create a function to lookup and return messages at a given path in the YML file for the localized strings. + * To handle missing strings, use from react-intl upon rendering. + * TODO: Still need to handle missing strings when display messages in an alert box. + * @param pathArray Example: ['path', 'to', 'message', 'id'] + */ +export function getMessages (pathArray, locale) { + // TODO: check that translatedMessages[locale] is a valid object + return objectPath.get(translatedMessages[locale], pathArray) || {} +} From 00be1704618ff0a3b488e203ff08d7c9cc3aa04d Mon Sep 17 00:00:00 2001 From: philip-cline Date: Thu, 27 May 2021 15:50:09 -0400 Subject: [PATCH 04/22] refactor(default-itinerary.js): remove comments --- lib/components/narrative/default/default-itinerary.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/components/narrative/default/default-itinerary.js b/lib/components/narrative/default/default-itinerary.js index 1d414d0e0..e0e0da93b 100644 --- a/lib/components/narrative/default/default-itinerary.js +++ b/lib/components/narrative/default/default-itinerary.js @@ -165,7 +165,6 @@ class DefaultItinerary extends NarrativeItinerary { expanded, itinerary, LegIcon, - // userLanguage: locale, locale, setActiveLeg, showRealtimeAnnotation, @@ -175,7 +174,6 @@ class DefaultItinerary extends NarrativeItinerary { format: timeFormat, offset: coreUtils.itinerary.getTimeZoneOffset(itinerary) } - // const locale = getUrlParams().locale || 'en-US' const componentMessages = getMessages(['components', 'DefaultItinerary'], locale) const otpModes = getMessages(['common', 'otpModes'], locale) const allMessages = {...componentMessages, ...otpModes} From d0f962fcf847a7ba0d3b9a9b9fa33501ce41e00a Mon Sep 17 00:00:00 2001 From: philip-cline Date: Tue, 1 Jun 2021 13:46:49 -0400 Subject: [PATCH 05/22] feat(default-itinerary): Responding to PR comments Restructuring language yml files, adding currency based on config settings, storing locale settings in redux store --- i18n/en-US.yml | 16 +++++++++------ i18n/fr-FR.yml | 16 +++++++++------ lib/actions/ui.js | 2 ++ lib/actions/user.js | 2 -- lib/components/app/responsive-webapp.js | 10 ++++++++++ .../narrative/default/default-itinerary.js | 20 +++++++++++-------- lib/reducers/create-otp-reducer.js | 4 ++++ 7 files changed, 48 insertions(+), 22 deletions(-) diff --git a/i18n/en-US.yml b/i18n/en-US.yml index 93ac81ef0..59feaedb7 100644 --- a/i18n/en-US.yml +++ b/i18n/en-US.yml @@ -2,24 +2,28 @@ _id: en-US _name: English components: DefaultItinerary: - bike: Bike - bikeshare: Bikeshare clickDetails: Click to view details # Use ordered placeholders for the departure-arrival string # (this will accommodate right-to-left languages by swapping the order in this string). departureArrivalTimes: "{startTime}—{endTime}" - drive: Drive # Use ordered placeholders when multiple modes are involved # (this will accommodate right-to-left languages by swapping the order/separator in this string). multiModeSummary: "{accessMode} to {transitMode}" - walk: Walk # If trip is less than one hour only display the minutes. tripDurationFormatZeroHours: "{minutes, number} min" # TODO: Distinguish between one hour (singular) and 2 hours or more? tripDurationFormat: "{hours, number} hr {minutes, number} min" common: - otpModes: + accessModes: + bike: Bike + bikeshare: Bikeshare + drive: Drive + micromobility: E-Scooter + micromobilityRent: Rental E-Scooter + walk: Walk + + otpTransitModes: TRAM: Streetcar SUBWAY: Subway RAIL: Rail @@ -27,4 +31,4 @@ common: FERRY: Ferry CABLE_CAR: Cable Car GONDOLA: Gondola - FUNICULAR: Funicular \ No newline at end of file + FUNICULAR: Funicular diff --git a/i18n/fr-FR.yml b/i18n/fr-FR.yml index ae90359c9..35c142c6c 100644 --- a/i18n/fr-FR.yml +++ b/i18n/fr-FR.yml @@ -1,19 +1,23 @@ _id: fr-FR -_name: French +_name: Unofficial French Translations! components: DefaultItinerary: - bike: Vélo - bikeshare: Vélo en libre-service clickDetails: Cliquez pour afficher les détails departureArrivalTimes: "{startTime}—{endTime}" - drive: Voiture multiModeSummary: "{accessMode} + {transitMode}" - walk: Marche tripDurationFormatZeroHours: "{minutes, number} mn" tripDurationFormat: "{hours, number} h, {minutes, number} mn" common: - otpModes: + accessModes: + bike: Vélo + bikeshare: Vélo en libre-service + drive: Voiture + micromobility: Trottinette électrique + micromobilityRent: Trottinette électrique en libre-service + walk: Marche + + otpTransitModes: TRAM: Tram SUBWAY: Métro RAIL: Train diff --git a/lib/actions/ui.js b/lib/actions/ui.js index 955515798..f7e28faf6 100644 --- a/lib/actions/ui.js +++ b/lib/actions/ui.js @@ -14,6 +14,8 @@ import { clearLocation } from './map' import { setActiveItinerary } from './narrative' import { getUiUrlParams } from '../util/state' +export const updateLocale = createAction('UPDATE_LOCALE') + /** * Wrapper function for history#push (or, if specified, replace, etc.) * that preserves the current search or, if diff --git a/lib/actions/user.js b/lib/actions/user.js index ab790be3d..acc657c6b 100644 --- a/lib/actions/user.js +++ b/lib/actions/user.js @@ -136,8 +136,6 @@ export function fetchOrInitializeUser (auth0User) { const isNewAccount = status === 'error' || (user && user.result === 'ERR') const userData = isNewAccount ? createNewUser(auth0User) : user - userData.language = 'fr' - userData.use24HourFormat = true // Set user in redux state. // (This sorts saved places, and, for existing users, fetches trips.) diff --git a/lib/components/app/responsive-webapp.js b/lib/components/app/responsive-webapp.js index 8dfd7f934..2f76b8c11 100644 --- a/lib/components/app/responsive-webapp.js +++ b/lib/components/app/responsive-webapp.js @@ -48,6 +48,8 @@ import SavedTripScreen from '../user/monitored-trip/saved-trip-screen' import UserAccountScreen from '../user/user-account-screen' import withLoggedInUserSupport from '../user/with-logged-in-user-support' + + const { isMobile } = coreUtils.ui class ResponsiveWebapp extends Component { @@ -102,6 +104,11 @@ class ResponsiveWebapp extends Component { // console.log('url changed to', location.pathname) this.props.matchContentToUrl(location) } + // If the URL search parameters change (e.g., user modifies anything after ?, e.g. locale) + // update the corresponding redux state. + if (urlParams.locale && urlParams.locale !== prevProps.locale) { + this.props.updateLocale(urlParams.locale) + } // Check for change between ITINERARY and PROFILE routingTypes // TODO: restore this for profile mode /* if (query.routingType !== nextProps.query.routingType) { @@ -231,6 +238,8 @@ const mapStateToProps = (state, ownProps) => { activeItinerary: getActiveItinerary(state.otp), activeSearchId: state.otp.activeSearchId, currentPosition: state.otp.location.currentPosition, + locale: state.otp.ui.locale, + currency: state.otp.ui.currency, initZoomOnLocate: state.otp.config.map && state.otp.config.map.initZoomOnLocate, mobileScreen: state.otp.ui.mobileScreen, modeGroups: state.otp.config.modeGroups, @@ -250,6 +259,7 @@ const mapDispatchToProps = { receivedPositionResponse: locationActions.receivedPositionResponse, setLocationToCurrent: mapActions.setLocationToCurrent, setMapCenter: configActions.setMapCenter, + updateLocale: uiActions.updateLocale, setMapZoom: configActions.setMapZoom } diff --git a/lib/components/narrative/default/default-itinerary.js b/lib/components/narrative/default/default-itinerary.js index e0e0da93b..a5d064da8 100644 --- a/lib/components/narrative/default/default-itinerary.js +++ b/lib/components/narrative/default/default-itinerary.js @@ -3,7 +3,6 @@ import coreUtils from '@opentripplanner/core-utils' import React from 'react' import { FormattedMessage, FormattedNumber, IntlProvider } from 'react-intl' import { connect } from 'react-redux' -import { getUrlParams } from '@opentripplanner/core-utils/lib/query' import NarrativeItinerary from '../narrative-itinerary' import ItineraryBody from '../line-itin/connected-itinerary-body' @@ -12,7 +11,7 @@ import { getMessages } from '../../../util/i18n' import ItinerarySummary from './itinerary-summary' -const { calculateFares, isBicycle, isTransit } = coreUtils.itinerary +const { calculateFares, isBicycle, isTransit, isMicromobility } = coreUtils.itinerary /** * Obtains the description of an itinerary in the given locale. @@ -23,13 +22,15 @@ function getItineraryDescription (itinerary) { let mainModeStringId = 'walk' let transitMode itinerary.legs.forEach((leg, i) => { - const {duration, mode, rentedBike} = leg + const {duration, mode, rentedBike, rentedVehicle} = leg if (isTransit(mode) && duration > primaryTransitDuration) { // TODO: convert OTP's TRAM mode to the correct wording for Portland primaryTransitDuration = duration transitMode = } if (isBicycle(mode)) mainModeStringId = 'bike' + if (isMicromobility(mode)) mainModeStringId = 'micromobility' + if (rentedVehicle) mainModeStringId = 'micromobilityRent' if (rentedBike) mainModeStringId = 'bikeshare' if (mode === 'CAR') mainModeStringId = 'drive' }) @@ -101,7 +102,7 @@ const ITINERARY_ATTRIBUTES = [ // Get unformatted transit fare portion only (in cents). const { transitFare } = calculateFares(itinerary) return ( - + ) } }, @@ -166,6 +167,7 @@ class DefaultItinerary extends NarrativeItinerary { itinerary, LegIcon, locale, + currency, setActiveLeg, showRealtimeAnnotation, timeFormat @@ -175,8 +177,9 @@ class DefaultItinerary extends NarrativeItinerary { offset: coreUtils.itinerary.getTimeZoneOffset(itinerary) } const componentMessages = getMessages(['components', 'DefaultItinerary'], locale) - const otpModes = getMessages(['common', 'otpModes'], locale) - const allMessages = {...componentMessages, ...otpModes} + const accessModes = getMessages(['common', 'accessModes'], locale) + const otpTransitModes = getMessages(['common', 'otpTransitModes'], locale) + const allMessages = {...componentMessages, ...otpTransitModes, ...accessModes} return ( @@ -218,6 +221,7 @@ class DefaultItinerary extends NarrativeItinerary { } options.LegIcon = LegIcon options.timeFormat = this.props.use24HourFormat ? 'H:mm' : 'h:mm a' + options.currency = currency return (
    • {attribute.render(itinerary, options)} @@ -247,8 +251,8 @@ class DefaultItinerary extends NarrativeItinerary { const mapStateToProps = (state, ownProps) => { return { - locale: getUrlParams().locale || 'en-US', - userLanguage: state.user.loggedInUser == null ? 'en-US' : state.user.loggedInUser.language, + locale: state.otp.ui.locale, + currency: state.otp.ui.currency, use24HourFormat: state.user.loggedInUser == null ? false : state.user.loggedInUser.use24HourFormat } } diff --git a/lib/reducers/create-otp-reducer.js b/lib/reducers/create-otp-reducer.js index 14d835197..af807347b 100644 --- a/lib/reducers/create-otp-reducer.js +++ b/lib/reducers/create-otp-reducer.js @@ -238,6 +238,8 @@ export function getInitialState (userDefinedConfig) { rideEstimates: {} }, ui: { + currency: config.localization?.currency || 'USD', + locale: config.localization?.locale || 'en-US', mobileScreen: MobileScreens.WELCOME_SCREEN, printView: window.location.href.indexOf('/print/') !== -1, diagramLeg: null @@ -950,6 +952,8 @@ function createOtpReducer (config) { ) case 'SET_PREVIOUS_ITINERARY_VIEW': return update(state, { ui: { previousItineraryView: { $set: action.payload } } }) + case 'UPDATE_LOCALE': + return update(state, { ui: { locale: { $set: action.payload || 'en-US' } } }) default: return state } From 4b4fe7ace5abc0120c2ae4bc00f36de769c2e164 Mon Sep 17 00:00:00 2001 From: philip-cline Date: Thu, 3 Jun 2021 12:44:00 -0400 Subject: [PATCH 06/22] refactor(i18n messages): Move IntlProvider to responsive webapp class Move IntlProvider to responsive webapp class from redux state and flatten messages to be used in components. --- i18n/en-US.yml | 17 +- i18n/fr-FR.yml | 17 +- lib/components/app/responsive-webapp.js | 178 ++++++++++-------- .../narrative/default/default-itinerary.js | 153 +++++++-------- lib/reducers/create-otp-reducer.js | 4 +- lib/util/i18n.js | 34 ++-- package.json | 1 + yarn.lock | 5 + 8 files changed, 217 insertions(+), 192 deletions(-) diff --git a/i18n/en-US.yml b/i18n/en-US.yml index 59feaedb7..4da90fa40 100644 --- a/i18n/en-US.yml +++ b/i18n/en-US.yml @@ -1,5 +1,6 @@ _id: en-US _name: English + components: DefaultItinerary: clickDetails: Click to view details @@ -24,11 +25,11 @@ common: walk: Walk otpTransitModes: - TRAM: Streetcar - SUBWAY: Subway - RAIL: Rail - BUS: Bus - FERRY: Ferry - CABLE_CAR: Cable Car - GONDOLA: Gondola - FUNICULAR: Funicular + tram: Streetcar + subway: Subway + rail: Rail + bus: Bus + ferry: Ferry + cable_car: Cable Car + gondola: Gondola + funicular: Funicular diff --git a/i18n/fr-FR.yml b/i18n/fr-FR.yml index 35c142c6c..cdbf33b27 100644 --- a/i18n/fr-FR.yml +++ b/i18n/fr-FR.yml @@ -1,5 +1,6 @@ _id: fr-FR _name: Unofficial French Translations! + components: DefaultItinerary: clickDetails: Cliquez pour afficher les détails @@ -18,11 +19,11 @@ common: walk: Marche otpTransitModes: - TRAM: Tram - SUBWAY: Métro - RAIL: Train - BUS: Bus - FERRY: Ferry - CABLE_CAR: Tram tiré par câble - GONDOLA: Téléphérique - FUNICULAR: Funiculaire + tram: Tram + subway: Métro + rail: Train + bus: Bus + ferry: Ferry + cable_car: Tram tiré par câble + gondola: Téléphérique + funicular: Funiculaire diff --git a/lib/components/app/responsive-webapp.js b/lib/components/app/responsive-webapp.js index 2f76b8c11..7e290b805 100644 --- a/lib/components/app/responsive-webapp.js +++ b/lib/components/app/responsive-webapp.js @@ -7,6 +7,7 @@ import PropTypes from 'prop-types' import qs from 'qs' import React, { Component } from 'react' import { Col, Grid, Row } from 'react-bootstrap' +import { IntlProvider } from 'react-intl' import { connect } from 'react-redux' import { Route, Switch, withRouter } from 'react-router' @@ -39,6 +40,7 @@ import { URL_ROOT } from '../../util/constants' import { ComponentContext } from '../../util/contexts' +import { loadLocaleData } from '../../util/i18n' import { getActiveItinerary, getTitle } from '../../util/state' import AfterSignInScreen from '../user/after-signin-screen' import BeforeSignInScreen from '../user/before-signin-screen' @@ -48,8 +50,6 @@ import SavedTripScreen from '../user/monitored-trip/saved-trip-screen' import UserAccountScreen from '../user/user-account-screen' import withLoggedInUserSupport from '../user/with-logged-in-user-support' - - const { isMobile } = coreUtils.ui class ResponsiveWebapp extends Component { @@ -259,8 +259,8 @@ const mapDispatchToProps = { receivedPositionResponse: locationActions.receivedPositionResponse, setLocationToCurrent: mapActions.setLocationToCurrent, setMapCenter: configActions.setMapCenter, - updateLocale: uiActions.updateLocale, - setMapZoom: configActions.setMapZoom + setMapZoom: configActions.setMapZoom, + updateLocale: uiActions.updateLocale } const history = createHashHistory() @@ -278,10 +278,31 @@ const WebappWithRouter = withRouter( * so that Auth0 services are available everywhere. */ class RouterWrapperWithAuth0 extends Component { + state = { + messages: null + } + + async componentDidMount () { + this.updateMessages() + } + + async componentDidUpdate (prevProps) { + this.updateMessages(prevProps) + } + + async updateMessages (prevProps) { + // If the locale changes (or no prevProps are provided), update the messages for that locale. + const locale = this.props.locale + if (locale && (!prevProps || locale !== prevProps.locale)) { + this.setState({ messages: await loadLocaleData(locale) }) + } + } + render () { const { auth0Config, components, + locale, processSignIn, routerConfig, showAccessTokenError, @@ -290,79 +311,81 @@ class RouterWrapperWithAuth0 extends Component { const router = ( - - - } - /> - - - - - - - - - - - - - - - {/* For any other route, simply return the web app. */} - } - /> - - + + + + } + /> + + + + + + + + + + + + + + + {/* For any other route, simply return the web app. */} + } + /> + + + ) @@ -390,6 +413,7 @@ class RouterWrapperWithAuth0 extends Component { const mapStateToWrapperProps = (state, ownProps) => ({ auth0Config: getAuth0Config(state.otp.config.persistence), + locale: state.otp.ui.locale, routerConfig: state.otp.config.reactRouter }) diff --git a/lib/components/narrative/default/default-itinerary.js b/lib/components/narrative/default/default-itinerary.js index a5d064da8..615904c78 100644 --- a/lib/components/narrative/default/default-itinerary.js +++ b/lib/components/narrative/default/default-itinerary.js @@ -1,13 +1,12 @@ import moment from 'moment-timezone' import coreUtils from '@opentripplanner/core-utils' import React from 'react' -import { FormattedMessage, FormattedNumber, IntlProvider } from 'react-intl' +import { FormattedMessage, FormattedNumber } from 'react-intl' import { connect } from 'react-redux' import NarrativeItinerary from '../narrative-itinerary' import ItineraryBody from '../line-itin/connected-itinerary-body' import SimpleRealtimeAnnotation from '../simple-realtime-annotation' -import { getMessages } from '../../../util/i18n' import ItinerarySummary from './itinerary-summary' @@ -19,25 +18,25 @@ const { calculateFares, isBicycle, isTransit, isMicromobility } = coreUtils.itin // FIXME move to core utils function getItineraryDescription (itinerary) { let primaryTransitDuration = 0 - let mainModeStringId = 'walk' + let accessModeId = 'walk' let transitMode itinerary.legs.forEach((leg, i) => { const {duration, mode, rentedBike, rentedVehicle} = leg if (isTransit(mode) && duration > primaryTransitDuration) { // TODO: convert OTP's TRAM mode to the correct wording for Portland primaryTransitDuration = duration - transitMode = + transitMode = } - if (isBicycle(mode)) mainModeStringId = 'bike' - if (isMicromobility(mode)) mainModeStringId = 'micromobility' - if (rentedVehicle) mainModeStringId = 'micromobilityRent' - if (rentedBike) mainModeStringId = 'bikeshare' - if (mode === 'CAR') mainModeStringId = 'drive' + if (isBicycle(mode)) accessModeId = 'bike' + if (isMicromobility(mode)) accessModeId = 'micromobility' + if (rentedVehicle) accessModeId = 'micromobilityRent' + if (rentedBike) accessModeId = 'bikeshare' + if (mode === 'CAR') accessModeId = 'drive' }) - const mainMode = + const mainMode = return transitMode - ? + ? : mainMode } @@ -51,14 +50,14 @@ function formatDuration (duration) { if (hours === 0) { return ( ) } else { return ( ) @@ -67,7 +66,7 @@ function formatDuration (duration) { function formatTime (startTime, endTime, timeFormat) { return ( - @@ -163,11 +162,10 @@ class DefaultItinerary extends NarrativeItinerary { render () { const { active, + currency, expanded, itinerary, LegIcon, - locale, - currency, setActiveLeg, showRealtimeAnnotation, timeFormat @@ -176,75 +174,69 @@ class DefaultItinerary extends NarrativeItinerary { format: timeFormat, offset: coreUtils.itinerary.getTimeZoneOffset(itinerary) } - const componentMessages = getMessages(['components', 'DefaultItinerary'], locale) - const accessModes = getMessages(['common', 'accessModes'], locale) - const otpTransitModes = getMessages(['common', 'otpTransitModes'], locale) - const allMessages = {...componentMessages, ...otpTransitModes, ...accessModes} return ( - -
      + - {(active && expanded) && - <> - {showRealtimeAnnotation && } - - +
    + + {(active && !expanded) && + + + } -
    -
    + + {(active && expanded) && + <> + {showRealtimeAnnotation && } + + + } + ) } } @@ -253,6 +245,7 @@ const mapStateToProps = (state, ownProps) => { return { locale: state.otp.ui.locale, currency: state.otp.ui.currency, + messages: state.otp.ui.messages, use24HourFormat: state.user.loggedInUser == null ? false : state.user.loggedInUser.use24HourFormat } } diff --git a/lib/reducers/create-otp-reducer.js b/lib/reducers/create-otp-reducer.js index af807347b..a0003fbc0 100644 --- a/lib/reducers/create-otp-reducer.js +++ b/lib/reducers/create-otp-reducer.js @@ -953,7 +953,9 @@ function createOtpReducer (config) { case 'SET_PREVIOUS_ITINERARY_VIEW': return update(state, { ui: { previousItineraryView: { $set: action.payload } } }) case 'UPDATE_LOCALE': - return update(state, { ui: { locale: { $set: action.payload || 'en-US' } } }) + return update(state, { ui: { + locale: { $set: action.payload || 'en-US' } + }}) default: return state } diff --git a/lib/util/i18n.js b/lib/util/i18n.js index 65711c800..0f41cc42b 100644 --- a/lib/util/i18n.js +++ b/lib/util/i18n.js @@ -1,21 +1,19 @@ -import objectPath from 'object-path' +import flatten from 'flat' -// Load the localized strings for all languages here. -// (We cannot really load languages on demand using fetch... that would require tinkering with mastarm build, -// so here we build an index of translated messages.) -const translatedMessages = { - 'en-US': require('../../i18n/en-US.yml'), - 'fr-FR': require('../../i18n/fr-FR.yml') - // TODO: Add and load other language files here. -} - -/** - * Create a function to lookup and return messages at a given path in the YML file for the localized strings. - * To handle missing strings, use from react-intl upon rendering. - * TODO: Still need to handle missing strings when display messages in an alert box. - * @param pathArray Example: ['path', 'to', 'message', 'id'] +/* + * Load the localized strings for all languages here, and flatten to a map of + * id => string. + * FIXME: Load languages on demand using fetch. */ -export function getMessages (pathArray, locale) { - // TODO: check that translatedMessages[locale] is a valid object - return objectPath.get(translatedMessages[locale], pathArray) || {} +export async function loadLocaleData (locale) { + let messages + switch (locale) { + case 'fr-FR': + messages = await import('../../i18n/fr-FR.yml') + break + default: + messages = await import('../../i18n/en-US.yml') + break + } + return flatten(messages) } diff --git a/package.json b/package.json index 99339bec0..8134b89b3 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "currency-formatter": "^1.4.2", "d3-selection": "^1.3.0", "d3-zoom": "^1.7.1", + "flat": "^5.0.2", "font-awesome": "^4.7.0", "formik": "^2.1.5", "formik-error-focus": "^1.1.0", diff --git a/yarn.lock b/yarn.lock index 17fec2754..ebd456ace 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7172,6 +7172,11 @@ flat@^4.0.0: dependencies: is-buffer "~2.0.3" +flat@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" + integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== + flatted@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.1.tgz#69e57caa8f0eacbc281d2e2cb458d46fdb449e08" From 68a1fd51d76d5c84f4cb34819969e7a4dd100db5 Mon Sep 17 00:00:00 2001 From: philip-cline Date: Thu, 3 Jun 2021 13:59:55 -0400 Subject: [PATCH 07/22] test(creat-otp-reducer snapshot): update create-otp-reducer snapshot --- __tests__/reducers/__snapshots__/create-otp-reducer.js.snap | 2 ++ 1 file changed, 2 insertions(+) diff --git a/__tests__/reducers/__snapshots__/create-otp-reducer.js.snap b/__tests__/reducers/__snapshots__/create-otp-reducer.js.snap index 35365d63f..50c96f0c8 100644 --- a/__tests__/reducers/__snapshots__/create-otp-reducer.js.snap +++ b/__tests__/reducers/__snapshots__/create-otp-reducer.js.snap @@ -100,7 +100,9 @@ Object { "trips": Object {}, }, "ui": Object { + "currency": "USD", "diagramLeg": null, + "locale": "en-US", "mobileScreen": 1, "printView": false, }, From 9009c79aa83767acae377272a12fde75b784980b Mon Sep 17 00:00:00 2001 From: philip-cline Date: Tue, 8 Jun 2021 11:18:15 -0400 Subject: [PATCH 08/22] feat(i18n): Add overrides for translated strings --- example-config.yml | 14 ++++++++++++++ lib/components/app/responsive-webapp.js | 3 ++- lib/util/i18n.js | 20 ++++++++++++++++---- 3 files changed, 32 insertions(+), 5 deletions(-) diff --git a/example-config.yml b/example-config.yml index 921e180ac..643f89937 100644 --- a/example-config.yml +++ b/example-config.yml @@ -208,6 +208,20 @@ itinerary: # - WALK # - BICYCLE +### Language section to override strings. +### Strings can be set globally for all languages (e.g. for strings that are brands/nouns, +### e.g. TriMet's "TransitTracker") or by language. +### The nested structure should be the same as the language files under the i18n folder. +# language: +# allLanguages +# common: +# accessModes: +# bikeshare: Relay Bike +# en-US: +# common: +# accessModes: +# bikeshare: Blue Bike + ### If using OTP Middleware, you can define the optional phone number options below. # phoneFormatOptions: # # ISO 2-letter country code for phone number formats (defaults to 'US') diff --git a/lib/components/app/responsive-webapp.js b/lib/components/app/responsive-webapp.js index 7e290b805..fc921a4f6 100644 --- a/lib/components/app/responsive-webapp.js +++ b/lib/components/app/responsive-webapp.js @@ -294,7 +294,7 @@ class RouterWrapperWithAuth0 extends Component { // If the locale changes (or no prevProps are provided), update the messages for that locale. const locale = this.props.locale if (locale && (!prevProps || locale !== prevProps.locale)) { - this.setState({ messages: await loadLocaleData(locale) }) + this.setState({ messages: await loadLocaleData(locale, this.props.customMessages) }) } } @@ -413,6 +413,7 @@ class RouterWrapperWithAuth0 extends Component { const mapStateToWrapperProps = (state, ownProps) => ({ auth0Config: getAuth0Config(state.otp.config.persistence), + customMessages: state.otp.config.language, locale: state.otp.ui.locale, routerConfig: state.otp.config.reactRouter }) diff --git a/lib/util/i18n.js b/lib/util/i18n.js index 0f41cc42b..a4c1c0672 100644 --- a/lib/util/i18n.js +++ b/lib/util/i18n.js @@ -1,11 +1,14 @@ import flatten from 'flat' /* - * Load the localized strings for all languages here, and flatten to a map of - * id => string. + * Load the localized strings for all languages here, + * merge with the provided customized strings from state.otp.config.language + * (overrides can be defined per language or for all languages) + * and flatten to a map of + * id => string. * FIXME: Load languages on demand using fetch. */ -export async function loadLocaleData (locale) { +export async function loadLocaleData (locale, customMessages) { let messages switch (locale) { case 'fr-FR': @@ -15,5 +18,14 @@ export async function loadLocaleData (locale) { messages = await import('../../i18n/en-US.yml') break } - return flatten(messages) + + // Merge custom strings into the standard language strings. + const mergedMessages = { + ...flatten(messages), + // Override the predefined strings with the custom ones, if any provided. + ...flatten(customMessages['allLanguages'] || {}), + ...flatten(customMessages[locale] || {}) + } + + return mergedMessages } From f5a74fabe0e292e3a3f6cb17278548d063945c14 Mon Sep 17 00:00:00 2001 From: Phil Cline Date: Tue, 15 Jun 2021 10:56:05 -0400 Subject: [PATCH 09/22] refactor(i18n): Respond to PR feedback. --- lib/components/app/responsive-webapp.js | 37 +++++++++++-------- .../narrative/default/default-itinerary.js | 4 +- lib/reducers/create-otp-reducer.js | 3 +- lib/util/i18n.js | 29 ++++++++++++++- 4 files changed, 53 insertions(+), 20 deletions(-) diff --git a/lib/components/app/responsive-webapp.js b/lib/components/app/responsive-webapp.js index fc921a4f6..d9bf15f87 100644 --- a/lib/components/app/responsive-webapp.js +++ b/lib/components/app/responsive-webapp.js @@ -282,19 +282,21 @@ class RouterWrapperWithAuth0 extends Component { messages: null } - async componentDidMount () { + componentDidMount () { this.updateMessages() } - async componentDidUpdate (prevProps) { - this.updateMessages(prevProps) + componentDidUpdate (prevProps) { + this.updateMessages(prevProps.locale) } - async updateMessages (prevProps) { - // If the locale changes (or no prevProps are provided), update the messages for that locale. - const locale = this.props.locale - if (locale && (!prevProps || locale !== prevProps.locale)) { - this.setState({ messages: await loadLocaleData(locale, this.props.customMessages) }) + async updateMessages (previousLocale) { + // If the locale changes (or no prevProps are provided), update the messages + // for that locale. + const {customMessages, locale} = this.props + if (locale && (!previousLocale || locale !== previousLocale)) { + const messages = await loadLocaleData(locale, customMessages) + this.setState({messages}) } } @@ -302,6 +304,7 @@ class RouterWrapperWithAuth0 extends Component { const { auth0Config, components, + defaultLocale, locale, processSignIn, routerConfig, @@ -311,7 +314,7 @@ class RouterWrapperWithAuth0 extends Component { const router = ( - + @@ -411,12 +414,16 @@ class RouterWrapperWithAuth0 extends Component { } } -const mapStateToWrapperProps = (state, ownProps) => ({ - auth0Config: getAuth0Config(state.otp.config.persistence), - customMessages: state.otp.config.language, - locale: state.otp.ui.locale, - routerConfig: state.otp.config.reactRouter -}) +const mapStateToWrapperProps = (state, ownProps) => { + const { defaultLocale, language, persistence, reactRouter } = state.otp.config + return { + auth0Config: getAuth0Config(persistence), + customMessages: language, + defaultLocale: defaultLocale || 'en-US', + locale: state.otp.ui.locale, + routerConfig: reactRouter + } +} const mapWrapperDispatchToProps = { processSignIn: authActions.processSignIn, diff --git a/lib/components/narrative/default/default-itinerary.js b/lib/components/narrative/default/default-itinerary.js index 615904c78..51d4c135a 100644 --- a/lib/components/narrative/default/default-itinerary.js +++ b/lib/components/narrative/default/default-itinerary.js @@ -244,9 +244,9 @@ class DefaultItinerary extends NarrativeItinerary { const mapStateToProps = (state, ownProps) => { return { locale: state.otp.ui.locale, - currency: state.otp.ui.currency, + currency: state.otp.config.localization?.currency || 'USD', messages: state.otp.ui.messages, - use24HourFormat: state.user.loggedInUser == null ? false : state.user.loggedInUser.use24HourFormat + use24HourFormat: state.user.loggedInUser?.use24HourFormat ?? false } } diff --git a/lib/reducers/create-otp-reducer.js b/lib/reducers/create-otp-reducer.js index a0003fbc0..8c07e9032 100644 --- a/lib/reducers/create-otp-reducer.js +++ b/lib/reducers/create-otp-reducer.js @@ -238,8 +238,7 @@ export function getInitialState (userDefinedConfig) { rideEstimates: {} }, ui: { - currency: config.localization?.currency || 'USD', - locale: config.localization?.locale || 'en-US', + locale: config.localization?.defaultLocale || 'en-US', mobileScreen: MobileScreens.WELCOME_SCREEN, printView: window.location.href.indexOf('/print/') !== -1, diagramLeg: null diff --git a/lib/util/i18n.js b/lib/util/i18n.js index a4c1c0672..ab7b832ac 100644 --- a/lib/util/i18n.js +++ b/lib/util/i18n.js @@ -1,5 +1,31 @@ import flatten from 'flat' +/** + * List of the supported locales. + * FIXME: generate this list from available yml files in the i18n folder instead + * (even if we end up loading locale data using fetch). + */ +const locales = { + en: { + us: 'en-US' + }, + fr: { + fr: 'fr-FR' + } +} + +/** + * Match the provided locale (language and region) to the list of supported locales. + * Default to english if unsupported. + */ +function getMatchingLocaleString (locale = '', defaultLocale = 'en-US') { + const [lang, region] = locale.toLowerCase().split('-') + const language = locales[lang] + if (!language) return defaultLocale + const defaultRegion = Object.keys(language)[0] + return language[region] || language[defaultRegion] +} + /* * Load the localized strings for all languages here, * merge with the provided customized strings from state.otp.config.language @@ -9,8 +35,9 @@ import flatten from 'flat' * FIXME: Load languages on demand using fetch. */ export async function loadLocaleData (locale, customMessages) { + const matchedLocale = getMatchingLocaleString(locale) let messages - switch (locale) { + switch (matchedLocale) { case 'fr-FR': messages = await import('../../i18n/fr-FR.yml') break From e9690318512249bcfbd887b08b048cb58d00fe5c Mon Sep 17 00:00:00 2001 From: Phil Cline Date: Tue, 15 Jun 2021 12:11:20 -0400 Subject: [PATCH 10/22] test(create-otp-reducer snapshot): Fix snapshots --- __tests__/reducers/__snapshots__/create-otp-reducer.js.snap | 1 - 1 file changed, 1 deletion(-) diff --git a/__tests__/reducers/__snapshots__/create-otp-reducer.js.snap b/__tests__/reducers/__snapshots__/create-otp-reducer.js.snap index 50c96f0c8..0b741c224 100644 --- a/__tests__/reducers/__snapshots__/create-otp-reducer.js.snap +++ b/__tests__/reducers/__snapshots__/create-otp-reducer.js.snap @@ -100,7 +100,6 @@ Object { "trips": Object {}, }, "ui": Object { - "currency": "USD", "diagramLeg": null, "locale": "en-US", "mobileScreen": 1, From 8a7b518d77ae33515333e90e0a3b51690bf8431e Mon Sep 17 00:00:00 2001 From: Phil Cline Date: Tue, 15 Jun 2021 18:14:48 -0400 Subject: [PATCH 11/22] refactor(example-config): Update example config --- example-config.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/example-config.yml b/example-config.yml index 643f89937..e64c772d8 100644 --- a/example-config.yml +++ b/example-config.yml @@ -222,6 +222,11 @@ itinerary: # accessModes: # bikeshare: Blue Bike +### Localization section to provide language/locale settings +#localization: +# currency: 'USD' +# defaultLocale: 'en-US' + ### If using OTP Middleware, you can define the optional phone number options below. # phoneFormatOptions: # # ISO 2-letter country code for phone number formats (defaults to 'US') From 94c6084e5b06d2905c541d1f6a492de5627db2cd Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Thu, 17 Jun 2021 10:11:48 -0400 Subject: [PATCH 12/22] docs(i18n): Add description to the base (English) message file. --- i18n/en-US.yml | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/i18n/en-US.yml b/i18n/en-US.yml index 4da90fa40..aad8c3ddc 100644 --- a/i18n/en-US.yml +++ b/i18n/en-US.yml @@ -1,6 +1,30 @@ _id: en-US _name: English +# This file contains localized strings (a.k.a. messages) for the language indicated above: +# - Messages are organized in various categories and sub-categories. +# - A component or JS module can use messages from one or more categories. +# - In the code, messages are retrieved using an ID that is simply the path to the message. +# Use the dot '.' to separate categories and sub-categories in the path. +# For instance, for the message defined in YML below: +# common +# otpTransitModes +# subway: Metro# +# then use the snippet below with the corresponding message id: +# // renders "Metro". +# +# It is important that message ids in the code be consistent with +# the categories in this file. Below are some general guidelines: +# - For starters, there is a 'components' category and a 'common' category. +# Additional categories may be added as needed. +# - Each sub-category under 'components' denotes a component and +# should contain messages that are used only by that component (e.g. button captions). +# - In contrast, some strings are common to multiple components, +# so it makes sense to group them by theme (e.g. accessModes) under the 'common' category. + + +# Component-specific messages (e.g. button captions) +# are defined for each component under the 'components' category. components: DefaultItinerary: clickDetails: Click to view details @@ -15,7 +39,10 @@ components: # TODO: Distinguish between one hour (singular) and 2 hours or more? tripDurationFormat: "{hours, number} hr {minutes, number} min" +# Common messages that appear in multiple components and modules +# are grouped below by topic. common: + # OTP access modes accessModes: bike: Bike bikeshare: Bikeshare @@ -23,13 +50,16 @@ common: micromobility: E-Scooter micromobilityRent: Rental E-Scooter walk: Walk - + + # OTP transit modes + # Note that identifers are OTP modes converted to lowercase. otpTransitModes: tram: Streetcar subway: Subway rail: Rail bus: Bus ferry: Ferry + # The original OTP mode id is CABLE_CAR. Lowercase makes it cable_car. cable_car: Cable Car gondola: Gondola funicular: Funicular From 6127eb880317f1e5458c250106af35ef19f30922 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Thu, 17 Jun 2021 10:15:21 -0400 Subject: [PATCH 13/22] docs(i18n): Fix typo in en-US.yml --- i18n/en-US.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/i18n/en-US.yml b/i18n/en-US.yml index aad8c3ddc..f8a220fad 100644 --- a/i18n/en-US.yml +++ b/i18n/en-US.yml @@ -52,7 +52,7 @@ common: walk: Walk # OTP transit modes - # Note that identifers are OTP modes converted to lowercase. + # Note that identifiers are OTP modes converted to lowercase. otpTransitModes: tram: Streetcar subway: Subway From 5e2b6708003925b1163418bd629f3870889ec6a2 Mon Sep 17 00:00:00 2001 From: Phil Cline Date: Thu, 17 Jun 2021 14:55:03 -0400 Subject: [PATCH 14/22] refactor(Formatting): Respond to comments --- lib/components/app/responsive-webapp.js | 6 +- .../narrative/default/default-itinerary.js | 74 ++++++++++++++----- 2 files changed, 59 insertions(+), 21 deletions(-) diff --git a/lib/components/app/responsive-webapp.js b/lib/components/app/responsive-webapp.js index d9bf15f87..1da4eee4a 100644 --- a/lib/components/app/responsive-webapp.js +++ b/lib/components/app/responsive-webapp.js @@ -314,7 +314,11 @@ class RouterWrapperWithAuth0 extends Component { const router = ( - + diff --git a/lib/components/narrative/default/default-itinerary.js b/lib/components/narrative/default/default-itinerary.js index 51d4c135a..7649f8742 100644 --- a/lib/components/narrative/default/default-itinerary.js +++ b/lib/components/narrative/default/default-itinerary.js @@ -15,8 +15,7 @@ const { calculateFares, isBicycle, isTransit, isMicromobility } = coreUtils.itin /** * Obtains the description of an itinerary in the given locale. */ -// FIXME move to core utils -function getItineraryDescription (itinerary) { +function ItineraryDescription ({itinerary}) { let primaryTransitDuration = 0 let accessModeId = 'walk' let transitMode @@ -25,7 +24,9 @@ function getItineraryDescription (itinerary) { if (isTransit(mode) && duration > primaryTransitDuration) { // TODO: convert OTP's TRAM mode to the correct wording for Portland primaryTransitDuration = duration - transitMode = + transitMode = } if (isBicycle(mode)) accessModeId = 'bike' if (isMicromobility(mode)) accessModeId = 'micromobility' @@ -34,16 +35,21 @@ function getItineraryDescription (itinerary) { if (mode === 'CAR') accessModeId = 'drive' }) - const mainMode = + const mainMode = return transitMode - ? + ? : mainMode } /** * Formats the given duration according to the selected locale. */ -function formatDuration (duration) { +function FormattedDuration ({duration}) { const dur = moment.duration(duration, 'seconds') const hours = dur.hours() const minutes = dur.minutes() @@ -64,12 +70,15 @@ function formatDuration (duration) { } } -function formatTime (startTime, endTime, timeFormat) { +function FormattedTime ({startTime, endTime, timeFormat}) { return ( - + ) } @@ -78,7 +87,9 @@ const ITINERARY_ATTRIBUTES = [ alias: 'best', id: 'duration', order: 0, - render: (itinerary, options) => formatDuration(itinerary.duration) + render: (itinerary, options) => () }, { alias: 'departureTime', @@ -86,11 +97,28 @@ const ITINERARY_ATTRIBUTES = [ order: 1, render: (itinerary, options) => { if (options.isSelected) { - if (options.selection === 'ARRIVALTIME') return formatTime(itinerary.endTime, options) - else return formatTime(itinerary.startTime, options) + if (options.selection === 'ARRIVALTIME') { + return ( + + ) + } else { + return ( + + ) + } } return ( - formatTime(itinerary.startTime, itinerary.endTime, options.timeFormat) + ) } }, @@ -101,7 +129,12 @@ const ITINERARY_ATTRIBUTES = [ // Get unformatted transit fare portion only (in cents). const { transitFare } = calculateFares(itinerary) return ( - + ) } }, @@ -114,7 +147,7 @@ const ITINERARY_ATTRIBUTES = [ return ( // FIXME: For CAR mode, walk time considers driving time. - {formatDuration(itinerary.walkTime)}{' '} + {}{' '}
    - {getItineraryDescription(itinerary)} + {}
      {ITINERARY_ATTRIBUTES @@ -213,7 +247,7 @@ class DefaultItinerary extends NarrativeItinerary { options.selection = this.props.sort.type } options.LegIcon = LegIcon - options.timeFormat = this.props.use24HourFormat ? 'H:mm' : 'h:mm a' + options.timeFormat = use24HourFormat ? 'H:mm' : 'h:mm a' options.currency = currency return (
    • From 86177c5c161dd57eadf97573538ac96ec074c9d7 Mon Sep 17 00:00:00 2001 From: Phil Cline Date: Thu, 17 Jun 2021 15:05:24 -0400 Subject: [PATCH 15/22] refactor(formatting): fix formatting --- lib/components/app/responsive-webapp.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/components/app/responsive-webapp.js b/lib/components/app/responsive-webapp.js index 1da4eee4a..4b39a5046 100644 --- a/lib/components/app/responsive-webapp.js +++ b/lib/components/app/responsive-webapp.js @@ -315,8 +315,8 @@ class RouterWrapperWithAuth0 extends Component { const router = ( Date: Tue, 22 Jun 2021 17:22:03 -0400 Subject: [PATCH 16/22] refactor(actions/ui): Create action to set locale, load localized strings. --- lib/actions/ui.js | 20 ++++++- lib/components/app/responsive-webapp.js | 76 +++++++++++-------------- lib/reducers/create-otp-reducer.js | 6 +- lib/util/i18n.js | 8 +++ 4 files changed, 65 insertions(+), 45 deletions(-) diff --git a/lib/actions/ui.js b/lib/actions/ui.js index f7e28faf6..e511ad0c7 100644 --- a/lib/actions/ui.js +++ b/lib/actions/ui.js @@ -11,10 +11,11 @@ import { setActiveSearch } from './form' import { clearLocation } from './map' +import { getDefaultLocale, loadLocaleData } from '../util/i18n' import { setActiveItinerary } from './narrative' import { getUiUrlParams } from '../util/state' -export const updateLocale = createAction('UPDATE_LOCALE') +const updateLocale = createAction('UPDATE_LOCALE') /** * Wrapper function for history#push (or, if specified, replace, etc.) @@ -314,3 +315,20 @@ export function showMobileSearchScreen () { dispatch(setMobileScreen(MobileScreens.SEARCH_FORM)) } } + +/** + * Sets the locale to the specified value, + * and loads the corresponding localized messages. + * If the specified locale is null, fall back to the defaultLocale + * set in the configuration. + */ +export function setLocale (locale) { + return async function (dispatch, getState) { + const { config } = getState().otp + const { language: customMessages } = config + const effectiveLocale = locale || getDefaultLocale(config) + const messages = await loadLocaleData(effectiveLocale, customMessages) + + dispatch(updateLocale({ locale: effectiveLocale, messages })) + } +} diff --git a/lib/components/app/responsive-webapp.js b/lib/components/app/responsive-webapp.js index 4b39a5046..ad10a40bc 100644 --- a/lib/components/app/responsive-webapp.js +++ b/lib/components/app/responsive-webapp.js @@ -40,7 +40,7 @@ import { URL_ROOT } from '../../util/constants' import { ComponentContext } from '../../util/contexts' -import { loadLocaleData } from '../../util/i18n' +import { getDefaultLocale } from '../../util/i18n' import { getActiveItinerary, getTitle } from '../../util/state' import AfterSignInScreen from '../user/after-signin-screen' import BeforeSignInScreen from '../user/before-signin-screen' @@ -63,7 +63,20 @@ class ResponsiveWebapp extends Component { /** Lifecycle methods **/ componentDidUpdate (prevProps) { - const { currentPosition, location, query, title } = this.props + const { + activeSearchId, + currentPosition, + formChanged, + initZoomOnLocate, + location, + matchContentToUrl, + query, + setLocale, + setLocationToCurrent, + setMapCenter, + setMapZoom, + title + } = this.props document.title = title const urlParams = coreUtils.query.getUrlParams() const newSearchId = urlParams.ui_activeSearch @@ -72,13 +85,13 @@ class ResponsiveWebapp extends Component { // has been routed to (see handleBackButtonPress) and there is no need to // trigger a form change because necessarily the query will be different // from the previous query. - const replanningTrip = newSearchId && this.props.activeSearchId && newSearchId !== this.props.activeSearchId + const replanningTrip = newSearchId && activeSearchId && newSearchId !== activeSearchId if (!isEqual(prevProps.query, query) && !replanningTrip) { // Trigger on form change action if previous query is different from // current one AND trip is not being replanned already. This will // determine whether a search needs to be made, the mobile view needs // updating, etc. - this.props.formChanged(prevProps.query, query) + formChanged(prevProps.query, query) } // check if device position changed (typically only set once, on initial page load) @@ -90,11 +103,11 @@ class ResponsiveWebapp extends Component { } // if in mobile mode and from field is not set, use current location as from and recenter map - if (isMobile() && this.props.query.from === null) { - this.props.setLocationToCurrent({ locationType: 'from' }) - this.props.setMapCenter(pt) - if (this.props.initZoomOnLocate) { - this.props.setMapZoom({ zoom: this.props.initZoomOnLocate }) + if (isMobile() && query.from === null) { + setLocationToCurrent({ locationType: 'from' }) + setMapCenter(pt) + if (initZoomOnLocate) { + setMapZoom({ zoom: initZoomOnLocate }) } } } @@ -102,12 +115,12 @@ class ResponsiveWebapp extends Component { // main content needs to switch between, for example, a viewer and a search. if (!isEqual(location.pathname, prevProps.location.pathname)) { // console.log('url changed to', location.pathname) - this.props.matchContentToUrl(location) + matchContentToUrl(location) } - // If the URL search parameters change (e.g., user modifies anything after ?, e.g. locale) + // If the URL locale parameter changes or is initially blank, (e.g., user modifies anything after ?, e.g. locale) // update the corresponding redux state. - if (urlParams.locale && urlParams.locale !== prevProps.locale) { - this.props.updateLocale(urlParams.locale) + if ((!urlParams.locale && !prevProps.locale) || (urlParams.locale && urlParams.locale !== prevProps.locale)) { + setLocale(urlParams.setLocale) } // Check for change between ITINERARY and PROFILE routingTypes // TODO: restore this for profile mode @@ -257,10 +270,10 @@ const mapDispatchToProps = { matchContentToUrl: uiActions.matchContentToUrl, parseUrlQueryString: formActions.parseUrlQueryString, receivedPositionResponse: locationActions.receivedPositionResponse, + setLocale: uiActions.setLocale, setLocationToCurrent: mapActions.setLocationToCurrent, setMapCenter: configActions.setMapCenter, - setMapZoom: configActions.setMapZoom, - updateLocale: uiActions.updateLocale + setMapZoom: configActions.setMapZoom } const history = createHashHistory() @@ -278,34 +291,13 @@ const WebappWithRouter = withRouter( * so that Auth0 services are available everywhere. */ class RouterWrapperWithAuth0 extends Component { - state = { - messages: null - } - - componentDidMount () { - this.updateMessages() - } - - componentDidUpdate (prevProps) { - this.updateMessages(prevProps.locale) - } - - async updateMessages (previousLocale) { - // If the locale changes (or no prevProps are provided), update the messages - // for that locale. - const {customMessages, locale} = this.props - if (locale && (!previousLocale || locale !== previousLocale)) { - const messages = await loadLocaleData(locale, customMessages) - this.setState({messages}) - } - } - render () { const { auth0Config, components, defaultLocale, locale, + localizedMessages, processSignIn, routerConfig, showAccessTokenError, @@ -315,8 +307,8 @@ class RouterWrapperWithAuth0 extends Component { const router = ( { - const { defaultLocale, language, persistence, reactRouter } = state.otp.config + const { persistence, reactRouter } = state.otp.config return { auth0Config: getAuth0Config(persistence), - customMessages: language, - defaultLocale: defaultLocale || 'en-US', + defaultLocale: getDefaultLocale(state.otp.config), locale: state.otp.ui.locale, + localizedMessages: state.otp.ui.localizedMessages, routerConfig: reactRouter } } diff --git a/lib/reducers/create-otp-reducer.js b/lib/reducers/create-otp-reducer.js index 8c07e9032..3f802884c 100644 --- a/lib/reducers/create-otp-reducer.js +++ b/lib/reducers/create-otp-reducer.js @@ -238,7 +238,8 @@ export function getInitialState (userDefinedConfig) { rideEstimates: {} }, ui: { - locale: config.localization?.defaultLocale || 'en-US', + locale: null, + localizedMessages: {}, mobileScreen: MobileScreens.WELCOME_SCREEN, printView: window.location.href.indexOf('/print/') !== -1, diagramLeg: null @@ -953,7 +954,8 @@ function createOtpReducer (config) { return update(state, { ui: { previousItineraryView: { $set: action.payload } } }) case 'UPDATE_LOCALE': return update(state, { ui: { - locale: { $set: action.payload || 'en-US' } + locale: { $set: action.payload.locale }, + localizedMessages: { $set: action.payload.messages } }}) default: return state diff --git a/lib/util/i18n.js b/lib/util/i18n.js index ab7b832ac..b0e91f165 100644 --- a/lib/util/i18n.js +++ b/lib/util/i18n.js @@ -56,3 +56,11 @@ export async function loadLocaleData (locale, customMessages) { return mergedMessages } + +/** + * Gets the localization > defaultLocale configuration setting, or 'en-US' if not set. + */ +export function getDefaultLocale (config) { + const { localization = {} } = config + return localization.defaultLocale || 'en-US' +} From 85f2d0f764a45dd77e1ab37e9b5ba4bb307ac1b3 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Tue, 22 Jun 2021 17:26:02 -0400 Subject: [PATCH 17/22] test(create-otp-reducer): Update snapshot. --- __tests__/reducers/__snapshots__/create-otp-reducer.js.snap | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/__tests__/reducers/__snapshots__/create-otp-reducer.js.snap b/__tests__/reducers/__snapshots__/create-otp-reducer.js.snap index 0b741c224..2b89cebf3 100644 --- a/__tests__/reducers/__snapshots__/create-otp-reducer.js.snap +++ b/__tests__/reducers/__snapshots__/create-otp-reducer.js.snap @@ -101,7 +101,8 @@ Object { }, "ui": Object { "diagramLeg": null, - "locale": "en-US", + "locale": null, + "localizedMessages": Object {}, "mobileScreen": 1, "printView": false, }, From 7102a6c76a7749a8986e4428b142a5d6eeb656d6 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Tue, 22 Jun 2021 17:35:55 -0400 Subject: [PATCH 18/22] refactor(DefaultItinerary): Tweak formatting. --- .../narrative/default/default-itinerary.js | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/lib/components/narrative/default/default-itinerary.js b/lib/components/narrative/default/default-itinerary.js index 7649f8742..0fa8f01bb 100644 --- a/lib/components/narrative/default/default-itinerary.js +++ b/lib/components/narrative/default/default-itinerary.js @@ -35,9 +35,7 @@ function ItineraryDescription ({itinerary}) { if (mode === 'CAR') accessModeId = 'drive' }) - const mainMode = + const mainMode = return transitMode ? () + render: (itinerary, options) => ( + + ) }, { alias: 'departureTime', @@ -147,7 +145,7 @@ const ITINERARY_ATTRIBUTES = [ return ( // FIXME: For CAR mode, walk time considers driving time. - {}{' '} + {' '}
      -
      - {} +
      +
        {ITINERARY_ATTRIBUTES @@ -277,8 +274,8 @@ class DefaultItinerary extends NarrativeItinerary { const mapStateToProps = (state, ownProps) => { return { - locale: state.otp.ui.locale, currency: state.otp.config.localization?.currency || 'USD', + locale: state.otp.ui.locale, messages: state.otp.ui.messages, use24HourFormat: state.user.loggedInUser?.use24HourFormat ?? false } From 2f66ec4f23cc34feb530cac5621277dc7f82e6cb Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Tue, 22 Jun 2021 17:40:13 -0400 Subject: [PATCH 19/22] refactor(actions/user): Remove unused language attributes. --- lib/actions/user.js | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/actions/user.js b/lib/actions/user.js index acc657c6b..ddc7eb1f0 100644 --- a/lib/actions/user.js +++ b/lib/actions/user.js @@ -31,7 +31,6 @@ function createNewUser (auth0User) { auth0UserId: auth0User.sub, email: auth0User.email, hasConsentedToTerms: false, // User must agree to terms. - language: 'fr', notificationChannel: 'email', phoneNumber: '', savedLocations: [], From a9fdcf11538255d1d8ace74fd7fe1ee0fbd7cd64 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Thu, 24 Jun 2021 14:42:52 -0400 Subject: [PATCH 20/22] refactor(DefaultItinerary): Remove unused props. --- lib/components/narrative/default/default-itinerary.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/components/narrative/default/default-itinerary.js b/lib/components/narrative/default/default-itinerary.js index 0fa8f01bb..5271880ab 100644 --- a/lib/components/narrative/default/default-itinerary.js +++ b/lib/components/narrative/default/default-itinerary.js @@ -275,8 +275,6 @@ class DefaultItinerary extends NarrativeItinerary { const mapStateToProps = (state, ownProps) => { return { currency: state.otp.config.localization?.currency || 'USD', - locale: state.otp.ui.locale, - messages: state.otp.ui.messages, use24HourFormat: state.user.loggedInUser?.use24HourFormat ?? false } } From 9b7d311187b3fb919200faf427251bb58668cf0e Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Wed, 30 Jun 2021 14:42:18 -0400 Subject: [PATCH 21/22] docs: Add clarification for using a currency setting. --- example-config.yml | 4 ++++ lib/components/narrative/default/default-itinerary.js | 3 +++ 2 files changed, 7 insertions(+) diff --git a/example-config.yml b/example-config.yml index e64c772d8..b7af9e2b1 100644 --- a/example-config.yml +++ b/example-config.yml @@ -224,6 +224,10 @@ itinerary: ### Localization section to provide language/locale settings #localization: +# # An ambient currency should be defined here (defaults to USD). +# # In some components such as DefaultItinerary, we display a cost element +# # that falls back to $0.00 (or its equivalent in the configured ambient currency +# # and in the user-selected locale) if no fare or currency info is available. # currency: 'USD' # defaultLocale: 'en-US' diff --git a/lib/components/narrative/default/default-itinerary.js b/lib/components/narrative/default/default-itinerary.js index 5271880ab..8f14ac742 100644 --- a/lib/components/narrative/default/default-itinerary.js +++ b/lib/components/narrative/default/default-itinerary.js @@ -274,6 +274,9 @@ class DefaultItinerary extends NarrativeItinerary { const mapStateToProps = (state, ownProps) => { return { + // The configured (ambient) currency is needed for rendering the cost + // of itineraries whether they include a fare or not, in which case + // we show $0.00 or its equivalent in the configured currency and selected locale. currency: state.otp.config.localization?.currency || 'USD', use24HourFormat: state.user.loggedInUser?.use24HourFormat ?? false } From 8a8ee89c9383ad12d9cb4668cab61ac60d5b74c0 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Wed, 30 Jun 2021 14:51:32 -0400 Subject: [PATCH 22/22] refactor(ResponsiveWebapp): Fix variable typo, remove unused prop. --- lib/components/app/responsive-webapp.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/components/app/responsive-webapp.js b/lib/components/app/responsive-webapp.js index ad10a40bc..f727a60c2 100644 --- a/lib/components/app/responsive-webapp.js +++ b/lib/components/app/responsive-webapp.js @@ -120,7 +120,7 @@ class ResponsiveWebapp extends Component { // If the URL locale parameter changes or is initially blank, (e.g., user modifies anything after ?, e.g. locale) // update the corresponding redux state. if ((!urlParams.locale && !prevProps.locale) || (urlParams.locale && urlParams.locale !== prevProps.locale)) { - setLocale(urlParams.setLocale) + setLocale(urlParams.locale) } // Check for change between ITINERARY and PROFILE routingTypes // TODO: restore this for profile mode @@ -252,7 +252,6 @@ const mapStateToProps = (state, ownProps) => { activeSearchId: state.otp.activeSearchId, currentPosition: state.otp.location.currentPosition, locale: state.otp.ui.locale, - currency: state.otp.ui.currency, initZoomOnLocate: state.otp.config.map && state.otp.config.map.initZoomOnLocate, mobileScreen: state.otp.ui.mobileScreen, modeGroups: state.otp.config.modeGroups,