diff --git a/.github/workflows/codespell.yml b/.github/workflows/codespell.yml index ff497a107..f40c59996 100644 --- a/.github/workflows/codespell.yml +++ b/.github/workflows/codespell.yml @@ -12,6 +12,7 @@ jobs: - uses: codespell-project/actions-codespell@master with: check_filenames: true - # The a11y test file has a false positive and the ignore list does not work + # skip git, yarn, and i18n non-english resources. + # Also, the a11y test file has a false positive and the ignore list does not work # see https://github.com/opentripplanner/otp-react-redux/pull/436/checks?check_run_id=3369380014 - skip: ./.git,yarn.lock,./a11y/a11y.test.js + skip: ./.git,yarn.lock,./a11y/a11y.test.js,./i18n/fr* diff --git a/__tests__/components/viewers/__snapshots__/stop-viewer.js.snap b/__tests__/components/viewers/__snapshots__/stop-viewer.js.snap index 81e21e621..e83ba8e43 100644 --- a/__tests__/components/viewers/__snapshots__/stop-viewer.js.snap +++ b/__tests__/components/viewers/__snapshots__/stop-viewer.js.snap @@ -235,12 +235,10 @@ exports[`components > viewers > stop viewer should render countdown times after @@ -294,12 +292,10 @@ exports[`components > viewers > stop viewer should render countdown times after @@ -494,7 +490,7 @@ exports[`components > viewers > stop viewer should render countdown times after
viewers > stop viewer should render countdown times after "marginRight": 2, } } - type="clock-o" > viewers > stop viewer should render countdown times after "marginRight": 2, } } - type="clock-o" /> @@ -944,12 +938,10 @@ exports[`components > viewers > stop viewer should render countdown times after @@ -1030,12 +1022,10 @@ exports[`components > viewers > stop viewer should render countdown times after className="" fixedWidth={true} name="refresh" - type="refresh" > @@ -1189,12 +1179,10 @@ exports[`components > viewers > stop viewer should render countdown times for st @@ -1248,12 +1236,10 @@ exports[`components > viewers > stop viewer should render countdown times for st @@ -1448,7 +1434,7 @@ exports[`components > viewers > stop viewer should render countdown times for st
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, } } - type="clock-o" /> @@ -1709,12 +1693,10 @@ exports[`components > viewers > stop viewer should render countdown times for st @@ -1795,12 +1777,10 @@ exports[`components > viewers > stop viewer should render countdown times for st className="" fixedWidth={true} name="refresh" - type="refresh" > @@ -2053,12 +2033,10 @@ exports[`components > viewers > stop viewer should render times after midnight w @@ -2112,12 +2090,10 @@ exports[`components > viewers > stop viewer should render times after midnight w @@ -2312,7 +2288,7 @@ exports[`components > viewers > stop viewer should render times after midnight w
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, } } - type="clock-o" /> @@ -2771,12 +2745,10 @@ exports[`components > viewers > stop viewer should render times after midnight w @@ -2857,12 +2829,10 @@ exports[`components > viewers > stop viewer should render times after midnight w className="" fixedWidth={true} name="refresh" - type="refresh" > @@ -3373,12 +3343,10 @@ exports[`components > viewers > stop viewer should render with OTP transit index @@ -3432,12 +3400,10 @@ exports[`components > viewers > stop viewer should render with OTP transit index @@ -3632,7 +3598,7 @@ exports[`components > viewers > stop viewer should render with OTP transit index
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, } } - type="clock-o" /> @@ -4347,12 +4311,10 @@ exports[`components > viewers > stop viewer should render with OTP transit index @@ -4549,7 +4511,6 @@ exports[`components > 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, } } - type="clock-o" /> @@ -4606,12 +4566,10 @@ exports[`components > viewers > stop viewer should render with OTP transit index @@ -4808,7 +4766,6 @@ exports[`components > 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, } } - type="clock-o" /> @@ -4865,12 +4821,10 @@ exports[`components > viewers > stop viewer should render with OTP transit index @@ -5121,7 +5075,6 @@ exports[`components > 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, } } - type="clock-o" /> @@ -5178,12 +5130,10 @@ exports[`components > viewers > stop viewer should render with OTP transit index @@ -5264,12 +5214,10 @@ exports[`components > viewers > stop viewer should render with OTP transit index className="" fixedWidth={true} name="refresh" - type="refresh" > @@ -5775,12 +5723,10 @@ exports[`components > viewers > stop viewer should render with TriMet transit in @@ -5834,12 +5780,10 @@ exports[`components > viewers > stop viewer should render with TriMet transit in @@ -6034,7 +5978,7 @@ exports[`components > viewers > stop viewer should render with TriMet transit in
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, } } - type="clock-o" /> @@ -6746,12 +6688,10 @@ exports[`components > viewers > stop viewer should render with TriMet transit in @@ -6832,12 +6772,10 @@ exports[`components > viewers > stop viewer should render with TriMet transit in className="" fixedWidth={true} name="refresh" - type="refresh" > @@ -6925,12 +6863,10 @@ exports[`components > viewers > stop viewer should render with initial stop id a diff --git a/i18n/en-US.yml b/i18n/en-US.yml index f8a220fad..7e5711a5e 100644 --- a/i18n/en-US.yml +++ b/i18n/en-US.yml @@ -28,16 +28,88 @@ _name: English components: DefaultItinerary: clickDetails: Click to view details - # Use ordered placeholders for the departure-arrival string - # (this will accommodate right-to-left languages by swapping the order in this string). - departureArrivalTimes: "{startTime}—{endTime}" # 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}" - # If trip is less than one hour only display the minutes. - tripDurationFormatZeroHours: "{minutes, number} min" - # TODO: Distinguish between one hour (singular) and 2 hours or more? - tripDurationFormat: "{hours, number} hr {minutes, number} min" + ItinerarySummary: + fareCost: "{useMaxFare, select, + true {{minTotalFare} - {maxTotalFare}} + other {{minTotalFare}} + }" + NarrativeItinerariesHeader: + numIssues: "{issueNum, number} issues" + resultText: "{pending, select, + true {Finding your options...} + other { + {itineraryNum, number} {itineraryNum, plural, + one {itinerary found} + other {itineraries found} + } + } + }" + selectArrivalTime: Arrival time + selectBest: Best option + selectCost: Cost + 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} + } + } + }" + viewAll: View all options + PlanFirstLastButtons: + # Note to translator: these values are width-constrained. + first: First + last: Last + next: Next + previous: Previous + 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." + 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 + 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. + SimpleRealtimeAnnotation: + usingRealtimeInfo: This trip uses real-time traffic and delay information + TabbedItineraries: + optionNumber: "Option {optionNum, number}" + fareCost: "{hasMaxFare, select, + true {{minTotalFare}+} + other {{minTotalFare}} + }" + TripTools: + # Note to translator: copyLink, linkCopied, print, reportIssue, + # and startOver 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) # Common messages that appear in multiple components and modules # are grouped below by topic. @@ -49,7 +121,11 @@ common: drive: Drive micromobility: E-Scooter micromobilityRent: Rental E-Scooter - walk: Walk + walk: Walk + + itineraryDescriptions: + calories: "{calories, number} Cal" + transfers: "{transfers, plural, =0 {} one {{transfers} transfer} other {{transfers} transfers}}" # OTP transit modes # Note that identifiers are OTP modes converted to lowercase. @@ -63,3 +139,12 @@ common: cable_car: Cable Car gondola: Gondola funicular: Funicular + + time: + # 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}" + tripDurationFormat: "{hours, plural, + =0 {{minutes, number} min} + other {{hours, number} hr {minutes, number} min}}" + \ No newline at end of file diff --git a/i18n/fr-FR.yml b/i18n/fr-FR.yml index cdbf33b27..ad0874e2e 100644 --- a/i18n/fr-FR.yml +++ b/i18n/fr-FR.yml @@ -4,10 +4,85 @@ _name: Unofficial French Translations! components: DefaultItinerary: clickDetails: Cliquez pour afficher les détails - departureArrivalTimes: "{startTime}—{endTime}" multiModeSummary: "{accessMode} + {transitMode}" - tripDurationFormatZeroHours: "{minutes, number} mn" - tripDurationFormat: "{hours, number} h, {minutes, number} mn" + ItinerarySummary: + fareCost: "{useMaxFare, select, + true {{minTotalFare} - {maxTotalFare}} + other {{minTotalFare}} + }" + NarrativeItinerariesHeader: + numIssues: "{issueNum, number} problèmes" + resultText: "{pending, select, + true {Recherche de vos options en cours...} + other { + {itineraryNum, number} {itineraryNum, plural, + one {trajet trouvé} + other {trajets trouvés} + } + } + }" + selectArrivalTime: Heure d'arrivée + selectBest: Meilleure option + selectCost: Prix + selectDepartureTime: Heure de départ + selectDuration: Durée + selectWalkTime: Temps de marche + titleText: "{pending, select, + true {Recherche de vos options en cours...} + other { + {itineraryNum, number} {itineraryNum, plural, + one {trajet} + other {trajets}} + {issueNum, plural, + =0 {trouvé} + one {(et {issueNum, number} problème) trouvé} + other {(and {issueNum, number} problèmes) trouvés} + } + } + }" + viewAll: Voir toutes les options + PlanFirstLastButtons: + # Note to translator: these values are width-constrained. + first: Premier + last: Dernier + next: Suivant + previous: Précédent + 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 + 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. + saveTripText: Enregistrer + signInText: Connectez-vous pour enregistrer + signInTooltip: Veuillez vous connecter pour enregistrer ce trajet. + SimpleRealtimeAnnotation: + usingRealtimeInfo: Ce trajet utilise les informations en temps réel sur le trafic et les retards + TabbedItineraries: + optionNumber: "Option {optionNum, number}" + fareCost: "{hasMaxFare, select, + true {À partir de {minTotalFare}} + other {{minTotalFare}} + }" + TripTools: + # Note to translator: copyLink, linkCopied, print, reportIssue, + # and startOver 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. + 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 common: accessModes: @@ -16,7 +91,12 @@ common: drive: Voiture micromobility: Trottinette électrique micromobilityRent: Trottinette électrique en libre-service - walk: Marche + walk: À pied + + itineraryDescriptions: + calories: "{calories, number} kcal" # SI unit + transfers: "{transfers, plural, =0 {} one {{transfers} correspondance} other {{transfers} correspondances}}" + otpTransitModes: tram: Tram @@ -27,3 +107,9 @@ common: cable_car: Tram tiré par câble gondola: Téléphérique funicular: Funiculaire + + time: + departureArrivalTimes: "{startTime, time, short}—{endTime, time, short}" + tripDurationFormat: "{hours, plural, + =0 {{minutes, number} mn} + other {{hours, number} h {minutes, number} mn}}" diff --git a/lib/components/admin/call-history-window.js b/lib/components/admin/call-history-window.js index 1f72e8a5a..4bbf897ce 100644 --- a/lib/components/admin/call-history-window.js +++ b/lib/components/admin/call-history-window.js @@ -2,7 +2,7 @@ import React from 'react' import { connect } from 'react-redux' import * as callTakerActions from '../../actions/call-taker' -import Icon from '../narrative/icon' +import Icon from '../util/icon' import CallRecord from './call-record' import DraggableWindow from './draggable-window' diff --git a/lib/components/admin/call-record.js b/lib/components/admin/call-record.js index e4d821616..3f37491cd 100644 --- a/lib/components/admin/call-record.js +++ b/lib/components/admin/call-record.js @@ -2,8 +2,8 @@ import humanizeDuration from 'humanize-duration' import moment from 'moment' import React, { Component } from 'react' -import Icon from '../narrative/icon' import {searchToQuery} from '../../util/call-taker' +import Icon from '../util/icon' import CallTimeCounter from './call-time-counter' import QueryRecord from './query-record' diff --git a/lib/components/admin/call-taker-controls.js b/lib/components/admin/call-taker-controls.js index 23ef687ad..3fbd38458 100644 --- a/lib/components/admin/call-taker-controls.js +++ b/lib/components/admin/call-taker-controls.js @@ -5,8 +5,8 @@ import * as apiActions from '../../actions/api' import * as callTakerActions from '../../actions/call-taker' import * as fieldTripActions from '../../actions/field-trip' import * as uiActions from '../../actions/ui' -import Icon from '../narrative/icon' import { isModuleEnabled, Modules } from '../../util/config' +import Icon from '../util/icon' import { CallHistoryButton, diff --git a/lib/components/admin/draggable-window.js b/lib/components/admin/draggable-window.js index 2d6aba12f..75532fd2f 100644 --- a/lib/components/admin/draggable-window.js +++ b/lib/components/admin/draggable-window.js @@ -1,7 +1,7 @@ import React, { Component } from 'react' import Draggable from 'react-draggable' -import Icon from '../narrative/icon' +import Icon from '../util/icon' const noop = () => {} diff --git a/lib/components/admin/field-trip-details.js b/lib/components/admin/field-trip-details.js index 2707fb64d..48c125435 100644 --- a/lib/components/admin/field-trip-details.js +++ b/lib/components/admin/field-trip-details.js @@ -6,7 +6,6 @@ import { connect } from 'react-redux' import styled from 'styled-components' import * as fieldTripActions from '../../actions/field-trip' -import Icon from '../narrative/icon' import { getActiveFieldTripRequest, getGroupSize, @@ -14,6 +13,7 @@ import { PAYMENT_FIELDS, TICKET_TYPES } from '../../util/call-taker' +import Icon from '../util/icon' import DraggableWindow from './draggable-window' import EditableSection from './editable-section' diff --git a/lib/components/admin/field-trip-itinerary-group-size.js b/lib/components/admin/field-trip-itinerary-group-size.js index 27e3015f7..40d7bfd18 100644 --- a/lib/components/admin/field-trip-itinerary-group-size.js +++ b/lib/components/admin/field-trip-itinerary-group-size.js @@ -1,7 +1,7 @@ import React from 'react' import { Badge } from 'react-bootstrap' -import Icon from '../narrative/icon' +import Icon from '../util/icon' export default function FieldTripGroupSize ({ itinerary }) { return itinerary.fieldTripGroupSize > 0 && ( diff --git a/lib/components/admin/field-trip-list.js b/lib/components/admin/field-trip-list.js index 2cabcd1cb..399351eaa 100644 --- a/lib/components/admin/field-trip-list.js +++ b/lib/components/admin/field-trip-list.js @@ -5,10 +5,10 @@ import { Badge, Button } from 'react-bootstrap' import { connect } from 'react-redux' import * as fieldTripActions from '../../actions/field-trip' -import Icon from '../narrative/icon' import Loading from '../narrative/loading' import {getVisibleRequests, TABS} from '../../util/call-taker' import {FETCH_STATUS} from '../../util/constants' +import Icon from '../util/icon' import FieldTripStatusIcon from './field-trip-status-icon' import {FieldTripRecordButton, WindowHeader} from './styled' diff --git a/lib/components/admin/field-trip-notes.js b/lib/components/admin/field-trip-notes.js index a190f5f5d..c0f2be4a4 100644 --- a/lib/components/admin/field-trip-notes.js +++ b/lib/components/admin/field-trip-notes.js @@ -2,7 +2,8 @@ import React, { Component } from 'react' import { Badge, Button as BsButton } from 'react-bootstrap' import styled from 'styled-components' -import Icon from '../narrative/icon' +import Icon from '../util/icon' + import { Button, Full, diff --git a/lib/components/admin/field-trip-status-icon.js b/lib/components/admin/field-trip-status-icon.js index e7ed9b0eb..901a73939 100644 --- a/lib/components/admin/field-trip-status-icon.js +++ b/lib/components/admin/field-trip-status-icon.js @@ -1,6 +1,6 @@ import React from 'react' -import Icon from '../narrative/icon' +import Icon from '../util/icon' const FieldTripStatusIcon = ({ ok }) => ( ok diff --git a/lib/components/admin/mailables-window.js b/lib/components/admin/mailables-window.js index 55f558c54..80f8112e6 100644 --- a/lib/components/admin/mailables-window.js +++ b/lib/components/admin/mailables-window.js @@ -3,8 +3,8 @@ import {Badge, Button} from 'react-bootstrap' import {connect} from 'react-redux' import * as callTakerActions from '../../actions/call-taker' -import Icon from '../narrative/icon' import {getModuleConfig, isModuleEnabled, Modules} from '../../util/config' +import Icon from '../util/icon' import {createLetter, LETTER_FIELDS} from '../../util/mailables' import { diff --git a/lib/components/admin/styled.js b/lib/components/admin/styled.js index 6c9ecdb5b..7ee01dbe4 100644 --- a/lib/components/admin/styled.js +++ b/lib/components/admin/styled.js @@ -1,7 +1,7 @@ import { Button as BsButton } from 'react-bootstrap' import styled, {css} from 'styled-components' -import Icon from '../narrative/icon' +import Icon from '../util/icon' import DefaultCounter from './call-time-counter' diff --git a/lib/components/app/app-menu.js b/lib/components/app/app-menu.js index ccbf42fa1..159d22446 100644 --- a/lib/components/app/app-menu.js +++ b/lib/components/app/app-menu.js @@ -5,11 +5,11 @@ import { connect } from 'react-redux' import { DropdownButton, MenuItem } from 'react-bootstrap' import { withRouter } from 'react-router' -import Icon from '../narrative/icon' 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 diff --git a/lib/components/form/batch-settings.js b/lib/components/form/batch-settings.js index 6e22168cc..f4150a576 100644 --- a/lib/components/form/batch-settings.js +++ b/lib/components/form/batch-settings.js @@ -5,7 +5,7 @@ import styled from 'styled-components' import * as apiActions from '../../actions/api' import * as formActions from '../../actions/form' -import Icon from '../narrative/icon' +import Icon from '../util/icon' import { hasValidLocation, getActiveSearch, getShowUserSettings } from '../../util/state' import BatchPreferences from './batch-preferences' diff --git a/lib/components/form/mode-buttons.js b/lib/components/form/mode-buttons.js index eed382be2..3362cf979 100644 --- a/lib/components/form/mode-buttons.js +++ b/lib/components/form/mode-buttons.js @@ -2,8 +2,8 @@ import React, { useContext } from 'react' import { OverlayTrigger, Tooltip } from 'react-bootstrap' import styled from 'styled-components' -import Icon from '../narrative/icon' import { ComponentContext } from '../../util/contexts' +import Icon from '../util/icon' import {buttonCss} from './batch-styled' diff --git a/lib/components/form/user-settings.js b/lib/components/form/user-settings.js index d68b22e8c..870161e7c 100644 --- a/lib/components/form/user-settings.js +++ b/lib/components/form/user-settings.js @@ -4,11 +4,11 @@ import React, { Component } from 'react' import { Button } from 'react-bootstrap' import { connect } from 'react-redux' -import Icon from '../narrative/icon' import { forgetSearch, toggleTracking } from '../../actions/api' import { setQueryParam } from '../../actions/form' import { forgetPlace, forgetStop, setLocation } from '../../actions/map' import { setViewedStop } from '../../actions/ui' +import Icon from '../util/icon' const { formatStoredPlaceName, getDetailText, matchLatLon } = coreUtils.map const { summarizeQuery } = coreUtils.query diff --git a/lib/components/form/user-trip-settings.js b/lib/components/form/user-trip-settings.js index 0844203ba..d681e2cb8 100644 --- a/lib/components/form/user-trip-settings.js +++ b/lib/components/form/user-trip-settings.js @@ -3,12 +3,12 @@ import React, { Component } from 'react' import { Button } from 'react-bootstrap' import { connect } from 'react-redux' -import Icon from '../narrative/icon' import { clearDefaultSettings, resetForm, storeDefaultSettings } from '../../actions/form' +import Icon from '../util/icon' /** * This component contains the `Remember/Forget my trip options` and `Restore defaults` commands diff --git a/lib/components/mobile/batch-results-screen.js b/lib/components/mobile/batch-results-screen.js index 273c53bbb..d9be6ce0b 100644 --- a/lib/components/mobile/batch-results-screen.js +++ b/lib/components/mobile/batch-results-screen.js @@ -6,8 +6,8 @@ import styled, { css } from 'styled-components' import * as uiActions from '../../actions/ui' import Map from '../map/map' -import Icon from '../narrative/icon' import NarrativeItineraries from '../narrative/narrative-itineraries' +import Icon from '../util/icon' import { getActiveItineraries, getActiveSearch, diff --git a/lib/components/mobile/navigation-bar.js b/lib/components/mobile/navigation-bar.js index cdd845530..3c4f57e65 100644 --- a/lib/components/mobile/navigation-bar.js +++ b/lib/components/mobile/navigation-bar.js @@ -6,9 +6,9 @@ import { connect } from 'react-redux' import { setMobileScreen } from '../../actions/ui' import AppMenu from '../app/app-menu' import NavLoginButtonAuth0 from '../../components/user/nav-login-button-auth0' -import Icon from '../narrative/icon' import { accountLinks, getAuth0Config } from '../../util/auth' import { ComponentContext } from '../../util/contexts' +import Icon from '../util/icon' class MobileNavigationBar extends Component { static propTypes = { diff --git a/lib/components/narrative/default/access-leg.js b/lib/components/narrative/default/access-leg.js index e6687def5..9e370a06d 100644 --- a/lib/components/narrative/default/access-leg.js +++ b/lib/components/narrative/default/access-leg.js @@ -3,7 +3,7 @@ import { humanizeDistanceString } from '@opentripplanner/humanize-distance' import PropTypes from 'prop-types' import React, {Component} from 'react' -import Icon from '../icon' +import Icon from '../../util/icon' import LegDiagramPreview from '../leg-diagram-preview' /** diff --git a/lib/components/narrative/default/default-itinerary.js b/lib/components/narrative/default/default-itinerary.js index 599a18ebf..6dcecd092 100644 --- a/lib/components/narrative/default/default-itinerary.js +++ b/lib/components/narrative/default/default-itinerary.js @@ -1,19 +1,43 @@ -import moment from 'moment-timezone' import coreUtils from '@opentripplanner/core-utils' import React from 'react' -import { FormattedMessage, FormattedNumber } from 'react-intl' +import { FormattedMessage, FormattedNumber, FormattedTime } from 'react-intl' import { connect } from 'react-redux' +import styled from 'styled-components' import FieldTripGroupSize from '../../admin/field-trip-itinerary-group-size' import NarrativeItinerary from '../narrative-itinerary' import ItineraryBody from '../line-itin/connected-itinerary-body' import SimpleRealtimeAnnotation from '../simple-realtime-annotation' +import FormattedDuration from '../../util/formatted-duration' +import FormattedTimeRange from '../../util/formatted-time-range' import { getTotalFare } from '../../../util/state' import ItinerarySummary from './itinerary-summary' const { isBicycle, isMicromobility, isTransit } = coreUtils.itinerary +// Styled components +const LegIconWrapper = styled.div` + display: inline-block; + height: 20px; + padding-bottom: 6px; + padding-left: 2px; + width: 20px; + + /* Equivalent of a single space before the leg icon. */ + &::before { + content: ""; + margin: 0 0.125em; + } +` + +const DetailsHint = styled.div` + clear: both; + color: #685C5C; + font-size: small; + text-align: center; +` + /** * Obtains the description of an itinerary in the given locale. */ @@ -46,42 +70,6 @@ function ItineraryDescription ({itinerary}) { : mainMode } -/** - * Formats the given duration according to the selected locale. - */ -function FormattedDuration ({duration}) { - const dur = moment.duration(duration, 'seconds') - const hours = dur.hours() - const minutes = dur.minutes() - if (hours === 0) { - return ( - - ) - } else { - return ( - - ) - } -} - -function FormattedTime ({endTime, startTime, timeFormat}) { - return ( - - ) -} - const ITINERARY_ATTRIBUTES = [ { alias: 'best', @@ -98,26 +86,15 @@ const ITINERARY_ATTRIBUTES = [ render: (itinerary, options) => { if (options.isSelected) { if (options.selection === 'ARRIVALTIME') { - return ( - - ) + return } else { - return ( - - ) + return } } return ( - ) } @@ -144,18 +121,12 @@ const ITINERARY_ATTRIBUTES = [ const {LegIcon} = options return ( // FIXME: For CAR mode, walk time considers driving time. - - {' '} -
+ <> + + -
-
+ + ) } } @@ -200,8 +171,7 @@ class DefaultItinerary extends NarrativeItinerary { LegIcon, setActiveLeg, showRealtimeAnnotation, - timeFormat, - use24HourFormat + timeFormat } = this.props const timeOptions = { format: timeFormat, @@ -214,11 +184,6 @@ class DefaultItinerary extends NarrativeItinerary { onMouseEnter={this._onMouseEnter} onMouseLeave={this._onMouseLeave} role='presentation' - // FIXME: Move style to css - style={{ - backgroundColor: expanded ? 'white' : undefined, - borderLeft: active && !expanded ? '4px teal solid' : undefined - }} > {(active && expanded) && @@ -281,8 +245,7 @@ const mapStateToProps = (state, ownProps) => { // The configured (ambient) currency is needed for rendering the cost // of itineraries whether they include a fare or not, in which case // we show $0.00 or its equivalent in the configured currency and selected locale. - currency: state.otp.config.localization?.currency || 'USD', - use24HourFormat: state.user.loggedInUser?.use24HourFormat ?? false + currency: state.otp.config.localization?.currency || 'USD' } } diff --git a/lib/components/narrative/default/itinerary.css b/lib/components/narrative/default/itinerary.css index b4203ba0f..63bdb7bec 100644 --- a/lib/components/narrative/default/itinerary.css +++ b/lib/components/narrative/default/itinerary.css @@ -5,7 +5,7 @@ } /* If child component is focused, highlight itinerary option */ -.otp .option.default-itin:focus-within { +.otp .option.default-itin:focus-within:not(.expanded) { background-color: var(--hover-color); } @@ -19,6 +19,11 @@ border-top: 1px solid grey; } +/* Show side border if active and not expanded */ +.otp .option.default-itin.active:not(.expanded) { + border-left: 4px teal solid; +} + /* FIXME: don't highlight if not active */ .otp .option.default-itin:hover:not(.active) { background-color: var(--hover-color); diff --git a/lib/components/narrative/default/transit-leg.js b/lib/components/narrative/default/transit-leg.js index 2e2f3023d..c70107c5e 100644 --- a/lib/components/narrative/default/transit-leg.js +++ b/lib/components/narrative/default/transit-leg.js @@ -2,7 +2,7 @@ import coreUtils from '@opentripplanner/core-utils' import PropTypes from 'prop-types' import React, { Component } from 'react' -import Icon from '../icon' +import Icon from '../../util/icon' import ViewTripButton from '../../viewers/view-trip-button' import ViewStopButton from '../../viewers/view-stop-button' diff --git a/lib/components/narrative/icon.js b/lib/components/narrative/icon.js deleted file mode 100644 index cca4799b3..000000000 --- a/lib/components/narrative/icon.js +++ /dev/null @@ -1,16 +0,0 @@ -import React, { Component } from 'react' -import FontAwesome from 'react-fontawesome' - -export default class Icon extends Component { - static propTypes = { - // type: PropTypes.string.required - } - render () { - return ( - ) - } -} diff --git a/lib/components/narrative/itinerary-carousel.js b/lib/components/narrative/itinerary-carousel.js index f6b125edb..02fcee418 100644 --- a/lib/components/narrative/itinerary-carousel.js +++ b/lib/components/narrative/itinerary-carousel.js @@ -8,8 +8,8 @@ import SwipeableViews from 'react-swipeable-views' import { setActiveItinerary, setActiveLeg, setActiveStep } from '../../actions/narrative' import { ComponentContext } from '../../util/contexts' import { getActiveItineraries, getActiveSearch } from '../../util/state' +import Icon from '../util/icon' -import Icon from './icon' import Loading from './loading' class ItineraryCarousel extends Component { diff --git a/lib/components/narrative/line-itin/itin-summary.js b/lib/components/narrative/line-itin/itin-summary.js index b457e95ac..96f130dab 100644 --- a/lib/components/narrative/line-itin/itin-summary.js +++ b/lib/components/narrative/line-itin/itin-summary.js @@ -2,8 +2,12 @@ import coreUtils from '@opentripplanner/core-utils' import PropTypes from 'prop-types' import React, { Component } from 'react' import styled from 'styled-components' +import { connect } from 'react-redux' +import { FormattedNumber, FormattedMessage } from 'react-intl' import { ComponentContext } from '../../../util/contexts' +import FormattedDuration from '../../util/formatted-duration' +import FormattedTimeRange from '../../util/formatted-time-range' // TODO: make this a prop const defaultRouteColor = '#008' @@ -71,7 +75,7 @@ const ShortName = styled.div` width: 30px; ` -export default class ItinerarySummary extends Component { +export class ItinerarySummary extends Component { static propTypes = { itinerary: PropTypes.object } @@ -83,11 +87,10 @@ export default class ItinerarySummary extends Component { } render () { - const { itinerary, timeOptions } = this.props + const { currency, itinerary } = this.props const { LegIcon } = this.context const { - centsToString, maxTNCFare, minTNCFare, transitFare @@ -97,34 +100,59 @@ export default class ItinerarySummary extends Component { const maxTotalFare = maxTNCFare * 100 + transitFare const { caloriesBurned } = coreUtils.itinerary.calculatePhysicalActivity(itinerary) - return (
{/* Travel time in hrs/mins */} -
{coreUtils.time.formatDuration(itinerary.duration)}
+
+ +
{/* Duration as time range */} - {coreUtils.time.formatTime(itinerary.startTime, timeOptions)} - {coreUtils.time.formatTime(itinerary.endTime, timeOptions)} + {/* Fare / Calories */} {minTotalFare > 0 && - {centsToString(minTotalFare)} - {minTotalFare !== maxTotalFare && - {centsToString(maxTotalFare)}} + + ), + minTotalFare: ( + + ), + useMaxFare: minTotalFare !== maxTotalFare ? 'true' : 'false' + }} + /> } - {Math.round(caloriesBurned)} Cals + {/* Number of transfers, if applicable */} - {itinerary.transfers > 0 && ( - - {itinerary.transfers} transfer{itinerary.transfers > 1 ? 's' : ''} - - )} + + +
@@ -179,3 +207,10 @@ function getRouteNameForBadge (leg) { function getRouteColorForBadge (leg) { return leg.routeColor ? '#' + leg.routeColor : defaultRouteColor } + +const mapStateToProps = (state, ownProps) => { + return { + currency: state.otp.config.localization?.currency || 'USD' + } +} +export default connect(mapStateToProps)(ItinerarySummary) diff --git a/lib/components/narrative/loading.js b/lib/components/narrative/loading.js index adc668564..efc749562 100644 --- a/lib/components/narrative/loading.js +++ b/lib/components/narrative/loading.js @@ -1,6 +1,6 @@ import React, { Component } from 'react' -import Icon from './icon' +import Icon from '../util/icon' export default class Loading extends Component { render () { diff --git a/lib/components/narrative/mode-icon.js b/lib/components/narrative/mode-icon.js index eacb1436e..fd88b3054 100644 --- a/lib/components/narrative/mode-icon.js +++ b/lib/components/narrative/mode-icon.js @@ -1,7 +1,7 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' -import Icon from './icon' +import Icon from '../util/icon' export default class ModeIcon extends Component { static propTypes = { diff --git a/lib/components/narrative/narrative-itineraries-errors.js b/lib/components/narrative/narrative-itineraries-errors.js index 37ca1a098..fa94c77af 100644 --- a/lib/components/narrative/narrative-itineraries-errors.js +++ b/lib/components/narrative/narrative-itineraries-errors.js @@ -1,7 +1,7 @@ import { getCompanyIcon } from '@opentripplanner/icons/lib/companies' import styled from 'styled-components' -import Icon from '../narrative/icon' +import Icon from '../util/icon' import { getErrorMessage } from '../../util/state' const IssueContainer = styled.div` diff --git a/lib/components/narrative/narrative-itineraries-header.js b/lib/components/narrative/narrative-itineraries-header.js index 54300bc81..e462de55d 100644 --- a/lib/components/narrative/narrative-itineraries-header.js +++ b/lib/components/narrative/narrative-itineraries-header.js @@ -1,6 +1,7 @@ import styled from 'styled-components' +import { FormattedMessage, useIntl } from 'react-intl' -import Icon from '../narrative/icon' +import Icon from '../util/icon' import PlanFirstLastButtons from './plan-first-last-buttons' import SaveTripButton from './save-trip-button' @@ -26,22 +27,7 @@ export default function NarrativeItinerariesHeader ({ showingErrors, sort }) { - let resultText, titleText - if (pending) { - resultText = 'Finding your options...' - titleText = 'Finding your options...' - } else { - const itineraryPlural = itineraries.length === 1 - ? 'itinerary' - : 'itineraries' - const issuePlural = errors.length === 1 - ? 'issue' - : 'issues' - resultText = `${itineraries.length} ${itineraryPlural} found.` - titleText = errors.length > 0 - ? `${itineraries.length} ${itineraryPlural} (and ${errors.length} ${issuePlural}) found` - : resultText - } + const intl = useIntl() return (
- View all options + {itineraryIsExpanded && ( // marginLeft: auto is a way of making something "float right" @@ -72,15 +58,33 @@ export default function NarrativeItinerariesHeader ({ : <>
- {resultText} + {errors.length > 0 && ( - {errors.length} issues + + + )}
@@ -97,12 +101,12 @@ export default function NarrativeItinerariesHeader ({ onChange={onSortChange} value={sort.type} > - - - - - - + + + + + +
diff --git a/lib/components/narrative/narrative.css b/lib/components/narrative/narrative.css index fd0d6534d..db5f6e664 100644 --- a/lib/components/narrative/narrative.css +++ b/lib/components/narrative/narrative.css @@ -198,6 +198,10 @@ color: #685c5c; } +.otp .tabbed-itineraries .tab-button .details > span { + display: block; +} + .otp .tabbed-itineraries .tab-button:hover .title { border-bottom: 3px solid #ddd; } diff --git a/lib/components/narrative/plan-first-last-buttons.js b/lib/components/narrative/plan-first-last-buttons.js index fae5e4bf9..9209696e1 100644 --- a/lib/components/narrative/plan-first-last-buttons.js +++ b/lib/components/narrative/plan-first-last-buttons.js @@ -1,5 +1,6 @@ import React from 'react' import {Button} from 'react-bootstrap' +import { FormattedMessage } from 'react-intl' import {connect} from 'react-redux' import * as planActions from '../../actions/plan' @@ -15,16 +16,16 @@ function PlanFirstLastButtons (props) { return ( ) diff --git a/lib/components/narrative/realtime-annotation.js b/lib/components/narrative/realtime-annotation.js index 325fe8167..a8662cc33 100644 --- a/lib/components/narrative/realtime-annotation.js +++ b/lib/components/narrative/realtime-annotation.js @@ -1,7 +1,10 @@ -import coreUtils from '@opentripplanner/core-utils' import PropTypes from 'prop-types' import React, { Component } from 'react' import { Button, OverlayTrigger, Popover } from 'react-bootstrap' +import { FormattedList, FormattedMessage } from 'react-intl' + +import FormattedDuration from '../util/formatted-duration' +import Icon from '../util/icon' export default class RealtimeAnnotation extends Component { static propTypes = { @@ -25,28 +28,30 @@ export default class RealtimeAnnotation extends Component { const innerContent =

- Service update + +

-

+

{useRealtime - ? - Your trip results have been adjusted based on real-time - information. Under normal conditions, this trip would take{' '} - {coreUtils.time.formatDuration(realtimeEffects.normalDuration)} - using the following routes:{' '} - {filteredRoutes - .map((route, idx) => ( - - {route} - {filteredRoutes.length - 1 > idx && ', '} - - )) - }. - - : - Your trip results are currently being affected by service delays. - These delays do not factor into travel times shown below. - + ? ( + + + + ), + routes: ( + {route})} + /> + ) + }} + /> + ) + : }

@@ -55,7 +60,10 @@ export default class RealtimeAnnotation extends Component { className='toggle-realtime' onClick={toggleRealtime} > - {useRealtime ? `Ignore` : `Apply`} service delays + {useRealtime + ? + : + }
diff --git a/lib/components/narrative/save-trip-button.js b/lib/components/narrative/save-trip-button.js index d5cabe2e7..5fe5f84a5 100644 --- a/lib/components/narrative/save-trip-button.js +++ b/lib/components/narrative/save-trip-button.js @@ -1,9 +1,11 @@ import React from 'react' import { OverlayTrigger, Tooltip } from 'react-bootstrap' +import { FormattedMessage, useIntl } from 'react-intl' import { connect } from 'react-redux' import { LinkContainerWithQuery } from '../form/connected-links' import { CREATE_TRIP_PATH } from '../../util/constants' +import Icon from '../util/icon' import { itineraryCanBeMonitored } from '../../util/itinerary' import { getActiveItinerary } from '../../util/state' @@ -16,6 +18,7 @@ const SaveTripButton = ({ loggedInUser, persistence }) => { + const intl = useIntl() // We are dealing with the following states: // 1. Persistence disabled => just return null // 2. User is not logged in => render something like: "Please sign in to save trip". @@ -24,23 +27,23 @@ const SaveTripButton = ({ let buttonDisabled let buttonText let tooltipText - let icon + let iconType if (!persistence || !persistence.enabled) { return null } else if (!loggedInUser) { buttonDisabled = true - buttonText = 'Sign in to save trip' - icon = 'fa fa-lock' - tooltipText = 'Please sign in to save trip.' + buttonText = + iconType = 'lock' + tooltipText = intl.formatMessage({id: 'components.SaveTripButton.signInTooltip'}) } else if (!itineraryCanBeMonitored(itinerary)) { buttonDisabled = true - buttonText = 'Cannot save' - icon = 'fa fa-ban' - tooltipText = 'Only itineraries that include transit and no rentals or ride hailing can be monitored.' + buttonText = + iconType = 'ban' + tooltipText = intl.formatMessage({id: 'components.SaveTripButton.cantSaveTooltip'}) } else { - buttonText = 'Save trip' - icon = 'fa fa-plus-circle' + buttonText = + iconType = 'plus-circle' } const button = ( ) // Show tooltip with help text if button is disabled. if (buttonDisabled) { return ( {tooltipText}} + overlay={( + + {/* Must get text using intl.formatMessage here because the rendering + of OverlayTrigger seems to occur outside of the IntlProvider context. */} + {tooltipText} + + )} placement='top' > -
+ {/* An active element around the disabled button is necessary + for the OverlayTrigger to render. */} +
{button}
diff --git a/lib/components/narrative/simple-realtime-annotation.js b/lib/components/narrative/simple-realtime-annotation.js index 07e1a4e7b..254714278 100644 --- a/lib/components/narrative/simple-realtime-annotation.js +++ b/lib/components/narrative/simple-realtime-annotation.js @@ -1,9 +1,13 @@ -import React, { Component } from 'react' +import React from 'react' +import { FormattedMessage } from 'react-intl' -export default class SimpleRealtimeAnnotation extends Component { - render () { - return
- This trip uses real-time traffic and delay information -
- } -} +import Icon from '../util/icon' + +const SimpleRealtimeAnnotation = () => ( +
+ + +
+) + +export default SimpleRealtimeAnnotation diff --git a/lib/components/narrative/tabbed-itineraries.js b/lib/components/narrative/tabbed-itineraries.js index 28950c273..1352e542e 100644 --- a/lib/components/narrative/tabbed-itineraries.js +++ b/lib/components/narrative/tabbed-itineraries.js @@ -2,14 +2,25 @@ import coreUtils from '@opentripplanner/core-utils' import PropTypes from 'prop-types' import React, { Component } from 'react' import { Button } from 'react-bootstrap' +import { FormattedMessage, FormattedNumber } from 'react-intl' import { connect } from 'react-redux' +import styled from 'styled-components' import * as narrativeActions from '../../actions/narrative' import { ComponentContext } from '../../util/contexts' +import { getTimeFormat } from '../../util/i18n' import { getActiveSearch, getRealtimeEffects } from '../../util/state' +import FormattedDuration from '../util/formatted-duration' +import FormattedTimeRange from '../util/formatted-time-range' -const { calculateFares, calculatePhysicalActivity, getTimeZoneOffset } = coreUtils.itinerary -const { formatDuration, formatTime, getTimeFormat } = coreUtils.time +const { calculateFares, calculatePhysicalActivity } = coreUtils.itinerary + +const Bullet = styled.span` + ::before { + content: "•"; + margin: 0 0.25em; + } +` class TabbedItineraries extends Component { static propTypes = { @@ -33,6 +44,7 @@ class TabbedItineraries extends Component { render () { const { activeItinerary, + currency, itineraries, realtimeEffects, setActiveItinerary, @@ -57,11 +69,12 @@ class TabbedItineraries extends Component { {itineraries.map((itinerary, index) => { return ( ) })} @@ -105,22 +118,16 @@ class TabButton extends Component { } render () { - const {index, isActive, itinerary, timeFormat} = this.props - const timeOptions = { - format: timeFormat, - offset: getTimeZoneOffset(itinerary) - } + const {currency, index, isActive, itinerary} = this.props const classNames = ['tab-button', 'clear-button-formatting'] const { caloriesBurned } = calculatePhysicalActivity(itinerary) const { - centsToString, maxTNCFare, minTNCFare, transitFare } = calculateFares(itinerary) // TODO: support non-USD const minTotalFare = minTNCFare * 100 + transitFare - const plus = maxTNCFare && maxTNCFare > minTNCFare ? '+' : '' if (isActive) classNames.push('selected') return ( ) } @@ -163,18 +196,21 @@ class TabButton extends Component { const mapStateToProps = (state, ownProps) => { const activeSearch = getActiveSearch(state) + const currency = state.otp.config.localization?.currency || 'USD' const pending = activeSearch ? Boolean(activeSearch.pending) : false const realtimeEffects = getRealtimeEffects(state) 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, companies: state.otp.currentQuery.companies, + currency, pending, + // swap out realtime itineraries with non-realtime depending on boolean realtimeEffects, - timeFormat: getTimeFormat(state.otp.config), + timeFormat: getTimeFormat(state), tnc: state.otp.tnc, useRealtime } diff --git a/lib/components/narrative/trip-tools.js b/lib/components/narrative/trip-tools.js index aa5f908a9..641ca8bf8 100644 --- a/lib/components/narrative/trip-tools.js +++ b/lib/components/narrative/trip-tools.js @@ -1,9 +1,12 @@ +import bowser from 'bowser' +import copyToClipboard from 'copy-to-clipboard' import React, {Component} from 'react' import { connect } from 'react-redux' import { Button } from 'react-bootstrap' // import { DropdownButton, MenuItem } from 'react-bootstrap' -import copyToClipboard from 'copy-to-clipboard' -import bowser from 'bowser' +import { FormattedMessage, injectIntl } from 'react-intl' + +import Icon from '../util/icon' class TripTools extends Component { static defaultProps = { @@ -32,7 +35,14 @@ class TripTools extends Component { if (reactRouterConfig && reactRouterConfig.basename) { startOverUrl += reactRouterConfig.basename } - buttonComponents.push() + buttonComponents.push( + // FIXME: The Spanish string does not fit in button width. + } + url={startOverUrl} + /> + ) break } }) @@ -89,6 +99,7 @@ class CopyUrlButton extends Component { if (parts.length === 2) { url = `${parts[0]}#/start/x/x/x/${routerId}${parts[1]}` } else { + // Console logs are not internationalized. console.warn('URL not formatted as expected, copied URL will not contain session routerId.', routerId) } } @@ -105,8 +116,18 @@ class CopyUrlButton extends Component { onClick={this._onClick} > {this.state.showCopied - ? Copied - : Copy Link + ? ( + + + + + ) + : ( + + + + + ) }
@@ -130,7 +151,8 @@ class PrintButton extends Component { className='tool-button' onClick={this._onClick} > - Print + +
) @@ -139,20 +161,14 @@ class PrintButton extends Component { // Report Issue Button Component -class ReportIssueButton extends Component { - static defaultProps = { - subject: 'Reporting an Issue with OpenTripPlanner' - } - +class ReportIssueButtonBase extends Component { _onClick = () => { - const { mailto, subject } = this.props - + const { intl, mailto, subject: configuredSubject } = this.props + const subject = configuredSubject || intl.formatMessage({id: 'components.TripTools.reportEmailSubject'}) const bodyLines = [ - ' *** 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.', + intl.formatMessage({id: 'components.TripTools.reportEmailTemplate'}), '', + // Search data section is for support and is not translated. 'SEARCH DATA:', 'Address: ' + window.location.href, 'Browser: ' + bowser.name + ' ' + bowser.version, @@ -171,12 +187,18 @@ class ReportIssueButton extends Component { className='tool-button' onClick={this._onClick} > - Report Issue + + {/* FIXME: Depending on translation, Spanish and French strings may not fit in button width. */} + ) } } +// The ReportIssueButton component above, with an intl prop +// for retrieving messages shown outside of React rendering. +const ReportIssueButton = injectIntl(ReportIssueButtonBase) + // Link to URL Button class LinkButton extends Component { @@ -192,7 +214,7 @@ class LinkButton extends Component { className='tool-button' onClick={this._onClick} > - {icon && } + {icon && } {text}
diff --git a/lib/components/user/monitored-trip/trip-notifications-pane.js b/lib/components/user/monitored-trip/trip-notifications-pane.js index 302e928d4..e174f7478 100644 --- a/lib/components/user/monitored-trip/trip-notifications-pane.js +++ b/lib/components/user/monitored-trip/trip-notifications-pane.js @@ -3,7 +3,7 @@ import React, { Component } from 'react' import { Alert, FormControl, Glyphicon } from 'react-bootstrap' import styled from 'styled-components' -import Icon from '../../narrative/icon' +import Icon from '../../util/icon' const notificationChannelLabels = { email: 'email', diff --git a/lib/components/user/monitored-trip/trip-summary.js b/lib/components/user/monitored-trip/trip-summary.js index 816d58352..a4ec3c8be 100644 --- a/lib/components/user/monitored-trip/trip-summary.js +++ b/lib/components/user/monitored-trip/trip-summary.js @@ -12,7 +12,7 @@ const TripSummary = ({ monitoredTrip }) => { // TODO: use the modern itinerary summary built for trip comparison. return (
Itinerary{' '} diff --git a/lib/components/user/places/favorite-place-row.js b/lib/components/user/places/favorite-place-row.js index dea65aebe..0b1f7e533 100644 --- a/lib/components/user/places/favorite-place-row.js +++ b/lib/components/user/places/favorite-place-row.js @@ -4,7 +4,7 @@ import { Button } from 'react-bootstrap' import styled, { css } from 'styled-components' import { LinkContainerWithQuery } from '../../form/connected-links' -import Icon from '../../narrative/icon' +import Icon from '../../util/icon' const FIELD_HEIGHT_PX = '60px' diff --git a/lib/components/user/places/place-editor.js b/lib/components/user/places/place-editor.js index 562fd3907..7a78fbff3 100644 --- a/lib/components/user/places/place-editor.js +++ b/lib/components/user/places/place-editor.js @@ -10,7 +10,7 @@ import { } from 'react-bootstrap' import styled from 'styled-components' -import Icon from '../../narrative/icon' +import Icon from '../../util/icon' import { getErrorStates } from '../../../util/ui' import { CUSTOM_PLACE_TYPES, isHomeOrWork } from '../../../util/user' diff --git a/lib/components/user/styled.js b/lib/components/user/styled.js index 35019af1c..fea421734 100644 --- a/lib/components/user/styled.js +++ b/lib/components/user/styled.js @@ -1,7 +1,7 @@ import { Panel } from 'react-bootstrap' import styled from 'styled-components' -import Icon from '../narrative/icon' +import Icon from '../util/icon' export const PageHeading = styled.h2` margin: 10px 0px 45px 0px; diff --git a/lib/components/util/formatted-duration.js b/lib/components/util/formatted-duration.js new file mode 100644 index 000000000..3ca1a80c4 --- /dev/null +++ b/lib/components/util/formatted-duration.js @@ -0,0 +1,17 @@ +import moment from 'moment-timezone' +import { FormattedMessage } from 'react-intl' + +/** + * Formats the given duration according to the selected locale. + */ +export default function FormattedDuration ({duration}) { + const dur = moment.duration(duration, 'seconds') + const hours = dur.hours() + const minutes = dur.minutes() + return ( + + ) +} diff --git a/lib/components/util/formatted-time-range.js b/lib/components/util/formatted-time-range.js new file mode 100644 index 000000000..f1e9a2c8b --- /dev/null +++ b/lib/components/util/formatted-time-range.js @@ -0,0 +1,18 @@ +import moment from 'moment-timezone' +import { FormattedMessage } from 'react-intl' + +/** + * Renders a time range e.g. 3:45pm-4:15pm according to the + * react-intl default time format for the ambient locale. + */ +export default function FormattedTimeRange ({ endTime, startTime }) { + return ( + + ) +} diff --git a/lib/components/util/icon.js b/lib/components/util/icon.js new file mode 100644 index 000000000..0662b2c94 --- /dev/null +++ b/lib/components/util/icon.js @@ -0,0 +1,41 @@ +import PropTypes from 'prop-types' +import React from 'react' +import FontAwesome from 'react-fontawesome' +import styled from 'styled-components' + +/** + * A Font Awesome icon followed by a with a pseudo-element equivalent to a single space. + */ +const FontAwesomeWithSpace = styled(FontAwesome)` + &::after { + content: ""; + margin: 0 0.125em; + } +` + +/** + * Wrapper for the FontAwesome component that, if specified in the withSpace prop, + * supports CSS spacing after the specified icon type, to replace the {' '} workaround, + * and that should work for both left-to-right and right-to-left layouts. + * Other props from FontAwesome are passed to that component. + */ +const Icon = ({ fixedWidth = true, type, withSpace, ...props }) => { + const FontComponent = withSpace + ? FontAwesomeWithSpace + : FontAwesome + return ( + + ) +} + +Icon.propTypes = { + fixedWidth: PropTypes.bool, + type: PropTypes.string.isRequired, + withSpace: PropTypes.bool +} + +export default Icon diff --git a/lib/components/viewers/live-stop-times.js b/lib/components/viewers/live-stop-times.js index 76f0cb050..403f627ed 100644 --- a/lib/components/viewers/live-stop-times.js +++ b/lib/components/viewers/live-stop-times.js @@ -3,7 +3,7 @@ import 'moment-timezone' import coreUtils from '@opentripplanner/core-utils' import React, { Component } from 'react' -import Icon from '../narrative/icon' +import Icon from '../util/icon' import { getRouteIdForPattern, getStopTimesByPattern, diff --git a/lib/components/viewers/pattern-row.js b/lib/components/viewers/pattern-row.js index 0286154d6..5f5d7ecc1 100644 --- a/lib/components/viewers/pattern-row.js +++ b/lib/components/viewers/pattern-row.js @@ -1,7 +1,7 @@ import React, { Component } from 'react' import { VelocityTransitionGroup } from 'velocity-react' -import Icon from '../narrative/icon' +import Icon from '../util/icon' import { stopTimeComparator } from '../../util/viewer' import RealtimeStatusLabel from './realtime-status-label' diff --git a/lib/components/viewers/route-viewer.js b/lib/components/viewers/route-viewer.js index 95ad0cbd6..362016c35 100644 --- a/lib/components/viewers/route-viewer.js +++ b/lib/components/viewers/route-viewer.js @@ -6,7 +6,7 @@ import { VelocityTransitionGroup } from 'velocity-react' import { connect } from 'react-redux' import styled from 'styled-components' -import Icon from '../narrative/icon' +import Icon from '../util/icon' import { setMainPanelContent, setViewedRoute } from '../../actions/ui' import { findRoutes, findRoute } from '../../actions/api' import { ComponentContext } from '../../util/contexts' diff --git a/lib/components/viewers/stop-time-cell.js b/lib/components/viewers/stop-time-cell.js index ac1dae23d..be732823d 100644 --- a/lib/components/viewers/stop-time-cell.js +++ b/lib/components/viewers/stop-time-cell.js @@ -4,7 +4,7 @@ import coreUtils from '@opentripplanner/core-utils' import PropTypes from 'prop-types' import React from 'react' -import Icon from '../narrative/icon' +import Icon from '../util/icon' import { getSecondsUntilDeparture } from '../../util/viewer' const { formatDuration, formatSecondsAfterMidnight } = coreUtils.time diff --git a/lib/components/viewers/stop-viewer.js b/lib/components/viewers/stop-viewer.js index fa540f208..8f538cef4 100644 --- a/lib/components/viewers/stop-viewer.js +++ b/lib/components/viewers/stop-viewer.js @@ -11,7 +11,7 @@ import styled from 'styled-components' import * as apiActions from '../../actions/api' import * as mapActions from '../../actions/map' import * as uiActions from '../../actions/ui' -import Icon from '../narrative/icon' +import Icon from '../util/icon' import { getShowUserSettings, getStopViewerConfig } from '../../util/state' import LiveStopTimes from './live-stop-times' diff --git a/lib/components/viewers/trip-viewer.js b/lib/components/viewers/trip-viewer.js index 3835b99da..7a6543aeb 100644 --- a/lib/components/viewers/trip-viewer.js +++ b/lib/components/viewers/trip-viewer.js @@ -6,7 +6,7 @@ import React, { Component } from 'react' import { Button, Label } from 'react-bootstrap' import { connect } from 'react-redux' -import Icon from '../narrative/icon' +import Icon from '../util/icon' import { setViewedTrip } from '../../actions/ui' import { findTrip } from '../../actions/api' import { setLocation } from '../../actions/map' diff --git a/lib/util/i18n.js b/lib/util/i18n.js index b0e91f165..c6f69d57d 100644 --- a/lib/util/i18n.js +++ b/lib/util/i18n.js @@ -64,3 +64,12 @@ export function getDefaultLocale (config) { const { localization = {} } = config return localization.defaultLocale || 'en-US' } + +/** + * Obtains the time format (12 or 24 hr) based on the redux user state. + * FIXME: Remove after OTP-UI components can determine the time format on their own. + */ +export function getTimeFormat (state) { + const use24HourFormat = state.user.loggedInUser?.use24HourFormat ?? false + return use24HourFormat ? 'H:mm' : 'h:mm a' +}