Skip to content

Commit

Permalink
feat(i18n): language support of German and English
Browse files Browse the repository at this point in the history
  • Loading branch information
devshred committed May 6, 2024
1 parent edaf1f4 commit 9f34d66
Show file tree
Hide file tree
Showing 36 changed files with 362 additions and 138 deletions.
1 change: 1 addition & 0 deletions src/@types/language.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type Language = 'en' | 'de'
11 changes: 0 additions & 11 deletions src/@types/upload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,3 @@ export interface UploadedFile {
href: string
size: number
}

export type UploadContextType = {
uploadedFiles: Array<UploadedFile>
setUploadedFiles: (uploadedFiles: Array<UploadedFile>) => void
mergedFile: UploadedFile | null
setMergedFile: (mergedFile: UploadedFile | null) => void
uploadFile: (fileToUpload: File) => void
removeUploadedFile: (fileToRemove: UploadedFile) => void
mergeFiles: () => void
isLoading: Boolean
}
14 changes: 7 additions & 7 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -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 = () => (
<FeedbackProvider>
<Providers>
<Router>
<div className='flex flex-col justify-between h-screen'>
<Navbar />
Expand All @@ -26,7 +26,7 @@ const App = () => (
<Footer />
</div>
</Router>
</FeedbackProvider>
</Providers>
)

export default App
8 changes: 5 additions & 3 deletions src/components/layout/Feedback.tsx
Original file line number Diff line number Diff line change
@@ -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()
Expand All @@ -29,9 +31,9 @@ function Feedback() {
</svg>
}
>
<span>{state.message}</span>
<span>{getMessage(state.messageKey)}</span>
<Button size='sm' onClick={() => handleRemoveFeedback()}>
Close
{getMessage('close')}
</Button>
</Alert>
)
Expand Down
19 changes: 19 additions & 0 deletions src/components/layout/LanguageSelector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import useLanguage from '../../hooks/useLanguage'
import { Language } from '../../@types/language'

const LanguageSelector = () => {
const { currentLanguage, changeLanguage } = useLanguage()

return (
<select
className='select select-bordered select-sm w-min'
defaultValue={currentLanguage}
onChange={(e) => changeLanguage(e.target.value as Language)}
>
<option value='en'>EN</option>
<option value='de'>DE</option>
</select>
)
}

export default LanguageSelector
47 changes: 27 additions & 20 deletions src/components/layout/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,43 @@ 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
}

const DEFAULT_TITLE: string = 'GPS-Tools'

const Navbar: React.FC<Props> = ({ title = DEFAULT_TITLE }) => (
<nav className='navbar mb-12 shadow-lg base-300 text-base-content'>
<div className='container mx-auto'>
<div className='flex-none px-2 mx-2'>
<FaGlobe className='inline pr-2 text-3xl' />
<Link to='/' className='text-lg font-bold align-middle'>
{title}
</Link>
</div>
const Navbar: React.FC<Props> = ({ title = DEFAULT_TITLE }) => {
const { getMessage } = useLanguage()

<div className='flex-1 px-2 mx-2'>
<div className='flex justify-end'>
<Link to='/' className='btn btn-ghost btn-sm rounded-btn'>
Home
</Link>
<Link to='/about' className='btn btn-ghost btn-sm rounded-btn'>
About
return (
<nav className='navbar mb-12 shadow-lg base-300 text-base-content'>
<div className='container mx-auto'>
<div className='flex-none px-2 mx-2'>
<FaGlobe className='inline pr-2 text-3xl' />
<Link to='/' className='text-lg font-bold align-middle'>
{title}
</Link>
<ThemeSwitcher />
</div>

<div className='flex-1 px-2 mx-2'>
<div className='flex justify-end'>
<Link to='/' className='btn btn-ghost btn-sm rounded-btn'>
{getMessage('home')}
</Link>
<Link to='/about' className='btn btn-ghost btn-sm rounded-btn'>
{getMessage('about')}
</Link>
<LanguageSelector />
<ThemeSwitcher />
</div>
</div>
</div>
</div>
</nav>
)
</nav>
)
}

