diff --git a/__tests__/components/viewers/__snapshots__/stop-viewer.js.snap b/__tests__/components/viewers/__snapshots__/stop-viewer.js.snap index e83ba8e43..a6415dde8 100644 --- a/__tests__/components/viewers/__snapshots__/stop-viewer.js.snap +++ b/__tests__/components/viewers/__snapshots__/stop-viewer.js.snap @@ -1,358 +1,376 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`components > viewers > stop viewer should render countdown times after midnight with no date if it is the previous day 1`] = ` - - - + + -
- - + common.forms.back + + + +
+
+ + W Burnside & SW 18th + +
+
- - W Burnside & SW 18th - -
-
-
-
-
-
- - Stop ID - - : - 9860 - -
- - Plan a trip: - - - - + +
+ + + components.StopViewer.planTrip + + + + + + + - - - - viewers > stop viewer should render countdown times after size="0.9em" title="From Location Icon" > - - - From Location Icon - - - - - - - - - - - - - - - - + From here + + + + + + - - - - viewers > stop viewer should render countdown times after size="0.9em" title="To Location Icon" > - - - To Location Icon - - - - - - - - - - - - - - - - -
- -
- + To here + + + + + + + +
+ +
+ +
+ viewers > stop viewer should render countdown times after "timepoint": true, "tripId": "TriMet:9230377", }, - ], - }, - Object { - "pattern": Object { - "desc": "20 to Gresham Transit Center (TriMet:2253) from Beaverton Transit Center (TriMet:9978) express", - "headsign": "Gresham TC", - "id": "TriMet:20:1:01", - }, - "times": Array [ Object { "arrivalDelay": 0, "blockId": "2067", @@ -653,238 +841,75 @@ exports[`components > viewers > stop viewer should render countdown times after "timepoint": true, "tripId": "TriMet:9230307", }, - ], - }, - ], - "stopTimesLastUpdated": 1565248650040, - "type": 3, - "url": "http://trimet.org/#tracker/stop/9860", - } - } - stopViewerConfig={ - Object { - "numberOfDepartures": 3, - "showBlockIds": false, - "timeRange": 345600, - } - } - timeFormat="HH:mm" - toggleAutoRefresh={[Function]} - transitOperators={Array []} - viewedStop={ - Object { - "stopId": "TriMet:9860", - } - } - > -
- -
- - 20 - - To - Gresham TC -
-
- + + components.PatternRow.routeName + +
+
-
-
- +
+
- viewers > stop viewer should render countdown times after "marginRight": 2, } } + type="clock-o" > - viewers > stop viewer should render countdown times after "marginRight": 2, } } - /> - - -
-
+ + + +
+
-
- 52 min + > +
+ , + "isDue": "false", + } + } + > + components.StopTimeCell.imminentArrival + +
-
- -
-
-
+
- - - - - - + + + + + +
-
- - -
- - -
- -
-
-
+
- - - Auto-refresh arrivals? - - -
- -
- + + + + + + 12:17 am + +
+ +
+ +
-
-
-
-
+ + + + `; exports[`components > viewers > stop viewer should render countdown times for stop times departing 48+ hours from start of service 1`] = ` - - - + + -
- - + + + + + + common.forms.back + + + +
+
+ + W Burnside & SW 18th + +
+
- - W Burnside & SW 18th - -
-
-
-
-
-
- - Stop ID - - : - 9860 - -
- - Plan a trip: - - - - + +
+ + + components.StopViewer.planTrip + + + + + + + - - - - viewers > stop viewer should render countdown times for st size="0.9em" title="From Location Icon" > - - - From Location Icon - - - - - - - - - - - - - - - - + From here + + + + + + - - - - viewers > stop viewer should render countdown times for st size="0.9em" title="To Location Icon" > - - - To Location Icon - - - - - - - - - - - - - - - - -
- -
- + To here + + + + + + + +
+ +
+ +
+ viewers > stop viewer should render countdown times for st "timepoint": true, "tripId": "TriMet:9230375", }, - ], - }, - ], - "stopTimesLastUpdated": 1565248650040, - "type": 3, - "url": "http://trimet.org/#tracker/stop/9860", - } - } - stopViewerConfig={ - Object { - "numberOfDepartures": 3, - "showBlockIds": false, - "timeRange": 345600, - } - } - timeFormat="HH:mm" - toggleAutoRefresh={[Function]} - transitOperators={Array []} - viewedStop={ - Object { - "stopId": "TriMet:9860", - } - } - > -
- -
- - 20 - - To - Gresham TC -
-
- + + components.PatternRow.routeName + +
+
-
-
- +
+
- viewers > stop viewer should render countdown times for st "marginRight": 2, } } + type="clock-o" > - viewers > stop viewer should render countdown times for st "marginRight": 2, } } - /> - - -
-
+ + + +
+
-
- 52 min + > +
+ , + "isDue": "false", + } + } + > + components.StopTimeCell.imminentArrival + +
-
- -
-
-
+
- - - - - - + + + + + +
-
- - -
- - -
- -
-
-
+
- - - Auto-refresh arrivals? - - -
- -
- + + + + + + 12:17 am + +
+ +
+ +
-
-
-
-
+ + + + `; exports[`components > viewers > stop viewer should render times after midnight with the correct day of week 1`] = ` - - - + + -
- - + common.forms.back + + + +
+
+ + W Burnside & SW 18th + +
+
- - W Burnside & SW 18th - -
-
-
-
-
-
- - Stop ID - - : - 9860 - -
- - Plan a trip: - - - - + +
+ + + components.StopViewer.planTrip + + + + + + + - - - - viewers > stop viewer should render times after midnight w size="0.9em" title="From Location Icon" > - - - From Location Icon - - - - - - - - - - - - - - - - + From here + + + + + + - - - - viewers > stop viewer should render times after midnight w size="0.9em" title="To Location Icon" > - - - To Location Icon - - - - - - - - - - - - - - - - -
- -
- + To here + + + + + + + +
+ +
+ +
+ viewers > stop viewer should render times after midnight w "timepoint": true, "tripId": "TriMet:9230377", }, - ], - }, - Object { - "pattern": Object { - "desc": "20 to Gresham Transit Center (TriMet:2253) from Beaverton Transit Center (TriMet:9978) express", - "headsign": "Gresham TC", - "id": "TriMet:20:1:01", - }, - "times": Array [ Object { "arrivalDelay": 0, "blockId": "2067", @@ -2451,238 +2747,75 @@ exports[`components > viewers > stop viewer should render times after midnight w "timepoint": true, "tripId": "TriMet:9230307", }, - ], - }, - ], - "stopTimesLastUpdated": 1565248650040, - "type": 3, - "url": "http://trimet.org/#tracker/stop/9860", - } - } - stopViewerConfig={ - Object { - "numberOfDepartures": 3, - "showBlockIds": false, - "timeRange": 345600, - } - } - timeFormat="HH:mm" - toggleAutoRefresh={[Function]} - transitOperators={Array []} - viewedStop={ - Object { - "stopId": "TriMet:9860", - } - } - > -
- -
- - 20 - - To - Gresham TC -
-
- + + components.PatternRow.routeName + +
+
-
-
- +
+
- viewers > stop viewer should render times after midnight w "marginRight": 2, } } + type="clock-o" > - viewers > stop viewer should render times after midnight w "marginRight": 2, } } - /> - - -
-
+ > + + + +
- Thursday -
-
- 00:51 +
+ + common.daysOfWeek.thursday + +
+
+ 12:51 am +
-
- -
-
-
+
- - - - - - + + + + + +
-
- - -
- - -
- -
-
-
+
- - - Auto-refresh arrivals? - - -
- -
- + + + + + + 12:17 am + +
+
+
+
+
- -
-
-
+ + + + `; exports[`components > viewers > stop viewer should render with OTP transit index data 1`] = ` - - - + + -
- - + common.forms.back + + + +
+
+ + W Burnside & SW 8th + +
+
- - W Burnside & SW 8th - -
-
-
-
-
-
- - Stop ID - - : - 715 - -
- - Plan a trip: - - - - + +
+ + + components.StopViewer.planTrip + + + + + + + - - - - viewers > stop viewer should render with OTP transit index size="0.9em" title="From Location Icon" > - - - From Location Icon - - - - - - - - - - - - - - - - + From here + + + + + + - - - - viewers > stop viewer should render with OTP transit index size="0.9em" title="To Location Icon" > - - - To Location Icon - - - - - - - - - - - - - - - - -
- -
- + To here + + + + + + + +
+ +
+ +
+ viewers > stop viewer should render with OTP transit index "timepoint": false, "tripId": "TriMet:9230361", }, - ], - }, - Object { - "pattern": Object { - "desc": "20 to Gresham Transit Center (TriMet:2253) from Beaverton Transit Center (TriMet:9978) express", - "headsign": "Gresham TC", - "id": "TriMet:20:1:01", - }, - "times": Array [ Object { "arrivalDelay": 0, "blockId": "2046", @@ -4016,239 +4359,75 @@ exports[`components > viewers > stop viewer should render with OTP transit index "timepoint": false, "tripId": "TriMet:9230365", }, - ], - }, - ], - "stopTimesLastUpdated": 1565052624406, - "url": "http://trimet.org/#tracker/stop/715", - "vehicleType": -999, - "vehicleTypeSet": false, - "wheelchairBoarding": 0, - "zoneId": "B", - } - } - stopViewerConfig={ - Object { - "numberOfDepartures": 3, - "showBlockIds": false, - "timeRange": 345600, - } - } - timeFormat="HH:mm" - toggleAutoRefresh={[Function]} - transitOperators={Array []} - viewedStop={ - Object { - "stopId": "TriMet:715", - } - } - > -
- -
- - 20 - - To - Gresham TC -
-
- + + components.PatternRow.routeName + +
+
-
-
- +
+
- viewers > stop viewer should render with OTP transit index "marginRight": 2, } } + type="clock-o" > - viewers > stop viewer should render with OTP transit index "marginRight": 2, } } - /> - - -
-
+ > + + + +
- Monday -
-
- 18:00 +
+ + common.daysOfWeek.monday + +
+
+ 6:00 pm +
-
- -
-
-
+
- - - - - - + + + + + +
-
- - -
- - -
- - +
+ + +
+
+ -
- - 36 - - To - Tualatin Park & Ride -
-
- + + components.PatternRow.routeName + +
+
-
-
- +
+
- viewers > stop viewer should render with OTP transit index "marginRight": 2, } } + type="clock-o" > - viewers > stop viewer should render with OTP transit index "marginRight": 2, } } - /> - - -
-
+ > + + + +
- Tuesday -
-
- 16:11 +
+ + common.daysOfWeek.tuesday + +
+
+ 4:11 pm +
-
- -
-
-
+
- - - - - - + + + + + +
-
- - -
- - -
- - +
+ + +
+
+ -
-
-
- - 94 - - To - King City -
-
- -
-
- - - +
+
+
+ + components.PatternRow.routeName + +
+
+ +
+
+ + - - -
-
+ > + + + +
- Tuesday -
-
- 15:22 +
+ + common.daysOfWeek.tuesday + +
+
+ 3:22 pm +
-
- -
-
-
+
- - - - - - + + + + + +
-
- - -
- - -
- - +
+ + +
+
+ -
- - 94 - - To - Sherwood -
-
- + + components.PatternRow.routeName + +
+
-
-
- +
+
- viewers > stop viewer should render with OTP transit index "marginRight": 2, } } + type="clock-o" > - viewers > stop viewer should render with OTP transit index "marginRight": 2, } } - /> - - -
-
+ > + + + +
- Tuesday -
-
- 14:28 +
+ + common.daysOfWeek.tuesday + +
+
+ 2:28 pm +
-
- -
-
-
+
- - - - - - + + + + + +
-
- - -
- - -
- -
-
-
+
- - - Auto-refresh arrivals? - - -
- -
- + + + + + + 5:50 pm + +
+ +
+ +
-
- - - + + + + `; exports[`components > viewers > stop viewer should render with TriMet transit index data 1`] = ` - - - + + -
- - + common.forms.back + + + +
+
+ + W Burnside & SW 8th + +
+
- - W Burnside & SW 8th - -
-
-
-
-
-
- - Stop ID - - : - 715 - -
- - Plan a trip: - - - - + +
+ + + components.StopViewer.planTrip + + + + + + + - - - - viewers > stop viewer should render with TriMet transit in size="0.9em" title="From Location Icon" > - - - From Location Icon - - - - - - - - - - - - - - - - + From here + + + + + + - - - - viewers > stop viewer should render with TriMet transit in size="0.9em" title="To Location Icon" > - - - To Location Icon - - - - - - - - - - - - - - - - -
- -
- + To here + + + + + + + +
+ +
+ +
+ viewers > stop viewer should render with TriMet transit in "timepoint": false, "tripId": "TriMet:9230360", }, - ], - }, - Object { - "pattern": Object { - "desc": "20 to Gresham Transit Center (TriMet:2253) from Beaverton Transit Center (TriMet:9978) express", - "headsign": "Gresham TC", - "id": "TriMet:20:1:01", - }, - "times": Array [ Object { "arrivalDelay": 0, "blockId": "2046", @@ -6394,238 +6812,75 @@ exports[`components > viewers > stop viewer should render with TriMet transit in "timepoint": false, "tripId": "TriMet:9230365", }, - ], - }, - ], - "stopTimesLastUpdated": 1565051923389, - "type": 3, - "url": "http://trimet.org/#tracker/stop/715", - } - } - stopViewerConfig={ - Object { - "numberOfDepartures": 3, - "showBlockIds": false, - "timeRange": 345600, - } - } - timeFormat="HH:mm" - toggleAutoRefresh={[Function]} - transitOperators={Array []} - viewedStop={ - Object { - "stopId": "TriMet:715", - } - } - > -
- -
- - 20 - - To - Gresham TC -
-
- + + components.PatternRow.routeName + +
+
-
-
- +
+
- viewers > stop viewer should render with TriMet transit in "marginRight": 2, } } + type="clock-o" > - viewers > stop viewer should render with TriMet transit in "marginRight": 2, } } - /> - - -
-
+ > + + + +
- Monday -
-
- 17:45 +
+ + common.daysOfWeek.monday + +
+
+ 5:45 pm +
-
- -
-
-
+
- - - - - - + + + + + +
-
- - -
- - -
- -
-
-
+
- - - Auto-refresh arrivals? - - -
- -
- + + + + + + 5:38 pm + +
+
+
+
+
-
- - - + + + + `; exports[`components > viewers > stop viewer should render with initial stop id and no stop times 1`] = ` - - - + + -
- - -
-
- - Loading Stop... - -
-
+ + + + + common.forms.back + + + +
+
+ + + components.StopViewer.loadingText + + +
+
+ /> +
-
-
-
-
+ + + + `; diff --git a/__tests__/reducers/__snapshots__/create-otp-reducer.js.snap b/__tests__/reducers/__snapshots__/create-otp-reducer.js.snap index 9cad075ba..3ca0370c0 100644 --- a/__tests__/reducers/__snapshots__/create-otp-reducer.js.snap +++ b/__tests__/reducers/__snapshots__/create-otp-reducer.js.snap @@ -99,7 +99,7 @@ Object { "ui": Object { "diagramLeg": null, "locale": null, - "localizedMessages": Object {}, + "localizedMessages": null, "mobileScreen": 1, "printView": false, }, diff --git a/__tests__/test-utils/mock-data/store.js b/__tests__/test-utils/mock-data/store.js index 724435f0d..4e4131cf2 100644 --- a/__tests__/test-utils/mock-data/store.js +++ b/__tests__/test-utils/mock-data/store.js @@ -1,6 +1,7 @@ import { connectRouter, routerMiddleware } from 'connected-react-router' import Enzyme, {mount} from 'enzyme' import EnzymeReactAdapter from 'enzyme-adapter-react-16' +import { IntlProvider } from 'react-intl' import {mountToJson} from 'enzyme-to-json' import { createHashHistory } from 'history' import clone from 'lodash/cloneDeep' @@ -44,9 +45,14 @@ export function mockWithProvider ( ) { const store = configureStore(storeMiddleWare)(storeState) const wrapper = mount( - - - + + + + + ) return { diff --git a/example-config.yml b/example-config.yml index 889ba5ab8..9b3d20866 100644 --- a/example-config.yml +++ b/example-config.yml @@ -314,6 +314,19 @@ itinerary: # default is 60 seconds. # onTimeThresholdSeconds: 60 +### You can customize OTP error messages for itinerary searches based on OTP HTTP codes. +### A separate message can be set for each language or locale if necessary. +# errorMessages: +# - id: 404 +# msg: +# en: Sorry, we couldn't find any transit or rideshare/carshare options at the time and/or location you chose. Please try again later, or change the settings of your trip. +# fr-FR: Aucun trajet en transports publics ou en partage de voiture n'a été trouvé pour l'heure et le lieu que vous avez indiqués. Veuillez réessayer plus tard, ou changez les paramètres de votre recherche. +# modes: +# - CAR_HAIL +# - CAR_RENT +# - id: 480 +# msg: No available transit routes or rideshare/carshare service at origin. + # Format the date time format for display. dateTime: longDateFormat: DD-MM-YYYY diff --git a/i18n/en-US.yml b/i18n/en-US.yml index 7e5711a5e..ad3076de8 100644 --- a/i18n/en-US.yml +++ b/i18n/en-US.yml @@ -15,37 +15,173 @@ _name: English # # 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. +# - For starters, there are an 'actions', 'components' and 'common' +# categories. 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. +# Messages that are generated from actions +actions: + callTaker: + callQuerySaveError: "Error storing call queries: {err}" + callSaveError: "Could not save call: {err}" + checkSessionError: "Error establishing auth session: {err}" + couldNotFindCallError: Could not find call. Cancelling save queries request. + fetchCallsError: "Error fetching calls: {err}" + queryFetchError: "Error fetching queries: {err}" + fieldTrip: + addNoteError: "Error adding field trip note: {err}" + confirmOverwriteItineraries: "This action will overwrite a previously planned {outbound, select, + true {outbound} + other {inbound} + } itinerary for this request. Do you wish to continue?" + deleteItinerariesError: "Error deleting field trip plan: {err}" + deleteNoteError: "Error deleting field trip note: {err}" + editSubmitterNotesError: "Error editing submitter notes: {err}" + fetchFieldTripError: "Error fetching field trip: {err}" + fetchFieldTripsError: "Error fetching field trips: {err}" + fetchTripsForDateError: "Error fetching trips for field trip travel date: {err}" + incompatibleTripDateError: Planned trip date ({tripDate}) is not the requested day of travel ({requestDate}) + itineraryCapacityError: "Cannot Save Plan: This plan could not be saved due to a lack of capacity on one or more vehicles. Please re-plan your trip." + maxTripRequestsExceeded: Number of trip requests exceeded without valid results + saveItinerariesError: "Failed to save itineraries: {err}" + setGroupSizeError: "Error setting group size: {err}" + setPaymentError: "Error setting payment info: {err}" + setRequestStatusError: "Error setting request status: {err}" + location: + geolocationNotSupportedError: Geolocation not supported by your browser + unknownPositionError: Unknown error getting position + map: + currentLocation: (Current Location) + user: + accountDeleted: Your user account ({email}) has been deleted. + authTokenError: Error obtaining an authorization token. + confirmDeleteMonitoredTrip: Would you like to remove this trip? + confirmDeletePlace: Would you like to remove this place? + emailVerificationResent: The email verification message has been resent. + genericError: "An error was encountered: {err}" + itineraryExistenceCheckFailed: Error checking whether your selected trip is possible. + preferencesSaved: Your preferences have been saved. + smsInvalidCode: The code you entered is invalid. Please try again. + smsResendThrottled: A verification SMS was sent to the indicated phone number less than a minute ago. Please try again in a few moments. + smsVerificationFailed: Your phone could not be verified. Perhaps the code you entered has expired. Please request a new code and try again. + # Component-specific messages (e.g. button captions) # are defined for each component under the 'components' category. components: + AccountSetupFinishPane: + message: You are ready to start planning your trips. + AddPlaceButton: + addPlace: Add place + needOriginDestination: Define origin/destination to add intermediate places + tooManyPlaces: Maximum intermediate places reached + AfterSignInScreen: + mainTitle: Redirecting... + # For message, use the special ... markup to automatically insert the + # corresponding link in the text. + message: "If the page doesn't load after a few seconds, click here." + AppMenu: + callHistory: Call History + fieldTrip: Field Trip + mailables: Mailables + routeViewer: Route Viewer + BackToTripPlanner: + backToTripPlanner: Back to trip planner + BatchResultsScreen: + expandMap: Expand map + showResults: Show results + BatchRoutingPanel: + enterDestination: "Enter destination or {mobile, select, + true {tap} + other {click} + } on map..." + enterStartLocation: "Enter start location or {mobile, select, + true {tap} + other {click} + } on map..." + BatchSearchScreen: + header: Plan Your Trip + BatchSettings: + destination: destination + origin: origin + planTripTooltip: Plan trip + validationMessage: "Please define the following fields to plan a trip: {issues}" + BeforeSignInScreen: + mainTitle: Signing you in + message: "In order to access this page you will need to sign in. + Please wait while we redirect you to the login page..." + DateTimeScreen: + header: Set Date/Time DefaultItinerary: clickDetails: Click to view details # 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}" + DeleteUser: + deleteMyAccount: Delete my account + ErrorMessage: + header: Could Not Plan Trip + ExistingAccountDisplay: + mainTitle: My settings + notifications: Notifications + places: Favorite places + terms: Terms + FavoritePlaceRow: + addAnotherPlace: Add another place + # deleteThisPlace and editThisPlace are aria/tooltip texts. + deleteThisPlace: Delete this place + editThisPlace: Edit this place + setAddressForPlaceType: Set your {placeType} address + FavoritePlaceScreen: + addNewPlace: Add new place + editPlace: Edit {placeName} + editPlaceGeneric: Edit place + invalidAddress: Please set a location for this place. + invalidName: Please enter a name for this place. + nameAlreadyUsed: You are already using this name for another place. Please enter a different name. + placeNotFound: Place not found + placeNotFoundDescription: Sorry, the requested place was not found. + FavoritePlacesList: + description: "Add the places you frequent often to save time planning trips:" + FormNavigationButtons: + ariaLabel: Form navigation ItinerarySummary: - fareCost: "{useMaxFare, select, + fareCost: "{useMaxFare, select, true {{minTotalFare} - {maxTotalFare}} other {{minTotalFare}} }" + LiveStopTimes: + autoRefresh: Auto-refresh arrivals? + LocationSearch: + enterLocation: Enter location + setDestination: Set Destination + setOrigin: Set Origin + MainMobile: + invalidScreen: Invalid mobile screen + MobileOptions: + header: Set Search Options + ModeButtons: + bicycle: Bicycle + car: Drive + rent: Rental options + transit: Transit + walk: Walking NarrativeItinerariesHeader: - numIssues: "{issueNum, number} issues" + numIssues: "{issueNum, number} {issueNum, plural, + one {issue} + other {issues} + }" + # Note to translator: resultText is width-constrained + # (about half pane width) resultText: "{pending, select, true {Finding your options...} - other { - {itineraryNum, number} {itineraryNum, plural, + other {{itineraryNum, number} {itineraryNum, plural, one {itinerary found} other {itineraries found} - } - } + }} }" selectArrivalTime: Arrival time selectBest: Best option @@ -53,63 +189,314 @@ components: selectDepartureTime: Departure time selectDuration: Duration selectWalkTime: Walk time - titleText: "{pending, select, - true {Finding your options...} - other { - {itineraryNum, number} {itineraryNum, plural, - one {itinerary} - other {itineraries}} - {issueNum, plural, - =0 {found} - one {(and {issueNum, number} issue) found} - other {(and {issueNum, number} issues) found} + titleText: "{pending, select, + true {Finding your options...} + other { + {itineraryNum, number} {itineraryNum, plural, + one {itinerary} + other {itineraries}} {issueNum, plural, + =0 {found} + one {(and {issueNum, number} issue) found} + other {(and {issueNum,number} issues) found} + } } - } - }" + }" viewAll: View all options + NavLoginButton: + help: Help + myAccount: My account + signIn: Sign in + signOut: Sign out + NewAccountWizard: + finish: Account setup complete! + notifications: Notification preferences + places: Add your locations + terms: Create a new account + verify: Verify your email address + NotFound: + description: The content you requested is not available. + header: Content not found + NotificationPrefsPane: + description: You can receive notifications about trips you frequently take. + notificationChannelPrompt: How would you like to receive notifications? + notificationEmailDetail: "Notification emails will be sent to:" + emailSelect: Email + noneSelect: Don't notify me + smsSelect: SMS + PatternRow: + departure: Departure + routeName: "{routeName} To {headsign}" + routeShort: "To {headsign}" + status: Status + PhoneNumberEditor: + changeNumber: Change number + invalidCode: Please enter 6 digits for the validation code. + invalidPhone: Please enter a valid phone number. + pending: Pending + # Note to translator: placeholder is width-constrained. + placeholder: "Enter your phone number" + prompt: "Enter your phone number for SMS notifications:" + requestNewCode: Request a new code + sendVerificationText: Send verification text + smsDetail: "SMS notifications will be sent to:" + verified: Verified + verificationCode: "Verification code:" + verificationInstructions: "Please check the SMS messaging app on your mobile phone + for a text message with a verification code, and enter the code below + (code expires after 10 minutes)." + verify: Verify + Place: + enterAlert: > + Enter origin/destination in the form (or set via map click) + and click the resulting marker to set as {type} location. + viewStop: View Stop + PlaceEditor: + genericLocationPlaceholder: Search for location + locationPlaceholder: Search for {placeName} location + namePlaceholder: Set place name PlanFirstLastButtons: # Note to translator: these values are width-constrained. first: First last: Last next: Next previous: Previous + PlanTripButton: + planTrip: Plan Trip + PrintLayout: + itinerary: Itinerary + toggleMap: Toggle Map RealtimeAnnotation: - ignoreServiceDelays: Apply service delays - delaysNotShownInResults: "Your trip results are currently being affected by service delays. - These delays do not factor into travel times shown below." + delaysNotShownInResults: "Your trip results are currently being affected by service delays. These delays do not factor into travel times shown below." delaysShownInResults: "Your trip results have been adjusted based on real-time information. Under normal conditions, this trip would take {normalDuration} using the following routes: {routes}." ignoreServiceDelays: Ignore service delays serviceUpdate: Service update + RealtimeStatusLabel: + # Note to translator: In itinerary body, early or late is single-line + # and stacked above/below the delay in minutes depending on word order, + # e.g. "5 min\nlate". + # In the StopViewer, delay and status are shown in a single line. + # Width is constrained for all messages. + early: "{minutes} early" + late: "{minutes} late" + onTime: on time + scheduled: scheduled + ResultsError: + backToSearch: Back to Search + ResultsHeader: + noTripFound: No Trip Found + tripsFound: We Found {count} {count, plural, one {Option} other {Options}} + waiting: Waiting... + ResultsScreen: + header: Option {index} + # Used in both desktop and mobile + RouteViewer: + header: Route Viewer + SavedTripEditor: + editSavedTrip: Edit saved trip + saveNewTrip: Save new trip + tripInformation: Trip information + tripNotFound: Trip not found + tripNotFoundDescription: Sorry, the requested trip was not found. + tripNotifications: Trip notifications + SavedTripList: + myTrips: My trips + noSavedTrips: You have no saved trips + noSavedTripsInstructions: Perform a trip search from the map first. + pause: Pause + resume: Resume + SavedTripScreen: + tooManyTrips: "You already have reached the maximum of five saved trips. + Please remove unused trips from your saved trips, and try again." + tripNameAlreadyUsed: Another saved trip already uses this name. Please choose a different name. + tripNameRequired: Please enter a trip name. SaveTripButton: cantSaveText: Cannot save cantSaveTooltip: Only itineraries that include transit and no rentals or ride hailing can be monitored. saveTripText: Save trip signInText: Sign in to save trip signInTooltip: Please sign in to save trip. + SearchScreen: + header: Plan Your Trip + SettingsPreview: + defaultPreviewText: "Transit Options\n& Preferences" SimpleRealtimeAnnotation: usingRealtimeInfo: This trip uses real-time traffic and delay information + StackedPaneDisplay: + savePreferences: Save preferences + StopScheduleTable: + block: Block + departure: Departure + destination: To + route: Route + StopTimeCell: + imminentArrival: "{isDue, select, + true {Due} + other {{formattedDuration}} + }" + # Used in both desktop and mobile + StopViewer: + displayStopId: "Stop ID: {stopId}" + header: Stop Viewer + loadingText: Loading Stop... + noStopsFound: No stop times found for date. + planTrip: "Plan a trip:" + timezoneWarning: "Departure times are shown in {timezoneCode}." + viewTypeBtnText: "{scheduleView, select, + true {View next arrivals} + other {View schedule} + }" + SubNav: + myAccount: My account + settings: Settings + trips: Trips + SwitchButton: + defaultContent: Switch + switchLocations: Switch locations + TabbedFormPanel: + hideSettings: " Hide Settings" TabbedItineraries: optionNumber: "Option {optionNum, number}" fareCost: "{hasMaxFare, select, true {{minTotalFare}+} other {{minTotalFare}} }" + TermsOfUsePane: + mustAgreeToTerms: You must agree to the terms of service to continue. + # For termsOfServiceStatement and termsOfStorageStatement, + # use the special ... markup to automatically insert the + # corresponding link in the text. + termsOfServiceStatement: "I confirm that I am at least 18 years old, and I have read and + consent to the Terms of service for using the Trip Planner." + termsOfStorageStatement: "Optional: I consent to the Trip Planner storing my historical planned trips in order to + improve transit services in my area. More info..." + TripBasicsPane: + checkingItineraryExistence: Checking itinerary existence for each day of the week... + selectAtLeastOneDay: Please select at least one day to monitor. + selectedItinerary: "Selected itinerary:" + tripIsAvailableOnDaysIndicated: Your trip is available on the days of the week as indicated above. + tripDaysPrompt: What days do you take this trip? + tripNamePrompt: "Please provide a name for this trip:" + # This is shown in a tooltip. + tripNotAvailableOnDay: Trip not available on {repeatedDay} + unsavedChangesNewTrip: You haven't saved your new trip yet. If you leave, it will be lost. + unsavedChangesExistingTrip: You haven't saved your trip yet. If you leave, changes will be lost. + TripNotificationsPane: + advancedSettings: Advanced settings + altRouteRecommended: An alternative route or transfer point is recommended + emailChannel: email + delaysAboveThreshold: There are delays or disruptions of more than + howToReceiveAlerts: "To receive alerts for your saved trips, enable notifications + in your account settings, and try saving a trip again." + monitorThisTrip: Monitor this trip {minutes} before it begins until it ends. + notificationsTurnedOff: Notifications are turned off for your account. + # Note to translator: The notifyViaChannelWhen message, combined with + # altRouteRecommended, delaysAboveTHreshold, realtimeAlertFlagged, + # should read like a sentence. + notifyViaChannelWhen: "Notify me via {channel} when:" + oneHour: 1 hour + realtimeAlertFlagged: There is a realtime alert flagged on my journey + smsChannel: SMS + TripStatus: + alerts: "{alerts, plural, one {{alerts} alert!} other {{alerts} alerts!}}" + deleteTrip: Delete Trip + planNewTrip: Plan New Trip + TripStatusRenderers: + active: + delayedHeading: "Trip is in progress and is delayed {formattedDuration}!" + description: "Trip is due to arrive at the destination at {arrivalTime}." + earlyHeading: "Trip is in progress and is arriving {formattedDuration} earlier than expected!" + noDataHeading: Trip is in progress (no realtime updates available). + onTimeHeading: Trip is in progress and is about on time. + base: + lastCheckedDefaultText: Last checked time unknown + lastCheckedText: "Last checked: {formattedDuration} ago" + togglePause: Pause + tripIsNotSnoozed: "Snooze for rest of today" + tripIsSnoozed: "Unsnooze trip analysis" + unknownState: Unknown Trip State + untogglePause: Resume + inactive: + description: Resume trip monitoring to see the updated status + heading: Trip monitoring is paused + nextTripNotPossible: + description: "The trip planner was unable to find your trip today. + Please try re-planning your itinerary to find an alternative route." + heading: Trip is not possible today + noLongerPossible: + description: "The trip planner was unable to find your trip on any selected days of the week. + Please try re-planning your itinerary to find an alternative route." + heading: Trip is no longer possible + notCalculated: + awaiting: Awaiting calculation... + description: Please wait a bit for the trip to calculate. + heading: Trip not yet calculated + snoozed: + description: Unsnooze trip monitoring to see the updated status. + heading: Trip monitoring is snoozed for today + upcoming: + nextTripBegins: Next trip starts on {tripDate, date, ::eeeee yyyyMMdd} at {tripTime}. + tripBegins: Trip is due to begin at {tripStart}. (Realtime monitoring will begin at {monitoringStart}.) + tripStartIsDelayed: Trip start time is delayed ${duration}! + tripStartIsEarly: Trip start time is happening ${duration} earlier than expected! + tripStartsSoonNoUpdates: Trip is starting soon (no realtime updates available). + tripStartsSoonOnTime: Trip is starting soon and is about on time. + TripSummary: + itinerary: Itinerary + TripSummaryPane: + happensOnDays: "Happens on: {days}" + notifications: "Notifications: {leadTimeInMinutes} min. before scheduled departure" + notificationsDisabled: "Notifications: Disabled" TripTools: - # Note to translator: copyLink, linkCopied, print, reportIssue, - # and startOver are width-constrained. - copyLink: Copy link + # Note to translator: copyLink, linkCopied, print, reportIssue are width-constrained. + copyLink: Copy Link # Text that replaces the copyLink button text after user clicks it. linkCopied: Copied - print: Print reportIssue: Report Issue reportEmailSubject: Reporting an Issue with OpenTripPlanner reportEmailTemplate: " *** INSTRUCTIONS TO USER *** This feature allows you to email a report to site administrators for review. Please add any additional feedback for this trip under the 'Additional Comments' section below and send using your regular email program." - startOver: Start Over # TODO: move to other category (common with hamburger 'Start Over' item) + # Used in both desktop and mobile + TripViewer: + accessible: Accessible + bicyclesAllowed: Allowed + header: Trip Viewer + routeHeader: "Route: {routeShortName} {routeLongName}" + viewStop: View + UserAccountScreen: + confirmDelete: Are you sure you would like to delete your user account? Once you do so, it cannot be recovered. + UserSettings: + confirmDeletion: You have recent searches and/or places stored. Disabling storage of recent places/searches will remove these items. Continue? + favoriteStops: Favorite stops + myPreferences: My preferences + noFavoriteStops: No favorite stops + recentPlaces: Recent places + recentSearches: Recent searches + rememberSearches: Remember recent searches/places? + storageDisclaimer: > + Any preferences, places, or settings you opt + to save will be stored in the local storage of your browser. + TriMet will not have access to knowledge about your home, work, + or any other location. At any point you can opt to turn off + remembering recent places/searches and clear your saved home/work locations and favorite stops. + UserTripSettings: + forgetOptions: Forget my options + rememberOptions: Remember trip options + restoreOptions: "Restore {defaults, select, + true {my defaults} + other {defaults}}" + VerifyEmailPane: + emailIsVerified: My email is verified! + instructions1: "Please check your email inbox and follow the link in the message + to verify your email address before finishing your account setup." + instructions2: Once you're verified, click the button below to continue. + resendVerification: Resend verification email + WelcomeScreen: + prompt: Where do you want to go? + # Common messages that appear in multiple components and modules # are grouped below by topic. @@ -122,7 +509,47 @@ common: micromobility: E-Scooter micromobilityRent: Rental E-Scooter walk: Walk - + daysOfWeek: + monday: Monday + tuesday: Tuesday + wednesday: Wednesday + thursday: Thursday + friday: Friday + saturday: Saturday + sunday: Sunday + daysOfWeekCompact: + monday: Mon. + tuesday: Tue. + wednesday: Wed. + thursday: Thu. + friday: Fri. + saturday: Sat. + sunday: Sun. + daysOfWeekPlural: + monday: Mondays + tuesday: Tuesdays + wednesday: Wednesdays + thursday: Thursdays + friday: Fridays + saturday: Saturdays + sunday: Sundays + # Common form UI messages + # Note to translator: these values are width-constrained. + forms: + back: Back + cancel: Cancel + clear: Clear + defaultValue: "{value} (default)" + delete: Delete + edit: Edit + finish: Finish + next: Next + no: No + print: Print + save: Save + startOver: Start Over + yes: Yes + # Shared itinerary description messages itineraryDescriptions: calories: "{calories, number} Cal" transfers: "{transfers, plural, =0 {} one {{transfers} transfer} other {{transfers} transfers}}" @@ -139,12 +566,61 @@ common: cable_car: Cable Car gondola: Gondola funicular: Funicular - + # Note to translator: Places names below are used in + # contexts such as: "Edit home", "Set home address". + places: + custom: custom + dining: dining + home: home + work: work # as in "work location" time: - # Use ordered placeholders for the departure-arrival string + # Use ordered placeholders for the departure-arrival string # (this will accommodate right-to-left languages by swapping the order in this string). departureArrivalTimes: "{startTime, time, short}—{endTime, time, short}" + # Replacing (sort of) moment.js fromNow() functionality. This is not a direct 1:1, since + # moment.js formats text to "a minute ago" once 44 seconds are reached, etc. This has + # been simplified here. + fromNowUpdate: "{days, plural, + =0 {{hours,plural, + =0 {{minutes, plural, + =0 {a few seconds ago} + =1 {one minute ago} + other {{minutes} minutes ago} + }} + =1 {an hour ago} + other {{hours} hours ago} + }} + other {{days} days ago} + }" + # 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, plural, =0 {{minutes, number} min} other {{hours, number} hr {minutes, number} min}}" - \ No newline at end of file + # Note to translator: the strings below are used in sentences such as: + # "No trip found for bike, walk, and transit." + # This set is based on OTP travel modes, in lower case, and accommodates the use + # of particles before/after each travel mode in some languages. + # In French, the above sentence could read: + # "Aucun trajet en vélo, à pied, et en transports publics n'a été trouvé." + travelBy: + bicycle: bike + bicycle_rent: bikeshare + car: car + car_park: car park + micromobility: e-scooter + micromobility_rent: rental e-scooter + transit: transit + walk: walk +util: + state: + errorPlanningTrip: An error occurred while planning a trip. + networkUnavailable: The {network} network is unavailable at this moment. + noTripFound: No trip found. + noTripFoundForMode: No trip found for {modes}. + noTripFoundReason: There may be no transit service within the maximum specified distance or at the specified time, or your start or end point might not be safely accessible. + noTripFoundWithReason: "{noTripFound} {reason}" + titleBarRouteId: "Route {routeId}" + titleBarStopId: "Stop {stopId}" + titleBarWithStatus: "{title} | {status}" diff --git a/i18n/fr-FR.yml b/i18n/fr-FR.yml index ad0874e2e..870189056 100644 --- a/i18n/fr-FR.yml +++ b/i18n/fr-FR.yml @@ -1,19 +1,170 @@ _id: fr-FR -_name: Unofficial French Translations! +_name: Exemple de traduction pour OTP-react-redux en français ! +# Noteworthy items for translating into French: +# - trip, itinerary, journey => trajet +# - trip monitoring => suivi de trajet +# - place => lieu (destination) +# - disable, deactivate => désactiver +# - email => 'e-mail' or 'mail' is widely used in France, although courriel is more common in Canada. +# - Instruction text is generally less cheerful in French than English (e.g. fewer uses of exclamation marks) +# and tends to rely on infinitive tense. +# + +# Messages that are generated from actions +actions: + callTaker: + callQuerySaveError: "Erreur lors de l'enregistrement des requêtes pour l'appel : {err}" + callSaveError: "Impossible d'enregistrer l'appel : {err}" + checkSessionError: "Erreur durant l'auth-entification : {err}" + couldNotFindCallError: Impossible de trouver l'appel. Tentative de sauvegarde des requêtes annulée. + fetchCallsError: "Erreur lors du chargement des appels : {err}" + queryFetchError: "Erreur lors du chargement des requêtes : {err}" + fieldTrip: + addNoteError: "Erreur lors de l'ajout d'une note sur le groupe : {err}" + confirmOverwriteItineraries: "Cette action replacera un itinéraire {outbound, select, + true {aller} + other {retour} + } planifié préalablement pour cette demande. Voulez-vous continuer ?" + deleteItinerariesError: "Erreur lors de la suppression d'un itinéraire de groupe : {err}" + deleteNoteError: "Erreur lors de la suppression d'une note sur le groupe : {err}" + editSubmitterNotesError: "Erreur lors de la modification des notes du demandeur : {err}" + fetchFieldTripError: "Erreur de chargement de l'itinéraire du groupe: {err}" + fetchFieldTripsError: "Error fetching des itinéraires du groupe : {err}" + fetchTripsForDateError: "Error fetching des itinéraires du groupe pour les dates de sorties : {err}" + incompatibleTripDateError: La date du trajet planifié ({tripDate}) ne correspond pas à la date demandée ({requestDate}). + itineraryCapacityError: "Impossible d'enregistrer les itinéraires : Capacité insuffisante dans un ou plusieurs véhicules. Veuillez relancer votre recherche." + maxTripRequestsExceeded: Le nombre de requêtes sans résultats valables a été dépassé. + saveItinerariesError: "Erreur lors de l'enregistrement des itinéraires : {err}" + setGroupSizeError: "Erreur sur la taille du groupe : {err}" + setPaymentError: "Erreur sur les coordonnées de paiement : {err}" + setRequestStatusError: "Erreur sur l'état de la requête : {err}" + location: + geolocationNotSupportedError: La géolocalisation n'est pas prise en charge par votre navigateur. + unknownPositionError: Erreur non-gérée lors de la détection de votre emplacement. + map: + currentLocation: (Emplacement actuel) + user: + accountDeleted: Votre compte utilisateur ({email}) a été supprimé. + authTokenError: Erreur lors de l'obtention d'un jeton d'authentification. + confirmDeleteMonitoredTrip: Voulez-vous supprimer ce trajet ? + confirmDeletePlace: Voulez-vous supprimer ce lieu ? + emailVerificationResent: Le message de vérification de votre adresse e-mail a été envoyé de nouveau. + genericError: "Une erreur s'est produite : {err}" + itineraryExistenceCheckFailed: Erreur lors de la vérification de la validité du trajet choisi. + preferencesSaved: Vos préférences ont été enregistrées. + smsInvalidCode: Le code saisi est incorrect. Veuillez réessayer. + smsResendThrottled: Un SMS de vérification a été envoyé au numéro de téléphone indiqué il y a moins d'une minute. Patientez quelques moments avant de réessayer. + smsVerificationFailed: Votre numéro de téléphone n'a pas pu etre vérifié. Le code que vous avez entré a peut-etre expiré. Veuillez demander un nouveau code puis réessayez. + + +# Component-specific messages (e.g. button captions) +# are defined for each component under the 'components' category. components: + AccountSetupFinishPane: + message: Vous pouvez maintenant commencer à planifier vos trajets. + AddPlaceButton: + addPlace: Ajouter un point intermédiaire + needOriginDestination: Choisissez le départ et l'arrivée avant d'ajouter les points intermédiaires + tooManyPlaces: Nombre max. de points intermédiaires atteint + AfterSignInScreen: + mainTitle: Redirection... + # For message, use the special ... markup to automatically insert the + # corresponding link in the text. + message: "Si la page ne s'affiche pas après quelques secondes, cliquez ici." + AppMenu: + callHistory: Historique des appels + fieldTrip: Groupes scolaires + mailables: Prêt-à-poster + routeViewer: Index des lignes + BackToTripPlanner: + backToTripPlanner: Retour au planificateur de trajets + BatchResultsScreen: + expandMap: Étendre la carte + showResults: Voir les résultats + BatchRoutingPanel: + enterDestination: "Entrez votre destination ou {mobile, select, + true {touchez} + other {cliquez sur} + } la carte..." + enterStartLocation: "Entrez votre point de départ ou {mobile, select, + true {touchez} + other {cliquez sur} + } la carte..." + BatchSearchScreen: + header: Planifiez votre trajet + BatchSettings: + destination: destination + origin: point de départ + planTripTooltip: Planifier le trajet + validationMessage: "Veuillez définir les champs suivants afin de planifier votre trajet : {issues}" + BeforeSignInScreen: + mainTitle: Connexion en cours + message: "Pour accéder à cette page, vous devez vous connecter. + Veuillez patienter pendant que nous vous redirigeons vers la page de connexion..." + DateTimeScreen: + header: Jour et heure du trajet DefaultItinerary: clickDetails: Cliquez pour afficher les détails multiModeSummary: "{accessMode} + {transitMode}" + DeleteUser: + deleteMyAccount: Supprimer mon compte + ErrorMessage: + header: Impossible de planifier le trajet + ExistingAccountDisplay: + mainTitle: Mes préférences + notifications: Notifications + places: Lieux favoris + terms: Conditions d'utilisation + FavoritePlaceRow: + addAnotherPlace: Ajouter un autre lieu + # deleteThisPlace and editThisPlace are aria/tooltip texts. + deleteThisPlace: Supprimer ce lieu + editThisPlace: Modifier ce lieu + setAddressForPlaceType: Entrez l'adresse de votre {placeType} + FavoritePlaceScreen: + addNewPlace: Ajouter un nouveau lieu + editPlace: Modifier le {placeName} + editPlaceGeneric: Modifier le lieu + invalidAddress: Veuillez entrer l'adresse du lieu. + invalidName: Veuillez saisir le nom du lieu. + nameAlreadyUsed: Ce nom est déjà utilisé avec un autre lieu. Veuillez saisir un nom différent. + placeNotFound: Lieu introuvable + placeNotFoundDescription: Le lieu recherché est introuvable. + FavoritePlacesList: + description: "Ajoutez les lieux que vous fréquentez souvent pour faciliter vos recherches de trajets :" + FormNavigationButtons: + ariaLabel: Navigation du formulaire ItinerarySummary: fareCost: "{useMaxFare, select, true {{minTotalFare} - {maxTotalFare}} other {{minTotalFare}} }" + LiveStopTimes: + autoRefresh: Rafraîchir les passages automatiquement ? + LocationSearch: + enterLocation: Entrez le lieu + setDestination: Choisissez la destination + setOrigin: Choisissez le point de départ + MainMobile: + invalidScreen: Écran non valable. + MobileOptions: + header: Options de recherche + ModeButtons: + bicycle: Vélo + car: Voiture + rent: Location de véhicules + transit: Transports publics + walk: Marche NarrativeItinerariesHeader: - numIssues: "{issueNum, number} problèmes" + numIssues: "{issueNum, number} {issueNum, plural, + one {problème} + other {problèmes} + }" + # Note to translator: resultText is width-constrained + # (about half pane width) resultText: "{pending, select, - true {Recherche de vos options en cours...} + true {Recherche en cours...} other { {itineraryNum, number} {itineraryNum, plural, one {trajet trouvé} @@ -36,53 +187,265 @@ components: {issueNum, plural, =0 {trouvé} one {(et {issueNum, number} problème) trouvé} - other {(and {issueNum, number} problèmes) trouvés} + other {(et {issueNum,number} problèmes) trouvés} } } }" viewAll: Voir toutes les options + NavLoginButton: + help: Aide + myAccount: Mon compte + signIn: Se connecter + signOut: Déconnexion + NewAccountWizard: + finish: Votre nouveau compte est prêt ! + notifications: Recevez vos notifications + places: Ajoutez vos lieux + terms: Créez votre nouveau compte + verify: Vérifiez votre adresse e-mail + NotFound: + description: Le contenu que vous avez demandé n'est pas disponible. + header: Contenu introuvable + NotificationPrefsPane: + description: Vous pouvez recevoir des notifications sur les trajets que vous effectuez fréquemment. + notificationChannelPrompt: Comment voulez-vous recevoir vos notifications ? + notificationEmailDetail: "Les courriers de notification seront envoyés à :" + emailSelect: E-mail + noneSelect: Ne pas me notifier + smsSelect: SMS + PatternRow: + routeName: "{routeName} vers {headsign}" + routeShort: "Vers {headsign}" + PhoneNumberEditor: + changeNumber: Changer de numéro + invalidCode: Le code de vérification doit comporter 6 chiffres. + invalidPhone: Veuillez entrer un numéro de téléphone valable. + pending: Non vérifié + # Note to translator: placeholder is width-constrained. + placeholder: "Entrez votre numéro" + prompt: "Entrez votre numéro de téléphone pour les SMS de notification :" + requestNewCode: Envoyer un nouveau code + sendVerificationText: Envoyer le SMS de vérification + smsDetail: "Les SMS de notification seront envoyés au :" + verified: Verifié + verificationCode: "Code de vérification :" + verificationInstructions: "Un SMS vous a été envoyé avec un code de vérification. + Veuillez taper ce code ci-dessous (le code expire après 10 minutes)." + verify: Verifier + PlaceEditor: + genericLocationPlaceholder: Adresse du lieu + locationPlaceholder: Adresse de votre {placeName} + namePlaceholder: Entrez le nom du lieu PlanFirstLastButtons: # Note to translator: these values are width-constrained. first: Premier last: Dernier next: Suivant previous: Précédent + PlanTripButton: + planTrip: Planifier le trajet # or simply "Rechercher" + PrintLayout: + itinerary: Votre trajet + toggleMap: Afficher/masquer la carte RealtimeAnnotation: - ignoreServiceDelays: Appliquer les retards delaysNotShownInResults: "Vos trajets recherchés sont perturbés par des retards. Ces retards ne sont pas pris en compte dans les temps de trajet ci-dessous." delaysShownInResults: "Vos trajets recherchés ont été mis à jour avec les conditions en temps réel. En temps normal, ce trajet prendrait {normalDuration} en empruntant les lignes: {routes}." ignoreServiceDelays: Ignorer les retards serviceUpdate: Information sur le service + RealtimeStatusLabel: + # Note to translator: In itinerary body, early or late is single-line + # and stacked above/below the delay in minutes depending on word order, + # e.g. "5 min\nlate". + # In the StopViewer, delay and status are shown in a single line. + # Width is constrained for all messages. + early: "avance de {minutes}" + late: "retard {minutes}" + onTime: à l'heure + scheduled: horaire + ResultsError: + backToSearch: Retour à la recherche + ResultsHeader: + noTripFound: Aucun trajet trouvé + tripsFound: "{count} {count, plural, one {trajet trouvé} other {trajets trouvés}}" + waiting: Patientez... + ResultsScreen: + header: Option {index} + # Used in both desktop and mobile + RouteViewer: + header: Index des lignes + SavedTripEditor: + editSavedTrip: Modifier un trajet enregistré + saveNewTrip: Enregistrer un nouveau trajet + tripInformation: Informations sur le trajet + tripNotFound: Trajet introuvable + tripNotFoundDescription: Le trajet recherché est introuvable. + tripNotifications: Notifications du trajet + SavedTripList: + myTrips: Mes trajets + noSavedTrips: Vous n'avez aucun trajet enregistré + noSavedTripsInstructions: Effectuez une recherche depuis la carte avant de pouvoir enregistrer un nouveau trajet. + pause: Arrêter + resume: Reprendre + SavedTripScreen: + tooManyTrips: "Vous avez déjà atteint le nombre maximum de 5 trajets enregistrés. + Veuillez supprimer les trajets enregistrés qui sont inutilisés, puis réessayez." + tripNameAlreadyUsed: Ce nom est déjà utilisé avec un autre trajet. Veuillez choisir un nom différent. + tripNameRequired: Veuillez entrer un nom pour ce trajet. SaveTripButton: cantSaveText: Impossible d'enregistrer - cantSaveTooltip: Seuls les trajets en transports en commun sans location de véhicules et sans course en voiture peuvent être suivis. + cantSaveTooltip: Seuls les trajets en transports publics sans location de véhicules et sans course en voiture peuvent être suivis. saveTripText: Enregistrer signInText: Connectez-vous pour enregistrer signInTooltip: Veuillez vous connecter pour enregistrer ce trajet. + SearchScreen: + header: Planifiez votre trajet SimpleRealtimeAnnotation: usingRealtimeInfo: Ce trajet utilise les informations en temps réel sur le trafic et les retards + StackedPaneDisplay: + savePreferences: Enregistrer mes préférences + StopScheduleTable: + block: Bloc + departure: Départ + destination: Destination + route: Ligne + # Used in both desktop and mobile + StopViewer: + displayStopId: "Arrêt n° {stopId}" + header: Info arrêt + loadingText: Chargement de l'arrêt... + noStopsFound: Aucun passage n'a été trouvé pour cette date. + planTrip: "Planifer un trajet :" + timezoneWarning: "Les horaires sont affichés dans le fuseau {timezoneCode}." + viewTypeBtnText: "{scheduleView, select, + true {Afficher les prochains passages} + other {Afficher les horaires} + }" + SubNav: + myAccount: Mon compte + settings: Préférences + trips: Trajets TabbedItineraries: optionNumber: "Option {optionNum, number}" fareCost: "{hasMaxFare, select, true {À partir de {minTotalFare}} other {{minTotalFare}} }" + TermsOfUsePane: + mustAgreeToTerms: Vous devez accepter les conditions d'utilisation avant de continuer. + # For termsOfServiceStatement and termsOfStorageStatement, + # use the special ... markup to insert the + # corresponding link in the text. + termsOfServiceStatement: "J'atteste avoir au moins 18 ans et j'ai lu et consens aux + Conditions de service pour utiliser the Planificateur de trajets." + termsOfStorageStatement: "Facultatif: Je consens à ce que le Planificateur de trajets sauvegarde mes recherches + afin d'améliorer les transports publics dans ma region. Plus d'informations..." + TripBasicsPane: + checkingItineraryExistence: Verification de l'existence du trajet pour chaque jour de la semaine... + selectAtLeastOneDay: Veuillez choisir au moins un jour pour effectuer le suivi. + selectedItinerary: "Trajet selectionné :" + tripIsAvailableOnDaysIndicated: Votre trajet est possible les jours indiqués ci-dessus. + tripDaysPrompt: Quels jours effectuez-vous ce trajet ? + tripNamePrompt: "Saisissez un nom pour ce trajet :" + # This is shown in a tooltip. + tripNotAvailableOnDay: Ce trajet n'est pas possible les {repeatedDay} + unsavedChangesNewTrip: Vous n'avez pas encore enregistré votre nouveau trajet. Si vous annulez, ce trajet sera perdu. + unsavedChangesExistingTrip: Vous n'avez pas encore enregistré votre trajet. Si vous annulez, les changements seront perdus. + TripNotificationsPane: + advancedSettings: Paramètres avancés + altRouteRecommended: Un autre trajet ou une autre correspondance est conseillé·e + delaysAboveThreshold: Il y a des perturbations ou retards de plus de + emailChannel: e-mail + howToReceiveAlerts: "Pour recevoir les alertes pour vos trajets suivis, activez les notifications + dans la section Préférences de votre compte, et essayez d'enregistrer un trajet à nouveau." + monitorThisTrip: Effectuer le suivi du trajet {minutes} avant le départ et jusqu'à l'arrivée. + notificationsTurnedOff: Les notifications sont désactivées pour votre compte. + # Note to translator: The notifyViaChannelWhen message, combined with + # altRouteRecommended, delaysAboveTHreshold, realtimeAlertFlagged, + # should read like a sentence. + notifyViaChannelWhen: "Recevoir des notifications par {channel} lorsque :" + oneHour: 1 heure + realtimeAlertFlagged: Une alerte en temps réel affecte mon trajet + smsChannel: SMS + TripStatus: + alerts: "{alerts, plural, =0 {{alerts} alerte !} one {{alerts} alerte !} other {{alerts} alertes !}}" + deleteTrip: Supprimer le trajet + planNewTrip: Planifier un nouveau trajet + TripStatusRenderers: + active: + delayedHeading: "Trajet en cours, retardé de {deviationHumanDuration}." + description: "Arrivée à destination prévue à {arrivalTime}." + earlyHeading: "Trajet en cours, en avance de {deviationHumanDuration}." + noDataHeading: Trajet en cours (données en temps-réel non disponibles). + onTimeHeading: Trajet en cours et prévu à l'heure. + base: + lastCheckedDefaultText: Dernière vérification inconnue + lastCheckedText: "Dernière vérification effectuée il y a {formattedDuration}" + togglePause: Suspendre le suivi + tripIsNotSnoozed: Suspendre jusqu'à demain + tripIsSnoozed: Reprendre le suivi du trajet + unknownState: Etat du trajet inconnu + untogglePause: Reprendre + inactive: + description: Reprenez le suivi pour obtenir des dernières conditions de votre trajet. + heading: Suivi suspendu + nextTripNotPossible: + description: "Le planificateur n'a pas pu trouver votre trajet aujourd'hui. + Veuillez replanifier votre trajet pour trouver une alternative." + heading: Trajet infaisable aujourd'hui + noLongerPossible: + description: "Le planificateur n'a pas pu trouver votre trajet pour aucun des jours choisis. + Veuillez replanifier votre trajet pour trouver une alternative." + heading: Le trajet n'est plus possible + notCalculated: + awaiting: Calcul des conditions du trajet en cours... + description: Veuillez patienter pendant que les conditions du trajet soient déterminées. + heading: Conditions du trajet indéterminées + snoozed: + description: Reprenez le suivi pour obtenir des dernières conditions de votre trajet. + heading: Suivi suspendu jusqu'à demain + upcoming: + nextTripBegins: "Prochain départ : {tripDate, date, ::eeeee yyyyMMdd} à {tripTime}." + tripBegins: Départ prévu à {tripStart}. (Le suivi en temps réel débutera à {monitoringStart}.) + tripStartIsDelayed: Départ retardé de ${duration}. + tripStartIsEarly: Départ avancé de ${duration} ! + tripStartsSoonNoUpdates: Départ proche (pas de données en temps réel). + tripStartsSoonOnTime: Départ proche et prévu à l'heure. + TripSummary: + itinerary: Trajet + TripSummaryPane: + happensOnDays: "Effectué : {days}" + notifications: "Notifications : {leadTimeInMinutes} mn avant l'heure de départ prévue" + notificationsDisabled: "Notifications : Désactivées" TripTools: - # Note to translator: copyLink, linkCopied, print, reportIssue, - # and startOver are width-constrained. + # Note to translator: copyLink, linkCopied, print, reportIssue are width-constrained. copyLink: Copier le lien # Text that replaces the copyLink button text after user clicks it. linkCopied: Copié - print: Imprimer reportIssue: Un problème ? # "Signaler un problème" does not fit. reportEmailSubject: Signaler un problème avec OpenTripPlanner - reportEmailTemplate: " *** A L'ATTENTION DE L'UTILISATEUR *** - Vous pouvez communiquer votre problème en détail aux administrateurs de ce site, par courriel. + reportEmailTemplate: " *** À L'ATTENTION DE L'UTILISATEUR *** + Vous pouvez communiquer votre problème par e-mail et en détail aux administrateurs de ce site. Veuillez ajouter toute remarque sur cet itinéraire dans la section 'Additional Comments' ci-dessous, puis envoyez depuis votre logiciel de messagerie usuel." - startOver: Recommencer + # Used in both desktop and mobile + TripViewer: + accessible: Accessible + bicyclesAllowed: Autorisés + header: Info trajet + routeHeader: "Ligne : {routeShortName} {routeLongName}" + viewStop: Info + UserAccountScreen: + confirmDelete: "Voulez-vous vraiment supprimer votre compte ? Cette action est irréversible." + VerifyEmailPane: + emailIsVerified: Mon email est vérifié + instructions1: "Vous devriez recevoir un message par e-mail. Cliquez le lien dans le message + pour verifier votre adresse e-mail. Vous pourrez ensuite finir de créer votre compte." + instructions2: Une fois votre adresse vérifiée, cliquez sur le bouton ci-dessous pour continuer. + resendVerification: Envoyer un nouveau message de vérification + WelcomeScreen: + prompt: Où désirez-vous aller ? common: accessModes: @@ -92,12 +455,52 @@ common: micromobility: Trottinette électrique micromobilityRent: Trottinette électrique en libre-service walk: À pied - + daysOfWeek: + monday: lundi + tuesday: mardi + wednesday: mercredi + thursday: jeudi + friday: vendredi + saturday: samedi + sunday: dimanche + daysOfWeekCompact: + monday: lun. + tuesday: mar. + wednesday: mer. + thursday: jeu. + friday: ven. + saturday: sam. + sunday: dim. + daysOfWeekPlural: + monday: lundis + tuesday: mardis + wednesday: mercredis + thursday: jeudis + friday: vendredis + saturday: samedis + sunday: dimanches + # Common form UI messages + # Note to translator: these values are width-constrained. + forms: + back: Retour + cancel: Annuler + defaultValue: "{value} (défaut)" + delete: Supprimer + edit: Modifier + finish: Terminer + next: Suivant + no: Non + print: Imprimer + save: Enregistrer + startOver: Recommencer + yes: Oui itineraryDescriptions: calories: "{calories, number} kcal" # SI unit + departure: Départ + noItineraryToDisplay: Aucun trajet à afficher. + status: État transfers: "{transfers, plural, =0 {} one {{transfers} correspondance} other {{transfers} correspondances}}" - otpTransitModes: tram: Tram subway: Métro @@ -107,9 +510,43 @@ common: cable_car: Tram tiré par câble gondola: Téléphérique funicular: Funiculaire - + # Note to translator: Places names below are used in + # contexts such as: "Edit home", "Set home address". + places: + custom: divers + dining: restaurant + home: domicile + work: lieu de travail time: departureArrivalTimes: "{startTime, time, short}—{endTime, time, short}" + # If trip is less than one hour only display the minutes. tripDurationFormat: "{hours, plural, =0 {{minutes, number} mn} other {{hours, number} h {minutes, number} mn}}" + tripDurationFormatZeroHours: "{minutes, number} mn" + # Note to translator: the strings below are used in sentences such as: + # "No trip found for bike, walk, and transit." + # This set is based on OTP travel modes, in lower case, and accommodates the use + # of particles before/after each travel mode in some languages. + # In French, the above sentence could read: + # "Aucun trajet en vélo, à pied, et en transports publics n'a été trouvé." + travelBy: + bicycle: en vélo + bicycle_rent: en vélo en libre-service + car: en voiture + car_park: en voiture + parc relais + micromobility: en trottinette électrique + micromobility_rent: en trottinette électrique en libre-service + transit: en transports publics + walk: à pied +util: + state: + errorPlanningTrip: Une erreur s'est produite lors de la planification du trajet. + networkUnavailable: Le systeme {network} n'est pas disponible pour le moment. + noTripFound: Aucun trajet trouvé. + noTripFoundForMode: Aucun trajet {modes} n'a été trouvé. + noTripFoundReason: Vos points de départ et d'arrivée ne sont peut-être pas desservis dans le rayon spécifié ou aux heures indiquées, ou ne sont pas accessibles en toute sécurité. + noTripFoundWithReason: "{noTripFound} {reason}" + titleBarRouteId: "Ligne {routeId}" + titleBarStopId: "Arrêt {stopId}" + titleBarWithStatus: "{title} | {status}" diff --git a/index.html b/index.html index c9013dd56..71df15960 100644 --- a/index.html +++ b/index.html @@ -1,6 +1,8 @@ - - + diff --git a/lib/actions/call-taker.js b/lib/actions/call-taker.js index 45b5cfe8e..b98cbc57d 100644 --- a/lib/actions/call-taker.js +++ b/lib/actions/call-taker.js @@ -59,7 +59,7 @@ export function beginCallIfNeeded () { /** * End the active call and store the queries made during the call. */ -export function endCall () { +export function endCall (intl) { return async function (dispatch, getState) { const {callTaker, otp} = getState() const {activeCall, session} = callTaker @@ -80,12 +80,17 @@ export function endCall () { const id = await fetchResult.json() // Inject call ID into active call and save queries. - await dispatch(saveQueriesForCall({...activeCall, id})) + await dispatch(saveQueriesForCall({...activeCall, id}, intl)) // Wait until query was saved before re-fetching queries for this call. - await dispatch(fetchCalls()) + await dispatch(fetchCalls(intl)) } catch (err) { console.error(err) - alert(`Could not save call: ${JSON.stringify(err)}`) + alert( + intl.formatMessage( + { id: 'actions.callTaker.callSaveError' }, + { err: JSON.stringify(err) } + ) + ) } // Clear itineraries shown when ending call. dispatch(resetForm(true)) @@ -98,7 +103,7 @@ export function endCall () { * query param against sessions in the datastore backend or initializing a new * session via Trinet. */ -export function initializeModules () { +export function initializeModules (intl) { return function (dispatch, getState) { const {datastoreUrl, trinetReDirect} = getState().otp.config // Initialize session if datastore enabled. @@ -107,7 +112,7 @@ export function initializeModules () { const sessionId = getUrlParams().sessionId if (sessionId) { // Initialize the session if found in URL query params. - dispatch(checkSession(datastoreUrl, sessionId)) + dispatch(checkSession(datastoreUrl, sessionId, intl)) } else { // No sessionId was passed in, so we must request one from server. newSession(datastoreUrl, trinetReDirect, URL_ROOT) @@ -138,14 +143,19 @@ function newSession (datastoreUrl, verifyLoginUrl, redirect) { * Check that a particular session ID is valid and store resulting session * data. */ -function checkSession (datastoreUrl, sessionId) { +function checkSession (datastoreUrl, sessionId, intl) { return function (dispatch, getState) { fetch(datastoreUrl + `/auth/checkSession?sessionId=${sessionId}`) .then(res => res.json()) .then(session => dispatch(storeSession({session}))) - .catch(error => { + .catch(err => { dispatch(storeSession({session: null})) - alert(`Error establishing auth session: ${JSON.stringify(error)}`) + alert( + intl.formatMessage( + { id: 'actions.callTaker.checkSessionError' }, + { err: JSON.stringify(err) } + ) + ) }) } } @@ -153,7 +163,7 @@ function checkSession (datastoreUrl, sessionId) { /** * Fetch latest calls for a particular session. */ -export function fetchCalls () { +export function fetchCalls (intl) { return async function (dispatch, getState) { dispatch(requestingCalls()) const {callTaker, otp} = getState() @@ -166,7 +176,12 @@ export function fetchCalls () { const calls = await fetchResult.json() dispatch(receivedCalls({calls})) } catch (err) { - alert(`Error fetching calls: ${JSON.stringify(err)}`) + alert( + intl.formatMessage( + { id: 'actions.callTaker.fetchCallsError' }, + { err: JSON.stringify(err) } + ) + ) } } } @@ -175,13 +190,15 @@ export function fetchCalls () { * Store the trip queries made over the course of a call (to be called when the * call terminates). */ -export function saveQueriesForCall (call) { +function saveQueriesForCall (call, intl) { return function (dispatch, getState) { const {callTaker, otp} = getState() const {datastoreUrl} = otp.config if (sessionIsInvalid(callTaker.session)) return if (!call) { - alert(`Could not find call for ${call.id}. Cancelling save queries request.`) + alert( + intl.formatMessage({ id: 'actions.callTaker.couldNotFindCallError' }) + ) return } return Promise.all(call.searches.map(searchId => { @@ -194,7 +211,12 @@ export function saveQueriesForCall (call) { ) .then(res => res.json()) .catch(err => { - alert(`Error storing call queries: ${JSON.stringify(err)}`) + alert( + intl.formatMessage( + { id: 'actions.callTaker.callQuerySaveError' }, + { err: JSON.stringify(err) } + ) + ) }) })) } @@ -203,7 +225,7 @@ export function saveQueriesForCall (call) { /** * Fetch the trip queries that were made during a particular call. */ -export function fetchQueries (callId) { +export function fetchQueries (callId, intl) { return function (dispatch, getState) { dispatch(requestingQueries()) const {callTaker, otp} = getState() @@ -216,7 +238,12 @@ export function fetchQueries (callId) { dispatch(receivedQueries({callId, queries})) }) .catch(err => { - alert(`Error fetching queries: ${JSON.stringify(err)}`) + alert( + intl.formatMessage( + { id: 'actions.callTaker.queryFetchError' }, + { err: JSON.stringify(err) } + ) + ) }) } } diff --git a/lib/actions/field-trip.js b/lib/actions/field-trip.js index 0e4297ab8..4a8534582 100644 --- a/lib/actions/field-trip.js +++ b/lib/actions/field-trip.js @@ -72,7 +72,7 @@ export function resetAndToggleFieldTrips () { /** * Fetch all field trip requests (as summaries). */ -export function fetchFieldTrips () { +export function fetchFieldTrips (intl) { return async function (dispatch, getState) { dispatch(requestingFieldTrips()) const {callTaker, otp} = getState() @@ -83,8 +83,13 @@ export function fetchFieldTrips () { try { const res = await fetch(`${datastoreUrl}/fieldtrip/getRequestsSummary?${qs.stringify({sessionId})}`) fieldTrips = await res.json() - } catch (e) { - alert(`Error fetching field trips: ${JSON.stringify(e)}`) + } catch (err) { + alert( + intl.formatMessage( + { id: 'actions.fieldTrip.fetchFieldTripsError' }, + { err: JSON.stringify(err) } + ) + ) } dispatch(receivedFieldTrips({fieldTrips})) } @@ -93,7 +98,7 @@ export function fetchFieldTrips () { /** * Fetch details for a particular field trip request. */ -export function fetchFieldTripDetails (requestId) { +export function fetchFieldTripDetails (requestId, intl) { return function (dispatch, getState) { dispatch(requestingFieldTripDetails()) const {callTaker, otp} = getState() @@ -104,7 +109,12 @@ export function fetchFieldTripDetails (requestId) { .then(res => res.json()) .then(fieldTrip => dispatch(receivedFieldTripDetails({fieldTrip}))) .catch(err => { - alert(`Error fetching field trips: ${JSON.stringify(err)}`) + alert( + intl.formatMessage( + { id: 'actions.fieldTrip.fetchFieldTripError' }, + { err: JSON.stringify(err) } + ) + ) }) } } @@ -112,7 +122,7 @@ export function fetchFieldTripDetails (requestId) { /** * Add note for field trip request. */ -export function addFieldTripNote (request, note) { +export function addFieldTripNote (request, note, intl) { return function (dispatch, getState) { const {callTaker, otp} = getState() const {datastoreUrl} = otp.config @@ -126,9 +136,14 @@ export function addFieldTripNote (request, note) { return fetch(`${datastoreUrl}/fieldtrip/addNote`, {body: noteData, method: 'POST'} ) - .then(() => dispatch(fetchFieldTripDetails(request.id))) + .then(() => dispatch(fetchFieldTripDetails(request.id, intl))) .catch(err => { - alert(`Error adding field trip note: ${JSON.stringify(err)}`) + alert( + intl.formatMessage( + { id: 'actions.fieldTrip.addNoteError' }, + { err: JSON.stringify(err) } + ) + ) }) } } @@ -136,7 +151,7 @@ export function addFieldTripNote (request, note) { /** * Delete a specific note for a field trip request. */ -export function deleteFieldTripNote (request, noteId) { +export function deleteFieldTripNote (request, noteId, intl) { return function (dispatch, getState) { const {callTaker, otp} = getState() const {datastoreUrl} = otp.config @@ -145,9 +160,14 @@ export function deleteFieldTripNote (request, noteId) { return fetch(`${datastoreUrl}/fieldtrip/deleteNote`, {body: serialize({ noteId, sessionId }), method: 'POST'} ) - .then(() => dispatch(fetchFieldTripDetails(request.id))) + .then(() => dispatch(fetchFieldTripDetails(request.id, intl))) .catch(err => { - alert(`Error deleting field trip note: ${JSON.stringify(err)}`) + alert( + intl.formatMessage( + { id: 'actions.fieldTrip.deleteNoteError' }, + { err: JSON.stringify(err) } + ) + ) }) } } @@ -155,7 +175,7 @@ export function deleteFieldTripNote (request, noteId) { /** * Edit teacher (AKA submitter) notes for a field trip request. */ -export function editSubmitterNotes (request, submitterNotes) { +export function editSubmitterNotes (request, submitterNotes, intl) { return function (dispatch, getState) { const {callTaker, otp} = getState() const {datastoreUrl} = otp.config @@ -169,9 +189,14 @@ export function editSubmitterNotes (request, submitterNotes) { return fetch(`${datastoreUrl}/fieldtrip/editSubmitterNotes`, {body: noteData, method: 'POST'} ) - .then(() => dispatch(fetchFieldTripDetails(request.id))) + .then(() => dispatch(fetchFieldTripDetails(request.id, intl))) .catch(err => { - alert(`Error editing submitter notes: ${JSON.stringify(err)}`) + alert( + intl.formatMessage( + { id: 'actions.fieldTrip.editSubmitterNotesError' }, + { err: JSON.stringify(err) } + ) + ) }) } } @@ -183,8 +208,9 @@ export function editSubmitterNotes (request, submitterNotes) { * @param {Object} request The field trip request * @param {boolean} outbound If true, save the current itineraries to the * outbound field trip journey. + * @param {Object} intl A format.js intl object */ -export function saveRequestTripItineraries (request, outbound) { +export function saveRequestTripItineraries (request, outbound, intl) { return async function (dispatch, getState) { const state = getState() const { session } = state.callTaker @@ -193,10 +219,10 @@ export function saveRequestTripItineraries (request, outbound) { const itineraries = getActiveItineraries(state) // If plan is not valid, return before persisting trip. - if (fieldTripPlanIsInvalid(request, itineraries)) return + if (fieldTripPlanIsInvalid(request, itineraries, intl)) return // Show a confirmation dialog before overwriting existing plan - if (!overwriteExistingRequestTripsConfirmed(request, outbound)) return + if (!overwriteExistingRequestTripsConfirmed(request, outbound, intl)) return // Send data to server for saving. let text @@ -209,15 +235,24 @@ export function saveRequestTripItineraries (request, outbound) { } ) text = await res.text() - } catch (e) { - return alert(`Failed to save itineraries: ${JSON.stringify(e)}`) + } catch (err) { + alert( + intl.formatMessage( + { id: 'actions.fieldTrip.saveItinerariesError' }, + { err: JSON.stringify(err) } + ) + ) + return } if (text === '-1') { - return alert('Cannot Save Plan: This plan could not be saved due to a lack of capacity on one or more vehicles. Please re-plan your trip.') + alert( + intl.formatMessage({ id: 'actions.fieldTrip.itineraryCapacityError' }) + ) + return } - dispatch(fetchFieldTripDetails(request.id)) + dispatch(fetchFieldTripDetails(request.id, intl)) } } @@ -227,14 +262,12 @@ export function saveRequestTripItineraries (request, outbound) { * * @param request field trip request * @param itineraries the currently active itineraries + * @param {Object} intl A format.js intl object * @return true if invalid */ -function fieldTripPlanIsInvalid (request, itineraries) { +function fieldTripPlanIsInvalid (request, itineraries, intl) { if (!itineraries || itineraries.length === 0) { - return { - isValid: false, - message: 'No active plan to save' - } + return true } const earliestStartTime = getEarliestStartTime(itineraries) @@ -249,7 +282,13 @@ function fieldTripPlanIsInvalid (request, itineraries) { planDeparture.year() !== requestDate.year() ) { alert( - `Planned trip date (${planDeparture.format(FIELD_TRIP_DATE_FORMAT)}) is not the requested day of travel (${requestDate.format(FIELD_TRIP_DATE_FORMAT)})` + intl.formatMessage( + { id: 'actions.fieldTrip.incompatibleTripDateError' }, + { + requestDate: requestDate.format(FIELD_TRIP_DATE_FORMAT), + tripDate: planDeparture.format(FIELD_TRIP_DATE_FORMAT) + } + ) ) return true } @@ -276,13 +315,17 @@ function getEarliestStartTime (itineraries) { * @param {Object} request The field trip request * @param {boolean} outbound If true, save the current itineraries to the * outbound field trip journey. + * @param {Object} intl A format.js intl object */ -function overwriteExistingRequestTripsConfirmed (request, outbound) { - const type = outbound ? 'outbound' : 'inbound' +function overwriteExistingRequestTripsConfirmed (request, outbound, intl) { const preExistingTrip = getTripFromRequest(request, outbound) if (preExistingTrip) { - const msg = `This action will overwrite a previously planned ${type} itinerary for this request. Do you wish to continue?` - return confirm(msg) + return confirm( + intl.formatMessage( + { id: 'actions.fieldTrip.confirmOverwriteItineraries' }, + { outbound } + ) + ) } return true } @@ -372,12 +415,12 @@ function getOtpLocationString (location) { * Begins the process of making trip requests to find suitable itineraries for * either an inbound or outbound journey of a field trip. */ -export function planTrip (request, outbound) { +export function planTrip (request, outbound, intl) { return async function (dispatch, getState) { dispatch(clearSaveable()) dispatch(setGroupSize(getGroupSize(request))) await dispatch(prepareQueryParams(request, outbound)) - dispatch(makeFieldTripPlanRequests(request, outbound)) + dispatch(makeFieldTripPlanRequests(request, outbound, intl)) } } @@ -431,7 +474,7 @@ function prepareQueryParams (request, outbound) { * itinerary in subsequent OTP plan requests. * b. If all travelers have been assigned, exit the loop and cleanup */ -function makeFieldTripPlanRequests (request, outbound) { +function makeFieldTripPlanRequests (request, outbound, intl) { return async function (dispatch, getState) { const fieldTripModuleConfig = getModuleConfig( getState(), @@ -442,9 +485,12 @@ function makeFieldTripPlanRequests (request, outbound) { // field trip request date try { await dispatch(getTripIdsForTravelDate(request)) - } catch (e) { + } catch (err) { alert( - `Error fetching trips for field trip travel date: ${JSON.stringify(e)}` + intl.formatMessage( + { id: 'actions.fieldTrip.fetchTripsForDateError' }, + { err: JSON.stringify(err) } + ) ) return } @@ -470,7 +516,11 @@ function makeFieldTripPlanRequests (request, outbound) { numRequests++ if (numRequests > maxRequests) { // max number of requests exceeded. Show error. - alert('Number of trip requests exceeded without valid results') + alert( + intl.formatMessage( + { id: 'actions.fieldTrip.maxTripRequestsExceeded' } + ) + ) return dispatch(doFieldTripPlanRequestCleanup(searchId)) } @@ -781,7 +831,7 @@ function doFieldTripPlanRequestCleanup (searchId) { /** * Removes the planned journey associated with the given id. */ -export function deleteRequestTripItineraries (request, tripId) { +export function deleteRequestTripItineraries (request, tripId, intl) { return function (dispatch, getState) { const {callTaker, otp} = getState() const {datastoreUrl} = otp.config @@ -790,9 +840,14 @@ export function deleteRequestTripItineraries (request, tripId) { return fetch(`${datastoreUrl}/fieldtrip/deleteTrip`, {body: serialize({ id: tripId, sessionId }), method: 'POST'} ) - .then(() => dispatch(fetchFieldTripDetails(request.id))) + .then(() => dispatch(fetchFieldTripDetails(request.id, intl))) .catch(err => { - alert(`Error deleting field trip plan: ${JSON.stringify(err)}`) + alert( + intl.formatMessage( + { id: 'actions.fieldTrip.deleteItinerariesError' }, + { err: JSON.stringify(err) } + ) + ) }) } } @@ -834,7 +889,7 @@ export function viewRequestTripItineraries (request, outbound) { * Set group size for a field trip request. Group size consists of numStudents, * numFreeStudents, and numChaperones. */ -export function setRequestGroupSize (request, groupSize) { +export function setRequestGroupSize (request, groupSize, intl) { return function (dispatch, getState) { const {callTaker, otp} = getState() const {datastoreUrl} = otp.config @@ -848,9 +903,14 @@ export function setRequestGroupSize (request, groupSize) { return fetch(`${datastoreUrl}/fieldtrip/setRequestGroupSize`, {body: groupSizeData, method: 'POST'} ) - .then(() => dispatch(fetchFieldTripDetails(request.id))) + .then(() => dispatch(fetchFieldTripDetails(request.id, intl))) .catch(err => { - alert(`Error setting group size: ${JSON.stringify(err)}`) + alert( + intl.formatMessage( + { id: 'actions.fieldTrip.setGroupSizeError' }, + { err: JSON.stringify(err) } + ) + ) }) } } @@ -858,7 +918,7 @@ export function setRequestGroupSize (request, groupSize) { /** * Set payment info for a field trip request. */ -export function setRequestPaymentInfo (request, paymentInfo) { +export function setRequestPaymentInfo (request, paymentInfo, intl) { return function (dispatch, getState) { const {callTaker, otp} = getState() const {datastoreUrl} = otp.config @@ -872,9 +932,14 @@ export function setRequestPaymentInfo (request, paymentInfo) { return fetch(`${datastoreUrl}/fieldtrip/setRequestPaymentInfo`, {body: paymentData, method: 'POST'} ) - .then(() => dispatch(fetchFieldTripDetails(request.id))) + .then(() => dispatch(fetchFieldTripDetails(request.id, intl))) .catch(err => { - alert(`Error setting payment info: ${JSON.stringify(err)}`) + alert( + intl.formatMessage( + { id: 'actions.fieldTrip.setPaymentError' }, + { err: JSON.stringify(err) } + ) + ) }) } } @@ -882,7 +947,7 @@ export function setRequestPaymentInfo (request, paymentInfo) { /** * Set field trip request status (e.g., cancelled). */ -export function setRequestStatus (request, status) { +export function setRequestStatus (request, status, intl) { return function (dispatch, getState) { const {callTaker, otp} = getState() const {datastoreUrl} = otp.config @@ -896,9 +961,14 @@ export function setRequestStatus (request, status) { return fetch(`${datastoreUrl}/fieldtrip/setRequestStatus`, {body: statusData, method: 'POST'} ) - .then(() => dispatch(fetchFieldTripDetails(request.id))) + .then(() => dispatch(fetchFieldTripDetails(request.id, intl))) .catch(err => { - alert(`Error setting request status: ${JSON.stringify(err)}`) + alert( + intl.formatMessage( + { id: 'actions.fieldTrip.setRequestStatusError' }, + { err: JSON.stringify(err) } + ) + ) }) } } diff --git a/lib/actions/location.js b/lib/actions/location.js index e4b778986..5b26ba3f1 100644 --- a/lib/actions/location.js +++ b/lib/actions/location.js @@ -2,11 +2,12 @@ import { createAction } from 'redux-actions' import { setLocationToCurrent } from './map' +export const addLocationSearch = createAction('ADD_LOCATION_SEARCH') export const receivedPositionError = createAction('POSITION_ERROR') export const fetchingPosition = createAction('POSITION_FETCHING') export const receivedPositionResponse = createAction('POSITION_RESPONSE') -export function getCurrentPosition (setAsType = null, onSuccess) { +export function getCurrentPosition (intl, setAsType = null, onSuccess) { return async function (dispatch, getState) { if (navigator.geolocation) { dispatch(fetchingPosition({ type: setAsType })) @@ -18,16 +19,26 @@ export function getCurrentPosition (setAsType = null, onSuccess) { dispatch(receivedPositionResponse({ position })) if (setAsType) { console.log('setting location to current position') - dispatch(setLocationToCurrent({ locationType: setAsType })) + dispatch(setLocationToCurrent({ locationType: setAsType }, intl)) onSuccess && onSuccess() } } else { - dispatch(receivedPositionError({ error: { message: 'Unknown error getting position' } })) + dispatch( + receivedPositionError({ + error: { + message: intl.formatMessage( + { id: 'actions.location.unknownPositionError' } + ) + } + }) + ) } }, // On error error => { console.log('error getting current position', error) + // FIXME, analyze error code to produce better error message. + // See https://developer.mozilla.org/en-US/docs/Web/API/GeolocationPositionError dispatch(receivedPositionError({ error })) }, // Options @@ -35,9 +46,15 @@ export function getCurrentPosition (setAsType = null, onSuccess) { ) } else { console.log('current position not supported') - dispatch(receivedPositionError({ error: { message: 'Geolocation not supported by your browser' } })) + dispatch( + receivedPositionError({ + error: { + message: intl.formatMessage( + { id: 'actions.location.geolocationNotSupportedError' } + ) + } + }) + ) } } } - -export const addLocationSearch = createAction('ADD_LOCATION_SEARCH') diff --git a/lib/actions/map.js b/lib/actions/map.js index 99b45318a..354adf2b3 100644 --- a/lib/actions/map.js +++ b/lib/actions/map.js @@ -39,10 +39,13 @@ export function clearLocation (payload) { /** * Handler for @opentripplanner/location-field onLocationSelected */ -export function onLocationSelected ({ location, locationType, resultType }) { +export function onLocationSelected ( + intl, + { location, locationType, resultType } +) { return function (dispatch, getState) { if (resultType === 'CURRENT_LOCATION') { - dispatch(setLocationToCurrent({ locationType })) + dispatch(setLocationToCurrent({ locationType }, intl)) } else { dispatch(setLocation({ location, locationType })) } @@ -78,7 +81,7 @@ export function setLocation (payload) { /* payload is simply { type: 'from'|'to' }; location filled in automatically */ -export function setLocationToCurrent (payload) { +export function setLocationToCurrent (payload, intl) { return function (dispatch, getState) { const currentPosition = getState().otp.location.currentPosition if (currentPosition.error || !currentPosition.coords) return @@ -86,7 +89,7 @@ export function setLocationToCurrent (payload) { category: 'CURRENT_LOCATION', lat: currentPosition.coords.latitude, lon: currentPosition.coords.longitude, - name: '(Current Location)' + name: intl.formatMessage({ id: 'actions.map.currentLocation' }) } dispatch(settingLocation(payload)) } diff --git a/lib/actions/ui.js b/lib/actions/ui.js index dee3c40c2..2c09f2b45 100644 --- a/lib/actions/ui.js +++ b/lib/actions/ui.js @@ -322,10 +322,10 @@ export function showMobileSearchScreen () { } /** - * Sets the locale to the specified value, - * and loads the corresponding localized messages. + * Sets the locale to the specified value and loads the corresponding messages. * If the specified locale is null, fall back to the defaultLocale * set in the configuration. + * Also update the lang attribute on the root element for accessibility. */ export function setLocale (locale) { return async function (dispatch, getState) { @@ -334,6 +334,12 @@ export function setLocale (locale) { const effectiveLocale = locale || getDefaultLocale(config) const messages = await loadLocaleData(effectiveLocale, customMessages) + // Update the redux state dispatch(updateLocale({ locale: effectiveLocale, messages })) + + // Update the lang attribute in the root element. + // (The lang is the first portion of the locale.) + const lang = effectiveLocale.split('-')[0] + document.documentElement.lang = lang } } diff --git a/lib/actions/user.js b/lib/actions/user.js index bf437917c..2ae2734c3 100644 --- a/lib/actions/user.js +++ b/lib/actions/user.js @@ -65,8 +65,9 @@ function getMiddlewareVariables (state) { /** * Attempts to fetch the auth0 access token and store it in the redux state, under state.user. * @param auth0 The auth0 context used to obtain the access token for subsequent middleware fetches. + * @param intl A formatjs intl object */ -export function fetchAuth0Token (auth0) { +export function fetchAuth0Token (auth0, intl) { return async function (dispatch, getState) { try { const accessToken = await auth0.getAccessTokenSilently() @@ -74,7 +75,7 @@ export function fetchAuth0Token (auth0) { dispatch(setAccessToken(accessToken)) } catch (error) { // TODO: improve UI if there is an error. - alert('Error obtaining an authorization token.') + alert(intl.formatMessage({ id: 'actions.user.authTokenError' })) } } } @@ -88,9 +89,13 @@ export function fetchAuth0Token (auth0) { * - initializing the user state with an existing persisted user, or * - POST-ing a user for the first time. */ -function setUser (user, fetchTrips) { +function setUser (user, fetchTrips, intl) { return function (dispatch, getState) { - positionHomeAndWorkFirst(user) + positionHomeAndWorkFirst( + user, + intl.formatMessage({ id: 'common.places.home' }), + intl.formatMessage({ id: 'common.places.work' }) + ) dispatch(setCurrentUser(user)) @@ -104,8 +109,9 @@ function setUser (user, fetchTrips) { * Attempts to fetch user preferences (or set initial values if the user is being created) * into the redux state, under state.user. * @param auth0User If provided, the auth0.user object used to initialize the default user object (with email and auth0 id). + * @param intl A formatjs intl object */ -export function fetchOrInitializeUser (auth0User) { +export function fetchOrInitializeUser (auth0User, intl) { return async function (dispatch, getState) { const { accessToken, apiBaseUrl, apiKey } = getMiddlewareVariables(getState()) const requestUrl = `${apiBaseUrl}${API_OTPUSER_PATH}/fromtoken` @@ -138,7 +144,7 @@ export function fetchOrInitializeUser (auth0User) { // Set user in redux state. // (This sorts saved places, and, for existing users, fetches trips.) - dispatch(setUser(userData, !isNewAccount)) + dispatch(setUser(userData, !isNewAccount, intl)) } } @@ -147,8 +153,9 @@ export function fetchOrInitializeUser (auth0User) { * then, if that was successful, updates the redux state with that user. * @param userData the user entry to persist. * @param silentOnSuccess true to suppress the confirmation if the operation is successful (e.g. immediately after user accepts the terms). + * @param intl the react-intl formatter */ -export function createOrUpdateUser (userData, silentOnSuccess = false) { +export function createOrUpdateUser (userData, silentOnSuccess = false, intl) { return async function (dispatch, getState) { const { accessToken, apiBaseUrl, apiKey, loggedInUser } = getMiddlewareVariables(getState()) const { id } = userData // Middleware ID, NOT auth0 (or similar) id. @@ -176,15 +183,20 @@ export function createOrUpdateUser (userData, silentOnSuccess = false) { // TODO: improve the UI feedback messages for this. if (status === 'success' && returnedUser) { if (!silentOnSuccess) { - alert('Your preferences have been saved.') + alert(intl.formatMessage({ id: 'actions.user.preferencesSaved' })) } // Update application state with the user entry as saved // (as returned) by the middleware. // (This sorts saved places, and, for existing users, fetches trips.) - dispatch(setUser(returnedUser, isCreatingUser)) + dispatch(setUser(returnedUser, isCreatingUser, intl)) } else { - alert(`An error was encountered:\n${JSON.stringify(message)}`) + alert( + intl.formatMessage( + { id: 'actions.user.genericError' }, + { err: JSON.stringify(message) } + ) + ) } } } @@ -194,8 +206,9 @@ export function createOrUpdateUser (userData, silentOnSuccess = false) { * middleware database. * @param userData the user account to delete * @param auth0 auth0 object (gives access to logout function) + * @param intl A formatjs intl object */ -export function deleteUser (userData, auth0) { +export function deleteUser (userData, auth0, intl) { return async function (dispatch, getState) { const { accessToken, apiBaseUrl, apiKey } = getMiddlewareVariables(getState()) const { id } = userData // Middleware ID, NOT auth0 (or similar) id. @@ -207,11 +220,21 @@ export function deleteUser (userData, auth0) { ) // TODO: improve the UI feedback messages for this. if (status === 'success' && deletedUser) { - alert(`Your user account (${userData.email}) has been deleted.`) + alert( + intl.formatMessage( + { id: 'actions.user.accountDeleted' }, + { email: userData.email } + ) + ) // Log out user and route them to the home page. auth0.logout({returnTo: URL_ROOT}) } else { - alert(`An error was encountered:\n${JSON.stringify(message)}`) + alert( + intl.formatMessage( + { id: 'actions.user.genericError' }, + { err: JSON.stringify(message) } + ) + ) } } } @@ -219,7 +242,7 @@ export function deleteUser (userData, auth0) { /** * Requests the verification email for the new user to be resent. */ -export function resendVerificationEmail () { +export function resendVerificationEmail (intl) { return async function (dispatch, getState) { const { accessToken, apiBaseUrl, apiKey } = getMiddlewareVariables(getState()) @@ -229,7 +252,7 @@ export function resendVerificationEmail () { // TODO: improve the UI feedback messages for this. if (status === 'success') { - alert('The email verification message has been resent.') + alert(intl.formatMessage({ id: 'actions.user.emailVerificationResent' })) } } } @@ -260,7 +283,8 @@ export function createOrUpdateUserMonitoredTrip ( tripData, isNew, silentOnSuccess, - noRedirect + noRedirect, + intl ) { return async function (dispatch, getState) { const { accessToken, apiBaseUrl, apiKey } = getMiddlewareVariables(getState()) @@ -283,7 +307,7 @@ export function createOrUpdateUserMonitoredTrip ( // TODO: improve the UI feedback messages for this. if (status === 'success' && data) { if (!silentOnSuccess) { - alert('Your preferences have been saved.') + alert(intl.formatMessage({ id: 'actions.user.preferencesSaved' })) } // Reload user's monitored trips after add/update. @@ -294,7 +318,12 @@ export function createOrUpdateUserMonitoredTrip ( // Finally, navigate to the saved trips page. dispatch(routeTo(TRIPS_PATH)) } else { - alert(`An error was encountered:\n${JSON.stringify(message)}`) + alert( + intl.formatMessage( + { id: 'actions.user.genericError' }, + { err: JSON.stringify(message) } + ) + ) } } } @@ -302,26 +331,28 @@ export function createOrUpdateUserMonitoredTrip ( /** * Toggles the isActive status of a monitored trip */ -export function togglePauseTrip (trip) { +export function togglePauseTrip (trip, intl) { return function (dispatch, getState) { const clonedTrip = clone(trip) clonedTrip.isActive = !clonedTrip.isActive // Silent update of existing trip. - dispatch(createOrUpdateUserMonitoredTrip(clonedTrip, false, true, true)) + dispatch( + createOrUpdateUserMonitoredTrip(clonedTrip, false, true, true, intl) + ) } } /** * Toggles the snoozed status of a monitored trip */ -export function toggleSnoozeTrip (trip) { +export function toggleSnoozeTrip (trip, intl) { return function (dispatch, getState) { const newTrip = clone(trip) newTrip.snoozed = !newTrip.snoozed // Silent update of existing trip. - dispatch(createOrUpdateUserMonitoredTrip(newTrip, false, true, true)) + dispatch(createOrUpdateUserMonitoredTrip(newTrip, false, true, true, intl)) } } @@ -329,9 +360,13 @@ export function toggleSnoozeTrip (trip) { * Deletes a logged-in user's monitored trip, * then, if that was successful, refreshes the redux monitoredTrips state. */ -export function confirmAndDeleteUserMonitoredTrip (tripId) { +export function confirmAndDeleteUserMonitoredTrip (tripId, intl) { return async function (dispatch, getState) { - if (!confirm('Would you like to remove this trip?')) return + if ( + !confirm( + intl.formatMessage({ id: 'actions.user.confirmDeleteMonitoredTrip' }) + ) + ) return const { accessToken, apiBaseUrl, apiKey } = getMiddlewareVariables(getState()) const requestUrl = `${apiBaseUrl}${API_MONITORED_TRIP_PATH}/${tripId}` @@ -341,7 +376,12 @@ export function confirmAndDeleteUserMonitoredTrip (tripId) { // Reload user's monitored trips after deletion. dispatch(fetchMonitoredTrips()) } else { - alert(`An error was encountered:\n${JSON.stringify(message)}`) + alert( + intl.formatMessage( + { id: 'actions.user.genericError' }, + { err: JSON.stringify(message) } + ) + ) } } } @@ -349,7 +389,7 @@ export function confirmAndDeleteUserMonitoredTrip (tripId) { /** * Requests a verification code via SMS for the logged-in user. */ -export function requestPhoneVerificationSms (newPhoneNumber) { +export function requestPhoneVerificationSms (newPhoneNumber, intl) { return async function (dispatch, getState) { const state = getState() const { number, timestamp } = state.user.lastPhoneSmsRequest @@ -372,14 +412,19 @@ export function requestPhoneVerificationSms (newPhoneNumber) { if (status === 'success') { // Refetch user and update application state with new phone number and verification status. - dispatch(fetchOrInitializeUser()) + dispatch(fetchOrInitializeUser(intl)) } else { - alert(`An error was encountered:\n${JSON.stringify(message)}`) + alert( + intl.formatMessage( + { id: 'actions.user.genericError' }, + { err: JSON.stringify(message) } + ) + ) } } else { // Alert user if they have been throttled. // TODO: improve this alert. - alert('A verification SMS was sent to the indicated phone number less than a minute ago. Please try again in a few moments.') + alert(intl.formatMessage({ id: 'actions.user.smsResendThrottled' })) } } } @@ -387,7 +432,7 @@ export function requestPhoneVerificationSms (newPhoneNumber) { /** * Validate the phone number verification code for the logged-in user. */ -export function verifyPhoneNumber (code) { +export function verifyPhoneNumber (code, intl) { return async function (dispatch, getState) { const { accessToken, apiBaseUrl, apiKey, loggedInUser } = getMiddlewareVariables(getState()) const requestUrl = `${apiBaseUrl}${API_OTPUSER_PATH}/${loggedInUser.id}${API_OTPUSER_VERIFY_SMS_SUBPATH}/${code}` @@ -398,15 +443,15 @@ export function verifyPhoneNumber (code) { if (status === 'success' && data) { if (data.status === 'approved') { // Refetch user and update application state with new phone number and verification status. - dispatch(fetchOrInitializeUser()) + dispatch(fetchOrInitializeUser(intl)) } else { // Otherwise, the user entered a wrong/incorrect code. - alert('The code you entered is invalid. Please try again.') + alert(intl.formatMessage({ id: 'actions.user.smsInvalidCode' })) } } else { // This happens when an error occurs on backend side, especially // when the code has expired (or was cancelled by Twilio after too many attempts). - alert(`Your phone could not be verified. Perhaps the code you entered has expired. Please request a new code and try again.`) + alert(intl.formatMessage({ id: 'actions.user.smsVerificationFailed' })) } } } @@ -414,7 +459,7 @@ export function verifyPhoneNumber (code) { /** * Check itinerary existence for the given monitored trip. */ -export function checkItineraryExistence (trip) { +export function checkItineraryExistence (trip, intl) { return async function (dispatch, getState) { const { accessToken, apiBaseUrl, apiKey } = getMiddlewareVariables(getState()) const requestUrl = `${apiBaseUrl}${API_MONITORED_TRIP_PATH}/checkitinerary` @@ -429,7 +474,9 @@ export function checkItineraryExistence (trip) { if (status === 'success' && data) { dispatch(setitineraryExistence(data)) } else { - alert('Error checking whether your selected trip is possible.') + alert( + intl.formatMessage({ id: 'actions.user.itineraryExistenceCheckFailed' }) + ) } } } @@ -464,7 +511,7 @@ export function planNewTripFromMonitoredTrip (monitoredTrip) { * Saves the given place data at the specified index for the logged-in user. * Note: places with blank addresses will not appear in persistence. */ -export function saveUserPlace (placeToSave, placeIndex) { +export function saveUserPlace (placeToSave, placeIndex, intl) { return function (dispatch, getState) { const { loggedInUser } = getState().user @@ -474,20 +521,22 @@ export function saveUserPlace (placeToSave, placeIndex) { loggedInUser.savedLocations[placeIndex] = placeToSave } - dispatch(createOrUpdateUser(loggedInUser, true)) + dispatch(createOrUpdateUser(loggedInUser, true, intl)) } } /** * Delete the place data at the specified index for the logged-in user. */ -export function deleteUserPlace (placeIndex) { +export function deleteUserPlace (placeIndex, intl) { return function (dispatch, getState) { - if (!confirm('Would you like to remove this place?')) return + if ( + !confirm(intl.formatMessage({ id: 'actions.user.confirmDeletePlace' })) + ) return const { loggedInUser } = getState().user loggedInUser.savedLocations.splice(placeIndex, 1) - dispatch(createOrUpdateUser(loggedInUser, true)) + dispatch(createOrUpdateUser(loggedInUser, true, intl)) } } diff --git a/lib/components/admin/call-history-window.js b/lib/components/admin/call-history-window.js index 4bbf897ce..d623fdf58 100644 --- a/lib/components/admin/call-history-window.js +++ b/lib/components/admin/call-history-window.js @@ -1,4 +1,5 @@ import React from 'react' +import { injectIntl } from 'react-intl' import { connect } from 'react-redux' import * as callTakerActions from '../../actions/call-taker' @@ -9,7 +10,7 @@ import DraggableWindow from './draggable-window' import {WindowHeader} from './styled' function CallHistoryWindow (props) { - const {callTaker, fetchQueries, searches, toggleCallHistory} = props + const {callTaker, fetchQueries, intl, searches, toggleCallHistory} = props const {activeCall, callHistory} = callTaker if (!callHistory.visible) return null return ( @@ -22,6 +23,7 @@ function CallHistoryWindow (props) { ? : null } @@ -33,6 +35,7 @@ function CallHistoryWindow (props) { call={call} fetchQueries={fetchQueries} index={i} + intl={intl} key={`${call.id}-${i}`} /> )) :
No calls in history
@@ -54,4 +57,6 @@ const mapDispatchToProps = { toggleCallHistory: callTakerActions.toggleCallHistory } -export default connect(mapStateToProps, mapDispatchToProps)(CallHistoryWindow) +export default connect(mapStateToProps, mapDispatchToProps)( + injectIntl(CallHistoryWindow) +) diff --git a/lib/components/admin/call-record.js b/lib/components/admin/call-record.js index 3f37491cd..80cbd41ca 100644 --- a/lib/components/admin/call-record.js +++ b/lib/components/admin/call-record.js @@ -26,9 +26,11 @@ export default class CallRecord extends Component { } _toggleExpanded = () => { - const {call, fetchQueries} = this.props + const {call, fetchQueries, intl} = this.props const {expanded} = this.state - if (!expanded) fetchQueries(call.id) + if (!expanded) { + fetchQueries(call.id, intl) + } this.setState({expanded: !expanded}) } diff --git a/lib/components/admin/call-taker-controls.js b/lib/components/admin/call-taker-controls.js index 3fbd38458..263749089 100644 --- a/lib/components/admin/call-taker-controls.js +++ b/lib/components/admin/call-taker-controls.js @@ -1,4 +1,5 @@ import React, { Component } from 'react' +import { injectIntl } from 'react-intl' import { connect } from 'react-redux' import * as apiActions from '../../actions/api' @@ -30,18 +31,23 @@ class CallTakerControls extends Component { fetchCalls, fetchFieldTrips, fieldTripEnabled, + intl, session } = this.props // Once session is available, fetch calls. if (session && !prevProps.session) { - if (callTakerEnabled) fetchCalls() - if (fieldTripEnabled) fetchFieldTrips() + if (callTakerEnabled) fetchCalls(intl) + if (fieldTripEnabled) fetchFieldTrips(intl) } } _onClickCall = () => { - if (this._callInProgress()) this.props.endCall() - else this.props.beginCall() + const { beginCall, endCall, intl } = this.props + if (this._callInProgress()) { + endCall(intl) + } else { + beginCall() + } } _renderCallButtonIcon = () => { @@ -145,4 +151,6 @@ const mapDispatchToProps = { setMainPanelContent: uiActions.setMainPanelContent } -export default connect(mapStateToProps, mapDispatchToProps)(CallTakerControls) +export default connect(mapStateToProps, mapDispatchToProps)( + injectIntl(CallTakerControls) +) diff --git a/lib/components/admin/editable-section.js b/lib/components/admin/editable-section.js index b8bba7975..59c60945b 100644 --- a/lib/components/admin/editable-section.js +++ b/lib/components/admin/editable-section.js @@ -24,14 +24,14 @@ export default class EditableSection extends Component { : '' _onClickSave = data => { - const {onChange, request} = this.props + const {intl, onChange, request} = this.props // Convert all null values received to '', // otherwise they will appear in the backend as the 'null' string. for (const field in data) { if (data[field] === null) data[field] = '' } - onChange(request, data) + onChange(request, data, intl) this.setState({isEditing: false}) } diff --git a/lib/components/admin/field-trip-details.js b/lib/components/admin/field-trip-details.js index 48c125435..135a12dde 100644 --- a/lib/components/admin/field-trip-details.js +++ b/lib/components/admin/field-trip-details.js @@ -2,6 +2,7 @@ import { getDateFormat } from '@opentripplanner/core-utils/lib/time' import moment from 'moment' import React, { Component } from 'react' import { DropdownButton, MenuItem } from 'react-bootstrap' +import { injectIntl } from 'react-intl' import { connect } from 'react-redux' import styled from 'styled-components' @@ -42,19 +43,22 @@ const WindowHeader = styled(DefaultWindowHeader)` * Shows the details for the active Field Trip Request. */ class FieldTripDetails extends Component { - _editSubmitterNotes = (val) => this.props.editSubmitterNotes(this.props.request, val) + _editSubmitterNotes = (val) => { + const { editSubmitterNotes, intl, request } = this.props + editSubmitterNotes(request, val, intl) + } _getRequestLink = (path, isPublic = false) => `${this.props.datastoreUrl}/${isPublic ? 'public/' : ''}fieldtrip/${path}?requestId=${this.props.request.id}` _onToggleStatus = () => { - const {request, setRequestStatus} = this.props + const {intl, request, setRequestStatus} = this.props if (request.status !== 'cancelled') { if (confirm('Are you sure you want to cancel this request? Any associated trips will be deleted.')) { - setRequestStatus(request, 'cancelled') + setRequestStatus(request, 'cancelled', intl) } } else { - setRequestStatus(request, 'active') + setRequestStatus(request, 'active', intl) } } @@ -128,6 +132,7 @@ class FieldTripDetails extends Component { addFieldTripNote, clearActiveFieldTrip, deleteFieldTripNote, + intl, request, setRequestGroupSize, setRequestPaymentInfo, @@ -181,6 +186,7 @@ class FieldTripDetails extends Component { } fields={GROUP_FIELDS} inputStyle={{lineHeight: '0.8em', padding: '0px', width: '50px'}} + intl={intl} onChange={setRequestGroupSize} request={request} valueFirst @@ -201,6 +207,7 @@ class FieldTripDetails extends Component { } fields={PAYMENT_FIELDS} inputStyle={{lineHeight: '0.8em', padding: '0px', width: '100px'}} + intl={intl} onChange={setRequestPaymentInfo} request={request} /> @@ -208,6 +215,7 @@ class FieldTripDetails extends Component { @@ -239,4 +247,6 @@ const mapDispatchToProps = { toggleFieldTrips: fieldTripActions.toggleFieldTrips } -export default connect(mapStateToProps, mapDispatchToProps)(FieldTripDetails) +export default connect(mapStateToProps, mapDispatchToProps)( + injectIntl(FieldTripDetails) +) diff --git a/lib/components/admin/field-trip-list.js b/lib/components/admin/field-trip-list.js index 399351eaa..e2b96c0da 100644 --- a/lib/components/admin/field-trip-list.js +++ b/lib/components/admin/field-trip-list.js @@ -2,6 +2,7 @@ import moment from 'moment' import qs from 'qs' import React, { Component } from 'react' import { Badge, Button } from 'react-bootstrap' +import { injectIntl } from 'react-intl' import { connect } from 'react-redux' import * as fieldTripActions from '../../actions/field-trip' @@ -25,16 +26,24 @@ class FieldTripList extends Component { } } _onClickFieldTrip = (request) => { - const {callTaker, fetchFieldTripDetails, setActiveFieldTrip} = this.props + const { + callTaker, + fetchFieldTripDetails, + intl, + setActiveFieldTrip + } = this.props if (request.id === callTaker.fieldTrip.activeId) { this._onCloseActiveFieldTrip() } else { setActiveFieldTrip(request.id) - fetchFieldTripDetails(request.id) + fetchFieldTripDetails(request.id, intl) } } - _onClickRefresh = () => this.props.fetchFieldTrips() + _onClickRefresh = () => { + const { fetchFieldTrips, intl } = this.props + fetchFieldTrips(intl) + } _onCloseActiveFieldTrip = () => { this.props.setActiveFieldTrip(null) @@ -256,4 +265,6 @@ const mapDispatchToProps = { toggleFieldTrips: fieldTripActions.toggleFieldTrips } -export default connect(mapStateToProps, mapDispatchToProps)(FieldTripList) +export default connect(mapStateToProps, mapDispatchToProps)( + injectIntl(FieldTripList) +) diff --git a/lib/components/admin/field-trip-notes.js b/lib/components/admin/field-trip-notes.js index c0f2be4a4..8c9813897 100644 --- a/lib/components/admin/field-trip-notes.js +++ b/lib/components/admin/field-trip-notes.js @@ -57,15 +57,15 @@ export default class FieldTripNotes extends Component { _addOperationalNote = () => this._addNote('operational') _addNote = (type) => { - const {addFieldTripNote, request} = this.props + const {addFieldTripNote, intl, request} = this.props const note = prompt(`Type ${type} note to be attached to this request:`) - if (note) addFieldTripNote(request, {note, type}) + if (note) addFieldTripNote(request, {note, type}, intl) } _deleteNote = (note) => { - const {deleteFieldTripNote, request} = this.props + const {deleteFieldTripNote, intl, request} = this.props if (confirm(`Are you sure you want to delete note "${note.note}"?`)) { - deleteFieldTripNote(request, note.id) + deleteFieldTripNote(request, note.id, intl) } } diff --git a/lib/components/admin/print-field-trip-layout.js b/lib/components/admin/print-field-trip-layout.js index acd71279f..96dda397e 100644 --- a/lib/components/admin/print-field-trip-layout.js +++ b/lib/components/admin/print-field-trip-layout.js @@ -1,6 +1,7 @@ import PrintableItinerary from '@opentripplanner/printable-itinerary' import React, { Component } from 'react' import { Button } from 'react-bootstrap' +import { injectIntl } from 'react-intl' import { connect } from 'react-redux' import * as callTakerActions from '../../actions/call-taker' @@ -33,18 +34,19 @@ class PrintFieldTripLayout extends Component { } componentDidMount () { - const { initializeModules } = this.props + const { initializeModules, intl } = this.props // Add print-view class to html tag to ensure that iOS scroll fix only applies // to non-print views. addPrintViewClassToRootHtml() // Load call-taker/field-trip functionality (performs a fetch). - initializeModules() + initializeModules(intl) } componentDidUpdate (prevProps) { const { fetchFieldTripDetails, + intl, receivedFieldTrips, request, requestId, @@ -60,7 +62,7 @@ class PrintFieldTripLayout extends Component { id: requestId }] }) - fetchFieldTripDetails(requestId) + fetchFieldTripDetails(requestId, intl) } if (request && request !== prevProps.request) { @@ -181,4 +183,6 @@ const mapDispatchToProps = { receivedFieldTrips: fieldTripActions.receivedFieldTrips } -export default connect(mapStateToProps, mapDispatchToProps)(PrintFieldTripLayout) +export default connect(mapStateToProps, mapDispatchToProps)( + injectIntl(PrintFieldTripLayout) +) diff --git a/lib/components/admin/trip-status.js b/lib/components/admin/trip-status.js index 79d8e8ba8..833a82a1a 100644 --- a/lib/components/admin/trip-status.js +++ b/lib/components/admin/trip-status.js @@ -1,6 +1,7 @@ import { getTimeFormat } from '@opentripplanner/core-utils/lib/time' import moment from 'moment' import React, {Component} from 'react' +import { injectIntl } from 'react-intl' import { connect } from 'react-redux' import * as fieldTripActions from '../../actions/field-trip' @@ -20,26 +21,26 @@ class TripStatus extends Component { _formatTime = (time) => moment(time).format(this.props.timeFormat) _onDeleteTrip = () => { - const { deleteRequestTripItineraries, request, trip } = this.props + const { deleteRequestTripItineraries, intl, request, trip } = this.props if (!confirm('Are you sure you want to delete the planned trip?')) { return } - deleteRequestTripItineraries(request, trip.id) + deleteRequestTripItineraries(request, trip.id, intl) } _onPlanTrip = () => { - const { outbound, planTrip, request, status, trip } = this.props + const { intl, outbound, planTrip, request, status, trip } = this.props if (status && trip) { if (!confirm('Re-planning this trip will cause the trip planner to avoid the currently saved trip. Are you sure you want to continue?')) { return } } - planTrip(request, outbound) + planTrip(request, outbound, intl) } _onSaveTrip = () => { - const { outbound, request, saveRequestTripItineraries } = this.props - saveRequestTripItineraries(request, outbound) + const { intl, outbound, request, saveRequestTripItineraries } = this.props + saveRequestTripItineraries(request, outbound, intl) } _onViewTrip = () => { @@ -139,4 +140,6 @@ const mapDispatchToProps = { viewRequestTripItineraries: fieldTripActions.viewRequestTripItineraries } -export default connect(mapStateToProps, mapDispatchToProps)(TripStatus) +export default connect(mapStateToProps, mapDispatchToProps)( + injectIntl(TripStatus) +) diff --git a/lib/components/app/app-menu.js b/lib/components/app/app-menu.js index 159d22446..d898866d7 100644 --- a/lib/components/app/app-menu.js +++ b/lib/components/app/app-menu.js @@ -3,13 +3,15 @@ import PropTypes from 'prop-types' import qs from 'qs' import { connect } from 'react-redux' import { DropdownButton, MenuItem } from 'react-bootstrap' +import { FormattedMessage } from 'react-intl' import { withRouter } from 'react-router' +import Icon from '../util/icon' +import IconWithSpace from '../util/icon-with-space' import * as callTakerActions from '../../actions/call-taker' import * as fieldTripActions from '../../actions/field-trip' import { MainPanelContent, setMainPanelContent } from '../../actions/ui' import { isModuleEnabled, Modules } from '../../util/config' -import Icon from '../util/icon' // TODO: make menu items configurable via props/config @@ -45,7 +47,6 @@ class AppMenu extends Component { const { callTakerEnabled, fieldTripEnabled, - languageConfig, mailablesEnabled, resetAndToggleCallHistory, resetAndToggleFieldTrips, @@ -61,25 +62,30 @@ class AppMenu extends Component { noCaret title={()}> - {languageConfig.routeViewer || 'Route Viewer'} + + {callTakerEnabled && - Call History + + } {fieldTripEnabled && - Field Trip + + } {mailablesEnabled && - Mailables + + } - Start Over + +
@@ -90,11 +96,9 @@ class AppMenu extends Component { // connect to the redux store const mapStateToProps = (state, ownProps) => { - const {language} = state.otp.config return { callTakerEnabled: isModuleEnabled(state, Modules.CALL_TAKER), fieldTripEnabled: isModuleEnabled(state, Modules.FIELD_TRIP), - languageConfig: language, mailablesEnabled: isModuleEnabled(state, Modules.MAILABLES) } } diff --git a/lib/components/app/batch-routing-panel.js b/lib/components/app/batch-routing-panel.js index eb63e51d1..981b1d02b 100644 --- a/lib/components/app/batch-routing-panel.js +++ b/lib/components/app/batch-routing-panel.js @@ -1,4 +1,5 @@ import React, { Component } from 'react' +import { injectIntl } from 'react-intl' import { connect } from 'react-redux' import styled from 'styled-components' @@ -33,18 +34,27 @@ const NarrativeContainer = styled.div` */ class BatchRoutingPanel extends Component { render () { - const {mobile} = this.props - const actionText = mobile ? 'tap' : 'click' + const { intl, mobile } = this.props return (
@@ -93,4 +103,6 @@ const mapDispatchToProps = { setQueryParam: formActions.setQueryParam } -export default connect(mapStateToProps, mapDispatchToProps)(BatchRoutingPanel) +export default connect(mapStateToProps, mapDispatchToProps)( + injectIntl(BatchRoutingPanel) +) diff --git a/lib/components/app/not-found.js b/lib/components/app/not-found.js index 4733b08b7..4202bdaa7 100644 --- a/lib/components/app/not-found.js +++ b/lib/components/app/not-found.js @@ -1,7 +1,10 @@ import React from 'react' -import { Alert, Glyphicon } from 'react-bootstrap' +import { Alert } from 'react-bootstrap' +import { FormattedMessage } from 'react-intl' import styled from 'styled-components' +import IconWithSpace from '../util/icon-with-space' + const StyledAlert = styled(Alert)` margin-top: 25px; ` @@ -11,8 +14,13 @@ const StyledAlert = styled(Alert)` */ const NotFound = () => ( -

Content not found

-

The content you requested is not available.

+

+ + +

+

+ +

) diff --git a/lib/components/app/print-layout.js b/lib/components/app/print-layout.js index 1a48d07db..fc8402760 100644 --- a/lib/components/app/print-layout.js +++ b/lib/components/app/print-layout.js @@ -2,6 +2,7 @@ import PrintableItinerary from '@opentripplanner/printable-itinerary' import PropTypes from 'prop-types' import React, { Component } from 'react' import { Button } from 'react-bootstrap' +import { FormattedMessage } from 'react-intl' import { connect } from 'react-redux' import { parseUrlQueryString } from '../../actions/form' @@ -9,6 +10,7 @@ import { routingQuery } from '../../actions/api' import DefaultMap from '../map/default-map' import TripDetails from '../narrative/connected-trip-details' import { ComponentContext } from '../../util/contexts' +import IconWithSpace from '../util/icon-with-space' import { addPrintViewClassToRootHtml, clearClassFromRootHtml } from '../../util/print' import { getActiveItinerary } from '../../util/state' @@ -60,14 +62,16 @@ class PrintLayout extends Component {
{' '}
- Itinerary +
{/* The map, if visible */} diff --git a/lib/components/app/responsive-webapp.js b/lib/components/app/responsive-webapp.js index b9d3b5220..a7dad2dda 100644 --- a/lib/components/app/responsive-webapp.js +++ b/lib/components/app/responsive-webapp.js @@ -7,7 +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 { injectIntl, IntlProvider } from 'react-intl' import { connect } from 'react-redux' import { Route, Switch, withRouter } from 'react-router' @@ -71,10 +71,10 @@ class ResponsiveWebapp extends Component { currentPosition, formChanged, initZoomOnLocate, + intl, location, matchContentToUrl, query, - setLocale, setLocationToCurrent, setMapCenter, setMapZoom, @@ -107,7 +107,7 @@ class ResponsiveWebapp extends Component { // if in mobile mode and from field is not set, use current location as from and recenter map if (isMobile() && query.from === null) { - setLocationToCurrent({ locationType: 'from' }) + setLocationToCurrent({ locationType: 'from' }, intl) setMapCenter(pt) if (initZoomOnLocate) { setMapZoom({ zoom: initZoomOnLocate }) @@ -120,11 +120,7 @@ class ResponsiveWebapp extends Component { // console.log('url changed to', location.pathname) matchContentToUrl(location) } - // 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.locale) - } + // Check for change between ITINERARY and PROFILE routingTypes // TODO: restore this for profile mode /* if (query.routingType !== nextProps.query.routingType) { @@ -148,6 +144,7 @@ class ResponsiveWebapp extends Component { getCurrentPosition, handleBackButtonPress, initializeModules, + intl, location, matchContentToUrl, parseUrlQueryString, @@ -174,7 +171,7 @@ class ResponsiveWebapp extends Component { if (isMobile()) { // If on mobile browser, check position on load - getCurrentPosition() + getCurrentPosition(intl) // Also, watch for changes in position on mobile navigator.geolocation.watchPosition( @@ -196,7 +193,7 @@ class ResponsiveWebapp extends Component { parseUrlQueryString() } // Initialize call taker/field trip modules (check for valid auth session). - initializeModules() + initializeModules(intl) } componentWillUnmount () { @@ -249,7 +246,7 @@ class ResponsiveWebapp extends Component { // connect to the redux store const mapStateToProps = (state, ownProps) => { - const title = getTitle(state) + const title = getTitle(state, ownProps.intl) return { activeItinerary: getActiveItinerary(state), activeSearchId: state.otp.activeSearchId, @@ -272,7 +269,6 @@ const mapDispatchToProps = { matchContentToUrl: uiActions.matchContentToUrl, parseUrlQueryString: formActions.parseUrlQueryString, receivedPositionResponse: locationActions.receivedPositionResponse, - setLocale: uiActions.setLocale, setLocationToCurrent: mapActions.setLocationToCurrent, setMapCenter: configActions.setMapCenter, setMapZoom: configActions.setMapZoom @@ -282,7 +278,11 @@ const history = createHashHistory() const WebappWithRouter = withRouter( withLoggedInUserSupport( - connect(mapStateToProps, mapDispatchToProps)(ResponsiveWebapp) + injectIntl( + connect(mapStateToProps, mapDispatchToProps)( + ResponsiveWebapp + ) + ) ) ) @@ -377,6 +377,31 @@ export const routes = [ * so that Auth0 services are available everywhere. */ class RouterWrapperWithAuth0 extends Component { + constructor (props) { + super(props) + this._initializeOrUpdateLocale(props) + } + + componentDidUpdate (prevProps) { + this._initializeOrUpdateLocale(this.props, prevProps) + } + + /** + * On component initialization, or if the URL locale parameter + * changes or is initially blank, (e.g., user modifies anything after ?, e.g. locale) + * update the corresponding redux state. + * @param {*} props The current props for the component + * @param {*} prevProps Optional previous props, if available. + */ + _initializeOrUpdateLocale (props, prevProps) { + const urlParams = coreUtils.query.getUrlParams() + const { locale: newLocale } = urlParams + + if (!prevProps || (!newLocale && !prevProps.locale) || (newLocale && newLocale !== prevProps.locale)) { + props.setLocale(newLocale) + } + } + render () { const { auth0Config, @@ -390,7 +415,8 @@ class RouterWrapperWithAuth0 extends Component { showLoginError } = this.props - const router = ( + // Don't render anything until the locale/localized messages have been initialized. + const router = localizedMessages && ( { const mapWrapperDispatchToProps = { processSignIn: authActions.processSignIn, + setLocale: uiActions.setLocale, showAccessTokenError: authActions.showAccessTokenError, showLoginError: authActions.showLoginError } diff --git a/lib/components/form/add-place-button.js b/lib/components/form/add-place-button.js index 178fbf9ad..89120b675 100644 --- a/lib/components/form/add-place-button.js +++ b/lib/components/form/add-place-button.js @@ -1,4 +1,7 @@ import React from 'react' +import { FormattedMessage } from 'react-intl' + +import IconWithSpace from '../util/icon-with-space' const AddPlaceButton = ({from, intermediatePlaces, onClick, to}) => { // Only permit adding intermediate place if from/to is defined. @@ -11,12 +14,12 @@ const AddPlaceButton = ({from, intermediatePlaces, onClick, to}) => { onClick={onClick} style={{marginBottom: '5px', marginLeft: '10px'}} > - {' '} + {maxPlacesDefined - ? 'Maximum intermediate places reached' + ? : disabled - ? 'Define origin/destination to add intermediate places' - : 'Add place' + ? + : } ) diff --git a/lib/components/form/batch-settings.js b/lib/components/form/batch-settings.js index f4150a576..b6ef90727 100644 --- a/lib/components/form/batch-settings.js +++ b/lib/components/form/batch-settings.js @@ -1,5 +1,6 @@ import coreUtils from '@opentripplanner/core-utils' import React, { Component } from 'react' +import { injectIntl } from 'react-intl' import { connect } from 'react-redux' import styled from 'styled-components' @@ -10,7 +11,7 @@ import { hasValidLocation, getActiveSearch, getShowUserSettings } from '../../ut import BatchPreferences from './batch-preferences' import DateTimeModal from './date-time-modal' -import ModeButtons, {MODE_OPTIONS, StyledModeButton} from './mode-buttons' +import ModeButtons, {StyledModeButton} from './mode-buttons' import { BatchPreferencesContainer, DateTimeModalContainer, @@ -39,7 +40,13 @@ function combinationHasAnyOfModes (combination, modes) { } // List of possible modes that can be selected via mode buttons. -const POSSIBLE_MODES = MODE_OPTIONS.map(b => b.mode) +const POSSIBLE_MODES = [ + 'TRANSIT', + 'WALK', + 'CAR', + 'BICYCLE', + 'RENT' // TODO: include HAIL? +] const ModeButtonsFullWidthContainer = styled.div` display: flex; @@ -103,14 +110,21 @@ class BatchSettings extends Component { } _planTrip = () => { - const {currentQuery, routingQuery} = this.props + const {currentQuery, intl, routingQuery} = this.props // Check for any validation issues in query. const issues = [] - if (!hasValidLocation(currentQuery, 'from')) issues.push('from') - if (!hasValidLocation(currentQuery, 'to')) issues.push('to') + if (!hasValidLocation(currentQuery, 'from')) { + issues.push(intl.formatMessage({id: 'components.BatchSettings.origin'})) + } + if (!hasValidLocation(currentQuery, 'to')) { + issues.push(intl.formatMessage({id: 'components.BatchSettings.destination'})) + } if (issues.length > 0) { // TODO: replace with less obtrusive validation. - window.alert(`Please define the following fields to plan a trip: ${issues.join(', ')}`) + window.alert(intl.formatMessage( + {id: 'components.BatchSettings.validationMessage'}, + {issues: intl.formatList(issues, {type: 'conjunction'})} + )) return } // Close any expanded panels. @@ -127,7 +141,7 @@ class BatchSettings extends Component { _toggleSettings = () => this.setState(this._updateExpanded('SETTINGS')) render () { - const {config, currentQuery} = this.props + const {config, currentQuery, intl} = this.props const {expanded, selectedModes} = this.state return ( <> @@ -162,7 +176,7 @@ class BatchSettings extends Component { @@ -200,4 +214,6 @@ const mapDispatchToProps = { setQueryParam: formActions.setQueryParam } -export default connect(mapStateToProps, mapDispatchToProps)(BatchSettings) +export default connect(mapStateToProps, mapDispatchToProps)( + injectIntl(BatchSettings) +) diff --git a/lib/components/form/connect-location-field.js b/lib/components/form/connect-location-field.js index ca2c562d1..d90d3e757 100644 --- a/lib/components/form/connect-location-field.js +++ b/lib/components/form/connect-location-field.js @@ -1,3 +1,4 @@ +import { injectIntl } from 'react-intl' import { connect } from 'react-redux' import * as apiActions from '../../actions/api' @@ -16,7 +17,7 @@ import { getActiveSearch, getShowUserSettings } from '../../util/state' */ export default function connectLocationField (StyledLocationField, options = {}) { // By default, set actions to empty list and do not include location. - const {actions = [], includeLocation = false} = options + let {actions = {}, includeLocation = false, intlActions = {}} = options const mapStateToProps = (state, ownProps) => { const { config, currentQuery, location, transitIndex, user } = state.otp const { currentPosition, nearbyStops, sessionSearches } = location @@ -42,12 +43,30 @@ export default function connectLocationField (StyledLocationField, options = {}) return stateToProps } - const mapDispatchToProps = { - addLocationSearch: locationActions.addLocationSearch, - findNearbyStops: apiActions.findNearbyStops, - getCurrentPosition: locationActions.getCurrentPosition, - ...actions + const mapDispatchToProps = (dispatch, ownProps) => { + actions = { + addLocationSearch: locationActions.addLocationSearch, + findNearbyStops: apiActions.findNearbyStops, + ...actions + } + + intlActions = { + getCurrentPosition: locationActions.getCurrentPosition, + ...intlActions + } + + const dispatchActions = {} + + Object.entries(actions).forEach(([key, fn]) => { + dispatchActions[key] = (...args) => dispatch(fn(...args)) + }) + Object.entries(intlActions).forEach(([key, fn]) => { + dispatchActions[key] = (...args) => dispatch(fn(ownProps.intl, ...args)) + }) + return dispatchActions } - return connect(mapStateToProps, mapDispatchToProps)(StyledLocationField) + return injectIntl( + connect(mapStateToProps, mapDispatchToProps)(StyledLocationField) + ) } diff --git a/lib/components/form/connected-location-field.js b/lib/components/form/connected-location-field.js index 6be2c99d6..ac52fe42f 100644 --- a/lib/components/form/connected-location-field.js +++ b/lib/components/form/connected-location-field.js @@ -10,6 +10,7 @@ import { import styled from 'styled-components' import * as mapActions from '../../actions/map' + import connectLocationField from './connect-location-field' const StyledLocationField = styled(LocationField)` @@ -54,8 +55,10 @@ const StyledLocationField = styled(LocationField)` export default connectLocationField(StyledLocationField, { actions: { - clearLocation: mapActions.clearLocation, - onLocationSelected: mapActions.onLocationSelected + clearLocation: mapActions.clearLocation }, - includeLocation: true + includeLocation: true, + intlActions: { + onLocationSelected: mapActions.onLocationSelected + } }) diff --git a/lib/components/form/error-message.js b/lib/components/form/error-message.js index f00285626..2276888e8 100644 --- a/lib/components/form/error-message.js +++ b/lib/components/form/error-message.js @@ -1,7 +1,9 @@ import React from 'react' +import { FormattedMessage } from 'react-intl' import { connect } from 'react-redux' import TripTools from '../narrative/trip-tools' +import IconWithSpace from '../util/icon-with-space' import { getActiveError, getErrorMessage } from '../../util/state' const ErrorMessage = ({ message }) => { @@ -10,7 +12,8 @@ const ErrorMessage = ({ message }) => { return (
- Could Not Plan Trip + +
{message}
@@ -24,13 +27,10 @@ const mapStateToProps = (state, ownProps) => { return { message: getErrorMessage( getActiveError(state), - state.otp.config.errorMessages + state.otp.config.errorMessages, + state.otp.ui.locale ) } } -const mapDispatchToProps = (dispatch, ownProps) => { - return {} -} - -export default connect(mapStateToProps, mapDispatchToProps)(ErrorMessage) +export default connect(mapStateToProps)(ErrorMessage) diff --git a/lib/components/form/mode-buttons.js b/lib/components/form/mode-buttons.js index 3362cf979..b7c07bb46 100644 --- a/lib/components/form/mode-buttons.js +++ b/lib/components/form/mode-buttons.js @@ -1,5 +1,6 @@ import React, { useContext } from 'react' import { OverlayTrigger, Tooltip } from 'react-bootstrap' +import { useIntl } from 'react-intl' import styled from 'styled-components' import { ComponentContext } from '../../util/contexts' @@ -7,36 +8,41 @@ import Icon from '../util/icon' import {buttonCss} from './batch-styled' -export const MODE_OPTIONS = [ - { - label: 'Transit', - mode: 'TRANSIT' - }, - { - label: 'Walking', - mode: 'WALK' - }, - { - label: 'Drive', - mode: 'CAR' - }, - { - label: 'Bicycle', - mode: 'BICYCLE' - }, - { - icon: 'mobile', - label: 'Rental options', - mode: 'RENT' // TODO: include HAIL? - } -] +function getModeOptions (intl) { + // intl.formatMessage is used here instead of because the text is + // rendered inside , which renders outside of the context. + return [ + { + label: intl.formatMessage({id: 'components.ModeButtons.transit'}), + mode: 'TRANSIT' + }, + { + label: intl.formatMessage({id: 'components.ModeButtons.walk'}), + mode: 'WALK' + }, + { + label: intl.formatMessage({id: 'components.ModeButtons.car'}), + mode: 'CAR' + }, + { + label: intl.formatMessage({id: 'components.ModeButtons.bicycle'}), + mode: 'BICYCLE' + }, + { + icon: 'mobile', + label: intl.formatMessage({id: 'components.ModeButtons.rent'}), + mode: 'RENT' // TODO: include HAIL? + } + ] +} const ModeButtons = ({ className, onClick, selectedModes = [] }) => { - return MODE_OPTIONS.map((item, index) => ( + const intl = useIntl() + return getModeOptions(intl).map((item, index) => ( {text || 'Plan Trip'} + >{text || } ) } } diff --git a/lib/components/form/settings-preview.js b/lib/components/form/settings-preview.js index 8388e16f2..ca606a507 100644 --- a/lib/components/form/settings-preview.js +++ b/lib/components/form/settings-preview.js @@ -3,72 +3,74 @@ /* eslint-disable jsx-a11y/click-events-have-key-events */ import coreUtils from '@opentripplanner/core-utils' import PropTypes from 'prop-types' -import React, { Component } from 'react' +import React from 'react' import { Button } from 'react-bootstrap' +import { useIntl } from 'react-intl' import { connect } from 'react-redux' -import { mergeMessages } from '../../util/messages' - import { Dot } from './styled' -class SettingsPreview extends Component { - static propTypes = { - // component props - caret: PropTypes.string, - compressed: PropTypes.bool, - editButtonText: PropTypes.element, - onClick: PropTypes.func, - showCaret: PropTypes.bool, - - // application state - // eslint-disable-next-line sort-keys - companies: PropTypes.string, - modeGroups: PropTypes.array, - queryModes: PropTypes.array +function SettingsPreview (props) { + const { + caret, + config, + editButtonText, + onClick, + query + } = props + const intl = useIntl() + const messages = { + label: intl.formatMessage({id: 'components.SettingsPreview.defaultPreviewText'}) } + // Show dot indicator if the current query differs from the default query. + const showDot = coreUtils.query.isNotDefaultQuery(query, config) + const button = ( +
+ + {showDot && } +
+ ) + // Add tall class to account for vertical centering if there is only + // one line in the label (default is 2). + const addClass = messages.label.match(/\n/) ? '' : ' tall' + return ( +
+
+ {messages.label} +
+ {button} +
+
+ ) +} - static defaultProps = { - editButtonText: , - messages: { - label: 'Transit Options\n& Preferences' - } - } +SettingsPreview.propTypes = { + // component props + caret: PropTypes.string, + compressed: PropTypes.bool, + editButtonText: PropTypes.element, + onClick: PropTypes.func, + showCaret: PropTypes.bool, - render () { - const { caret, config, editButtonText, query } = this.props - const messages = mergeMessages(SettingsPreview.defaultProps, this.props) - // Show dot indicator if the current query differs from the default query. - const showDot = coreUtils.query.isNotDefaultQuery(query, config) - const button = ( -
- - {showDot && } -
- ) - // Add tall class to account for vertical centering if there is only - // one line in the label (default is 2). - const addClass = messages.label.match(/\n/) ? '' : ' tall' - return ( -
-
- {messages.label} -
- {button} -
-
- ) - } + // application state + // eslint-disable-next-line sort-keys + companies: PropTypes.string, + modeGroups: PropTypes.array, + queryModes: PropTypes.array +} + +SettingsPreview.defaultProps = { + editButtonText: } const mapStateToProps = (state, ownProps) => { return { config: state.otp.config, - messages: state.otp.config.language.settingsPreview, query: state.otp.currentQuery } } diff --git a/lib/components/form/switch-button.js b/lib/components/form/switch-button.js index afa9ef342..76d30c2ca 100644 --- a/lib/components/form/switch-button.js +++ b/lib/components/form/switch-button.js @@ -1,33 +1,31 @@ -import React, { Component } from 'react' +import React from 'react' import PropTypes from 'prop-types' import { Button } from 'react-bootstrap' +import { useIntl } from 'react-intl' import { connect } from 'react-redux' import { switchLocations } from '../../actions/map' -class SwitchButton extends Component { - static propTypes = { - onClick: PropTypes.func, - switchLocations: PropTypes.func - } +function SwitchButton (props) { + const intl = useIntl() - static defaultProps = { - content: 'Switch' + const _onClick = () => { + props.switchLocations() } - _onClick = () => { - this.props.switchLocations() - } + const content = props.content ?? intl.formatMessage({id: 'components.SwitchButton.defaultContent'}) - render () { - const { content } = this.props - return ( - - ) - } + return ( + + ) +} + +SwitchButton.propTypes = { + onClick: PropTypes.func, + switchLocations: PropTypes.func } const mapStateToProps = (state, ownProps) => { diff --git a/lib/components/form/tabbed-form-panel.js b/lib/components/form/tabbed-form-panel.js index dc9a55626..e7b4d29d9 100644 --- a/lib/components/form/tabbed-form-panel.js +++ b/lib/components/form/tabbed-form-panel.js @@ -1,14 +1,15 @@ import React, { Component } from 'react' import { Button } from 'react-bootstrap' +import { FormattedMessage } from 'react-intl' import { connect } from 'react-redux' +import { setMainPanelContent } from '../../actions/ui' + import DateTimePreview from './date-time-preview' import SettingsPreview from './settings-preview' import DateTimeModal from './date-time-modal' import ConnectedSettingsSelectorPanel from './connected-settings-selector-panel' -import { setMainPanelContent } from '../../actions/ui' - class TabbedFormPanel extends Component { _onEditDateTimeClick = () => { const { mainPanelContent, setMainPanelContent } = this.props @@ -47,7 +48,10 @@ class TabbedFormPanel extends Component { {mainPanelContent === 'EDIT_SETTINGS' && ()}
diff --git a/lib/components/form/user-settings.js b/lib/components/form/user-settings.js index 870161e7c..43bda57f5 100644 --- a/lib/components/form/user-settings.js +++ b/lib/components/form/user-settings.js @@ -2,6 +2,7 @@ import moment from 'moment' import coreUtils from '@opentripplanner/core-utils' import React, { Component } from 'react' import { Button } from 'react-bootstrap' +import { FormattedMessage, injectIntl } from 'react-intl' import { connect } from 'react-redux' import { forgetSearch, toggleTracking } from '../../actions/api' @@ -17,12 +18,12 @@ const BUTTON_WIDTH = 40 class UserSettings extends Component { _disableTracking = () => { - const { toggleTracking, user } = this.props + const { intl, toggleTracking, user } = this.props if (!user.trackRecent) return const hasRecents = user.recentPlaces.length > 0 || user.recentSearches.length > 0 // If user has recents and does not confirm deletion, return without doing // anything. - if (hasRecents && !window.confirm('You have recent searches and/or places stored. Disabling storage of recent places/searches will remove these items. Continue?')) { + if (hasRecents && !window.confirm(intl.formatMessage({id: 'components.UserSettings.confirmDeletion'}))) { return } // Disable tracking if we reach this statement. @@ -55,7 +56,7 @@ class UserSettings extends Component { } render () { - const { storageDisclaimer, user } = this.props + const { user } = this.props const { favoriteStops, recentPlaces, recentSearches, trackRecent } = user // Clone locations in order to prevent blank locations from seeping into the // app state/store. @@ -71,19 +72,19 @@ class UserSettings extends Component { })}
-
Favorite stops
+
    {favoriteStops.length > 0 ? favoriteStops.map(location => { return }) - : No favorite stops + : }
{trackRecent && recentPlaces.length > 0 &&

-
Recent places
+
    {recentPlaces.map(location => { return @@ -94,7 +95,7 @@ class UserSettings extends Component { {trackRecent && recentSearches.length > 0 &&

    -
    Recent searches
    +
      {recentSearches .sort((a, b) => b.timestamp - a.timestamp) @@ -107,27 +108,25 @@ class UserSettings extends Component { }
      -
      My preferences
      - Remember recent searches/places? +
      + + onClick={this._enableTracking}> + onClick={this._disableTracking}>
      - {storageDisclaimer && -
      -
      -
      - {storageDisclaimer} -
      +
      +
      +
      +
      - } +
      ) } @@ -135,9 +134,9 @@ class UserSettings extends Component { class Place extends Component { _onSelect = () => { - const { location, query, setLocation } = this.props + const { intl, location, query, setLocation } = this.props if (location.blank) { - window.alert(`Enter origin/destination in the form (or set via map click) and click the resulting marker to set as ${location.type} location.`) + window.alert(intl.formatMessage({id: 'components.Place.enterAlert'}, {type: location.type})) } else { // If 'to' not set and 'from' does not match location, set as 'to'. if ( @@ -173,6 +172,7 @@ class Place extends Component { ['stop', 'home', 'work', 'recent'].indexOf(this.props.location.type) !== -1 render () { + const { intl } = this.props const { location } = this.props const { blank, icon } = location const showView = this._isViewable() @@ -206,7 +206,9 @@ class Place extends Component { className='place-view' onClick={this._onView} style={{ width: `${BUTTON_WIDTH}px` }} - title='View stop'> + title={intl.formatMessage({id: 'components.Place.viewStop'})}> + + } {showForget && + style={{ width: `${BUTTON_WIDTH}px` }}> + + } ) @@ -231,9 +235,11 @@ class RecentSearch extends Component { _onForget = () => this.props.forgetSearch(this.props.search.id) render () { - const { search, user } = this.props + const { intl, search, user } = this.props const { query, timestamp } = search const name = summarizeQuery(query, user.locations) + const fromNowDur = moment.duration(moment().diff(moment(timestamp))) + return (
    • + style={{ paddingTop: '6px', width: `${BUTTON_WIDTH}px` }}> + +
    • ) } @@ -271,7 +291,6 @@ const mapStateToProps = (state, ownProps) => { query: state.otp.currentQuery, sessionSearches: state.otp.location.sessionSearches, stopsIndex: state.otp.transitIndex.stops, - storageDisclaimer: state.otp.config.language.storageDisclaimer, user: state.otp.user } } @@ -286,4 +305,4 @@ const mapDispatchToProps = { toggleTracking } -export default connect(mapStateToProps, mapDispatchToProps)(UserSettings) +export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(UserSettings)) diff --git a/lib/components/form/user-trip-settings.js b/lib/components/form/user-trip-settings.js index d681e2cb8..4c1924c71 100644 --- a/lib/components/form/user-trip-settings.js +++ b/lib/components/form/user-trip-settings.js @@ -1,6 +1,7 @@ import coreUtils from '@opentripplanner/core-utils' import React, { Component } from 'react' import { Button } from 'react-bootstrap' +import { FormattedMessage } from 'react-intl' import { connect } from 'react-redux' import { @@ -45,8 +46,8 @@ class UserTripSettings extends Component { disabled={rememberIsDisabled} onClick={this._toggleStoredSettings} >{defaults - ? Forget my options - : Remember trip options + ? + : }
    ) diff --git a/lib/components/mobile/batch-results-screen.js b/lib/components/mobile/batch-results-screen.js index d9be6ce0b..6f41cd388 100644 --- a/lib/components/mobile/batch-results-screen.js +++ b/lib/components/mobile/batch-results-screen.js @@ -1,6 +1,7 @@ import coreUtils from '@opentripplanner/core-utils' import React from 'react' import { Button } from 'react-bootstrap' +import { FormattedMessage } from 'react-intl' import { connect } from 'react-redux' import styled, { css } from 'styled-components' @@ -95,7 +96,7 @@ const BatchMobileResultsScreen = ({ onClick={toggleBatchResultsMap} > {' '} - {mapExpanded ? 'Show results' : 'Expand map'} + {hasErrorsAndNoResult diff --git a/lib/components/mobile/batch-search-screen.js b/lib/components/mobile/batch-search-screen.js index e6582fa96..4f5924af4 100644 --- a/lib/components/mobile/batch-search-screen.js +++ b/lib/components/mobile/batch-search-screen.js @@ -1,17 +1,17 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' import { connect } from 'react-redux' +import { FormattedMessage } from 'react-intl' import BatchSettings from '../form/batch-settings' import DefaultMap from '../map/default-map' import LocationField from '../form/connected-location-field' import SwitchButton from '../form/switch-button' +import { MobileScreens, setMobileScreen } from '../../actions/ui' import MobileContainer from './container' import MobileNavigationBar from './navigation-bar' -import { MobileScreens, setMobileScreen } from '../../actions/ui' - const { SET_DATETIME, SET_FROM_LOCATION, @@ -33,7 +33,9 @@ class BatchSearchScreen extends Component { render () { return ( - + } + />
    } showBackButton /> diff --git a/lib/components/mobile/location-search.js b/lib/components/mobile/location-search.js index 0e0df8d07..36b824d14 100644 --- a/lib/components/mobile/location-search.js +++ b/lib/components/mobile/location-search.js @@ -1,5 +1,6 @@ -import React, { Component } from 'react' import PropTypes from 'prop-types' +import React, { Component } from 'react' +import { injectIntl } from 'react-intl' import { connect } from 'react-redux' import LocationField from '../form/connected-location-field' @@ -21,6 +22,7 @@ class MobileLocationSearch extends Component { render () { const { backScreen, + intl, location, locationType, otherLocation @@ -31,13 +33,18 @@ class MobileLocationSearch extends Component {
    } showBackButton /> diff --git a/lib/components/mobile/results-error.js b/lib/components/mobile/results-error.js index eb8728a1e..38d0a6918 100644 --- a/lib/components/mobile/results-error.js +++ b/lib/components/mobile/results-error.js @@ -1,5 +1,6 @@ import PropTypes from 'prop-types' import React from 'react' +import { FormattedMessage } from 'react-intl' import styled from 'styled-components' import ErrorMessage from '../form/error-message' @@ -18,7 +19,9 @@ const ResultsError = ({ className, error }) => ( className='back-to-search-button' style={{ width: '100%' }} > - Back to Search + + {' '} +
    diff --git a/lib/components/mobile/results-header.js b/lib/components/mobile/results-header.js index 39431c201..fb92f0025 100644 --- a/lib/components/mobile/results-header.js +++ b/lib/components/mobile/results-header.js @@ -2,6 +2,7 @@ import LocationIcon from '@opentripplanner/location-icon' import PropTypes from 'prop-types' import React, { Component } from 'react' import { Col, Row } from 'react-bootstrap' +import { FormattedMessage } from 'react-intl' import { connect } from 'react-redux' import styled from 'styled-components' @@ -58,10 +59,15 @@ class ResultsHeader extends Component { const { errors, query, resultCount } = this.props const hasNoResult = resultCount === 0 && errors.length > 0 const headerText = hasNoResult - ? 'No Trip Found' + ? : (resultCount - ? `We Found ${resultCount} Option${resultCount > 1 ? 's' : ''}` - : 'Waiting...' + ? ( + + ) + : ) return ( @@ -80,7 +86,7 @@ class ResultsHeader extends Component { - Edit + diff --git a/lib/components/mobile/results-screen.js b/lib/components/mobile/results-screen.js index 170eeeb56..e52e7f162 100644 --- a/lib/components/mobile/results-screen.js +++ b/lib/components/mobile/results-screen.js @@ -1,6 +1,7 @@ import coreUtils from '@opentripplanner/core-utils' import PropTypes from 'prop-types' import React, { Component } from 'react' +import { FormattedMessage } from 'react-intl' import { connect } from 'react-redux' import styled from 'styled-components' @@ -129,7 +130,10 @@ class MobileResultsScreen extends Component { expanded={expanded} onClick={this._optionClicked} > - Option {activeItineraryIndex + 1} + diff --git a/lib/components/mobile/route-viewer.js b/lib/components/mobile/route-viewer.js index 97b25fe5b..70229929a 100644 --- a/lib/components/mobile/route-viewer.js +++ b/lib/components/mobile/route-viewer.js @@ -1,5 +1,6 @@ -import React, { Component } from 'react' import PropTypes from 'prop-types' +import React, { Component } from 'react' +import { FormattedMessage } from 'react-intl' import { connect } from 'react-redux' import RouteViewer from '../viewers/route-viewer' @@ -28,7 +29,7 @@ class MobileRouteViewer extends Component { return ( } onBackClicked={this._backClicked} showBackButton /> @@ -47,9 +48,7 @@ class MobileRouteViewer extends Component { // connect to the redux store const mapStateToProps = (state, ownProps) => { - return { - languageConfig: state.otp.config.language - } + return {} } const mapDispatchToProps = { diff --git a/lib/components/mobile/search-screen.js b/lib/components/mobile/search-screen.js index 01ce77a55..b8efa5817 100644 --- a/lib/components/mobile/search-screen.js +++ b/lib/components/mobile/search-screen.js @@ -1,7 +1,8 @@ -import React, { Component } from 'react' import PropTypes from 'prop-types' -import { connect } from 'react-redux' +import React, { Component } from 'react' import { Row, Col } from 'react-bootstrap' +import { FormattedMessage } from 'react-intl' +import { connect } from 'react-redux' import DateTimePreview from '../form/date-time-preview' import DefaultMap from '../map/default-map' @@ -43,7 +44,9 @@ class MobileSearchScreen extends Component { render () { return ( - + } + />
    } onBackClicked={() => { this.props.setViewedStop(null) }} showBackButton /> @@ -40,9 +41,7 @@ class MobileStopViewer extends Component { // connect to the redux store const mapStateToProps = (state, ownProps) => { - return { - languageConfig: state.otp.config.language - } + return { } } const mapDispatchToProps = { diff --git a/lib/components/mobile/trip-viewer.js b/lib/components/mobile/trip-viewer.js index d8c74e1b4..a21ab69c8 100644 --- a/lib/components/mobile/trip-viewer.js +++ b/lib/components/mobile/trip-viewer.js @@ -1,5 +1,6 @@ -import React, { Component } from 'react' import PropTypes from 'prop-types' +import React, { Component } from 'react' +import { FormattedMessage } from 'react-intl' import { connect } from 'react-redux' import TripViewer from '../viewers/trip-viewer' @@ -20,7 +21,7 @@ class MobileTripViewer extends Component { return ( } onBackClicked={this._onBackClicked} showBackButton /> @@ -42,9 +43,7 @@ class MobileTripViewer extends Component { // connect to the redux store const mapStateToProps = (state, ownProps) => { - return { - languageConfig: state.otp.config.language - } + return { } } const mapDispatchToProps = { diff --git a/lib/components/mobile/welcome-screen.js b/lib/components/mobile/welcome-screen.js index 31ced700b..4cb9accc5 100644 --- a/lib/components/mobile/welcome-screen.js +++ b/lib/components/mobile/welcome-screen.js @@ -1,15 +1,16 @@ -import React, { Component } from 'react' import PropTypes from 'prop-types' +import React, { Component } from 'react' +import { injectIntl } from 'react-intl' import { connect } from 'react-redux' -import MobileContainer from './container' import LocationField from '../form/connected-location-field' import DefaultMap from '../map/default-map' -import MobileNavigationBar from './navigation-bar' - import { MobileScreens, setMobileScreen } from '../../actions/ui' import { setLocationToCurrent } from '../../actions/map' +import MobileNavigationBar from './navigation-bar' +import MobileContainer from './container' + class MobileWelcomeScreen extends Component { static propTypes = { setLocationToCurrent: PropTypes.func, @@ -25,11 +26,12 @@ class MobileWelcomeScreen extends Component { * takes care of updating the query in the store w/ the selected location */ _locationSetFromPopup = (selection) => { + const { intl, setLocationToCurrent } = this.props // If the tapped location was selected as the 'from' endpoint, set the 'to' // endpoint to be the current user location. (If selected as the 'to' point, // no action is needed since 'from' is the current location by default.) if (selection.type === 'from') { - this.props.setLocationToCurrent({ locationType: 'to' }) + setLocationToCurrent({ locationType: 'to' }, intl) } } @@ -39,7 +41,9 @@ class MobileWelcomeScreen extends Component {
    { currency: state.otp.config.localization?.currency || 'USD' } } + export default connect(mapStateToProps)(ItinerarySummary) diff --git a/lib/components/narrative/narrative-itineraries-errors.js b/lib/components/narrative/narrative-itineraries-errors.js index fa94c77af..35977a799 100644 --- a/lib/components/narrative/narrative-itineraries-errors.js +++ b/lib/components/narrative/narrative-itineraries-errors.js @@ -1,4 +1,5 @@ import { getCompanyIcon } from '@opentripplanner/icons/lib/companies' +import { connect } from 'react-redux' import styled from 'styled-components' import Icon from '../util/icon' @@ -22,7 +23,7 @@ const IssueContents = styled.div` text-align: left; ` -export default function NarrativeItinerariesErrors ({ errorMessages, errors }) { +function NarrativeItinerariesErrors ({ errorMessages, errors, locale }) { return errors.map((error, idx) => { let icon = if (error.network) { @@ -38,9 +39,19 @@ export default function NarrativeItinerariesErrors ({ errorMessages, errors }) { {icon} - {getErrorMessage(error, errorMessages)} + {getErrorMessage(error, errorMessages, locale)} ) }) } + +// connect to the redux store + +const mapStateToProps = (state, ownProps) => { + return { + locale: state.otp.ui.locale + } +} + +export default connect(mapStateToProps)(NarrativeItinerariesErrors) diff --git a/lib/components/narrative/narrative-itineraries-header.js b/lib/components/narrative/narrative-itineraries-header.js index e462de55d..a23da33c6 100644 --- a/lib/components/narrative/narrative-itineraries-header.js +++ b/lib/components/narrative/narrative-itineraries-header.js @@ -58,14 +58,14 @@ export default function NarrativeItinerariesHeader ({ : <>
    { const useRealtime = state.otp.useRealtime return { + // swap out realtime itineraries with non-realtime depending on boolean activeItinerary: activeSearch && activeSearch.activeItinerary, activeLeg: activeSearch && activeSearch.activeLeg, activeStep: activeSearch && activeSearch.activeStep, diff --git a/lib/components/narrative/trip-tools.js b/lib/components/narrative/trip-tools.js index 641ca8bf8..9b89a814b 100644 --- a/lib/components/narrative/trip-tools.js +++ b/lib/components/narrative/trip-tools.js @@ -39,7 +39,7 @@ class TripTools extends Component { // FIXME: The Spanish string does not fit in button width. } + text={} url={startOverUrl} /> ) @@ -152,7 +152,7 @@ class PrintButton extends Component { onClick={this._onClick} > - +
    ) diff --git a/lib/components/user/account-setup-finish-pane.js b/lib/components/user/account-setup-finish-pane.js index 60f4b71cf..468b8fa0f 100644 --- a/lib/components/user/account-setup-finish-pane.js +++ b/lib/components/user/account-setup-finish-pane.js @@ -1,8 +1,11 @@ import React from 'react' +import { FormattedMessage } from 'react-intl' const AccountSetupFinishPane = () => (
    -

    You are ready to start planning your trips.

    +

    + +

    ) diff --git a/lib/components/user/after-signin-screen.js b/lib/components/user/after-signin-screen.js index f8d58ef36..0bd8f20f2 100644 --- a/lib/components/user/after-signin-screen.js +++ b/lib/components/user/after-signin-screen.js @@ -1,5 +1,6 @@ import * as routerActions from 'connected-react-router' import React, { Component } from 'react' +import { FormattedMessage } from 'react-intl' import { connect } from 'react-redux' import { Link } from 'react-router-dom' @@ -39,10 +40,16 @@ class AfterSignInScreen extends Component { return (
    -

    Redirecting...

    +

    + +

    - If the page does not load after a few seconds,{' '} - click here. + {contents} + }} + />

    ) diff --git a/lib/components/user/back-link.js b/lib/components/user/back-link.js index 7f2cc82e5..173233d79 100644 --- a/lib/components/user/back-link.js +++ b/lib/components/user/back-link.js @@ -1,5 +1,6 @@ import React from 'react' import { Button } from 'react-bootstrap' +import { FormattedMessage } from 'react-intl' import styled from 'styled-components' import { IconWithMargin } from './styled' @@ -19,8 +20,9 @@ const BackLink = () => ( bsStyle='link' onClick={navigateBack} > + {/** FIXME: handle right-to-left languages */} - Back + ) diff --git a/lib/components/user/back-to-trip-planner.js b/lib/components/user/back-to-trip-planner.js index fd0787da8..3139e4c7a 100644 --- a/lib/components/user/back-to-trip-planner.js +++ b/lib/components/user/back-to-trip-planner.js @@ -1,7 +1,9 @@ import React from 'react' +import { FormattedMessage } from 'react-intl' import styled from 'styled-components' import { LinkWithQuery } from '../form/connected-links' + import { IconWithMargin } from './styled' const StyledLinkWithQuery = styled(LinkWithQuery)` @@ -10,8 +12,9 @@ const StyledLinkWithQuery = styled(LinkWithQuery)` const BackToTripPlanner = () => ( + {/** FIXME: handle right-to-left languages */} - Back to trip planner + ) diff --git a/lib/components/user/before-signin-screen.js b/lib/components/user/before-signin-screen.js index b6f2e7e96..4d6ec56e4 100644 --- a/lib/components/user/before-signin-screen.js +++ b/lib/components/user/before-signin-screen.js @@ -1,4 +1,5 @@ import React from 'react' +import { FormattedMessage } from 'react-intl' /** * This screen is flashed just before the Auth0 login page is shown. @@ -6,10 +7,11 @@ import React from 'react' */ const BeforeSignInScreen = () => (
    -

    Signing you in

    +

    + +

    - In order to access this page you will need to sign in. - Please wait while we redirect you to the login page... +

    ) diff --git a/lib/components/user/delete-user.js b/lib/components/user/delete-user.js index 7aaaec235..e6d2c5851 100644 --- a/lib/components/user/delete-user.js +++ b/lib/components/user/delete-user.js @@ -1,5 +1,6 @@ import React from 'react' import { Button } from 'react-bootstrap' +import { FormattedMessage } from 'react-intl' import styled from 'styled-components' const DeleteButton = styled(Button)` @@ -14,9 +15,9 @@ const DeleteButton = styled(Button)` /** * Renders a delete user button for the account settings page. */ -const DeleteUser = ({onDelete}) => ( +const DeleteUser = ({ onDelete }) => ( - Delete my account + ) diff --git a/lib/components/user/existing-account-display.js b/lib/components/user/existing-account-display.js index 78ceabe8a..9d291136b 100644 --- a/lib/components/user/existing-account-display.js +++ b/lib/components/user/existing-account-display.js @@ -1,4 +1,5 @@ import React from 'react' +import { FormattedMessage } from 'react-intl' import BackToTripPlanner from './back-to-trip-planner' import DeleteUser from './delete-user' @@ -20,17 +21,17 @@ const ExistingAccountDisplay = props => { { pane: FavoritePlacesList, props, - title: 'My locations' + title: }, { pane: NotificationPrefsPane, props, - title: 'Notifications' + title: }, { pane: TermsOfUsePane, props: { ...props, disableCheckTerms: true }, - title: 'Terms' + title: }, { pane: DeleteUser, @@ -43,7 +44,7 @@ const ExistingAccountDisplay = props => { } />
    ) diff --git a/lib/components/user/form-navigation-buttons.js b/lib/components/user/form-navigation-buttons.js index f20cf9b5c..45e0ffe5b 100644 --- a/lib/components/user/form-navigation-buttons.js +++ b/lib/components/user/form-navigation-buttons.js @@ -1,6 +1,7 @@ import PropTypes from 'prop-types' import React from 'react' import { Button, FormGroup } from 'react-bootstrap' +import { injectIntl } from 'react-intl' import styled from 'styled-components' // Styles @@ -20,10 +21,11 @@ const RightButton = styled(Button)` */ const FormNavigationButtons = ({ backButton, + intl, okayButton }) => ( -