diff --git a/.gitignore b/.gitignore index 2938463..1331fcd 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,9 @@ dist __pycache__ package-lock.json .parcel-cache +.pnpm-store/ +pnpm-lock.yaml +.pytest_cache/ *.old data/*.db* diff --git a/Dockerfile b/Dockerfile index 4ce9fa5..095aa68 100644 --- a/Dockerfile +++ b/Dockerfile @@ -32,6 +32,7 @@ ENV DATA_PATH=/data ENV JETLOG_PORT=3000 ENV TOKEN_DURATION=7 ENV ENABLE_EXTERNAL_APIS=true +ENV DEFAULT_LANGUAGE=en ENV USE_IPV6=false RUN mkdir -p ${APP_PATH} diff --git a/README.md b/README.md index 490944d..44a8ef0 100644 --- a/README.md +++ b/README.md @@ -40,10 +40,11 @@ services: image: pbogre/jetlog:latest volumes: - /your/data/path:/data - environment: - JETLOG_PORT: 3000 # optional, default is 3000 - SECRET_KEY: yourLongAndRandomStringOfCharacters123! - restart: unless-stopped + environment: + JETLOG_PORT: 3000 # optional, default is 3000 + SECRET_KEY: yourLongAndRandomStringOfCharacters123! + DEFAULT_LANGUAGE: en # optional default UI language: en, zh-CN, zh-TW, ja, ko, fr, de, es + restart: unless-stopped ports: - 3000:3000 ``` diff --git a/client/api.ts b/client/api.ts index 0d17656..9510817 100644 --- a/client/api.ts +++ b/client/api.ts @@ -3,10 +3,15 @@ import TokenStorage from './storage/tokenStorage'; import { showToast } from './components/Toast'; const config = await fetch('./config').then((response) => response.json()) - .catch(() => ({ BASE_URL: '/', ENABLE_EXTERNAL_APIS: true })); + .catch(() => ({ + BASE_URL: '/', + ENABLE_EXTERNAL_APIS: true, + DEFAULT_LANGUAGE: undefined, + })); export const BASE_URL = config.BASE_URL == '/' ? '' : config.BASE_URL; export const ENABLE_EXTERNAL_APIS = config.ENABLE_EXTERNAL_APIS; +export const DEFAULT_LANGUAGE: string | undefined = config.DEFAULT_LANGUAGE ?? undefined; // TODO improve this because there's a lot of repetition (get, post, delete are pretty much exactly the same) // perhaps one method for each endpoint? i.e. API.getFlights(), ... diff --git a/client/components/Toast.tsx b/client/components/Toast.tsx index 76a3fec..7e63202 100644 --- a/client/components/Toast.tsx +++ b/client/components/Toast.tsx @@ -1,4 +1,5 @@ import React, { createContext, useContext, useState, useCallback, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; export type ToastType = 'success' | 'error' | 'info'; @@ -38,6 +39,8 @@ interface ToastItemProps { } function ToastItem({ toast, onRemove }: ToastItemProps) { + const { t } = useTranslation(); + useEffect(() => { const timer = setTimeout(() => { onRemove(toast.id); @@ -67,7 +70,7 @@ function ToastItem({ toast, onRemove }: ToastItemProps) { diff --git a/client/components/flights/FlightDetail.tsx b/client/components/flights/FlightDetail.tsx index 1baf558..9e9080b 100644 --- a/client/components/flights/FlightDetail.tsx +++ b/client/components/flights/FlightDetail.tsx @@ -2,6 +2,7 @@ import { useState, type FormEvent, lazy, Suspense } from 'react' import { Link, useNavigate } from 'react-router-dom' import { ArrowLeft, ArrowRight, Pencil, Trash2, X, Check } from 'lucide-react' import { useQueryClient } from '@tanstack/react-query' +import { useTranslation } from 'react-i18next' import API from '@/api' import { @@ -40,6 +41,7 @@ const PURPOSES = ['', 'leisure', 'business', 'crew', 'other'] export function FlightDetail({ flightId }: FlightDetailProps) { const navigate = useNavigate() + const { t } = useTranslation() const metric = ConfigStorage.getSetting('metricUnits') !== 'false' const localAirportTime = ConfigStorage.getSetting('localAirportTime') === 'true' const { data: flight, isLoading } = useFlight(flightId) @@ -113,7 +115,7 @@ export function FlightDetail({ flightId }: FlightDetailProps) { } const handleDelete = () => { - if (!confirm('Delete this flight?')) return + if (!confirm(t('flightDetail.deleteConfirm'))) return deleteMut.mutate(flightId, { onSuccess: () => navigate('/flights'), }) @@ -127,13 +129,13 @@ export function FlightDetail({ flightId }: FlightDetailProps) { to="/flights" className="board-label flex items-center gap-1.5 text-ink-muted hover:text-ink" > - All flights + {t('flightDetail.allFlights')}
{isOwner && !editing && ( <> )} @@ -154,7 +156,7 @@ export function FlightDetail({ flightId }: FlightDetailProps) { size="sm" onClick={() => setEditing(false)} > - Cancel + {t('common.cancel')} )} @@ -196,17 +198,17 @@ export function FlightDetail({ flightId }: FlightDetailProps) {
@@ -225,14 +227,14 @@ export function FlightDetail({ flightId }: FlightDetailProps) { {/* Airports */}
- - + +
- Route + {t('newFlight.route')} {flight.distance ? ( ) : ( - No route data + + {t('flightDetail.noRouteData')} + )} - {editing ? 'Edit details' : 'Details'} + + {editing ? t('flightDetail.editDetails') : t('flightDetail.details')} + {editing ? ( @@ -270,7 +276,7 @@ export function FlightDetail({ flightId }: FlightDetailProps) { {flight.notes && !editing && ( - Notes + {t('newFlight.notes')}

@@ -284,6 +290,8 @@ export function FlightDetail({ flightId }: FlightDetailProps) { } function AirportPanel({ title, airport }: { title: string; airport: any }) { + const { t } = useTranslation() + if (!airport || typeof airport === 'string') { return ( @@ -291,7 +299,7 @@ function AirportPanel({ title, airport }: { title: string; airport: any }) { {title} - {typeof airport === 'string' ? airport : 'No airport data'} + {typeof airport === 'string' ? airport : t('flightDetail.noAirportData')} ) @@ -305,39 +313,41 @@ function AirportPanel({ title, airport }: { title: string; airport: any }) { - - - - - - - + + + + + + + ) } function ReadFields({ flight }: { flight: Flight }) { + const { t } = useTranslation() + return (

- + - - - - + + + + {flight.connection && (
- Connection + {t('flightDetail.connection')} - Linked flight #{flight.connection} + {t('flightDetail.linkedFlight', { id: flight.connection })}
)} @@ -351,6 +361,7 @@ interface EditFieldsProps { } function EditFields({ draft, setDraft }: EditFieldsProps) { + const { t } = useTranslation() const set = (k: K, v: Flight[K] | undefined) => setDraft({ ...draft, [k]: v as any }) @@ -375,7 +386,7 @@ function EditFields({ draft, setDraft }: EditFieldsProps) { return (
- +
- + displayValue={ draft.origin @@ -396,7 +407,7 @@ function EditFields({ draft, setDraft }: EditFieldsProps) { />
- + displayValue={ draft.destination @@ -409,7 +420,7 @@ function EditFields({ draft, setDraft }: EditFieldsProps) {
- +
- +
- +
- +
- +
- + displayValue={ draft.airline @@ -468,7 +479,7 @@ function EditFields({ draft, setDraft }: EditFieldsProps) {
- +
- + set('ticketClass', e.target.value)} @@ -505,7 +516,7 @@ function EditFields({ draft, setDraft }: EditFieldsProps) {
- + set('airplane', e.target.value)} />
- + set('tailNumber', e.target.value)} @@ -531,7 +542,7 @@ function EditFields({ draft, setDraft }: EditFieldsProps) {
- + set('flightNumber', e.target.value)} @@ -539,7 +550,7 @@ function EditFields({ draft, setDraft }: EditFieldsProps) {
- +