export default Navbar
16 changes: 9 additions & 7 deletions src/components/merge/Intro.tsx
Original file line number Diff line number Diff line change
@@ -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 && (
<div>
This app allows you to:
{getMessage('intro_header')}
<ul className='list-disc pl-5'>
<li>Upload a single or multiple files in GPX- or FIT-format.</li>
<li>Merge them into a single file.</li>
<li>Visualize the merged file.</li>
<li>Add, change and remove waypoints.</li>
<li>Download the merged file as GPX or TCX.</li>
{(getMessage('intro_description_list') as Array<string>).map(
(item, index) => (
<li key={index}>{item}</li>
)
)}
</ul>
</div>
)}
Expand Down
8 changes: 5 additions & 3 deletions src/components/merge/MergeFiles.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<UploadedFile>,
Expand Down Expand Up @@ -73,8 +75,8 @@ const MergeFiles = () => {
<div className='mt-7'>
{uploadedFiles.length > 0 && (
<button className='btn btn-active' onClick={mergeFiles}>
{uploadedFiles.length == 1 && 'Visualize'}
{uploadedFiles.length > 1 && 'Merge & Visualize'}
{uploadedFiles.length == 1 && getMessage('visualize_file')}
{uploadedFiles.length > 1 && getMessage('visualize_files')}
</button>
)}
</div>
Expand Down
12 changes: 7 additions & 5 deletions src/components/merge/UploadForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -34,6 +35,7 @@ const rejectStyle = {

const UploadForm: React.FC = () => {
const { uploadFile, mergedFile, isLoading } = useUploadContext()
const { getMessage } = useLanguage()

const onDrop = useCallback((acceptedFiles: Array<File>) => {
acceptedFiles.map((file) => uploadFile(file))
Expand Down Expand Up @@ -73,13 +75,13 @@ const UploadForm: React.FC = () => {
<input {...getInputProps()} />
{isDragActive ? (
<p>
<FiUpload className='inline relative bottom-0.5' /> Drop the
files here ...
<FiUpload className='inline relative bottom-0.5' />{' '}
{getMessage('uploader_drop')}
</p>
) : (
<p>
<FiUpload className='inline relative bottom-0.5' /> Drag 'n'
drop GPX- or FIT-files, or click to select files.
<FiUpload className='inline relative bottom-0.5' />{' '}
{getMessage('uploader_description')}
</p>
)}
</div>
Expand Down
12 changes: 12 additions & 0 deletions src/components/services/providers/feedback/FeedbackContext.ts
Original file line number Diff line number Diff line change
@@ -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<FeedbackContextType | null>(null)

export default FeedbackContext
Original file line number Diff line number Diff line change
@@ -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<FeedbackContextType | null>(null)
import FeedbackContext from './FeedbackContext'

export type FeedbackProviderType = {
children: React.ReactNode
Expand All @@ -21,13 +13,12 @@ export const FeedbackProvider: React.FC<FeedbackProviderType> = ({
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)
Expand All @@ -43,5 +34,3 @@ export const FeedbackProvider: React.FC<FeedbackProviderType> = ({
</FeedbackContext.Provider>
)
}

export default FeedbackContext
Original file line number Diff line number Diff line change
@@ -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'
Expand Down
13 changes: 13 additions & 0 deletions src/components/services/providers/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { PropsWithChildren } from 'react'
import { LanguageProvider } from './language/LanguageProvider'
import { FeedbackProvider } from './feedback/FeedbackProvider'

const Providers = ({ children }: PropsWithChildren) => {
return (
<LanguageProvider language='en'>
<FeedbackProvider>{children} </FeedbackProvider>
</LanguageProvider>
)
}

export default Providers
14 changes: 14 additions & 0 deletions src/components/services/providers/language/LanguageContext.ts
Original file line number Diff line number Diff line change
@@ -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<string>
}

const LanguageContext = createContext<LanguageContextType>(
{} as LanguageContextType
)

export default LanguageContext
40 changes: 40 additions & 0 deletions src/components/services/providers/language/LanguageProvider.tsx
Original file line number Diff line number Diff line change
@@ -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<LanguageProviderProps> = ({
language,
children,
}) => {
const [currentLanguage, setCurrentLanguage] = useState<Language>(language)
const changeLanguage = (language: Language) => setCurrentLanguage(language)

const dictionary: {
[languageKey: string]: { [messageKey: string]: string }
} = { en, de }

Check failure on line 22 in src/components/services/providers/language/LanguageProvider.tsx

View workflow job for this annotation

GitHub Actions / Release

Type '{ contact: string; about: string; home: string; loading: string; close: string; visualize_file: string; visualize_files: string; button_new_process: string; app_description: string; technologies_header: string; ... 17 more ...; error_track_not_found: string; }' is not assignable to type '{ [messageKey: string]: string; }'.

Check failure on line 22 in src/components/services/providers/language/LanguageProvider.tsx

View workflow job for this annotation

GitHub Actions / Release

Type '{ contact: string; about: string; home: string; loading: string; close: string; visualize_file: string; visualize_files: string; button_new_process: string; app_description: string; technologies_header: string; ... 17 more ...; error_track_not_found: string; }' is not assignable to type '{ [messageKey: string]: string; }'.

const getMessage = (messageKey: string) => {
const message = dictionary[currentLanguage][messageKey]
if (!message)
throw new Error(
`MessageKey ${messageKey} not found in ${currentLanguage}.json`
)
return message
}

return (
<LanguageContext.Provider
value={{ currentLanguage, changeLanguage, getMessage }}
>
{children}
</LanguageContext.Provider>
)
}
Loading

0 comments on commit 9f34d66

Please sign in to comment.