Translate TrackPage

This commit is contained in:
Paul Bienkowski 2022-07-24 21:51:27 +02:00
parent ab6cc6f6d0
commit 6f7c8d54f2
9 changed files with 292 additions and 145 deletions

View file

@ -0,0 +1,18 @@
import React from "react";
import { Icon } from "semantic-ui-react";
import { useTranslation } from "react-i18next";
export default function Visibility({ public: public_ }: { public: boolean }) {
const { t } = useTranslation();
const icon = public_ ? (
<Icon color="blue" name="eye" fitted />
) : (
<Icon name="eye slash" fitted />
);
const text = public_ ? t("general.public") : t("general.private");
return (
<>
{icon} {text}
</>
);
}

View file

@ -9,3 +9,4 @@ export {default as Page} from './Page'
export {default as Stats} from './Stats'
export {default as StripMarkdown} from './StripMarkdown'
export {default as Chart} from './Chart'
export {default as Visibility} from './Visibility'

View file

@ -1,40 +1,40 @@
import React from 'react'
import {Link} from 'react-router-dom'
import {Icon, Popup, Button, Dropdown} from 'semantic-ui-react'
import { useTranslation } from "react-i18next";
export default function TrackActions({slug, isAuthor, onDownload}) {
const { t } = useTranslation();
return (
<>
{isAuthor && (
<Link to={`/tracks/${slug}/edit`}>
<Button primary>Edit track</Button>
</Link>
)}
<Dropdown text="Download" button upward>
<Dropdown.Menu>
<Dropdown.Item text="Original" onClick={() => onDownload('original.csv')} disabled={!isAuthor} />
<Dropdown.Item text="Track (GPX)" onClick={() => onDownload('track.gpx')} />
</Dropdown.Menu>
</Dropdown>
<Popup
trigger={<Icon name="info circle" />}
offset={[12, 0]}
content={
isAuthor ? (
<>
<p>Only you, the author of this track, can download the original file.</p>
<p>
This is the file as it was uploaded to the server, without modifications, and it can be used with other
tools.
</p>
<p>{t('TrackPage.actions.hintAuthorOnly')}</p>
<p>{t('TrackPage.actions.hintOriginal')}</p>
</>
) : (
<p>Only the author of this track can download the original file.</p>
<p>{t('TrackPage.actions.hintAuthorOnlyOthers')}</p>
)
}
/>
<Dropdown text={t('TrackPage.actions.download')} button>
<Dropdown.Menu>
<Dropdown.Item text={t('TrackPage.actions.original')}onClick={() => onDownload('original.csv')} disabled={!isAuthor} />
<Dropdown.Item text={t('TrackPage.actions.gpx')} onClick={() => onDownload('track.gpx')} />
</Dropdown.Menu>
</Dropdown>
{isAuthor && (
<Link to={`/tracks/${slug}/edit`}>
<Button primary>{t('TrackPage.actions.edit')}</Button>
</Link>
)}
</>
)
}

View file

@ -1,31 +1,57 @@
import React from 'react'
import {Message, Segment, Form, Button, Loader, Header, Comment} from 'semantic-ui-react'
import Markdown from 'react-markdown'
import React from "react";
import {
Message,
Segment,
Form,
Button,
Loader,
Header,
Comment,
} from "semantic-ui-react";
import Markdown from "react-markdown";
import { useTranslation } from "react-i18next";
import {Avatar, FormattedDate} from 'components'
import { Avatar, FormattedDate } from "components";
function CommentForm({ onSubmit }) {
const [body, setBody] = React.useState('')
const { t } = useTranslation();
const [body, setBody] = React.useState("");
const onSubmitComment = React.useCallback(() => {
onSubmit({body})
setBody('')
}, [onSubmit, body])
onSubmit({ body });
setBody("");
}, [onSubmit, body]);
return (
<Form reply onSubmit={onSubmitComment}>
<Form.TextArea rows={4} value={body} onChange={(e) => setBody(e.target.value)} />
<Button content="Post comment" labelPosition="left" icon="edit" primary />
<Form.TextArea
rows={4}
value={body}
onChange={(e) => setBody(e.target.value)}
/>
<Button
content={t("TrackPage.comments.post")}
labelPosition="left"
icon="edit"
primary
/>
</Form>
)
);
}
export default function TrackComments({comments, onSubmit, onDelete, login, hideLoader}) {
export default function TrackComments({
comments,
onSubmit,
onDelete,
login,
hideLoader,
}) {
const { t } = useTranslation();
return (
<>
<Comment.Group>
<Header as="h2" dividing>
Comments
{t("TrackPage.comments.title")}
</Header>
<Loader active={!hideLoader && comments == null} inline />
@ -47,11 +73,11 @@ export default function TrackComments({comments, onSubmit, onDelete, login, hide
<Comment.Actions>
<Comment.Action
onClick={(e) => {
onDelete(comment.id)
e.preventDefault()
onDelete(comment.id);
e.preventDefault();
}}
>
Delete
{t('general.delete')}
</Comment.Action>
</Comment.Actions>
)}
@ -59,10 +85,12 @@ export default function TrackComments({comments, onSubmit, onDelete, login, hide
</Comment>
))}
{comments != null && !comments.length && <Message>Nobody commented... yet</Message>}
{comments != null && !comments.length && (
<Message>{t("TrackPage.comments.empty")}</Message>
)}
{login && comments != null && <CommentForm onSubmit={onSubmit} />}
</Comment.Group>
</>
)
);
}

