diff --git a/package.json b/package.json index b8848943..18013d36 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,8 @@ "dom-to-image": "^2.6.0", "electron-window-state": "^5.0.3", "font-gis": "^1.0.6", + "i18next": "^24.2.3", + "i18next-browser-languagedetector": "^8.0.4", "leaflet": "^1.9.4", "leaflet.nauticscale": "^1.1.0", "leaflet.polylinemeasure": "^3.0.0", @@ -49,6 +51,7 @@ "react": "^19.0.0", "react-dom": "^19.0.0", "react-error-boundary": "^5.0.0", + "react-i18next": "^15.4.1", "react-leaflet": "^5.0.0", "react-leaflet-custom-control": "^1.4.0", "react-leaflet-geoman-v2": "^1.0.1", diff --git a/src/components/BackdropForm/index.tsx b/src/components/BackdropForm/index.tsx index cb94ff51..f527ab5a 100644 --- a/src/components/BackdropForm/index.tsx +++ b/src/components/BackdropForm/index.tsx @@ -1,6 +1,7 @@ import { Feature, Geometry } from 'geojson' import { Checkbox, Form, Input } from 'antd' import { useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' import { BackdropProps } from '../../types' import './index.css' @@ -12,6 +13,7 @@ export interface BackdropFormProps { export const BackdropForm: React.FC = ({backdrop, onChange, create = false}) => { + const { t } = useTranslation() const [state, setState] = useState(null) const [form] = Form.useForm() @@ -49,14 +51,14 @@ export const BackdropForm: React.FC = ({backdrop, onChange, c onValuesChange={localChange} size='small'> - label='Name' + label={t('forms.common.name')} name='name' style={itemStyle} - rules={[{ required: true, message: 'Please enter backdrop name!' }]}> + rules={[{ required: true, message: t('forms.common.nameRequired') }]}> - label='Visible' + label={t('forms.common.visible')} name={'visible'} style={itemStyle} valuePropName="checked" > @@ -64,24 +66,24 @@ export const BackdropForm: React.FC = ({backdrop, onChange, c { create && <> - label='URL' + label={t('forms.common.url')} name='url' style={itemStyle} - rules={[{ required: true, message: 'Please enter backdrop URL!' }]}> + rules={[{ required: true, message: t('forms.common.urlRequired') }]}> - label='Max Native Zoom' + label={t('forms.common.maxNativeZoom')} name= 'maxNativeZoom' style={itemStyle} - rules={[{ required: true, message: 'Please enter backdrop max native zoom!' }]}> + rules={[{ required: true, message: t('forms.common.maxNativeZoomRequired') }]}> - label='Max Zoom' + label={t('forms.common.maxZoom')} name='maxZoom' style={itemStyle} - rules={[{ required: true, message: 'Please enter backdrop max zoom!' }]}> + rules={[{ required: true, message: t('forms.common.maxZoomRequired') }]}> } diff --git a/src/components/BuoyFieldForm/index.tsx b/src/components/BuoyFieldForm/index.tsx index 04edc63e..c2f986a8 100644 --- a/src/components/BuoyFieldForm/index.tsx +++ b/src/components/BuoyFieldForm/index.tsx @@ -8,6 +8,7 @@ import { } from 'antd' import { Color } from 'antd/es/color-picker' import { useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' import { BuoyFieldProps } from '../../types' import { presetColors } from '../../helpers/standardShades' import './index.css' @@ -29,6 +30,7 @@ export const BuoyFieldForm: React.FC = ({ field, onChange, }) => { + const { t } = useTranslation() const [state, setState] = useState(null) useEffect(() => { @@ -96,23 +98,23 @@ export const BuoyFieldForm: React.FC = ({ size='small' > - label='Name' + label={t('forms.common.name')} name='name' style={itemStyle} - rules={[{ required: true, message: 'Please enter track name!' }]} + rules={[{ required: true, message: t('forms.common.nameRequired') }]} > - label='Short name' + label={t('forms.common.shortName')} name='shortName' style={itemStyle} - rules={[{ required: true, message: 'Please enter short name!' }]} + rules={[{ required: true, message: t('forms.common.shortNameRequired') }]} > - label='Visible' + label={t('forms.common.visible')} name={'visible'} style={itemStyle} valuePropName='checked' @@ -120,10 +122,10 @@ export const BuoyFieldForm: React.FC = ({ - label='Color' + label={t('forms.common.color')} name='marker-color' style={itemStyle} - rules={[{ required: true, message: 'color is required!' }]} + rules={[{ required: true, message: t('forms.common.colorRequired') }]} > = ({ presets={presetColors} /> - label='Time' style={itemStyle} name='dTime'> + label={t('forms.common.time')} style={itemStyle} name='dTime'> - label='Time end' + label={t('forms.common.timeEnd')} style={itemStyle} // validate that dTimeEnd is after dTime rules={[ @@ -145,7 +147,7 @@ export const BuoyFieldForm: React.FC = ({ // if there is a value, check if it's after the dTime return !value || getFieldValue('dTime') < value ? Promise.resolve() - : Promise.reject(new Error('Time-end must be after time!')) + : Promise.reject(new Error(t('forms.common.timeEndAfterTime'))) }, }), ]} diff --git a/src/components/ControlPanel/TimeControls.tsx b/src/components/ControlPanel/TimeControls.tsx index f04cbf0f..ac26823b 100644 --- a/src/components/ControlPanel/TimeControls.tsx +++ b/src/components/ControlPanel/TimeControls.tsx @@ -9,6 +9,7 @@ import { import { TimeSupport } from '../../helpers/time-support' import { formatInTimeZone } from 'date-fns-tz' import { useDocContext } from '../../state/DocContext' +import { useTranslation } from 'react-i18next' interface TimeControlsProps { bounds: [number, number] | null @@ -66,6 +67,7 @@ const TimeButton: React.FC = ({ } const TimeControls: FC = ({ bounds }) => { + const { t } = useTranslation() const { time, setTime, interval, setInterval } = useDocContext() const [stepTxt, setStepTxt] = useState('01h00m') @@ -140,9 +142,9 @@ const TimeControls: FC = ({ bounds }) => { > - Start - Step - End + {t('controlPanel.start')} + {t('controlPanel.step')} + {t('controlPanel.end')} @@ -164,7 +166,7 @@ const TimeControls: FC = ({ bounds }) => { } forward={false} large={true} @@ -172,7 +174,7 @@ const TimeControls: FC = ({ bounds }) => { disabled={!time.filterApplied} /> } forward={false} large={false} @@ -183,7 +185,7 @@ const TimeControls: FC = ({ bounds }) => { } forward={true} large={false} @@ -191,7 +193,7 @@ const TimeControls: FC = ({ bounds }) => { disabled={!time.filterApplied} /> } forward={true} large={true} diff --git a/src/components/ControlPanel/index.tsx b/src/components/ControlPanel/index.tsx index 2fa837eb..231e2f64 100644 --- a/src/components/ControlPanel/index.tsx +++ b/src/components/ControlPanel/index.tsx @@ -13,6 +13,7 @@ import React, { useMemo, useState } from 'react' import { useAppSelector } from '../../state/hooks' import { UndoModal } from '../UndoModal' import { useDocContext } from '../../state/DocContext' +import { useTranslation } from 'react-i18next' import { SampleDataLoader } from '../SampleDataLoader' @@ -27,6 +28,7 @@ export interface TimeProps { const ControlPanel: React.FC = ({ bounds, handleSave, isDirty }) => { + const { t } = useTranslation() const canUndo = useAppSelector(state => state.fColl.past.length > 1) const canRedo = useAppSelector(state => state.fColl.future.length > 0) const { viewportFrozen, setViewportFrozen, copyMapToClipboard, time, setTime } = useDocContext() @@ -34,15 +36,15 @@ const ControlPanel: React.FC = ({ bounds, handleSave, isDirty }) => { const undoRedoTitle = useMemo(() => { if(canUndo && canRedo) { - return 'Undo/Redo ...' + return t('controlPanel.undoRedo') } else if (canUndo) { - return 'Undo ...' + return t('controlPanel.undo') } else if (canRedo) { - return 'Redo ...' + return t('controlPanel.redo') } else { return null } - }, [canUndo, canRedo]) + }, [canUndo, canRedo, t]) const toggleFreezeViewport = () => { setViewportFrozen(!viewportFrozen) @@ -50,9 +52,9 @@ const ControlPanel: React.FC = ({ bounds, handleSave, isDirty }) => { const copyTooltip = useMemo(() => { return viewportFrozen - ? 'Copy snapshot of map to the clipboard' - : 'Lock the viewport in order to take a snapshot of the map' - }, [viewportFrozen]) + ? t('controlPanel.copySnapshot') + : t('controlPanel.lockViewportFirst') + }, [viewportFrozen, t]) const toggleFilterApplied = () => { setTime(prevTime => ({ ...prevTime, filterApplied: !prevTime.filterApplied })) @@ -61,18 +63,18 @@ const ControlPanel: React.FC = ({ bounds, handleSave, isDirty }) => { const buttonStyle = { margin: '0 5px' } const saveButton = useMemo(() => { - return + return - }, [handleSave, isDirty]) + }, [handleSave, isDirty, t]) const enableTip = useMemo(() => { return bounds - ? 'Enable time controls, to filter tracks by time' - : 'No time data available' - }, [bounds]) + ? t('controlPanel.enableTimeControls') + : t('controlPanel.noTimeData') + }, [bounds, t]) return ( <> @@ -82,7 +84,7 @@ const ControlPanel: React.FC = ({ bounds, handleSave, isDirty }) => { - + + onClick={() => handleNew()}>{t('documents.new')} ) renderValues.buttons.push( - - + + ) renderValues.buttons.push( - +
+ +
+ ) + renderValues.buttons.push( + } unCheckedChildren={} @@ -324,7 +332,7 @@ const Documents = () => { /> } { /> -

Are you sure you want to close this tab?

+

{t('documents.confirmClose')}

{tabs.length === 0 && ( @@ -364,7 +372,7 @@ const Documents = () => {   - Background on the tool, who to contact for support. Background on the tool, who to contact for support. Background on the tool, who to contact for support. Background on the tool, who to contact for support. Background on the tool, who to contact for support. Background on the tool, who to contact for support. + {t('welcome.background')} )} diff --git a/src/components/GraphsPanel/FeatureSelectorModal.tsx b/src/components/GraphsPanel/FeatureSelectorModal.tsx index 3a5f0b70..7028309d 100644 --- a/src/components/GraphsPanel/FeatureSelectorModal.tsx +++ b/src/components/GraphsPanel/FeatureSelectorModal.tsx @@ -1,5 +1,6 @@ import { Modal, Transfer, Space } from 'antd' import { useMemo, useState } from 'react' +import { useTranslation } from 'react-i18next' import { Feature, GeoJsonProperties, Geometry } from 'geojson' import { featureAsOption } from './featureUtils' @@ -13,6 +14,7 @@ interface FeatureSelectorModalProps { } export const FeatureSelectorModal: React.FC = ({isOpen, title, onSave, onClose, features, defaults}) => { + const { t } = useTranslation() const [selectedTracks, setSelectedTracks] = useState(defaults) const options = useMemo(() => features.map(featureAsOption), [features]) @@ -52,7 +54,7 @@ export const FeatureSelectorModal: React.FC = ({isOpe > setSelectedTracks(nextTargetKeys as string[])} render={renderItem} diff --git a/src/components/LanguageSelector/index.tsx b/src/components/LanguageSelector/index.tsx new file mode 100644 index 00000000..b730b14d --- /dev/null +++ b/src/components/LanguageSelector/index.tsx @@ -0,0 +1,34 @@ +import React from 'react' +import { Select } from 'antd' +import { useTranslation } from 'react-i18next' +import { GlobalOutlined } from '@ant-design/icons' + +const { Option } = Select + +const LanguageSelector: React.FC = () => { + const { i18n } = useTranslation() + + const handleLanguageChange = (value: string) => { + i18n.changeLanguage(value) + } + + return ( +
+ + +
+ ) +} + +export default LanguageSelector diff --git a/src/components/Layers/CopyButton.tsx b/src/components/Layers/CopyButton.tsx index 3a4c6003..0cb825f1 100644 --- a/src/components/Layers/CopyButton.tsx +++ b/src/components/Layers/CopyButton.tsx @@ -8,8 +8,10 @@ import { import { ToolButton } from './ToolButton' import { useAppContext } from '../../state/AppContext' import { selectFeatures } from '../../state/geoFeaturesSlice' +import { useTranslation } from 'react-i18next' export const CopyButton: React.FC = () => { + const { t } = useTranslation() const { selection, setMessage } = useDocContext() const { setClipboardUpdated } = useAppContext() const features = useAppSelector(selectFeatures) @@ -34,9 +36,9 @@ export const CopyButton: React.FC = () => { navigator.clipboard.writeText(asStr).then(() => { setClipboardUpdated(clipboardUpdated => !clipboardUpdated) }).catch((e) => { - setMessage({ title: 'Error', severity: 'error', message: 'Copy error: ' + e }) + setMessage({ title: 'Error', severity: 'error', message: t('layers.copyError') + e }) }) - }, [selection, features, setClipboardUpdated, setMessage]) + }, [selection, features, setClipboardUpdated, setMessage, t]) return ( { icon={} title={ selection.length > 0 - ? 'Copy selected items' - : 'Select non-track items to enable copy' + ? t('layers.copySelected') + : t('layers.selectNonTrack') } /> ) diff --git a/src/components/Layers/LayersToolbar.tsx b/src/components/Layers/LayersToolbar.tsx index 78735d93..78b0c956 100644 --- a/src/components/Layers/LayersToolbar.tsx +++ b/src/components/Layers/LayersToolbar.tsx @@ -10,6 +10,7 @@ import { import { ToolButton } from './ToolButton' import { CopyButton } from './CopyButton' import { PasteButton } from './PasteButton' +import { useTranslation } from 'react-i18next' interface LayersToolbarProps { onCollapse: () => void @@ -32,6 +33,7 @@ export const LayersToolbar: React.FC = ({ hasTimeFilter, onFilterForTime }) => { + const { t } = useTranslation() return (
= ({ onClick={onCollapse} icon={} className='layers-collapse-button' - title='Collapse All' + title={t('layers.collapseAll')} disabled={!isExpanded} /> = ({ disabled={!hasSelection} className='layers-clear-button' icon={} - title={'Clear selection'} + title={t('layers.clearSelection')} /> = ({ icon={} title={ hasSelection - ? 'Delete selected items' - : 'Select items to enable delete' + ? t('layers.deleteSelected') + : t('layers.selectToEnable') } /> @@ -86,8 +88,8 @@ export const LayersToolbar: React.FC = ({ icon={hasTimeFilter ? : } title={ hasTimeFilter - ? 'Cancel filter features by time' - : 'Filter features by time' + ? t('layers.cancelTimeFilter') + : t('layers.filterByTime') } /> {/* { + const { t } = useTranslation() const dispatch = useAppDispatch() const [pasteDisabled, setPasteDisabled] = useState(true) const { clipboardUpdated } = useAppContext() @@ -28,12 +30,12 @@ export const PasteButton: React.FC = () => { if (('' + error).includes('Document is not focused')) { // note: we get an error if dev-tools is open, ignore that error. } else { - setMessage({ title: 'Error', severity: 'error', message: 'Failed to read clipboard: ' + error }) + setMessage({ title: 'Error', severity: 'error', message: t('layers.readClipboardError') + error }) console.warn('Failed to read clipboard', error) setPasteDisabled(true) } } - }, [setMessage, clipboardUpdated]) + }, [setMessage, clipboardUpdated, t]) useEffect(() => { checkClipboard() @@ -76,7 +78,7 @@ export const PasteButton: React.FC = () => { } else if (Array.isArray(parsed)) { features = parsed as Feature[] } else { - throw new Error('Invalid GeoJSON format') + throw new Error(t('layers.invalidGeoJSON')) } dispatch({ @@ -84,10 +86,10 @@ export const PasteButton: React.FC = () => { payload: features, }) } catch (e) { - setMessage({ title: 'Error', severity: 'error', message: 'Paste error: ' + e }) + setMessage({ title: 'Error', severity: 'error', message: t('layers.pasteError') + e }) } setPasteDisabled(true) - }, [dispatch, setMessage, setPasteDisabled]) + }, [dispatch, setMessage, setPasteDisabled, t]) return ( { className='layers-paste-button' disabled={pasteDisabled} icon={} - title={pasteDisabled ? 'No valid GeoJSON data in clipboard' : 'Paste GeoJSON data'} + title={pasteDisabled ? t('layers.noValidData') : t('layers.pasteData')} /> ) } \ No newline at end of file diff --git a/src/components/Layers/TreeDataBuilder.ts b/src/components/Layers/TreeDataBuilder.ts index 87e641de..b64ecfce 100644 --- a/src/components/Layers/TreeDataBuilder.ts +++ b/src/components/Layers/TreeDataBuilder.ts @@ -143,11 +143,12 @@ export class TreeDataBuilder { features: Feature[], handleAdd: HandleAddFunction, iconCreators: IconCreators, - useTimeFilter: boolean + useTimeFilter: boolean, + unitsLabel: string = 'Units' ): TreeDataNode { // generate new root const root: TreeDataNode = { - title: 'Units', + title: unitsLabel, key: NODE_TRACKS, icon: iconCreators.createFolderIcon(), children: [], @@ -232,7 +233,14 @@ export class TreeDataBuilder { useTimeFilter: boolean = false, timeStart: number, timeEnd: number, - zonesIcon: React.ReactNode + zonesIcon: React.ReactNode, + translations?: { + units?: string; + buoyFields?: string; + zones?: string; + referencePoints?: string; + backgrounds?: string; + } ): Array { // If time filtering is enabled, filter the features let filteredFeatures = features @@ -245,11 +253,11 @@ export class TreeDataBuilder { } return [ - this.buildTrackNode(filteredFeatures, handleAdd, iconCreators, useTimeFilter), - this.buildTypeNode(filteredFeatures, 'Buoy Fields', NODE_FIELDS, BUOY_FIELD_TYPE, handleAdd, iconCreators, useTimeFilter, undefined), - this.buildTypeNode(filteredFeatures, 'Zones', NODE_ZONES, ZONE_TYPE, handleAdd, iconCreators, useTimeFilter, zonesIcon), - this.buildTypeNode(filteredFeatures, 'Reference Points', NODE_POINTS, REFERENCE_POINT_TYPE, handleAdd, iconCreators, useTimeFilter, undefined), - this.buildTypeNode(filteredFeatures, 'Backdrops', NODE_BACKDROPS, BACKDROP_TYPE, handleAdd, iconCreators, useTimeFilter, undefined), + this.buildTrackNode(filteredFeatures, handleAdd, iconCreators, useTimeFilter, translations?.units), + this.buildTypeNode(filteredFeatures, translations?.buoyFields || 'Buoy Fields', NODE_FIELDS, BUOY_FIELD_TYPE, handleAdd, iconCreators, useTimeFilter, undefined), + this.buildTypeNode(filteredFeatures, translations?.zones || 'Zones', NODE_ZONES, ZONE_TYPE, handleAdd, iconCreators, useTimeFilter, zonesIcon), + this.buildTypeNode(filteredFeatures, translations?.referencePoints || 'Reference Points', NODE_POINTS, REFERENCE_POINT_TYPE, handleAdd, iconCreators, useTimeFilter, undefined), + this.buildTypeNode(filteredFeatures, translations?.backgrounds || 'Backgrounds', NODE_BACKDROPS, BACKDROP_TYPE, handleAdd, iconCreators, useTimeFilter, undefined), ] } diff --git a/src/components/Layers/index.tsx b/src/components/Layers/index.tsx index 44e0f2c6..3209bdf5 100644 --- a/src/components/Layers/index.tsx +++ b/src/components/Layers/index.tsx @@ -6,6 +6,7 @@ import { useDocContext } from '../../state/DocContext' import { useAppSelector } from '../../state/hooks' import { LoadTrackModel } from '../LoadTrackModal' import { EnvOptions } from '../../types' +import { useTranslation } from 'react-i18next' // These components are now used in LayersToolbar import { AddZoneShape } from './AddZoneShape' import { LayersToolbar } from './LayersToolbar' @@ -42,6 +43,7 @@ interface LayerProps { // Components have been moved to their own files const Layers: React.FC = ({ openGraph, splitterWidths }) => { + const { t } = useTranslation() const treeRef = React.useRef(null) const { selection, setSelection, setNewFeature, preview, setMessage, time } = useDocContext() const { setClipboardUpdated } = useAppContext() @@ -138,7 +140,7 @@ const Layers: React.FC = ({ openGraph, splitterWidths }) => { const zonesIcon = TreeDataBuilder.getIcon( undefined, NODE_ZONES, - 'Zones', + t('layers.zones'), handleAdd, iconCreators, @@ -152,13 +154,20 @@ const Layers: React.FC = ({ openGraph, splitterWidths }) => { filterForTime, useTimeFilter ? time.start : 0, useTimeFilter ? time.end : 0, - zonesIcon + zonesIcon, + { + units: t('layers.units'), + buoyFields: t('layers.fields'), + zones: t('layers.zones'), + referencePoints: t('layers.points'), + backgrounds: t('layers.backdrops') + } ) const validModels = modelData.filter(node => node !== null) return validModels - }, [theFeatures, handleAdd, addZone, useTimeFilter, time.start, time.end, time.filterApplied, iconCreators]) + }, [theFeatures, handleAdd, addZone, useTimeFilter, time.start, time.end, time.filterApplied, iconCreators, t]) // onSelect is now provided by useSelectionHandlers diff --git a/src/components/LoadTrackModal/index.tsx b/src/components/LoadTrackModal/index.tsx index bad0c2fd..bdbcc81a 100644 --- a/src/components/LoadTrackModal/index.tsx +++ b/src/components/LoadTrackModal/index.tsx @@ -14,6 +14,7 @@ import { Typography, } from 'antd' import { Color } from 'antd/es/color-picker' +import { useTranslation } from 'react-i18next' import { presetColors } from '../../helpers/standardShades' import { useAppSelector } from '../../state/hooks' import { selectFeatures } from '../../state/geoFeaturesSlice' @@ -42,6 +43,7 @@ export const LoadTrackModel: React.FC = ({ environment, createTrackOnly = false, }) => { + const { t } = useTranslation() const features = useAppSelector(selectFeatures) const trackOptions = features .filter((feature) => feature.properties?.dataType === 'track') @@ -114,7 +116,7 @@ export const LoadTrackModel: React.FC = ({ const tabs: TabsProps['items'] = [ { key: 'add', - label: 'Add to existing track', + label: t('layers.addToExistingTrack'), children: (
= ({ disabled={trackOptions.length === 0} > { trackOptions.length ? - Select a track to add data to, from the list below: + {t('layers.selectTrackToAddData')} : - No tracks available to add to. Please load as a new track. + {t('layers.noTracksAvailable')} } - label='Track' + label={t('layers.tracks')} name='trackId' initialValue={defaultTrackId} style={itemStyle} rules={[ { required: true, - message: 'Please indicate which track to add data to', + message: t('layers.pleaseSelectTrack'), }, ]} > @@ -146,10 +148,10 @@ export const LoadTrackModel: React.FC = ({ @@ -157,7 +159,7 @@ export const LoadTrackModel: React.FC = ({ }, { key: 'create', - label: 'Create new track', + label: t('layers.addTrack'), children: (
= ({ autoComplete='off' > - Extra details are required for a new track. Please complete the - following: + {t('layers.extraDetailsForNewTrack')} - label='Name' + label={t('forms.common.name')} name='name' style={itemStyle} - rules={[{ required: true, message: 'Please enter track name!' }]} + rules={[{ required: true, message: t('forms.common.nameRequired') }]} > - + - label='Short Name' + label={t('forms.common.shortName')} name='shortName' style={itemStyle} rules={[ { required: true, - message: 'Please enter abbreviated track name!', + message: t('forms.common.shortNameRequired'), }, ]} > - + - label='Year' + label={t('layers.year')} name='initialYear' style={itemStyle} - rules={[{ required: true, message: 'Please enter Year for data' }]} + rules={[{ required: true, message: t('layers.yearRequired') }]} > - + - label='Month' + label={t('layers.month')} name='initialMonth' style={itemStyle} - rules={[{ required: true, message: 'Please enter Month for data' }]} + rules={[{ required: true, message: t('layers.monthRequired') }]} > - + - label='Environment' + label={t('forms.common.environment')} name='env' style={itemStyle} rules={[ { required: true, - message: 'Please specify the environment for the track', + message: t('forms.common.environmentRequired'), }, ]} > @@ -230,10 +231,10 @@ export const LoadTrackModel: React.FC = ({ - label='Colour' + label={t('forms.common.color')} name='stroke' style={itemStyle} - rules={[{ required: true, message: 'Please enter track color' }]} + rules={[{ required: true, message: t('forms.common.colorRequired') }]} > = ({ style={itemStyle} - label='Markers'> + label={t('forms.common.markers')}> - label='Labels' + label={t('forms.common.labels')} className="labelInterval" name='labelInterval' style={itemStyle}> - label='Visible' + label={t('forms.common.visible')} name={'visible'} style={itemStyle} valuePropName="checked" > - label='Position' + label={t('forms.common.position')} name='position' style={itemStyle} rules={[{ required: true }]} > - label="Color" + label={t('forms.common.color')} name='marker-color' style={itemStyle} - rules={[{ required: true, message: 'color is required!' }]}> + rules={[{ required: true, message: t('forms.common.colorRequired') }]}> - label="Time" + label={t('forms.common.time')} style={itemStyle} name='dTime'> - label="Time end" + label={t('forms.common.timeEnd')} style={itemStyle} // validate that dTimeEnd is after dTime rules={[ @@ -142,7 +144,7 @@ export const PointForm: React.FC = ({shape, onChange}) => { // if there is a value, check if it's after the dTime return !value || getFieldValue('dTime') < value ? Promise.resolve() - : Promise.reject(new Error('Time-end must be after time!')) + : Promise.reject(new Error(t('forms.common.timeEndAfterTime'))) }, }), ]} diff --git a/src/components/TrackForm/index.tsx b/src/components/TrackForm/index.tsx index e135b2a7..f5ae3765 100644 --- a/src/components/TrackForm/index.tsx +++ b/src/components/TrackForm/index.tsx @@ -2,6 +2,7 @@ import { Feature, LineString } from 'geojson' import { Checkbox, ColorPicker, Flex, Form, Input, Select } from 'antd' import { Color } from 'antd/es/color-picker' import { useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' import { TrackProps } from '../../types' import { presetColors } from '../../helpers/standardShades' import './index.css' @@ -20,6 +21,7 @@ type FormTypeProps = Omit & { export const TrackForm: React.FC = ({track, onChange}) => { + const { t } = useTranslation() const [state, setState] = useState(null) const [form] = Form.useForm() useEffect(() => { @@ -75,34 +77,34 @@ export const TrackForm: React.FC = ({track, onChange}) => { onValuesChange={localChange} size='small'> - label='Name' + label={t('forms.common.name')} name='name' style={itemStyle} - rules={[{ required: true, message: 'Please enter track name!' }]}> + rules={[{ required: true, message: t('forms.common.nameRequired') }]}> - label='Short name' + label={t('forms.common.shortName')} name='shortName' style={itemStyle} - rules={[{ required: true, message: 'Please enter short name!' }]}> + rules={[{ required: true, message: t('forms.common.shortNameRequired') }]}> - label='Visible' + label={t('forms.common.visible')} name={'visible'} style={itemStyle} valuePropName="checked" > - label='Environment' + label={t('forms.common.environment')} name='env' style={itemStyle} rules={[ { required: true, - message: 'Please specify the environment/symbol for the track', + message: t('forms.common.environmentRequired'), }, ]} > @@ -119,17 +121,17 @@ export const TrackForm: React.FC = ({track, onChange}) => { style={itemStyle} - label='Markers'> + label={t('forms.common.markers')}> - label='Labels' + label={t('forms.common.labels')} className="labelInterval" name='labelInterval' style={itemStyle}> - label='Visible' + label={t('forms.common.visible')} name={'visible'} style={itemStyle} valuePropName="checked" > - label="Color" + label={t('forms.common.color')} name='stroke' style={itemStyle} - rules={[{ required: true, message: 'color is required!' }]}> + rules={[{ required: true, message: t('forms.common.colorRequired') }]}> - label="Fill" + label={t('forms.common.fill')} name='fill' style={itemStyle} - rules={[{ required: true, message: 'color is required!' }]}> + rules={[{ required: true, message: t('forms.common.colorRequired') }]}> - + - label='Start' + label={t('forms.common.start')} name='dTime' style={itemStyle}> - label='End' + label={t('forms.common.end')} name='dTimeEnd' style={itemStyle}> diff --git a/src/i18n/i18n.ts b/src/i18n/i18n.ts new file mode 100644 index 00000000..0bc91ab0 --- /dev/null +++ b/src/i18n/i18n.ts @@ -0,0 +1,52 @@ +import i18n from 'i18next' +import { initReactI18next } from 'react-i18next' +import LanguageDetector from 'i18next-browser-languagedetector' + +import enUS from './locales/en-US.json' +import it from './locales/it.json' +import nl from './locales/nl.json' +import fr from './locales/fr.json' +import de from './locales/de.json' + +// Initialize i18next +i18n + // Detect user language + .use(LanguageDetector) + // Pass the i18n instance to react-i18next + .use(initReactI18next) + // Initialize i18next + .init({ + // Default language + fallbackLng: 'en-US', + // Debug mode + debug: false, + // Resources containing translations + resources: { + 'en-US': { + translation: enUS + }, + it: { + translation: it + }, + nl: { + translation: nl + }, + fr: { + translation: fr + }, + de: { + translation: de + } + }, + // Language detection options + detection: { + order: ['localStorage', 'navigator'], + caches: ['localStorage'] + }, + // Interpolation configuration + interpolation: { + escapeValue: false // React already escapes values + } + }) + +export default i18n diff --git a/src/i18n/locales/de.json b/src/i18n/locales/de.json new file mode 100644 index 00000000..f839034e --- /dev/null +++ b/src/i18n/locales/de.json @@ -0,0 +1,174 @@ +{ + "welcome": { + "title": "Willkommen bei Albatross", + "subtitle": "Öffnen Sie ein vorhandenes Dokument oder erstellen Sie ein neues", + "new": "Neu", + "samplePlot": "Beispiel", + "open": "Öffnen", + "background": "Hintergrundinformationen zum Tool, wen Sie für Support kontaktieren können. Hintergrundinformationen zum Tool, wen Sie für Support kontaktieren können. Hintergrundinformationen zum Tool, wen Sie für Support kontaktieren können. Hintergrundinformationen zum Tool, wen Sie für Support kontaktieren können. Hintergrundinformationen zum Tool, wen Sie für Support kontaktieren können. Hintergrundinformationen zum Tool, wen Sie für Support kontaktieren können." + }, + "common": { + "branch": "Branch", + "build": "Build", + "language": "Sprache" + }, + "documents": { + "newTab": "Neuer Tab", + "new": "Neu", + "openExisting": "Vorhandenes Dokument öffnen", + "open": "Öffnen", + "lightMode": "Zum hellen Modus wechseln", + "darkMode": "Zum dunklen Modus wechseln", + "documentName": "Bitte geben Sie einen Namen für das Dokument an", + "closeTab": "Tab schließen", + "close": "Schließen", + "cancel": "Abbrechen", + "confirmClose": "Sind Sie sicher, dass Sie diesen Tab schließen möchten?", + "error": "Fehler", + "warning": "Warnung", + "info": "Information", + "invalidFile": "Ungültige Datei", + "loadingError": "Problem beim Laden der Datei: ", + "multipleFilesError": "Es kann nur eine Datei gleichzeitig geladen werden", + "fileTypeError": "Nur .json und .geojson Dateien werden unterstützt", + "invalidJsonError": "Der Dateiinhalt ist kein gültiges JSON-Format. Bitte überprüfen Sie die Datei und versuchen Sie es erneut.", + "geoJsonError": "Die Datei muss eine GeoJSON FeatureCollection oder Feature enthalten", + "handlingError": "Verarbeitungsfehler: ", + "localSaveNotSupported": "Lokales Speichern wird im Browser nicht unterstützt" + }, + "document": { + "controlPanel": "Bedienfeld", + "layers": "Ebenen", + "detail": "Detail", + "graphs": "Diagramme" + }, + "controlPanel": { + "lockViewport": "Ansicht sperren, um versehentliche Kartenbewegungen zu verhindern. Bei Zeitfilterung aktualisiert das Mausrad die Zeit", + "enableTimeControls": "Zeitsteuerung aktivieren, um Tracks nach Zeit zu filtern", + "noTimeData": "Keine Zeitdaten verfügbar", + "copySnapshot": "Schnappschuss der Karte in die Zwischenablage kopieren", + "lockViewportFirst": "Sperren Sie zuerst die Ansicht, um einen Schnappschuss der Karte zu erstellen", + "saveChanges": "Änderungen speichern", + "documentUnchanged": "Dokument unverändert", + "undoRedo": "Rückgängig/Wiederherstellen ...", + "undo": "Rückgängig ...", + "redo": "Wiederherstellen ...", + "nothingToUndoRedo": "Nichts rückgängig zu machen/wiederherzustellen", + "selectVersionTo": "Version auswählen zum", + "currentState": "aktueller Zustand", + "restoreVersion": "Version wiederherstellen", + "hidingViewportChanges": "Ansichtsänderungen ausblenden", + "showingViewportChanges": "Ansichtsänderungen anzeigen", + "start": "Start", + "step": "Schritt", + "end": "Ende", + "jumpToStart": "Zum Anfang springen", + "stepBackward": "Schritt zurück", + "stepForward": "Schritt vorwärts", + "jumpToEnd": "Zum Ende springen" + }, + "layers": { + "collapseAll": "Alle Einklappen", + "clearSelection": "Auswahl löschen", + "deleteSelected": "Ausgewählte Elemente löschen", + "selectToEnable": "Wählen Sie Elemente aus, um das Löschen zu aktivieren", + "copySelected": "Ausgewählte Elemente kopieren", + "selectNonTrack": "Wählen Sie Nicht-Track-Elemente aus, um das Kopieren zu aktivieren", + "noValidData": "Keine gültigen GeoJSON-Daten in der Zwischenablage", + "pasteData": "GeoJSON-Daten einfügen", + "cancelTimeFilter": "Zeitfilter für Objekte aufheben", + "filterByTime": "Objekte nach Zeit filtern", + "viewGraph": "Diagramm der ausgewählten Objekte anzeigen", + "selectTimeFeature": "Wählen Sie ein zeitbezogenes Objekt aus, um Diagramme zu aktivieren", + "copyError": "Kopierfehler: ", + "pasteError": "Einfügefehler: ", + "readClipboardError": "Fehler beim Lesen der Zwischenablage: ", + "invalidGeoJSON": "Ungültiges GeoJSON-Format", + "units": "Einheiten", + "tracks": "Tracks", + "fields": "Felder", + "zones": "Zonen", + "points": "Punkte", + "backdrops": "Hintergründe", + "addTrack": "Neuen Track hinzufügen", + "addField": "Neues Feld hinzufügen", + "addZone": "Neue Zone hinzufügen", + "addPoint": "Neuen Punkt hinzufügen", + "addBackdrop": "Neuen Hintergrund hinzufügen", + "addToExistingTrack": "Zu bestehendem Track hinzufügen", + "selectTrackToAddData": "Wählen Sie einen Track aus, um Daten hinzuzufügen", + "noTracksAvailable": "Keine Tracks verfügbar", + "pleaseSelectTrack": "Bitte wählen Sie einen Track aus", + "add": "Hinzufügen", + "extraDetailsForNewTrack": "Geben Sie Details für den neuen Track ein", + "year": "Jahr", + "yearRequired": "Bitte geben Sie das Jahr ein", + "month": "Monat", + "monthRequired": "Bitte geben Sie den Monat ein" + }, + "forms": { + "core": { + "delete": "Löschen", + "deleteFeature": "Element löschen", + "cancel": "Abbrechen", + "cancelCreation": "Erstellung eines neuen Elements abbrechen", + "reset": "Zurücksetzen", + "resetChanges": "Änderungen für dieses Element zurücksetzen", + "save": "Speichern", + "create": "Erstellen", + "saveEdits": "Änderungen speichern" + }, + "common": { + "name": "Name", + "shortName": "Kurzname", + "visible": "Sichtbar", + "color": "Farbe", + "fill": "Füllung", + "colorRequired": "Farbe ist erforderlich!", + "nameRequired": "Bitte geben Sie einen Namen ein!", + "shortNameRequired": "Bitte geben Sie einen Kurznamen ein!", + "position": "Position", + "time": "Zeit", + "timeEnd": "Endzeit", + "timeEndAfterTime": "Die Endzeit muss nach der Startzeit liegen!", + "start": "Start", + "end": "Ende", + "url": "URL", + "urlRequired": "Bitte geben Sie eine URL ein!", + "maxNativeZoom": "Maximaler nativer Zoom", + "maxNativeZoomRequired": "Bitte geben Sie den maximalen nativen Zoom ein!", + "maxZoom": "Maximaler Zoom", + "maxZoomRequired": "Bitte geben Sie den maximalen Zoom ein!", + "environment": "Umgebung", + "environmentRequired": "Bitte geben Sie die Umgebung/Symbol an", + "markers": "Markierungen", + "labels": "Beschriftungen", + "symbols": "Symbole", + "shape": "Form", + "edit": "Bearbeiten", + "topLeft": "Oben links", + "bottomRight": "Unten rechts", + "origin": "Ursprung", + "radiusM": "Radius (m)", + "innerRadiusM": "Innerer Radius (m)", + "outerRadiusM": "Äußerer Radius (m)", + "startAngle": "Startwinkel", + "endAngle": "Endwinkel", + "polygonCoordinates": "Polygon-Koordinaten", + "addCoordinate": "Koordinate hinzufügen" + }, + "multiFeature": { + "itemsSelected": "Elemente ausgewählt", + "visibility": "Sichtbarkeit", + "hideAll": "Alle ausblenden", + "showAll": "Alle anzeigen", + "mixedVisibility": "Gemischte Sichtbarkeit, klicken Sie, um alle auszublenden", + "currentColor": "Aktuelle Farbe:", + "mixedColors": "Gemischte Farben" + } + }, + "graphs": { + "available": "Verfügbar", + "selected": "Ausgewählt" + } +} diff --git a/src/i18n/locales/en-US.json b/src/i18n/locales/en-US.json new file mode 100644 index 00000000..df1e3104 --- /dev/null +++ b/src/i18n/locales/en-US.json @@ -0,0 +1,170 @@ +{ + "welcome": { + "title": "Welcome to Albatross", + "subtitle": "Open an existing document or create a new one", + "new": "New", + "samplePlot": "Sample plot", + "open": "Open", + "background": "Background on the tool, who to contact for support. Background on the tool, who to contact for support. Background on the tool, who to contact for support. Background on the tool, who to contact for support. Background on the tool, who to contact for support. Background on the tool, who to contact for support." + }, + "common": { + "branch": "Branch", + "build": "Build", + "language": "Language" + }, + "documents": { + "newTab": "New Tab", + "new": "New", + "openExisting": "Open Existing Document", + "open": "Open", + "lightMode": "Switch to Light Mode", + "darkMode": "Switch to Dark Mode", + "documentName": "Please provide a name for the document", + "closeTab": "Close Tab", + "close": "Close", + "cancel": "Cancel", + "confirmClose": "Are you sure you want to close this tab?", + "error": "Error", + "warning": "Warning", + "info": "Information", + "invalidFile": "Invalid File", + "loadingError": "Problem loading file: ", + "multipleFilesError": "Only one file can be loaded at a time", + "fileTypeError": "Only .json and .geojson files are supported", + "invalidJsonError": "The file content is not a valid JSON format. Please check the file and try again.", + "geoJsonError": "File must contain a GeoJSON FeatureCollection or Feature", + "handlingError": "Handling error: ", + "localSaveNotSupported": "Local save not supported in browser" + }, + "document": { + "controlPanel": "Control Panel", + "layers": "Layers", + "detail": "Detail", + "graphs": "Graphs" + }, + "controlPanel": { + "lockViewport": "Lock viewport to prevent accidental map movement. When time filtering, mouse wheel updates time", + "enableTimeControls": "Enable time controls, to filter tracks by time", + "noTimeData": "No time data available", + "copySnapshot": "Copy snapshot of map to the clipboard", + "lockViewportFirst": "Lock the viewport in order to take a snapshot of the map", + "saveChanges": "Save changes", + "documentUnchanged": "Document unchanged", + "undoRedo": "Undo/Redo ...", + "undo": "Undo ...", + "redo": "Redo ...", + "nothingToUndoRedo": "Nothing to undo/redo", + "selectVersionTo": "Select version to", + "currentState": "current state", + "restoreVersion": "Restore Version", + "hidingViewportChanges": "Hiding viewport changes", + "showingViewportChanges": "Showing viewport changes", + "start": "Start", + "step": "Step", + "end": "End", + "jumpToStart": "Jump to start", + "stepBackward": "Step backward", + "stepForward": "Step forward", + "jumpToEnd": "Jump to end" + }, + "layers": { + "collapseAll": "Collapse All", + "clearSelection": "Clear selection", + "deleteSelected": "Delete selected items", + "selectToEnable": "Select items to enable delete", + "copySelected": "Copy selected items", + "selectNonTrack": "Select non-track items to enable copy", + "noValidData": "No valid GeoJSON data in clipboard", + "pasteData": "Paste GeoJSON data", + "cancelTimeFilter": "Cancel filter features by time", + "filterByTime": "Filter features by time", + "viewGraph": "View graph of selected features", + "selectTimeFeature": "Select a time-related feature to enable graphs", + "copyError": "Copy error: ", + "pasteError": "Paste error: ", + "readClipboardError": "Failed to read clipboard: ", + "invalidGeoJSON": "Invalid GeoJSON format", + "units": "Units", + "tracks": "Tracks", + "fields": "Fields", + "zones": "Zones", + "points": "Points", + "backdrops": "Backdrops", + "addTrack": "Add new track", + "addField": "Add new field", + "addZone": "Add new zone", + "addPoint": "Add new point", + "addBackdrop": "Add new backdrop", + "addToExistingTrack": "Add to existing track", + "selectTrackToAddData": "Select a track to add data to", + "noTracksAvailable": "No tracks available", + "pleaseSelectTrack": "Please select a track", + "add": "Add", + "extraDetailsForNewTrack": "Enter details for the new track", + "year": "Year", + "yearRequired": "Please enter year", + "month": "Month", + "monthRequired": "Please enter month" + }, + "forms": { + "core": { + "delete": "Delete", + "deleteFeature": "Delete feature", + "cancel": "Cancel", + "cancelCreation": "Cancel new feature creation", + "reset": "Reset", + "resetChanges": "Reset changes for this feature", + "save": "Save", + "create": "Create", + "saveEdits": "Save edits" + }, + "common": { + "name": "Name", + "shortName": "Short name", + "visible": "Visible", + "color": "Color", + "fill": "Fill", + "colorRequired": "color is required!", + "nameRequired": "Please enter name!", + "shortNameRequired": "Please enter short name!", + "position": "Position", + "time": "Time", + "timeEnd": "Time end", + "timeEndAfterTime": "Time-end must be after time!", + "start": "Start", + "end": "End", + "url": "URL", + "urlRequired": "Please enter URL!", + "maxNativeZoom": "Max Native Zoom", + "maxNativeZoomRequired": "Please enter max native zoom!", + "maxZoom": "Max Zoom", + "maxZoomRequired": "Please enter max zoom!", + "environment": "Environment", + "environmentRequired": "Please specify the environment/symbol", + "markers": "Markers", + "labels": "Labels", + "symbols": "Symbols", + "shape": "Shape", + "edit": "Edit", + "topLeft": "Top Left", + "bottomRight": "Bottom Right", + "origin": "Origin", + "radiusM": "Radius (m)", + "innerRadiusM": "Inner Radius (m)", + "outerRadiusM": "Outer Radius (m)", + "startAngle": "Start Angle", + "endAngle": "End Angle", + "polygonCoordinates": "Polygon Coordinates", + "addCoordinate": "Add Coordinate" + }, + "multiFeature": { + "itemsSelected": "items selected", + "visibility": "Visibility", + "hideAll": "Hide All", + "showAll": "Show All", + "mixedVisibility": "Mixed Visibility, click to hide all", + "currentColor": "Current color:", + "mixedColors": "Mixed colors" + } + } +} diff --git a/src/i18n/locales/fr.json b/src/i18n/locales/fr.json new file mode 100644 index 00000000..76f585e0 --- /dev/null +++ b/src/i18n/locales/fr.json @@ -0,0 +1,174 @@ +{ + "welcome": { + "title": "Bienvenue sur Albatross", + "subtitle": "Ouvrez un document existant ou créez-en un nouveau", + "new": "Nouveau", + "samplePlot": "Exemple", + "open": "Ouvrir", + "background": "Contexte sur l'outil, qui contacter pour obtenir de l'aide. Contexte sur l'outil, qui contacter pour obtenir de l'aide. Contexte sur l'outil, qui contacter pour obtenir de l'aide. Contexte sur l'outil, qui contacter pour obtenir de l'aide. Contexte sur l'outil, qui contacter pour obtenir de l'aide. Contexte sur l'outil, qui contacter pour obtenir de l'aide." + }, + "common": { + "branch": "Branche", + "build": "Compilation", + "language": "Langue" + }, + "documents": { + "newTab": "Nouvel Onglet", + "new": "Nouveau", + "openExisting": "Ouvrir un Document Existant", + "open": "Ouvrir", + "lightMode": "Passer au Mode Clair", + "darkMode": "Passer au Mode Sombre", + "documentName": "Veuillez fournir un nom pour le document", + "closeTab": "Fermer l'Onglet", + "close": "Fermer", + "cancel": "Annuler", + "confirmClose": "Êtes-vous sûr de vouloir fermer cet onglet ?", + "error": "Erreur", + "warning": "Avertissement", + "info": "Information", + "invalidFile": "Fichier Invalide", + "loadingError": "Problème lors du chargement du fichier : ", + "multipleFilesError": "Un seul fichier peut être chargé à la fois", + "fileTypeError": "Seuls les fichiers .json et .geojson sont pris en charge", + "invalidJsonError": "Le contenu du fichier n'est pas au format JSON valide. Veuillez vérifier le fichier et réessayer.", + "geoJsonError": "Le fichier doit contenir une FeatureCollection GeoJSON ou une Feature", + "handlingError": "Erreur de traitement : ", + "localSaveNotSupported": "Sauvegarde locale non prise en charge dans le navigateur" + }, + "document": { + "controlPanel": "Panneau de Contrôle", + "layers": "Couches", + "detail": "Détail", + "graphs": "Graphiques" + }, + "controlPanel": { + "lockViewport": "Verrouiller la vue pour éviter les mouvements accidentels de la carte. Lors du filtrage temporel, la molette de la souris met à jour le temps", + "enableTimeControls": "Activer les contrôles temporels pour filtrer les pistes par temps", + "noTimeData": "Aucune donnée temporelle disponible", + "copySnapshot": "Copier une capture de la carte dans le presse-papiers", + "lockViewportFirst": "Verrouiller d'abord la vue pour prendre une capture de la carte", + "saveChanges": "Enregistrer les modifications", + "documentUnchanged": "Document non modifié", + "undoRedo": "Annuler/Rétablir ...", + "undo": "Annuler ...", + "redo": "Rétablir ...", + "nothingToUndoRedo": "Rien à annuler/rétablir", + "selectVersionTo": "Sélectionner la version à", + "currentState": "état actuel", + "restoreVersion": "Restaurer la Version", + "hidingViewportChanges": "Masquer les changements de vue", + "showingViewportChanges": "Afficher les changements de vue", + "start": "Début", + "step": "Étape", + "end": "Fin", + "jumpToStart": "Aller au début", + "stepBackward": "Pas en arrière", + "stepForward": "Pas en avant", + "jumpToEnd": "Aller à la fin" + }, + "layers": { + "collapseAll": "Tout Replier", + "clearSelection": "Effacer la sélection", + "deleteSelected": "Supprimer les éléments sélectionnés", + "selectToEnable": "Sélectionnez des éléments pour activer la suppression", + "copySelected": "Copier les éléments sélectionnés", + "selectNonTrack": "Sélectionnez des éléments non-piste pour activer la copie", + "noValidData": "Aucune donnée GeoJSON valide dans le presse-papiers", + "pasteData": "Coller les données GeoJSON", + "cancelTimeFilter": "Annuler le filtrage des éléments par temps", + "filterByTime": "Filtrer les éléments par temps", + "viewGraph": "Voir le graphique des éléments sélectionnés", + "selectTimeFeature": "Sélectionnez un élément temporel pour activer les graphiques", + "copyError": "Erreur de copie : ", + "pasteError": "Erreur de collage : ", + "readClipboardError": "Impossible de lire le presse-papiers : ", + "invalidGeoJSON": "Format GeoJSON invalide", + "units": "Unités", + "tracks": "Pistes", + "fields": "Champs", + "zones": "Zones", + "points": "Points", + "backdrops": "Arrière-plans", + "addTrack": "Ajouter une nouvelle piste", + "addField": "Ajouter un nouveau champ", + "addZone": "Ajouter une nouvelle zone", + "addPoint": "Ajouter un nouveau point", + "addBackdrop": "Ajouter un nouvel arrière-plan", + "addToExistingTrack": "Ajouter à une piste existante", + "selectTrackToAddData": "Sélectionnez une piste à laquelle ajouter des données", + "noTracksAvailable": "Aucune piste disponible", + "pleaseSelectTrack": "Veuillez sélectionner une piste", + "add": "Ajouter", + "extraDetailsForNewTrack": "Entrez les détails pour la nouvelle piste", + "year": "Année", + "yearRequired": "Veuillez entrer l'année", + "month": "Mois", + "monthRequired": "Veuillez entrer le mois" + }, + "forms": { + "core": { + "delete": "Supprimer", + "deleteFeature": "Supprimer l'élément", + "cancel": "Annuler", + "cancelCreation": "Annuler la création d'un nouvel élément", + "reset": "Réinitialiser", + "resetChanges": "Réinitialiser les modifications pour cet élément", + "save": "Enregistrer", + "create": "Créer", + "saveEdits": "Enregistrer les modifications" + }, + "common": { + "name": "Nom", + "shortName": "Nom court", + "visible": "Visible", + "color": "Couleur", + "fill": "Remplissage", + "colorRequired": "la couleur est requise !", + "nameRequired": "Veuillez entrer un nom !", + "shortNameRequired": "Veuillez entrer un nom court !", + "position": "Position", + "time": "Temps", + "timeEnd": "Fin du temps", + "timeEndAfterTime": "La fin du temps doit être après le début !", + "start": "Début", + "end": "Fin", + "url": "URL", + "urlRequired": "Veuillez entrer une URL !", + "maxNativeZoom": "Zoom natif maximal", + "maxNativeZoomRequired": "Veuillez entrer le zoom natif maximal !", + "maxZoom": "Zoom maximal", + "maxZoomRequired": "Veuillez entrer le zoom maximal !", + "environment": "Environnement", + "environmentRequired": "Veuillez spécifier l'environnement/symbole", + "markers": "Marqueurs", + "labels": "Étiquettes", + "symbols": "Symboles", + "shape": "Forme", + "edit": "Éditer", + "topLeft": "En haut à gauche", + "bottomRight": "En bas à droite", + "origin": "Origine", + "radiusM": "Rayon (m)", + "innerRadiusM": "Rayon interne (m)", + "outerRadiusM": "Rayon externe (m)", + "startAngle": "Angle de départ", + "endAngle": "Angle de fin", + "polygonCoordinates": "Coordonnées du polygone", + "addCoordinate": "Ajouter une coordonnée" + }, + "multiFeature": { + "itemsSelected": "éléments sélectionnés", + "visibility": "Visibilité", + "hideAll": "Tout masquer", + "showAll": "Tout afficher", + "mixedVisibility": "Visibilité mixte, cliquez pour tout masquer", + "currentColor": "Couleur actuelle :", + "mixedColors": "Couleurs mixtes" + } + }, + "graphs": { + "available": "Disponibles", + "selected": "Sélectionnés" + } +} diff --git a/src/i18n/locales/it.json b/src/i18n/locales/it.json new file mode 100644 index 00000000..f73f9e79 --- /dev/null +++ b/src/i18n/locales/it.json @@ -0,0 +1,174 @@ +{ + "welcome": { + "title": "Benvenuto in Albatross", + "subtitle": "Apri un documento esistente o creane uno nuovo", + "new": "Nuovo", + "samplePlot": "Esempio", + "open": "Apri", + "background": "Informazioni sullo strumento, chi contattare per supporto. Informazioni sullo strumento, chi contattare per supporto. Informazioni sullo strumento, chi contattare per supporto. Informazioni sullo strumento, chi contattare per supporto. Informazioni sullo strumento, chi contattare per supporto. Informazioni sullo strumento, chi contattare per supporto." + }, + "common": { + "branch": "Ramo", + "build": "Compilazione", + "language": "Lingua" + }, + "documents": { + "newTab": "Nuova Scheda", + "new": "Nuovo", + "openExisting": "Apri Documento Esistente", + "open": "Apri", + "lightMode": "Passa alla Modalità Chiara", + "darkMode": "Passa alla Modalità Scura", + "documentName": "Fornisci un nome per il documento", + "closeTab": "Chiudi Scheda", + "close": "Chiudi", + "cancel": "Annulla", + "confirmClose": "Sei sicuro di voler chiudere questa scheda?", + "error": "Errore", + "warning": "Avviso", + "info": "Informazione", + "invalidFile": "File Non Valido", + "loadingError": "Problema durante il caricamento del file: ", + "multipleFilesError": "È possibile caricare solo un file alla volta", + "fileTypeError": "Sono supportati solo file .json e .geojson", + "invalidJsonError": "Il contenuto del file non è in un formato JSON valido. Controlla il file e riprova.", + "geoJsonError": "Il file deve contenere una FeatureCollection GeoJSON o una Feature", + "handlingError": "Errore di gestione: ", + "localSaveNotSupported": "Salvataggio locale non supportato nel browser" + }, + "document": { + "controlPanel": "Pannello di Controllo", + "layers": "Livelli", + "detail": "Dettaglio", + "graphs": "Grafici" + }, + "controlPanel": { + "lockViewport": "Blocca la viewport per evitare movimenti accidentali della mappa. Durante il filtraggio temporale, la rotellina del mouse aggiorna il tempo", + "enableTimeControls": "Abilita i controlli temporali per filtrare le tracce per tempo", + "noTimeData": "Nessun dato temporale disponibile", + "copySnapshot": "Copia uno snapshot della mappa negli appunti", + "lockViewportFirst": "Blocca la viewport per poter fare uno snapshot della mappa", + "saveChanges": "Salva modifiche", + "documentUnchanged": "Documento non modificato", + "undoRedo": "Annulla/Ripeti ...", + "undo": "Annulla ...", + "redo": "Ripeti ...", + "nothingToUndoRedo": "Niente da annullare/ripetere", + "selectVersionTo": "Seleziona versione da", + "currentState": "stato attuale", + "restoreVersion": "Ripristina Versione", + "hidingViewportChanges": "Nascondi modifiche alla viewport", + "showingViewportChanges": "Mostra modifiche alla viewport", + "start": "Inizio", + "step": "Passo", + "end": "Fine", + "jumpToStart": "Vai all'inizio", + "stepBackward": "Passo indietro", + "stepForward": "Passo avanti", + "jumpToEnd": "Vai alla fine" + }, + "layers": { + "collapseAll": "Comprimi Tutto", + "clearSelection": "Cancella selezione", + "deleteSelected": "Elimina elementi selezionati", + "selectToEnable": "Seleziona elementi per abilitare l'eliminazione", + "copySelected": "Copia elementi selezionati", + "selectNonTrack": "Seleziona elementi non traccia per abilitare la copia", + "noValidData": "Nessun dato GeoJSON valido negli appunti", + "pasteData": "Incolla dati GeoJSON", + "cancelTimeFilter": "Annulla filtro delle caratteristiche per tempo", + "filterByTime": "Filtra caratteristiche per tempo", + "viewGraph": "Visualizza grafico delle caratteristiche selezionate", + "selectTimeFeature": "Seleziona una caratteristica temporale per abilitare i grafici", + "copyError": "Errore di copia: ", + "pasteError": "Errore di incollaggio: ", + "readClipboardError": "Impossibile leggere gli appunti: ", + "invalidGeoJSON": "Formato GeoJSON non valido", + "units": "Unità", + "tracks": "Tracce", + "fields": "Campi", + "zones": "Zone", + "points": "Punti", + "backdrops": "Sfondi", + "addTrack": "Aggiungi nuova traccia", + "addField": "Aggiungi nuovo campo", + "addZone": "Aggiungi nuova zona", + "addPoint": "Aggiungi nuovo punto", + "addBackdrop": "Aggiungi nuovo sfondo", + "addToExistingTrack": "Aggiungi a traccia esistente", + "selectTrackToAddData": "Seleziona una traccia a cui aggiungere dati", + "noTracksAvailable": "Nessuna traccia disponibile", + "pleaseSelectTrack": "Seleziona una traccia", + "add": "Aggiungi", + "extraDetailsForNewTrack": "Inserisci dettagli per la nuova traccia", + "year": "Anno", + "yearRequired": "Inserisci l'anno", + "month": "Mese", + "monthRequired": "Inserisci il mese" + }, + "forms": { + "core": { + "delete": "Elimina", + "deleteFeature": "Elimina elemento", + "cancel": "Annulla", + "cancelCreation": "Annulla creazione nuovo elemento", + "reset": "Ripristina", + "resetChanges": "Ripristina modifiche per questo elemento", + "save": "Salva", + "create": "Crea", + "saveEdits": "Salva modifiche" + }, + "common": { + "name": "Nome", + "shortName": "Nome breve", + "visible": "Visibile", + "color": "Colore", + "fill": "Riempimento", + "colorRequired": "il colore è obbligatorio!", + "nameRequired": "Inserisci un nome!", + "shortNameRequired": "Inserisci un nome breve!", + "position": "Posizione", + "time": "Tempo", + "timeEnd": "Fine tempo", + "timeEndAfterTime": "La fine del tempo deve essere dopo l'inizio!", + "start": "Inizio", + "end": "Fine", + "url": "URL", + "urlRequired": "Inserisci un URL!", + "maxNativeZoom": "Zoom nativo massimo", + "maxNativeZoomRequired": "Inserisci lo zoom nativo massimo!", + "maxZoom": "Zoom massimo", + "maxZoomRequired": "Inserisci lo zoom massimo!", + "environment": "Ambiente", + "environmentRequired": "Specifica l'ambiente/simbolo", + "markers": "Marcatori", + "labels": "Etichette", + "symbols": "Simboli", + "shape": "Forma", + "edit": "Modifica", + "topLeft": "In alto a sinistra", + "bottomRight": "In basso a destra", + "origin": "Origine", + "radiusM": "Raggio (m)", + "innerRadiusM": "Raggio interno (m)", + "outerRadiusM": "Raggio esterno (m)", + "startAngle": "Angolo iniziale", + "endAngle": "Angolo finale", + "polygonCoordinates": "Coordinate del poligono", + "addCoordinate": "Aggiungi coordinata" + }, + "multiFeature": { + "itemsSelected": "elementi selezionati", + "visibility": "Visibilità", + "hideAll": "Nascondi tutto", + "showAll": "Mostra tutto", + "mixedVisibility": "Visibilità mista, clicca per nascondere tutto", + "currentColor": "Colore attuale:", + "mixedColors": "Colori misti" + } + }, + "graphs": { + "available": "Disponibili", + "selected": "Selezionati" + } +} diff --git a/src/i18n/locales/nl.json b/src/i18n/locales/nl.json new file mode 100644 index 00000000..6c65d7c8 --- /dev/null +++ b/src/i18n/locales/nl.json @@ -0,0 +1,174 @@ +{ + "welcome": { + "title": "Welkom bij Albatross", + "subtitle": "Open een bestaand document of maak een nieuw document", + "new": "Nieuw", + "samplePlot": "Voorbeeldplot", + "open": "Openen", + "background": "Achtergrond over het hulpmiddel, wie te contacteren voor ondersteuning. Achtergrond over het hulpmiddel, wie te contacteren voor ondersteuning. Achtergrond over het hulpmiddel, wie te contacteren voor ondersteuning. Achtergrond over het hulpmiddel, wie te contacteren voor ondersteuning. Achtergrond over het hulpmiddel, wie te contacteren voor ondersteuning. Achtergrond over het hulpmiddel, wie te contacteren voor ondersteuning." + }, + "common": { + "branch": "Branch", + "build": "Build", + "language": "Taal" + }, + "documents": { + "newTab": "Nieuw Tabblad", + "new": "Nieuw", + "openExisting": "Open Bestaand Document", + "open": "Openen", + "lightMode": "Schakel naar Lichte Modus", + "darkMode": "Schakel naar Donkere Modus", + "documentName": "Geef een naam voor het document", + "closeTab": "Tabblad Sluiten", + "close": "Sluiten", + "cancel": "Annuleren", + "confirmClose": "Weet je zeker dat je dit tabblad wilt sluiten?", + "error": "Fout", + "warning": "Waarschuwing", + "info": "Informatie", + "invalidFile": "Ongeldig Bestand", + "loadingError": "Probleem bij het laden van bestand: ", + "multipleFilesError": "Er kan slechts één bestand tegelijk worden geladen", + "fileTypeError": "Alleen .json en .geojson bestanden worden ondersteund", + "invalidJsonError": "De bestandsinhoud is geen geldig JSON-formaat. Controleer het bestand en probeer het opnieuw.", + "geoJsonError": "Bestand moet een GeoJSON FeatureCollection of Feature bevatten", + "handlingError": "Verwerkingsfout: ", + "localSaveNotSupported": "Lokaal opslaan wordt niet ondersteund in de browser" + }, + "document": { + "controlPanel": "Bedieningspaneel", + "layers": "Lagen", + "detail": "Detail", + "graphs": "Grafieken" + }, + "controlPanel": { + "lockViewport": "Vergrendel viewport om onbedoelde kaartbewegingen te voorkomen. Bij tijdfiltering werkt het muiswiel als tijdbesturing", + "enableTimeControls": "Schakel tijdbesturing in om tracks op tijd te filteren", + "noTimeData": "Geen tijdgegevens beschikbaar", + "copySnapshot": "Kopieer een snapshot van de kaart naar het klembord", + "lockViewportFirst": "Vergrendel eerst de viewport om een snapshot van de kaart te maken", + "saveChanges": "Wijzigingen opslaan", + "documentUnchanged": "Document ongewijzigd", + "undoRedo": "Ongedaan maken/Opnieuw ...", + "undo": "Ongedaan maken ...", + "redo": "Opnieuw ...", + "nothingToUndoRedo": "Niets om ongedaan te maken/opnieuw uit te voeren", + "selectVersionTo": "Selecteer versie om", + "currentState": "huidige staat", + "restoreVersion": "Versie herstellen", + "hidingViewportChanges": "Viewport-wijzigingen verbergen", + "showingViewportChanges": "Viewport-wijzigingen tonen", + "start": "Start", + "step": "Stap", + "end": "Einde", + "jumpToStart": "Ga naar begin", + "stepBackward": "Stap terug", + "stepForward": "Stap vooruit", + "jumpToEnd": "Ga naar einde" + }, + "layers": { + "collapseAll": "Alles Inklappen", + "clearSelection": "Selectie wissen", + "deleteSelected": "Geselecteerde items verwijderen", + "selectToEnable": "Selecteer items om verwijderen mogelijk te maken", + "copySelected": "Geselecteerde items kopiëren", + "selectNonTrack": "Selecteer niet-track items om kopiëren mogelijk te maken", + "noValidData": "Geen geldige GeoJSON-gegevens in klembord", + "pasteData": "GeoJSON-gegevens plakken", + "cancelTimeFilter": "Tijdfilter voor objecten annuleren", + "filterByTime": "Objecten filteren op tijd", + "viewGraph": "Grafiek van geselecteerde objecten bekijken", + "selectTimeFeature": "Selecteer een tijdgerelateerd object om grafieken te activeren", + "copyError": "Kopiëeerfout: ", + "pasteError": "Plakfout: ", + "readClipboardError": "Kan klembord niet lezen: ", + "invalidGeoJSON": "Ongeldig GeoJSON-formaat", + "units": "Eenheden", + "tracks": "Tracks", + "fields": "Velden", + "zones": "Zones", + "points": "Punten", + "backdrops": "Achtergronden", + "addTrack": "Nieuwe track toevoegen", + "addField": "Nieuw veld toevoegen", + "addZone": "Nieuwe zone toevoegen", + "addPoint": "Nieuw punt toevoegen", + "addBackdrop": "Nieuwe achtergrond toevoegen", + "addToExistingTrack": "Toevoegen aan bestaande track", + "selectTrackToAddData": "Selecteer een track om gegevens aan toe te voegen", + "noTracksAvailable": "Geen tracks beschikbaar", + "pleaseSelectTrack": "Selecteer een track", + "add": "Toevoegen", + "extraDetailsForNewTrack": "Voer details in voor de nieuwe track", + "year": "Jaar", + "yearRequired": "Voer het jaar in", + "month": "Maand", + "monthRequired": "Voer de maand in" + }, + "forms": { + "core": { + "delete": "Verwijderen", + "deleteFeature": "Object verwijderen", + "cancel": "Annuleren", + "cancelCreation": "Aanmaken van nieuw object annuleren", + "reset": "Resetten", + "resetChanges": "Wijzigingen voor dit object resetten", + "save": "Opslaan", + "create": "Aanmaken", + "saveEdits": "Wijzigingen opslaan" + }, + "common": { + "name": "Naam", + "shortName": "Korte naam", + "visible": "Zichtbaar", + "color": "Kleur", + "fill": "Vulling", + "colorRequired": "kleur is vereist!", + "nameRequired": "Voer een naam in!", + "shortNameRequired": "Voer een korte naam in!", + "position": "Positie", + "time": "Tijd", + "timeEnd": "Eindtijd", + "timeEndAfterTime": "Eindtijd moet na begintijd zijn!", + "start": "Start", + "end": "Einde", + "url": "URL", + "urlRequired": "Voer een URL in!", + "maxNativeZoom": "Max Eigen Zoom", + "maxNativeZoomRequired": "Voer max eigen zoom in!", + "maxZoom": "Max Zoom", + "maxZoomRequired": "Voer max zoom in!", + "environment": "Omgeving", + "environmentRequired": "Specificeer de omgeving/symbool", + "markers": "Markers", + "labels": "Labels", + "symbols": "Symbolen", + "shape": "Vorm", + "edit": "Bewerken", + "topLeft": "Linksboven", + "bottomRight": "Rechtsonder", + "origin": "Oorsprong", + "radiusM": "Straal (m)", + "innerRadiusM": "Binnenstraal (m)", + "outerRadiusM": "Buitenstraal (m)", + "startAngle": "Starthoek", + "endAngle": "Eindhoek", + "polygonCoordinates": "Polygoon coördinaten", + "addCoordinate": "Coördinaat toevoegen" + }, + "multiFeature": { + "itemsSelected": "items geselecteerd", + "visibility": "Zichtbaarheid", + "hideAll": "Alles verbergen", + "showAll": "Alles tonen", + "mixedVisibility": "Gemengde zichtbaarheid, klik om alles te verbergen", + "currentColor": "Huidige kleur:", + "mixedColors": "Gemengde kleuren" + } + }, + "graphs": { + "available": "Beschikbaar", + "selected": "Geselecteerd" + } +} diff --git a/src/main.tsx b/src/main.tsx index 444cae67..71a8a472 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -7,6 +7,8 @@ import { AppContextProvider } from './state/AppContextProvider' import { ErrorBoundary } from 'react-error-boundary' import '@ant-design/v5-patch-for-react-19' // shims to allow ant5 work on react 19 import { ConfigProvider, theme } from 'antd' +// Import i18n configuration +import './i18n/i18n' import { AppContext } from './state/AppContext' const fallbackRender: React.FC<{ error: Error, resetErrorBoundary: () => void }> = ({ error, resetErrorBoundary }) => { diff --git a/tests/control-panel/undo-redo-functionality.spec.ts b/tests/control-panel/undo-redo-functionality.spec.ts index e8b3034d..dd2e2d7f 100644 --- a/tests/control-panel/undo-redo-functionality.spec.ts +++ b/tests/control-panel/undo-redo-functionality.spec.ts @@ -51,11 +51,14 @@ test('Test undo/redo button in control panel', async ({ page }) => { await undoRedoButton.click() // Verify the undo modal appears - const undoModal = page.locator('.ant-modal-content').filter({ hasText: 'Select a version' }) + const undoModal = page.locator('.undo-title') await expect(undoModal).toBeVisible() - + + // Wait for the UI to update + await page.waitForTimeout(100) + // Close the modal - await undoModal.locator('button:has-text("Cancel")').click() + await page.locator('.undo-cancel-button').click() // Wait for the UI to update await page.waitForTimeout(100) @@ -70,11 +73,17 @@ test('Test undo/redo button in control panel', async ({ page }) => { await undoRedoButton.click() await expect(undoModal).toBeVisible() + // Wait for the UI to update + await page.waitForTimeout(100) + // Select the first version - await undoModal.locator('.ant-list-items').locator('.ant-list-item').first().click() + await page.locator('.undo-list-item').first().click() // do restore - await page.locator('button:has-text("Restore Version")').click() + await page.locator('.undo-restore-button').click() + + // Wait for the UI to update + await page.waitForTimeout(100) // check undo modal closed await expect(undoModal).not.toBeVisible() diff --git a/tests/details/edit-simple-details.spec.ts b/tests/details/edit-simple-details.spec.ts index 0d9ad923..30c830b0 100644 --- a/tests/details/edit-simple-details.spec.ts +++ b/tests/details/edit-simple-details.spec.ts @@ -25,13 +25,14 @@ test('test adding feature', async ({ page }) => { // introduce pause await page.waitForTimeout(100) await page.waitForSelector('.create-track') - await page.getByRole('tabpanel', { name: 'Create new track' }).getByLabel('Name').click() - await page.getByRole('tabpanel', { name: 'Create new track' }).getByLabel('Name').fill(newName) - await page.getByRole('tabpanel', { name: 'Create new track' }).getByLabel('Name').press('Tab') - await page.locator('#createTrack_shortName').getByRole('textbox').fill('DAIR') - await page.getByRole('spinbutton', { name: '* Year :' }).click() - await page.getByRole('button', { name: 'Increase Value' }).first().click() - await page.getByRole('button', { name: 'Create' }).click() + await page.locator('.create-track').locator('.create-track-name').click() + await page.locator('.create-track').locator('.create-track-name').fill(newName) + await page.locator('.create-track').locator('.create-track-name').press('Tab') + await page.locator('.create-track').locator('.create-track-shortName').fill('DAIR') + await page.locator('.create-track').locator('#createTrack_initialYear').click() + await page.locator('.create-track').locator('.create-track-year').locator('.ant-input-number-handler-up').click() + await page.locator('.create-track').locator('.create-track-year').locator('.ant-input-number-handler-up').click() + await page.locator('.create-track-button').click() await page.waitForTimeout(100) await page.getByText('AIR').click() await expect(page.getByRole('tree').getByText(newName)).toBeVisible() diff --git a/yarn.lock b/yarn.lock index d71824e5..3178f24a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -960,6 +960,13 @@ dependencies: regenerator-runtime "^0.14.0" +"@babel/runtime@^7.25.0", "@babel/runtime@^7.26.10": + version "7.27.0" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.27.0.tgz#fbee7cf97c709518ecc1f590984481d5460d4762" + integrity sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw== + dependencies: + regenerator-runtime "^0.14.0" + "@babel/template@^7.25.9", "@babel/template@^7.3.3": version "7.25.9" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.25.9.tgz#ecb62d81a8a6f5dc5fe8abfc3901fc52ddf15016" @@ -7346,6 +7353,13 @@ html-escaper@^2.0.0: resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== +html-parse-stringify@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz#dfc1017347ce9f77c8141a507f233040c59c55d2" + integrity sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg== + dependencies: + void-elements "3.1.0" + http-cache-semantics@^4.0.0, http-cache-semantics@^4.1.0: version "4.1.1" resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz#abe02fcb2985460bf0323be664436ec3476a6d5a" @@ -7409,6 +7423,20 @@ husky@^9.1.7: resolved "https://registry.yarnpkg.com/husky/-/husky-9.1.7.tgz#d46a38035d101b46a70456a850ff4201344c0b2d" integrity sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA== +i18next-browser-languagedetector@^8.0.4: + version "8.0.4" + resolved "https://registry.yarnpkg.com/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.0.4.tgz#9b16f6440b6aad3521f2ab1a2ffbb7d917397df2" + integrity sha512-f3frU3pIxD50/Tz20zx9TD9HobKYg47fmAETb117GKGPrhwcSSPJDoCposXlVycVebQ9GQohC3Efbpq7/nnJ5w== + dependencies: + "@babel/runtime" "^7.23.2" + +i18next@^24.2.3: + version "24.2.3" + resolved "https://registry.yarnpkg.com/i18next/-/i18next-24.2.3.tgz#3a05f72615cbd7c00d7e348667e2aabef1df753b" + integrity sha512-lfbf80OzkocvX7nmZtu7nSTNbrTYR52sLWxPtlXX1zAhVw8WEnFk4puUkCR4B1dNQwbSpEHHHemcZu//7EcB7A== + dependencies: + "@babel/runtime" "^7.26.10" + iconv-lite@0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" @@ -10054,6 +10082,14 @@ react-fast-compare@^3.2.0: resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.2.tgz#929a97a532304ce9fee4bcae44234f1ce2c21d49" integrity sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ== +react-i18next@^15.4.1: + version "15.4.1" + resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-15.4.1.tgz#33f3e89c2f6c68e2bfcbf9aa59986ad42fe78758" + integrity sha512-ahGab+IaSgZmNPYXdV1n+OYky95TGpFwnKRflX/16dY04DsYYKHtVLjeny7sBSCREEcoMbAgSkFiGLF5g5Oofw== + dependencies: + "@babel/runtime" "^7.25.0" + html-parse-stringify "^3.0.1" + react-icons@^4.2.0: version "4.12.0" resolved "https://registry.yarnpkg.com/react-icons/-/react-icons-4.12.0.tgz#54806159a966961bfd5cdb26e492f4dafd6a8d78" @@ -11946,6 +11982,11 @@ vite@^5.4.10: optionalDependencies: fsevents "~2.3.3" +void-elements@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-3.1.0.tgz#614f7fbf8d801f0bb5f0661f5b2f5785750e4f09" + integrity sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w== + walker@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.8.tgz#bd498db477afe573dc04185f011d3ab8a8d7653f"