diff --git a/src/@types/language.ts b/src/@types/language.ts new file mode 100644 index 0000000..d8ffda1 --- /dev/null +++ b/src/@types/language.ts @@ -0,0 +1 @@ +export type Language = 'en' | 'de' diff --git a/src/@types/upload.ts b/src/@types/upload.ts index 9b5a3aa..6bf1f15 100644 --- a/src/@types/upload.ts +++ b/src/@types/upload.ts @@ -5,14 +5,3 @@ export interface UploadedFile { href: string size: number } - -export type UploadContextType = { - uploadedFiles: Array - setUploadedFiles: (uploadedFiles: Array) => void - mergedFile: UploadedFile | null - setMergedFile: (mergedFile: UploadedFile | null) => void - uploadFile: (fileToUpload: File) => void - removeUploadedFile: (fileToRemove: UploadedFile) => void - mergeFiles: () => void - isLoading: Boolean -} diff --git a/src/App.tsx b/src/App.tsx index ab47af9..7921deb 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,15 +1,15 @@ import { BrowserRouter as Router, Route, Routes } from 'react-router-dom' -import { FeedbackProvider } from './components/services/context/FeedbackContext.tsx' import Navbar from './components/layout/Navbar' import Footer from './components/layout/Footer' import Feedback from './components/layout/Feedback' -import NotFoundScreen from './pages/NotFoundScreen.tsx' -import AboutScreen from './pages/AboutScreen.tsx' -import MergeScreen from './pages/MergeScreen.tsx' -import TrackScreen from './pages/TrackScreen.tsx' +import NotFoundScreen from './pages/NotFoundScreen' +import AboutScreen from './pages/AboutScreen' +import MergeScreen from './pages/MergeScreen' +import TrackScreen from './pages/TrackScreen' +import Providers from './components/services/providers' const App = () => ( - +
@@ -26,7 +26,7 @@ const App = () => (
-
+ ) export default App diff --git a/src/components/layout/Feedback.tsx b/src/components/layout/Feedback.tsx index cb38ad3..73f5bfc 100644 --- a/src/components/layout/Feedback.tsx +++ b/src/components/layout/Feedback.tsx @@ -1,8 +1,10 @@ import { Alert, Button } from 'react-daisyui' -import { useFeedbackContext } from '../../hooks/useFeedbackContext.ts' +import { useFeedbackContext } from '../../hooks/useFeedbackContext' +import useLanguage from '../../hooks/useLanguage' function Feedback() { const { state, removeFeedback } = useFeedbackContext() + const { getMessage } = useLanguage() const handleRemoveFeedback = () => { removeFeedback() @@ -29,9 +31,9 @@ function Feedback() { } > - {state.message} + {getMessage(state.messageKey)} ) diff --git a/src/components/layout/LanguageSelector.tsx b/src/components/layout/LanguageSelector.tsx new file mode 100644 index 0000000..823da7d --- /dev/null +++ b/src/components/layout/LanguageSelector.tsx @@ -0,0 +1,19 @@ +import useLanguage from '../../hooks/useLanguage' +import { Language } from '../../@types/language' + +const LanguageSelector = () => { + const { currentLanguage, changeLanguage } = useLanguage() + + return ( + + ) +} + +export default LanguageSelector diff --git a/src/components/layout/Navbar.tsx b/src/components/layout/Navbar.tsx index e4b9f85..77c77ac 100644 --- a/src/components/layout/Navbar.tsx +++ b/src/components/layout/Navbar.tsx @@ -2,6 +2,8 @@ import React from 'react' import { Link } from 'react-router-dom' import { FaGlobe } from 'react-icons/fa' import ThemeSwitcher from './ThemeSwitcher' +import LanguageSelector from './LanguageSelector' +import useLanguage from '../../hooks/useLanguage' type Props = { title?: string @@ -9,29 +11,34 @@ type Props = { const DEFAULT_TITLE: string = 'GPS-Tools' -const Navbar: React.FC = ({ title = DEFAULT_TITLE }) => ( - -) + + ) +} export default Navbar diff --git a/src/components/merge/Intro.tsx b/src/components/merge/Intro.tsx index d6e49cb..3fc5fff 100644 --- a/src/components/merge/Intro.tsx +++ b/src/components/merge/Intro.tsx @@ -1,19 +1,21 @@ -import { useUploadContext } from '../../hooks/useUploadContext.ts' +import useLanguage from '../../hooks/useLanguage' +import { useUploadContext } from '../../hooks/useUploadContext' const Intro = () => { const { mergedFile } = useUploadContext() + const { getMessage } = useLanguage() return ( <> {mergedFile === null && (
- This app allows you to: + {getMessage('intro_header')}
    -
  • Upload a single or multiple files in GPX- or FIT-format.
  • -
  • Merge them into a single file.
  • -
  • Visualize the merged file.
  • -
  • Add, change and remove waypoints.
  • -
  • Download the merged file as GPX or TCX.
  • + {(getMessage('intro_description_list') as Array).map( + (item, index) => ( +
  • {item}
  • + ) + )}
)} diff --git a/src/components/merge/MergeFiles.tsx b/src/components/merge/MergeFiles.tsx index 0406eaa..6add99f 100644 --- a/src/components/merge/MergeFiles.tsx +++ b/src/components/merge/MergeFiles.tsx @@ -5,12 +5,14 @@ import { DropResult, } from '@hello-pangea/dnd' import { FaEllipsisVertical, FaTrashCan } from 'react-icons/fa6' -import { useUploadContext } from '../../hooks/useUploadContext.ts' +import { useUploadContext } from '../../hooks/useUploadContext' import { UploadedFile } from '../../@types/upload' +import useLanguage from '../../hooks/useLanguage' const MergeFiles = () => { const { uploadedFiles, setUploadedFiles, removeUploadedFile, mergeFiles } = useUploadContext() + const { getMessage } = useLanguage() const reorder = ( list: Array, @@ -73,8 +75,8 @@ const MergeFiles = () => {
{uploadedFiles.length > 0 && ( )}
diff --git a/src/components/merge/UploadForm.tsx b/src/components/merge/UploadForm.tsx index d945e0c..fa098ee 100644 --- a/src/components/merge/UploadForm.tsx +++ b/src/components/merge/UploadForm.tsx @@ -2,7 +2,8 @@ import React, { useCallback, useMemo } from 'react' import { DropzoneRootProps, useDropzone } from 'react-dropzone' import { FiUpload } from 'react-icons/fi' import { Loading } from 'react-daisyui' -import { useUploadContext } from '../../hooks/useUploadContext.ts' +import { useUploadContext } from '../../hooks/useUploadContext' +import useLanguage from '../../hooks/useLanguage' const baseStyle = { flex: 1, @@ -34,6 +35,7 @@ const rejectStyle = { const UploadForm: React.FC = () => { const { uploadFile, mergedFile, isLoading } = useUploadContext() + const { getMessage } = useLanguage() const onDrop = useCallback((acceptedFiles: Array) => { acceptedFiles.map((file) => uploadFile(file)) @@ -73,13 +75,13 @@ const UploadForm: React.FC = () => { {isDragActive ? (

- Drop the - files here ... + {' '} + {getMessage('uploader_drop')}

) : (

- Drag 'n' - drop GPX- or FIT-files, or click to select files. + {' '} + {getMessage('uploader_description')}

)} diff --git a/src/components/services/providers/feedback/FeedbackContext.ts b/src/components/services/providers/feedback/FeedbackContext.ts new file mode 100644 index 0000000..6da3450 --- /dev/null +++ b/src/components/services/providers/feedback/FeedbackContext.ts @@ -0,0 +1,12 @@ +import { createContext } from 'react' +import { FeedbackState } from './FeedbackReducer' + +type FeedbackContextType = { + state: FeedbackState + setError: (message: string) => void + removeFeedback: () => void +} + +const FeedbackContext = createContext(null) + +export default FeedbackContext diff --git a/src/components/services/context/FeedbackContext.tsx b/src/components/services/providers/feedback/FeedbackProvider.tsx similarity index 64% rename from src/components/services/context/FeedbackContext.tsx rename to src/components/services/providers/feedback/FeedbackProvider.tsx index 50c7d21..3787968 100644 --- a/src/components/services/context/FeedbackContext.tsx +++ b/src/components/services/providers/feedback/FeedbackProvider.tsx @@ -1,17 +1,9 @@ -import React, { createContext, useReducer } from 'react' +import React, { useReducer } from 'react' import feedbackReducer, { - FeedbackState, REMOVE_FEEDBACK, SET_FEEDBACK, } from './FeedbackReducer' - -type FeedbackContextType = { - state: FeedbackState - setError: (message: string) => void - removeFeedback: () => void -} - -const FeedbackContext = createContext(null) +import FeedbackContext from './FeedbackContext' export type FeedbackProviderType = { children: React.ReactNode @@ -21,13 +13,12 @@ export const FeedbackProvider: React.FC = ({ children, }) => { const initialState = null - const [state, dispatchFeedback] = useReducer(feedbackReducer, initialState) - const setError = (message: string) => { + const setError = (messageKey: string) => { dispatchFeedback({ type: SET_FEEDBACK, - payload: { type: 'error', message }, + payload: { type: 'error', messageKey }, }) setTimeout(() => dispatchFeedback({ type: REMOVE_FEEDBACK }), 5000) @@ -43,5 +34,3 @@ export const FeedbackProvider: React.FC = ({ ) } - -export default FeedbackContext diff --git a/src/components/services/context/FeedbackReducer.ts b/src/components/services/providers/feedback/FeedbackReducer.ts similarity index 89% rename from src/components/services/context/FeedbackReducer.ts rename to src/components/services/providers/feedback/FeedbackReducer.ts index 1779140..ffb03be 100644 --- a/src/components/services/context/FeedbackReducer.ts +++ b/src/components/services/providers/feedback/FeedbackReducer.ts @@ -1,5 +1,5 @@ type FeedbackType = 'info' | 'success' | 'warning' | 'error' | undefined -export type FeedbackState = { type: FeedbackType; message: string } | null +export type FeedbackState = { type: FeedbackType; messageKey: string } | null export const SET_FEEDBACK = 'SET_FEEDBACK' export const REMOVE_FEEDBACK = 'REMOVE_FEEDBACK' diff --git a/src/components/services/providers/index.tsx b/src/components/services/providers/index.tsx new file mode 100644 index 0000000..08a34ed --- /dev/null +++ b/src/components/services/providers/index.tsx @@ -0,0 +1,13 @@ +import { PropsWithChildren } from 'react' +import { LanguageProvider } from './language/LanguageProvider' +import { FeedbackProvider } from './feedback/FeedbackProvider' + +const Providers = ({ children }: PropsWithChildren) => { + return ( + + {children} + + ) +} + +export default Providers diff --git a/src/components/services/providers/language/LanguageContext.ts b/src/components/services/providers/language/LanguageContext.ts new file mode 100644 index 0000000..1bf5251 --- /dev/null +++ b/src/components/services/providers/language/LanguageContext.ts @@ -0,0 +1,14 @@ +import { createContext } from 'react' +import { Language } from '../../../../@types/language' + +type LanguageContextType = { + currentLanguage: Language + changeLanguage: (language: Language) => void + getMessage: (labelId: string) => string | Array +} + +const LanguageContext = createContext( + {} as LanguageContextType +) + +export default LanguageContext diff --git a/src/components/services/providers/language/LanguageProvider.tsx b/src/components/services/providers/language/LanguageProvider.tsx new file mode 100644 index 0000000..4e19587 --- /dev/null +++ b/src/components/services/providers/language/LanguageProvider.tsx @@ -0,0 +1,40 @@ +import React, { ReactNode, useState } from 'react' + +import LanguageContext from './LanguageContext' +import en from './data/en.json' +import de from './data/de.json' +import { Language } from '../../../../@types/language' + +export type LanguageProviderProps = { + language: Language + children: ReactNode +} + +export const LanguageProvider: React.FC = ({ + language, + children, +}) => { + const [currentLanguage, setCurrentLanguage] = useState(language) + const changeLanguage = (language: Language) => setCurrentLanguage(language) + + const dictionary: { + [languageKey: string]: { [messageKey: string]: string } + } = { en, de } + + const getMessage = (messageKey: string) => { + const message = dictionary[currentLanguage][messageKey] + if (!message) + throw new Error( + `MessageKey ${messageKey} not found in ${currentLanguage}.json` + ) + return message + } + + return ( + + {children} + + ) +} diff --git a/src/components/services/providers/language/data/de.json b/src/components/services/providers/language/data/de.json new file mode 100644 index 0000000..ad024e3 --- /dev/null +++ b/src/components/services/providers/language/data/de.json @@ -0,0 +1,36 @@ +{ + "contact": "Kontakt", + "about": "Über uns", + "home": "Startseite", + "loading": "Wird geladen...", + "close": "Schließen", + "visualize_file": "Die Datei anzeigen.", + "visualize_files": "Die Dateien zusammenfügen/mergen und anzeigen.", + "button_new_process": "Eine neue Datei bearbeiten.", + "app_description": "Eine Anwendung zum Bearbeiten von GPS-Dateien.", + "technologies_header": "Diese Anwendung entstand, um einige Technologien zu testen. Dies sind:", + "uploader_description": "GPX- oder FIT-Dateien hierher ziehen oder klicken, um die Dateien auszuwählen.", + "uploader_drop": "Dateien hierher ziehen ...", + "intro_header": "Diese Anwendung bietet folgende Funktionalitäten:", + "intro_description_list": [ + "Upload einer oder mehrerer Dateien im GPX- oder FIT-format.", + "Zusammenfassen/Mergen dieser Dateien in eine Datei.", + "Kartendarstellung der zusammengefassten Datei.", + "Hinzufügen, Ändern und Löschen von Wegpunkten.", + "Download der bearbeiteten Datei im GPX– oder TCX-Format." + ], + "download_as": "Download als", + "optimize_waypoints": "Wegpunkte optimieren", + "optimize_waypoints_tooltip": "Wegpunkte, die näher als 500m zum Track liegen, werden auf den nächsten Punkt des Tracks verschoben. Dies erhöht die Kompatibilität mit einigen GPS-Geräten, die Probleme mit der Darstellung von Punkten, die nicht exakt auf dem Track liegen, haben.", + "search_for_waypoints": "Wegpunkte suchen…", + "mute_tooltip": "Track auf der Karte ausblenden (M)", + "error_generic": "Es ist leider etwas schief gelaufen.", + "error_backend_server_na": "Der Backend–Server ist im Augenblick leider nicht verfügbar.", + "error_not_gps_data": "Einige Dateien konnten nicht verarbeitet werden. Waren dies wirklich GPS-Dateien?", + "error_upload_failure": "Die Dateien konnten leider nicht geladen werden.", + "error_download_failure_sn": "Die Datei konnte nicht gefunden werden. Bitte wiederhole den Upload!", + "error_download_failure_pl": "Die Dateien konnten nicht gefunden werden. Bitte wiederhole den Upload!", + "error_delete": "Die Datei konnte leider nicht gelöscht werden.", + "error_loading_track": "Der Track konnte leider nicht geladen werden.", + "error_track_not_found": "Der Track wurde nicht gefunden." +} diff --git a/src/components/services/providers/language/data/en.json b/src/components/services/providers/language/data/en.json new file mode 100644 index 0000000..e741cde --- /dev/null +++ b/src/components/services/providers/language/data/en.json @@ -0,0 +1,36 @@ +{ + "contact": "Contact", + "about": "About", + "home": "Home", + "loading": "Loading...", + "close": "Close", + "visualize_file": "Visualize the file.", + "visualize_files": "Visualize the merged files.", + "button_new_process": "Start working on a new file.", + "app_description": "An app dealing with GPS files.", + "technologies_header": "This app was created to test some technologies. These are:", + "uploader_description": "Drag 'n' drop GPX- or FIT-files, or click to select files.", + "uploader_drop": "Drop the files here ...", + "intro_header": "This app allows you to:", + "intro_description_list": [ + "Upload a single or multiple files in GPX- or FIT-format.", + "Merge them into a single file.", + "Visualize the merged file.", + "Add, change and remove waypoints.", + "Download the merged file as GPX or TCX." + ], + "download_as": "Download as", + "optimize_waypoints": "Optimize Waypoints", + "optimize_waypoints_tooltip": "Waypoints that are closer than 500m to the track will be moved to a point on the track. This can improve readability on some GPS-devices, since these are having problems with points located not directly on the track.", + "search_for_waypoints": "Search for waypoints…", + "mute_tooltip": "Mute track on map (M)", + "error_generic": "Something went wrong. Sorry!", + "error_backend_server_na": "The backend server is not available at the moment. Sorry!", + "error_not_gps_data": "Couldn't process some files. Was it really GPS data?", + "error_upload_failure": "Failed to upload files. Sorry!", + "error_download_failure_sn": "The file could not be found on the server. Please upload again!", + "error_download_failure_pl": "The files could not be found on the server. Please upload again!", + "error_delete": "Failed to delete file. Sorry!", + "error_loading_track": "Failed to load track. Sorry!", + "error_track_not_found": "Track not found." +} diff --git a/src/components/services/providers/upload/UploadContext.tsx b/src/components/services/providers/upload/UploadContext.tsx new file mode 100644 index 0000000..e480ed8 --- /dev/null +++ b/src/components/services/providers/upload/UploadContext.tsx @@ -0,0 +1,17 @@ +import { createContext } from 'react' +import { UploadedFile } from '../../../../@types/upload' + +export type UploadContextType = { + uploadedFiles: Array + setUploadedFiles: (uploadedFiles: Array) => void + mergedFile: UploadedFile | null + setMergedFile: (mergedFile: UploadedFile | null) => void + uploadFile: (fileToUpload: File) => void + removeUploadedFile: (fileToRemove: UploadedFile) => void + mergeFiles: () => void + isLoading: Boolean +} + +export const UploadContext = createContext(null) + +export default UploadContext diff --git a/src/components/services/context/UploadContext.tsx b/src/components/services/providers/upload/UploadProvider.tsx similarity index 72% rename from src/components/services/context/UploadContext.tsx rename to src/components/services/providers/upload/UploadProvider.tsx index 9ba8b33..996106a 100644 --- a/src/components/services/context/UploadContext.tsx +++ b/src/components/services/providers/upload/UploadProvider.tsx @@ -1,10 +1,9 @@ -import React, { createContext, useState } from 'react' +import React, { useState } from 'react' +import { UploadedFile } from '../../../../@types/upload' +import { useFeedbackContext } from '../../../../hooks/useFeedbackContext' import { useNavigate } from 'react-router-dom' -import { UploadContextType, UploadedFile } from '../../../@types/upload' -import API from '../backend/gps-backend-api' -import { useFeedbackContext } from '../../../hooks/useFeedbackContext.ts' - -export const UploadContext = createContext(null) +import API from '../../backend/gps-backend-api' +import UploadContext from './UploadContext' export type UploadProviderType = { children: React.ReactNode @@ -33,11 +32,11 @@ export const UploadProvider: React.FC = ({ children }) => { ) .catch((error) => { if (error.code === 'ERR_NETWORK') { - setError('The backend server is not available at the moment. Sorry!') + setError('error_backend_server_na') } else if (error.code === 'ERR_BAD_REQUEST') { - setError("Couldn't process some files. Was it really GPS data?") + setError('error_not_gps_data') } else { - setError('Failed to upload files. Sorry!') + setError('error_upload_failure') } }) .finally(() => setLoading(false)) @@ -47,11 +46,11 @@ export const UploadProvider: React.FC = ({ children }) => { API.delete('/files/' + file.id) .catch((error) => { if (error.code === 'ERR_NETWORK') { - setError('The backend server is not available at the moment. Sorry!') + setError('error_backend_server_na') } else if (error.code === 'ERR_BAD_REQUEST') { - setError('Something went wrong. Sorry!') + setError('error_generic') } else { - setError('Failed to delete file. Sorry!') + setError('error_delete') } }) .finally(() => { @@ -84,15 +83,11 @@ export const UploadProvider: React.FC = ({ children }) => { }) .catch((error) => { if (error.code === 'ERR_NETWORK') { - setError( - 'The backend server is not available at the moment. Sorry!' - ) + setError('error_backend_server_na') } else if (error.code === 'ERR_BAD_REQUEST') { - const grammaticalNumber = - uploadedFiles.length == 1 ? 'file' : 'files' - setError( - `The ${grammaticalNumber} could not be found on the server. Please upload again!` - ) + uploadedFiles.length == 1 + ? setError('error_download_failure_sn') + : setError('error_download_failure_pl') uploadedFiles.map((file) => removeUploadedFile(file)) } else { setError('Failed to merge files. Sorry!') @@ -118,5 +113,3 @@ export const UploadProvider: React.FC = ({ children }) => { ) } - -export default UploadContext diff --git a/src/components/track/DownloadLink.tsx b/src/components/track/DownloadLink.tsx index 71044c8..37c95fd 100644 --- a/src/components/track/DownloadLink.tsx +++ b/src/components/track/DownloadLink.tsx @@ -1,7 +1,8 @@ import React from 'react' import { FiDownload } from 'react-icons/fi' import { GeoJsonObject } from 'geojson' -import { encodeToBase64 } from '../../utils/tools.ts' +import { encodeToBase64 } from '../../utils/tools' +import useLanguage from '../../hooks/useLanguage' type DownloadLinkProps = { fileId: string @@ -18,6 +19,7 @@ const DownloadLink: React.FC = ({ optimizeWaypoints, geoJson, }) => { + const { getMessage } = useLanguage() const baseUrl = import.meta.env.VITE_BACKEND_BASE_URL return ( = ({ } > - Download as {type.toUpperCase()} + {getMessage('download_as')} {type.toUpperCase()} ) } diff --git a/src/components/track/DraggableMarker.tsx b/src/components/track/DraggableMarker.tsx index 5b8d52f..d079d05 100644 --- a/src/components/track/DraggableMarker.tsx +++ b/src/components/track/DraggableMarker.tsx @@ -2,7 +2,7 @@ import React, { FocusEvent, memo, useMemo, useRef, useState } from 'react' import L, { LatLng } from 'leaflet' import { Marker, Popup } from 'react-leaflet' import { FaTrashCan } from 'react-icons/fa6' -import { PoiType, WayPoint } from '../../@types/gps.ts' +import { PoiType, WayPoint } from '../../@types/gps' import iconShadow from 'leaflet/dist/images/marker-shadow.png' import iconGeneric from '/icons/Generic.svg' import iconSummit from '/icons/Summit.svg' diff --git a/src/components/track/MarkerSearch.tsx b/src/components/track/MarkerSearch.tsx index 2ee1c20..67a9d1d 100644 --- a/src/components/track/MarkerSearch.tsx +++ b/src/components/track/MarkerSearch.tsx @@ -11,7 +11,8 @@ import { Feature, FeatureCollection, Point } from 'geojson' import { WayPoint } from '../../@types/gps' import { v4 as uuidv4 } from 'uuid' import { useMap } from 'react-leaflet' -import { convertOsmToPoiType } from '../../utils/tools.ts' +import { convertOsmToPoiType } from '../../utils/tools' +import useLanguage from '../../hooks/useLanguage' type MarkerSearchProps = { setMarkerPositions: Dispatch> @@ -24,6 +25,7 @@ const MarkerSearch: React.FC = ({ setMarkerPositions }) => { const dropdownDivRef = useRef(null) const inputRef = useRef(null) const map = useMap() + const { getMessage } = useLanguage() useEffect(() => { if (debouncedSearchTerm === undefined || debouncedSearchTerm.length == 0) { @@ -92,7 +94,7 @@ const MarkerSearch: React.FC = ({ setMarkerPositions }) => { className='input input-bordered w-full' value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)} - placeholder='Search for waypoints…' + placeholder={getMessage('search_for_waypoints') as string} tabIndex={0} />
diff --git a/src/components/track/NewMarkerButton.tsx b/src/components/track/NewMarkerButton.tsx index 5db2eed..24f5573 100644 --- a/src/components/track/NewMarkerButton.tsx +++ b/src/components/track/NewMarkerButton.tsx @@ -1,6 +1,6 @@ import React, { Dispatch, SetStateAction } from 'react' import { useMap } from 'react-leaflet' -import { WayPoint } from '../../@types/gps.ts' +import { WayPoint } from '../../@types/gps' import { v4 as uuidv4 } from 'uuid' import { MdAddLocation } from 'react-icons/md' diff --git a/src/components/track/ResetButton.tsx b/src/components/track/ResetButton.tsx index 3f94a49..0c44740 100644 --- a/src/components/track/ResetButton.tsx +++ b/src/components/track/ResetButton.tsx @@ -1,10 +1,13 @@ import { useNavigate } from 'react-router-dom' +import useLanguage from '../../hooks/useLanguage' const ResetButton = () => { const navigateTo = useNavigate() + const { getMessage } = useLanguage() + return ( ) } diff --git a/src/components/track/TrackHeader.tsx b/src/components/track/TrackHeader.tsx index 7c254b5..8196832 100644 --- a/src/components/track/TrackHeader.tsx +++ b/src/components/track/TrackHeader.tsx @@ -1,10 +1,11 @@ -import DownloadLink from './DownloadLink.tsx' +import DownloadLink from './DownloadLink' import { FaCircleInfo, FaPenToSquare } from 'react-icons/fa6' import ContentEditable, { ContentEditableEvent } from 'react-contenteditable' import React, { useMemo, useRef, useState } from 'react' -import { WayPoint } from '../../@types/gps.ts' -import { generateGeoJson, sanitizeFilename } from '../../utils/tools.ts' -import ResetButton from './ResetButton.tsx' +import { WayPoint } from '../../@types/gps' +import { generateGeoJson, sanitizeFilename } from '../../utils/tools' +import ResetButton from './ResetButton' +import useLanguage from '../../hooks/useLanguage' type TrackHeaderProps = { trackId: string @@ -22,6 +23,7 @@ const TrackHeader: React.FC = ({ const [optimizeWaypoints, setOptimizeWaypoints] = useState(false) const tracknameInputFieldRef: React.RefObject = useRef(null) const tracknameRef = useRef('') + const { getMessage } = useLanguage() const markerGeoJson = useMemo( () => generateGeoJson(markerPositions), [markerPositions] @@ -72,12 +74,12 @@ const TrackHeader: React.FC = ({ className='w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600' />  
diff --git a/src/components/track/VisualizeTrack.tsx b/src/components/track/VisualizeTrack.tsx index 4476cbf..1397112 100644 --- a/src/components/track/VisualizeTrack.tsx +++ b/src/components/track/VisualizeTrack.tsx @@ -16,10 +16,11 @@ import 'leaflet/dist/leaflet.css' import Control from 'react-leaflet-custom-control' import { FaEye, FaEyeSlash } from 'react-icons/fa6' import { PoiType, WayPoint } from '../../@types/gps' -import DraggableMarker from './DraggableMarker.tsx' -import FitBoundsButton from './FitBoundsButton.tsx' -import NewMarkerButton from './NewMarkerButton.tsx' -import MarkerSearch from './MarkerSearch.tsx' +import DraggableMarker from './DraggableMarker' +import FitBoundsButton from './FitBoundsButton' +import NewMarkerButton from './NewMarkerButton' +import MarkerSearch from './MarkerSearch' +import useLanguage from '../../hooks/useLanguage' type VisualizeTrackProps = { bounds: LatLngBoundsExpression @@ -36,6 +37,7 @@ const VisualizeTrack: React.FC = ({ }) => { const [showPolyline, setShowPolyline] = useState(true) const polylineRef = createRef() + const { getMessage } = useLanguage() const changeMarkerPosition = useCallback( (markerId: string, newPosition: LatLng) => { @@ -120,7 +122,10 @@ const VisualizeTrack: React.FC = ({ return ( <>
-
+
{showPolyline ? ( { const context = useContext(FeedbackContext) diff --git a/src/hooks/useLanguage.ts b/src/hooks/useLanguage.ts new file mode 100644 index 0000000..4528efd --- /dev/null +++ b/src/hooks/useLanguage.ts @@ -0,0 +1,12 @@ +import { useContext } from 'react' +import LanguageContext from '../components/services/providers/language/LanguageContext' + +const useLanguage = () => { + const context = useContext(LanguageContext) + if (!context) + throw new Error('useLanguage must be used within a LanguageProvider') + + return context +} + +export default useLanguage diff --git a/src/hooks/useUploadContext.ts b/src/hooks/useUploadContext.ts index ca4fe2f..a0bc149 100644 --- a/src/hooks/useUploadContext.ts +++ b/src/hooks/useUploadContext.ts @@ -1,5 +1,5 @@ import { useContext } from 'react' -import UploadContext from '../components/services/context/UploadContext.tsx' +import UploadContext from '../components/services/providers/upload/UploadContext' export const useUploadContext = () => { const context = useContext(UploadContext) diff --git a/src/main.tsx b/src/main.tsx index 3d7150d..4a1b150 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,10 +1,10 @@ import React from 'react' import ReactDOM from 'react-dom/client' -import App from './App.tsx' +import App from './App' import './index.css' ReactDOM.createRoot(document.getElementById('root')!).render( - , + ) diff --git a/src/pages/AboutScreen.test.tsx b/src/pages/AboutScreen.test.tsx index cd2a6f6..f6e3448 100644 --- a/src/pages/AboutScreen.test.tsx +++ b/src/pages/AboutScreen.test.tsx @@ -1,6 +1,9 @@ import { describe, expect, it, vi } from 'vitest' import { render, screen } from '@testing-library/react' -import AboutScreen from './AboutScreen.tsx' +import AboutScreen from './AboutScreen' +import React from 'react' +import { LanguageProvider } from '../components/services/providers/language/LanguageProvider' +import { Language } from '../@types/language' const mocks = vi.hoisted(() => ({ get: vi.fn() })) @@ -18,20 +21,34 @@ vi.mock('axios', async (importActual) => { }) describe('About Page', () => { + const renderWithLanguage = ( + children: React.ReactNode, + language: Language = 'en' + ) => { + render({children}) + } + it('load page', () => { - render() + renderWithLanguage() expect( screen.getByText('An app dealing with GPS files.') ).toBeInTheDocument() }) + it('load page german page', () => { + renderWithLanguage(, 'de') + expect( + screen.getByText('Eine Anwendung zum Bearbeiten von GPS-Dateien.') + ).toBeInTheDocument() + }) + it('show version of backend', async () => { const version = 'v1.2.3' mocks.get.mockResolvedValueOnce({ data: { app: version, git: 'githash' }, }) - render() + renderWithLanguage() expect(await screen.findByText(version)).toBeInTheDocument() expect(mocks.get).toHaveBeenCalled() diff --git a/src/pages/AboutScreen.tsx b/src/pages/AboutScreen.tsx index 6df9b05..e49c47e 100644 --- a/src/pages/AboutScreen.tsx +++ b/src/pages/AboutScreen.tsx @@ -2,10 +2,13 @@ import { useEffect, useState } from 'react' import { BackendVersion } from '../@types/common' import { AxiosResponse } from 'axios' import API from '../components/services/backend/gps-backend-api' +import useLanguage from '../hooks/useLanguage' const NO_VERSION = { app: 'N/A', git: 'N/A' } const AboutScreen = () => { + const { getMessage } = useLanguage() + const [backendVersion, setBackendVersion] = useState(NO_VERSION) @@ -20,13 +23,17 @@ const AboutScreen = () => { } useEffect(() => { - (async () => { await fetchBackendVersion() })() + ;(async () => { + await fetchBackendVersion() + })() }, []) return ( <>

GPS-Tools

-

An app dealing with GPS files.

+

+ {getMessage('app_description')} +

{ {backendVersion.app}

-

- This app was created to test some technologies. These are: -

+

{getMessage('technologies_header')}

- Contact: gps minus tools ät devshred dot org + {getMessage('contact')}: gps minus tools ät tigerflanke dot de

) diff --git a/src/pages/HomeScreen.test.tsx b/src/pages/HomeScreen.test.tsx index 2383969..8031837 100644 --- a/src/pages/HomeScreen.test.tsx +++ b/src/pages/HomeScreen.test.tsx @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' import { render, screen } from '@testing-library/react' -import HomeScreen from './HomeScreen.tsx' +import HomeScreen from './HomeScreen' describe('Home Page', () => { it('load page', () => { diff --git a/src/pages/MergeScreen.tsx b/src/pages/MergeScreen.tsx index 0a9b18b..01b1ddb 100644 --- a/src/pages/MergeScreen.tsx +++ b/src/pages/MergeScreen.tsx @@ -1,7 +1,7 @@ import UploadForm from '../components/merge/UploadForm' import MergeFiles from '../components/merge/MergeFiles' import Intro from '../components/merge/Intro' -import { UploadProvider } from '../components/services/context/UploadContext.tsx' +import { UploadProvider } from '../components/services/providers/upload/UploadProvider' const MergeScreen = () => (
diff --git a/src/pages/TrackScreen.tsx b/src/pages/TrackScreen.tsx index 5367d91..b66bb3e 100644 --- a/src/pages/TrackScreen.tsx +++ b/src/pages/TrackScreen.tsx @@ -2,19 +2,21 @@ import { useParams } from 'react-router-dom' import { useEffect, useState } from 'react' import API from '../components/services/backend/gps-backend-api' import { FeatureCollection, LineString, Point } from 'geojson' -import { PoiType, WayPoint } from '../@types/gps.ts' +import { PoiType, WayPoint } from '../@types/gps' import { v4 as uuidv4 } from 'uuid' import { LatLngBoundsExpression, LatLngExpression, LatLngTuple } from 'leaflet' -import { sanitizeFilename } from '../utils/tools.ts' -import VisualizeTrack from '../components/track/VisualizeTrack.tsx' -import TrackHeader from '../components/track/TrackHeader.tsx' -import ResetButton from '../components/track/ResetButton.tsx' -import { useFeedbackContext } from '../hooks/useFeedbackContext.ts' +import { sanitizeFilename } from '../utils/tools' +import VisualizeTrack from '../components/track/VisualizeTrack' +import TrackHeader from '../components/track/TrackHeader' +import ResetButton from '../components/track/ResetButton' +import { useFeedbackContext } from '../hooks/useFeedbackContext' +import useLanguage from '../hooks/useLanguage' const TrackScreen = () => { const { id: trackId } = useParams() const { setError } = useFeedbackContext() + const { getMessage } = useLanguage() const [isLoading, setIsLoading] = useState(true) const [trackname, setTrackname] = useState('') @@ -90,9 +92,9 @@ const TrackScreen = () => { }) .catch((error) => { if (error.response?.status === 404) { - setError('Track not found.') + setError('error_track_not_found') } else { - setError('Failed to load track. Sorry!') + setError('error_loading_track') } setIsLoading(false) }) @@ -102,7 +104,7 @@ const TrackScreen = () => { <>

GPS-Tool

{isLoading ? ( -

Loading...

+

{getMessage('loading')}

) : positions.length > 0 && trackId !== undefined ? ( <> { ) : ( <> -

Track not found.

+

{getMessage('error_track_not_found')}

)} diff --git a/src/utils/tools.ts b/src/utils/tools.ts index 43bb346..4100223 100644 --- a/src/utils/tools.ts +++ b/src/utils/tools.ts @@ -1,5 +1,5 @@ import DOMPurify from 'dompurify' -import { PoiType, WayPoint } from '../@types/gps.ts' +import { PoiType, WayPoint } from '../@types/gps' import { Feature, FeatureCollection } from 'geojson' export const sanitizeFilename = (input: string) => {