View file

@ -1,80 +1,85 @@
import React from 'react'
import {List} from 'semantic-ui-react'
import {Duration} from 'luxon'
import React from "react";
import _ from "lodash";
import { List, Header, Grid } from "semantic-ui-react";
import { Duration } from "luxon";
import { useTranslation } from "react-i18next";
import {FormattedDate} from 'components'
import { FormattedDate, Visibility } from "components";
function formatDuration(seconds) {
return Duration.fromMillis((seconds ?? 0) * 1000).toFormat("h'h' mm'm'")
return Duration.fromMillis((seconds ?? 0) * 1000).toFormat("h'h' mm'm'");
}
export default function TrackDetails({ track, isAuthor }) {
const { t } = useTranslation();
const items = [
track.public != null &&
isAuthor && [
t("TrackPage.details.visibility"),
<Visibility public={track.public} />,
],
track.uploadedByUserAgent != null && [
t("TrackPage.details.uploadedWith"),
track.uploadedByUserAgent,
],
track.duration != null && [
t("TrackPage.details.duration"),
formatDuration(track.duration),
],
track.createdAt != null && [
t("TrackPage.details.uploadedDate"),
<FormattedDate date={track.createdAt} />,
],
track?.recordedAt != null && [
t("TrackPage.details.recordedDate"),
<FormattedDate date={track?.recordedAt} />,
],
track?.numEvents != null && [
t("TrackPage.details.numEvents"),
track?.numEvents,
],
track?.length != null && [
t("TrackPage.details.length"),
`${(track?.length / 1000).toFixed(2)} km`,
],
track?.processingStatus != null &&
track?.processingStatus != "error" && [
t("TrackPage.details.processingStatus"),
track.processingStatus,
],
track.originalFileName != null && [
t("TrackPage.details.originalFileName"),
<code>{track.originalFileName}</code>,
],
].filter(Boolean);
const COLUMNS = 4;
const chunkSize = Math.ceil(items.length / COLUMNS)
return (
<List horizontal relaxed>
{track.public != null && isAuthor && (
<List.Item>
<List.Header>Visibility</List.Header>
{track.public ? 'Public' : 'Private'}
</List.Item>
)}
<Grid>
<Grid.Row columns={COLUMNS}>
{_.chunk(items, chunkSize).map((chunkItems, idx) => (
<Grid.Column key={idx}>
{track.originalFileName != null && (
<List.Item>
{isAuthor && <div style={{float: 'right'}}></div>}
<List.Header>Original Filename</List.Header>
<code>{track.originalFileName}</code>
</List.Item>
)}
{track.uploadedByUserAgent != null && (
<List.Item>
<List.Header>Uploaded with</List.Header>
{track.uploadedByUserAgent}
</List.Item>
)}
{track.duration != null && (
<List.Item>
<List.Header>Duration</List.Header>
{formatDuration(track.duration)}
</List.Item>
)}
{track.createdAt != null && (
<List.Item>
<List.Header>Uploaded on</List.Header>
<FormattedDate date={track.createdAt} />
</List.Item>
)}
{track?.recordedAt != null && (
<List.Item>
<List.Header>Recorded on</List.Header>
<FormattedDate date={track?.recordedAt} />
</List.Item>
)}
{track?.numEvents != null && (
<List.Item>
<List.Header>Confirmed events</List.Header>
{track?.numEvents}
</List.Item>
)}
{track?.length != null && (
<List.Item>
<List.Header>Length</List.Header>
{(track?.length / 1000).toFixed(2)} km
</List.Item>
)}
{track?.processingStatus != null && track?.processingStatus != 'error' && (
<List.Item>
<List.Header>Processing</List.Header>
{track.processingStatus}
</List.Item>
)}
<List>
{chunkItems.map(([title, value]) => (
<List.Item key={title}>
<List.Header>{title}</List.Header>
<List.Description>{value}</List.Description>
</List.Item>))}
</List>
)
</Grid.Column>
))}
</Grid.Row>
</Grid>
);
}

View file

@ -25,6 +25,7 @@ import {
} from "rxjs/operators";
import { useObservable } from "rxjs-hooks";
import Markdown from "react-markdown";
import { useTranslation } from "react-i18next";
import api from "api";
import { Page } from "components";
@ -52,16 +53,17 @@ function TrackMapSettings({
side,
setSide,
}) {
const { t } = useTranslation();
return (
<>
<Header as="h4">Map settings</Header>
<Header as="h4">{t("TrackPage.mapSettings.title")}</Header>
<List>
<List.Item>
<Checkbox
checked={showTrack}
onChange={(e, d) => setShowTrack(d.checked)}
/>{" "}
Show track
{t("TrackPage.mapSettings.showTrack")}
<div style={{ marginTop: 8 }}>
<span
style={{
@ -73,7 +75,7 @@ function TrackMapSettings({
marginRight: 4,
}}
/>
GPS track
{t("TrackPage.mapSettings.gpsTrack")}
</div>
<div>
<span
@ -86,11 +88,11 @@ function TrackMapSettings({
marginRight: 4,
}}
/>
Snapped to road
{t("TrackPage.mapSettings.snappedTrack")}
</div>
</List.Item>
<List.Item>
<List.Header>Points</List.Header>
<List.Header> {t("TrackPage.mapSettings.points")} </List.Header>
<Dropdown
selection
value={pointsMode}
@ -100,18 +102,18 @@ function TrackMapSettings({
{
key: "overtakingEvents",
value: "overtakingEvents",
text: "Confirmed",
text: t("TrackPage.mapSettings.confirmedPoints"),
},
{
key: "measurements",
value: "measurements",
text: "All measurements",
text: t("TrackPage.mapSettings.allPoints"),
},
]}
/>
</List.Item>
<List.Item>
<List.Header>Side (for color)</List.Header>
<List.Header>{t("TrackPage.mapSettings.side")}</List.Header>
<Dropdown
selection
value={side}
@ -120,12 +122,12 @@ function TrackMapSettings({
{
key: "overtaker",
value: "overtaker",
text: "Overtaker (Left)",
text: t("TrackPage.mapSettings.overtakerSide"),
},
{
key: "stationary",
value: "stationary",
text: "Stationary (Right)",
text: t("TrackPage.mapSettings.stationarySide"),
},
]}
/>
@ -138,6 +140,7 @@ function TrackMapSettings({
const TrackPage = connect((state) => ({ login: state.login }))(
function TrackPage({ login }) {
const { slug } = useParams();
const { t } = useTranslation();
const [reloadComments, reloadComments$] = useTriggerSubject();
const history = useHistory();
@ -234,9 +237,7 @@ const TrackPage = connect((state) => ({ login: state.login }))(
await api.downloadFile(`/tracks/${slug}/download/${filename}`);
} catch (err) {
if (/Failed to fetch/.test(String(err))) {
setDownloadError(
"The track probably has not been imported correctly or recently enough. Please ask your administrator for assistance."
);
setDownloadError(t("TrackPage.downloadError"));
} else {
setDownloadError(String(err));
}
@ -259,7 +260,7 @@ const TrackPage = connect((state) => ({ login: state.login }))(
const [pointsMode, setPointsMode] = React.useState("overtakingEvents"); // none|overtakingEvents|measurements
const [side, setSide] = React.useState("overtaker"); // overtaker|stationary
const title = track ? track.title || "Unnamed track" : null;
const title = track ? track.title || t("general.unnamedTrack") : null;
return (
<Page
title={title}
@ -268,9 +269,16 @@ const TrackPage = connect((state) => ({ login: state.login }))(
<Container>
{track && (
<Segment basic>
<div style={{display: 'flex', alignItems: 'baseline', marginBlockStart: 32, marginBlockEnd: 16}}>
<div
style={{
display: "flex",
alignItems: "baseline",
marginBlockStart: 32,
marginBlockEnd: 16,
}}
>
<Header as="h1">{title}</Header>
<div style={{marginLeft: 'auto'}}>
<div style={{ marginLeft: "auto" }}>
<TrackActions {...{ isAuthor, onDownload, slug }} />
</div>
</div>
@ -307,8 +315,7 @@ const TrackPage = connect((state) => ({ login: state.login }))(
{processing && (
<Message warning>
<Message.Content>
Track data is still being processed, please reload page in
a while.
{t("TrackPage.processing")}
</Message.Content>
</Message>
)}
@ -316,8 +323,7 @@ const TrackPage = connect((state) => ({ login: state.login }))(
{error && (
<Message error>
<Message.Content>
The processing of this track failed, please ask your site
administrator for help in debugging the issue.
{t("TrackPage.processingError")}
</Message.Content>
</Message>
)}
@ -328,7 +334,7 @@ const TrackPage = connect((state) => ({ login: state.login }))(
{track?.description && (
<>
<Header as="h2" dividing>
Description
{t("TrackPage.description")}
</Header>
<Markdown>{track.description}</Markdown>
</>
@ -347,7 +353,7 @@ const TrackPage = connect((state) => ({ login: state.login }))(
open={downloadError != null}
cancelButton={false}
onConfirm={hideDownloadError}
header="Download failed"
header={t("TrackPage.downloadFailed")}
content={String(downloadError)}
/>
</Page>

View file

@ -114,15 +114,7 @@ export function TrackListItem({track, privateTracks = false}) {
</Item.Description>
{privateTracks && (
<Item.Extra>
{track.public ? (
<>
<Icon color="blue" name="eye" fitted /> {t('general.public')}
</>
) : (
<>
<Icon name="eye slash" fitted /> {t('general.private')}
</>
)}
<Visibility public={track.public} />
<span style={{marginLeft: '1em'}}>
<Icon color={COLOR_BY_STATUS[track.processingStatus]} name="bolt" fitted />

View file

@ -7,6 +7,8 @@ general:
private: Privat
show: Anzeigen
edit: Bearbeiten
save: Speichern
delete: Löschen
App:
footer:
@ -221,3 +223,49 @@ SettingsPage:
generate: Neuen API-Schlüssel erstellen
TrackPage:
downloadFailed: Download fehlgeschlagen
downloadError: Diese Fahrt wurde vermutlich nicht korrekt importiert, oder in letzter Zeit nicht aktualisiert. Bitte frage den Administrator um Hilfe mit diesem Problem.
processing: Diese Fahrt wird gerade importiert, bitte lade die Seite später neu.
processingError: Beim Import dieser Fahrt ist ein Fehler aufgetreten, bitte frage den Administrator um Hilfe mit diesem Problem.
description: Beschreibung
mapSettings:
title: Karteneinstellungen
showTrack: Route anzeigen
gpsTrack: GPS-Route
snappedTrack: Erkannte Straßenroute
points: Punkte
confirmedPoints: Bestätigte Überholungen
allPoints: Alle Messungen
side: Seite für Einfärbung
overtakerSide: Überholung (links)
stationarySide: Ruhender Verkehr (rechts)
details:
visibility: Sichtbarkeit
originalFileName: Original Dateiname
uploadedWith: Hochgeladen mit
duration: Dauer
uploadedDate: Hochgeladen am
recordedDate: Aufgezeichnet am
numEvents: Bestätigte Überholungen
length: Länge
processingStatus: Verarbeitung
actions:
edit: Fahrt bearbeiten
download: Herunterladen
original: Original
gpx: GPX-Track
hintAuthorOnly: Nur du, als Autor:in dieser Fahrt, kannst die Originaldatei herunterladen.
hintOriginal: Dies ist die Originaldatei, wie sie auf den Server hochgeladen wurde, und kann mit anderen Werkzeugen verwendet werden.
hintAuthorOnlyOthers: Nur der:die Autor:in dieser Fahrt kann die Originaldatei herunterladen.
comments:
title: Kommentare
post: Kommentar abschicken
empty: Bisher hat niemand diese Fahrt kommentiert.

View file

@ -12,6 +12,7 @@ general:
show: Show
edit: Edit
save: Save
delete: Delete
copied: Copied.
copyError: Failed to copy.
@ -226,3 +227,51 @@ SettingsPage:
generate: Generate new API key
TrackPage:
downloadFailed: Download failed
downloadError: The track probably has not been imported correctly or recently enough. Please ask your administrator for assistance.
processing: Track data is still being processed, please reload page in a while.
processingError: The processing of this track failed, please ask your site administrator for help in debugging the issue.
description: Description
mapSettings:
title: Map settings
showTrack: Show track
gpsTrack: GPS track
snappedTrack: Snapped to road
points: Points
confirmedPoints: Confirmed
allPoints: All measurements
side: Side (for color)
overtakerSide: Overtaker (Left)
stationarySide: Stationary (Right)
details:
visibility: Visibility
originalFileName: Original Filename
uploadedWith: Uploaded with
duration: Duration
uploadedDate: Uploaded on
recordedDate: Recorded on
numEvents: Confirmed events
length: Length
processingStatus: Processing
actions:
edit: Edit track
download: Download
original: Original
gpx: Track (GPX)
hintAuthorOnly: Only you, the author of this track, can download the original file.
hintOriginal: This is the file as it was uploaded to the server, without modifications, and it can be used with other tools.
hintAuthorOnlyOthers: Only the author of this track can download the original file.
comments:
title: Comments
post: Post comment
empty: Nobody commented... yet