From 2e683f8609d014f6ddfd092f1310ca2d0929c7ef Mon Sep 17 00:00:00 2001 From: Maurice Date: Tue, 3 Nov 2026 20:19:43 +0100 Subject: [PATCH] fix(tours): keep the waypoints of a GPX/KML file that also holds a track With the Tours addon on, a file picked or dropped on the places column goes into the Tours import as soon as it holds one line, and that import reads lines only. A Sygic or Trip-o-matic export (a route plus its stops) came in as one tour and every place was dropped without a word. The preview now counts a GPX file's waypoints as it already counted a KML file's pins, and the dialog offers to add those points as places of the trip, on by default. Confirming runs the Tours import for the lines and the places import (points only) for the same files. A file whose tracks the trip already holds can still bring its points. Closes #2969 --- .../Tours/TourGpxPreviewDialog.test.tsx | 49 ++++++++++++-- .../components/Tours/TourGpxPreviewDialog.tsx | 66 ++++++++++++++----- .../components/Tours/useTourGpxImport.test.ts | 65 ++++++++++++++++++ .../src/components/Tours/useTourGpxImport.ts | 58 ++++++++++++---- .../nest/tours/tour-file-import.helpers.ts | 16 +++-- server/src/nest/tours/tours.service.ts | 4 +- server/tests/e2e/tours.e2e.test.ts | 12 ++++ shared/src/i18n/ar/tours.ts | 5 ++ shared/src/i18n/az/tours.ts | 5 ++ shared/src/i18n/br/tours.ts | 5 ++ shared/src/i18n/ca/tours.ts | 5 ++ shared/src/i18n/cs/tours.ts | 5 ++ shared/src/i18n/de/tours.ts | 5 ++ shared/src/i18n/en/tours.ts | 5 ++ shared/src/i18n/es/tours.ts | 5 ++ shared/src/i18n/et/tours.ts | 5 ++ shared/src/i18n/fr/tours.ts | 5 ++ shared/src/i18n/gr/tours.ts | 5 ++ shared/src/i18n/he/tours.ts | 5 ++ shared/src/i18n/hu/tours.ts | 5 ++ shared/src/i18n/id/tours.ts | 5 ++ shared/src/i18n/it/tours.ts | 5 ++ shared/src/i18n/ja/tours.ts | 5 ++ shared/src/i18n/ko/tours.ts | 5 ++ shared/src/i18n/nl/tours.ts | 5 ++ shared/src/i18n/pl/tours.ts | 5 ++ shared/src/i18n/ru/tours.ts | 5 ++ shared/src/i18n/sk/tours.ts | 5 ++ shared/src/i18n/sv/tours.ts | 5 ++ shared/src/i18n/th/tours.ts | 5 ++ shared/src/i18n/tr/tours.ts | 5 ++ shared/src/i18n/uk/tours.ts | 5 ++ shared/src/i18n/vi/tours.ts | 5 ++ shared/src/i18n/zh-TW/tours.ts | 5 ++ shared/src/i18n/zh/tours.ts | 5 ++ shared/src/tours/tour-gpx-import.schema.ts | 6 +- 36 files changed, 374 insertions(+), 42 deletions(-) diff --git a/client/src/components/Tours/TourGpxPreviewDialog.test.tsx b/client/src/components/Tours/TourGpxPreviewDialog.test.tsx index 4cb876dc8..9dcea28b8 100644 --- a/client/src/components/Tours/TourGpxPreviewDialog.test.tsx +++ b/client/src/components/Tours/TourGpxPreviewDialog.test.tsx @@ -1,8 +1,8 @@ -// FE-GPX-PREVIEW-001 to FE-GPX-PREVIEW-004 +// FE-GPX-PREVIEW-001 to FE-GPX-PREVIEW-007 // The GPX preview as ACT-01 uses it: preset to the tour's type, a city walk // imported as one, and when a track was recorded shown where it carried times. import { fireEvent, render, screen } from '@testing-library/react'; -import type { TourGpxPreviewTrack } from '@trek/shared'; +import type { TourFileLeftOut, TourGpxPreviewTrack } from '@trek/shared'; import { describe, expect, it, vi } from 'vitest'; import TourGpxPreviewDialog from './TourGpxPreviewDialog'; import type { TourGpxImport } from './useTourGpxImport'; @@ -27,10 +27,10 @@ function track(overrides: Partial = {}): TourGpxPreviewTrac }; } -function gpxImport(tracks: TourGpxPreviewTrack[]): TourGpxImport { +function gpxImport(tracks: TourGpxPreviewTrack[], leftOut?: TourFileLeftOut): TourGpxImport { return { importing: false, - preview: [{ file: new File([''], 'morning.gpx'), tracks }], + preview: [{ file: new File([''], 'morning.gpx'), tracks, leftOut }], confirm: vi.fn(), cancel: vi.fn(), pick: vi.fn(), @@ -63,19 +63,54 @@ describe('TourGpxPreviewDialog (ACT-01)', () => { /> ); fireEvent.click(screen.getByTestId('gpx-preview-import')); - expect(current.confirm).toHaveBeenCalledWith({ tour_type: 'kayak', boat_type: 'sup', water_context: 'lake' }); + expect(current.confirm).toHaveBeenCalledWith( + { tour_type: 'kayak', boat_type: 'sup', water_context: 'lake' }, + { points: false } + ); }); it('FE-GPX-PREVIEW-003: a city walk is imported as a city walk, and without a preset as a hike', () => { const walk = gpxImport([track()]); const { unmount } = render(); fireEvent.click(screen.getByTestId('gpx-preview-import')); - expect(walk.confirm).toHaveBeenCalledWith({ tour_type: 'walk' }); + expect(walk.confirm).toHaveBeenCalledWith({ tour_type: 'walk' }, { points: false }); unmount(); const hike = gpxImport([track()]); render(); fireEvent.click(screen.getByTestId('gpx-preview-import')); - expect(hike.confirm).toHaveBeenCalledWith({ tour_type: 'hike' }); + expect(hike.confirm).toHaveBeenCalledWith({ tour_type: 'hike' }, { points: false }); + }); +}); + +describe('TourGpxPreviewDialog with points beside the lines (#2969)', () => { + it('FE-GPX-PREVIEW-005: offers the points as places, on by default, and imports them with the tour', () => { + const current = gpxImport([track()], { points: 4, areas: 0, other: 0 }); + render(); + expect(screen.getByTestId('gpx-preview-points')).toBeInTheDocument(); + // Added as places, they are not listed as left out. + expect(screen.queryByTestId('gpx-preview-left-out')).toBeNull(); + fireEvent.click(screen.getByTestId('gpx-preview-import')); + expect(current.confirm).toHaveBeenCalledWith({ tour_type: 'hike' }, { points: true }); + }); + + it('FE-GPX-PREVIEW-006: switched off, the points are named as left out and not imported', () => { + const current = gpxImport([track()], { points: 4, areas: 1, other: 0 }); + render(); + fireEvent.click(screen.getByRole('button', { name: /tours\.import\.pointsAsPlaces|Add the points as places/ })); + expect(screen.getByTestId('gpx-preview-left-out')).toHaveTextContent(/tours\.import\.leftOutPoints|4 pins/); + fireEvent.click(screen.getByTestId('gpx-preview-import')); + expect(current.confirm).toHaveBeenCalledWith({ tour_type: 'hike' }, { points: false }); + }); + + it('FE-GPX-PREVIEW-007: a file whose every track the trip holds can still bring its points', () => { + const current = gpxImport([track({ duplicate: true })], { points: 2, areas: 0, other: 0 }); + render(); + const button = screen.getByTestId('gpx-preview-import'); + expect(button).toBeEnabled(); + expect(button).toHaveTextContent(/tours\.import\.pointsConfirm|Add places/); + expect(screen.queryByText(/tours\.import\.previewAllDuplicates|nothing to import/)).toBeNull(); + fireEvent.click(button); + expect(current.confirm).toHaveBeenCalledWith({ tour_type: 'hike' }, { points: true }); }); }); diff --git a/client/src/components/Tours/TourGpxPreviewDialog.tsx b/client/src/components/Tours/TourGpxPreviewDialog.tsx index a0b947f4a..c580db8dd 100644 --- a/client/src/components/Tours/TourGpxPreviewDialog.tsx +++ b/client/src/components/Tours/TourGpxPreviewDialog.tsx @@ -1,6 +1,6 @@ import type { TourFileLeftOut, TourGpxImportFields } from '@trek/shared'; import { AlertTriangle, Ban, Clock, FileDown, Layers, MapPin, Shapes } from 'lucide-react'; -import { useId, useMemo } from 'react'; +import { useId, useMemo, useState } from 'react'; import { OFM_POSITRON } from '../../constants/mapDefaults'; import { useTileUrl } from '../../hooks/useTileUrl'; import { useTranslation } from '../../i18n'; @@ -8,6 +8,7 @@ import { useSettingsStore } from '../../store/settingsStore'; import { formatDistance, formatElevation } from '../../utils/units'; import { MapViewAuto } from '../Map/MapViewAuto'; import { SoftPill } from '../Planner/planParts'; +import ToggleSwitch from '../Settings/ToggleSwitch'; import { DialogButton, DialogFooter, @@ -98,8 +99,8 @@ function PreviewTrackRow({ ); } -/** What all the files held besides their lines, summed by kind; null when they held nothing else. */ -function leftOutOf(files: readonly GpxPreviewFile[]): TourFileLeftOut | null { +/** What all the files held besides their lines, summed by kind. */ +function leftOutOf(files: readonly GpxPreviewFile[]): TourFileLeftOut { const sum = { points: 0, areas: 0, other: 0 }; for (const { leftOut } of files) { if (!leftOut) continue; @@ -107,18 +108,46 @@ function leftOutOf(files: readonly GpxPreviewFile[]): TourFileLeftOut | null { sum.areas += leftOut.areas; sum.other += leftOut.other; } - return sum.points + sum.areas + sum.other > 0 ? sum : null; + return sum; } /** - * IO-02: what a KML or KMZ file held besides its lines, named before the - * import so nothing goes missing unseen: a dashed card with each kind as a - * counted badge, and why in the info dot. + * #2969: the single points of the files (GPX waypoints, KML pins), which no + * tour is made of, offered as places of the trip. On by default: a file with + * a route and its stops is usually meant as both. */ -function PreviewLeftOut({ files }: { files: readonly GpxPreviewFile[] }) { +function PreviewPoints({ count, on, onToggle }: { count: number; on: boolean; onToggle: () => void }) { const { t } = useTranslation(); - const leftOut = leftOutOf(files); - if (!leftOut) return null; + const hintId = useId(); + return ( +
+
+ ); +} + +/** + * IO-02: what a file held besides its lines and does not take in, named + * before the import so nothing goes missing unseen: a dashed card with each + * kind as a counted badge, and why in the info dot. Its points only while + * they are not added as places. + */ +function PreviewLeftOut({ leftOut }: { leftOut: TourFileLeftOut }) { + const { t } = useTranslation(); + if (leftOut.points + leftOut.areas + leftOut.other === 0) return null; const kinds = [ { key: 'points', count: leftOut.points, icon: MapPin, labelKey: 'tours.import.leftOutPoints' }, { key: 'areas', count: leftOut.areas, icon: Shapes, labelKey: 'tours.import.leftOutAreas' }, @@ -183,6 +212,9 @@ function PreviewDialog({ const labelId = useId(); const tileUrl = useTileUrl(OFM_POSITRON); const choices = useGpxImportChoices(initial); + const leftOut = useMemo(() => leftOutOf(files), [files]); + const [withPoints, setWithPoints] = useState(true); + const addsPoints = withPoints && leftOut.points > 0; const tracks = files.flatMap((file) => file.tracks.map((track) => ({ file: file.file, track }))); const fresh = tracks.filter(({ track }) => !track.duplicate).length; @@ -194,7 +226,8 @@ function PreviewDialog({ [files] ); const focusPoints = useMemo(() => lines.flat(), [lines]); - const canImport = fresh > 0 && importMissing(choices.chosen) === null && !gpxImport.importing; + const canImport = + (fresh > 0 || addsPoints) && (fresh === 0 || importMissing(choices.chosen) === null) && !gpxImport.importing; const fields = importFields(choices.chosen); const title = files.length === 1 ? files[0].file.name : t('tours.import.previewFiles', { count: files.length }); @@ -231,9 +264,9 @@ function PreviewDialog({ data-testid="gpx-preview-import" icon={} disabled={!canImport} - onClick={() => void gpxImport.confirm(fields)} + onClick={() => void gpxImport.confirm(fields, { points: addsPoints })} > - {t('tours.import.previewConfirm', { count: fresh })} + {fresh > 0 ? t('tours.import.previewConfirm', { count: fresh }) : t('tours.import.pointsConfirm')} } @@ -258,8 +291,11 @@ function PreviewDialog({ ))} - - {fresh === 0 && ( + {leftOut.points > 0 && ( + setWithPoints((on) => !on)} /> + )} + + {fresh === 0 && !addsPoints && (
}> {t('tours.import.previewAllDuplicates')} diff --git a/client/src/components/Tours/useTourGpxImport.test.ts b/client/src/components/Tours/useTourGpxImport.test.ts index 7fd4cc958..efb44d98b 100644 --- a/client/src/components/Tours/useTourGpxImport.test.ts +++ b/client/src/components/Tours/useTourGpxImport.test.ts @@ -5,7 +5,9 @@ import { act, renderHook } from '@testing-library/react'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { resetAllStores } from '../../../tests/helpers/store'; +import { placesApi } from '../../api/client'; import { tourRepo } from '../../repo/tourRepo'; +import { useTripStore } from '../../store/tripStore'; import { useTourGpxImport } from './useTourGpxImport'; // The toasts are caught at useToast, so no window global has to be set for them. @@ -67,3 +69,66 @@ describe('useTourGpxImport without a line in the file', () => { expect(addToast).toHaveBeenCalledWith('tours.import.unreadableIn', 'error', undefined); }); }); + +describe('useTourGpxImport with points beside the lines (#2969)', () => { + const route = new File([''], 'sygic.gpx'); + const pins = new File([''], 'pins.kml'); + const line = { + name: 'Route', + geometry: [[47, 11] as [number, number], [47.01, 11] as [number, number]], + point_count: 2, + distance_km: 1, + elevation_gain_m: null, + elevation_loss_m: null, + duplicate: false, + }; + + beforeEach(() => { + useTripStore.setState({ refreshPlaces: vi.fn(async () => {}) } as never); + vi.spyOn(tourRepo, 'importGpx').mockResolvedValue({ tours: [], skipped: 0, caution: false } as never); + }); + + const previewed = async (preview: (picked: File) => object) => { + vi.spyOn(tourRepo, 'previewGpx').mockImplementation(async (_tripId, picked) => preview(picked as File) as never); + const hook = renderHook(() => useTourGpxImport({ tripId: 2, canEdit: true })); + await act(() => hook.result.current.importFiles([route, pins])); + return hook; + }; + + it('adds the points of each file as places through the places import, waypoints only', async () => { + const importGpx = vi.spyOn(placesApi, 'importGpx').mockResolvedValue({ count: 3 }); + const importMap = vi.spyOn(placesApi, 'importMapFile').mockResolvedValue({ count: 2 }); + const { result } = await previewed((picked) => ({ + tracks: [line], + left_out: { points: picked === route ? 3 : 2, areas: 0, other: 0 }, + })); + await act(() => result.current.confirm({ tour_type: 'hike' }, { points: true })); + expect(importGpx).toHaveBeenCalledWith(2, route, { waypoints: true, routes: false, tracks: false }); + expect(importMap).toHaveBeenCalledWith(2, pins, { points: true, paths: false }); + expect(tourRepo.importGpx).toHaveBeenCalledTimes(2); + expect(addToast).toHaveBeenCalledWith('tours.import.pointsImported', 'success', undefined); + }); + + it('leaves the points out when they are not asked for, and skips the Tours import for a file of known lines', async () => { + const importGpx = vi.spyOn(placesApi, 'importGpx').mockResolvedValue({ count: 3 }); + const { result } = await previewed(() => ({ + tracks: [{ ...line, duplicate: true }], + left_out: { points: 3, areas: 0, other: 0 }, + })); + await act(() => result.current.confirm({ tour_type: 'hike' })); + expect(importGpx).not.toHaveBeenCalled(); + expect(tourRepo.importGpx).not.toHaveBeenCalled(); + }); + + it('names a file whose points could not be added, and still takes in its tour', async () => { + vi.spyOn(placesApi, 'importGpx').mockRejectedValue(new Error('offline')); + vi.spyOn(placesApi, 'importMapFile').mockResolvedValue({ count: 0 }); + const { result } = await previewed((picked) => ({ + tracks: [line], + ...(picked === route ? { left_out: { points: 1, areas: 0, other: 0 } } : {}), + })); + await act(() => result.current.confirm({ tour_type: 'hike' }, { points: true })); + expect(addToast).toHaveBeenCalledWith('tours.import.pointsFailedIn', 'error', undefined); + expect(tourRepo.importGpx).toHaveBeenCalledTimes(2); + }); +}); diff --git a/client/src/components/Tours/useTourGpxImport.ts b/client/src/components/Tours/useTourGpxImport.ts index 435bb0ae9..60b4c7eec 100644 --- a/client/src/components/Tours/useTourGpxImport.ts +++ b/client/src/components/Tours/useTourGpxImport.ts @@ -1,5 +1,6 @@ import type { TourFileLeftOut, TourGpxImportFields, TourGpxPreviewTrack, TourListItem } from '@trek/shared'; import { useCallback, useState } from 'react'; +import { placesApi } from '../../api/client'; import { useTranslation } from '../../i18n'; import { tourRepo } from '../../repo/tourRepo'; import { useTripStore } from '../../store/tripStore'; @@ -40,10 +41,25 @@ function fileErrorMessage(error: unknown, name: string, t: Translate): string { export interface GpxPreviewFile { file: File; tracks: TourGpxPreviewTrack[]; - /** What a KML or KMZ file held besides its lines, which no tour is made of; absent for GPX. */ + /** What the file held besides its lines, which no tour is made of: GPX waypoints, KML pins, areas and the rest. */ leftOut?: TourFileLeftOut; } +/** The points of one file added as places of the trip, through the places import that has always taken them. */ +async function importPoints(tripId: number, file: File): Promise { + const isGpx = file.name.toLowerCase().endsWith('.gpx'); + const result = isGpx + ? await placesApi.importGpx(tripId, file, { waypoints: true, routes: false, tracks: false }) + : await placesApi.importMapFile(tripId, file, { points: true, paths: false }); + return result.count ?? 0; +} + +/** What to do with the previewed files besides their lines. */ +export interface TourImportExtras { + /** Add each file's points (GPX waypoints, KML pins) as places of the trip (#2969). */ + points?: boolean; +} + interface TourGpxImportOptions { tripId: number; canEdit: boolean; @@ -115,26 +131,46 @@ export function useTourGpxImport({ tripId, canEdit, onToursChanged, onImported, [canEdit, fileError, importing, onNoTrack, preview, t, toast, tripId] ); - /** The previewed files imported as the chosen type, summed up in one set of toasts. */ + /** + * The previewed files imported as the chosen type, summed up in one set of + * toasts. With `points`, what each file holds as single points comes in as + * places as well, so a file with a route and its stops loses neither (#2969). + * A file whose every line the trip already holds is not sent to the Tours + * import at all, only for its points. + */ const confirm = useCallback( - async (fields: TourGpxImportFields) => { + async (fields: TourGpxImportFields, extras: TourImportExtras = {}) => { if (!canEdit || !preview || importing) return; setImporting(true); const imported: TourListItem[] = []; let skipped = 0; let caution = false; + let places = 0; try { - for (const { file } of preview) { - try { - const result = await tourRepo.importGpx(tripId, file, fields); - imported.push(...result.tours); - skipped += result.skipped; - caution ||= result.caution; - } catch (error: unknown) { - fileError(error, file); + for (const { file, tracks, leftOut } of preview) { + if (tracks.some((track) => !track.duplicate)) { + try { + const result = await tourRepo.importGpx(tripId, file, fields); + imported.push(...result.tours); + skipped += result.skipped; + caution ||= result.caution; + } catch (error: unknown) { + fileError(error, file); + } + } + if (extras.points && (leftOut?.points ?? 0) > 0) { + try { + places += await importPoints(tripId, file); + } catch { + toast.error(t('tours.import.pointsFailedIn', { name: file.name })); + } } } setPreview(null); + if (places > 0) { + if (imported.length === 0) await refreshPlaces(tripId); + toast.success(t('tours.import.pointsImported', { count: places })); + } if (imported.length > 0) { // The server's place:created skips this socket, so the places are read // again here for the new ones and their lines. Only the places: a whole diff --git a/server/src/nest/tours/tour-file-import.helpers.ts b/server/src/nest/tours/tour-file-import.helpers.ts index 39727cea3..944ba7b16 100644 --- a/server/src/nest/tours/tour-file-import.helpers.ts +++ b/server/src/nest/tours/tour-file-import.helpers.ts @@ -3,8 +3,9 @@ * A GPX file gives its tracks and routes, a KML or KMZ file its lines; both * come back as the same rows, so the preview, the duplicate rule and the one * transaction that stores them are the GPX import's own. Loose points are - * never a tour: a GPX file's waypoints are skipped as they always were, and a - * KML file's pins, areas and the rest are counted for the preview to name. + * never a tour: a GPX file's waypoints and a KML file's pins, areas and the + * rest are counted for the preview, which offers to add the points as places + * of the trip through the places import (#2969). */ import { DomainError } from '../common/domain-error'; import type { PlaceImportService } from '../place-import/place-import.service'; @@ -13,7 +14,7 @@ import type { TourFileLeftOut } from '@trek/shared'; type TourFileFormat = 'gpx' | 'kml' | 'kmz'; -/** A file read for a Tours import: the rows it would store, and for a KML or KMZ file what it holds besides. */ +/** A file read for a Tours import: the rows it would store, and what the file holds besides. */ export interface TourFileRead { rows: PreparedGpxPlace[]; leftOut?: TourFileLeftOut; @@ -56,7 +57,14 @@ export async function readTourFile( importTracks: true, defaultName: fileName, }); - return { rows }; + if (rows.length === 0) return { rows }; + const points = placeImport.readGpx(fileBuffer, { + importWaypoints: true, + importRoutes: false, + importTracks: false, + defaultName: fileName, + }).length; + return points > 0 ? { rows, leftOut: { points, areas: 0, other: 0 } } : { rows }; } try { const { tracks, leftOut } = await placeImport.readMapFileTracks(fileBuffer, fileName ?? ''); diff --git a/server/src/nest/tours/tours.service.ts b/server/src/nest/tours/tours.service.ts index ecc6b766f..4ff0d080c 100644 --- a/server/src/nest/tours/tours.service.ts +++ b/server/src/nest/tours/tours.service.ts @@ -943,8 +943,8 @@ export class ToursService { /** * PAD-02: what a file would import, read with the import's own parser and * duplicate rule and written nowhere. Null when the file holds no track or - * route, the import's own refusal. A KML or KMZ file also says what it holds - * besides its lines (IO-02). + * route, the import's own refusal. The preview also says what the file holds + * besides its lines (IO-02), a GPX file its waypoints (#2969). */ async previewGpx(tripId: string, fileBuffer: Buffer, fileName?: string): Promise { const { rows, leftOut } = await readTourFile(this.placeImport, fileBuffer, fileName); diff --git a/server/tests/e2e/tours.e2e.test.ts b/server/tests/e2e/tours.e2e.test.ts index b1025573e..071587805 100644 --- a/server/tests/e2e/tours.e2e.test.ts +++ b/server/tests/e2e/tours.e2e.test.ts @@ -1049,6 +1049,18 @@ describe('Tours e2e (real guards + temp SQLite)', () => { expect(broadcast).not.toHaveBeenCalled(); }); + it('counts the waypoints beside the track, which no tour takes in (#2969)', async () => { + expect((await previewGpx(OWNER, TRACK_GPX)).body.left_out).toBeUndefined(); + const withPoints = TRACK_GPX.replace( + '', + 'HutLake', + ); + const res = await previewGpx(OWNER, withPoints); + expect(res.body.tracks).toHaveLength(1); + expect(res.body.left_out).toEqual({ points: 2, areas: 0, other: 0 }); + expect(await countRows(orm, Places)).toBe(0); + }); + it('leaves heights it does not have unknown, and names a track the trip already holds', async () => { const flat = await previewGpx(OWNER, FLAT_GPX); expect(flat.body.tracks[0]).toMatchObject({ elevation_gain_m: null, elevation_loss_m: null }); diff --git a/shared/src/i18n/ar/tours.ts b/shared/src/i18n/ar/tours.ts index 8a04af13e..91da46c49 100644 --- a/shared/src/i18n/ar/tours.ts +++ b/shared/src/i18n/ar/tours.ts @@ -967,6 +967,11 @@ const tours: TranslationStrings = { 'tours.import.leftOutOther.two': 'عنصران آخران', 'tours.import.leftOutOther.few': '{count} عناصر أخرى', 'tours.import.leftOutOther.many': '{count} عنصرًا آخر', + 'tours.import.pointsAsPlaces': 'أضف النقاط كأماكن', + 'tours.import.pointsAsPlacesHint': 'النقاط المفردة ليست جزءًا من الجولة، بل تُضاف إلى أماكن الرحلة.', + 'tours.import.pointsConfirm': 'أضف الأماكن', + 'tours.import.pointsFailedIn': 'تعذّرت إضافة النقاط في {name} كأماكن.', + 'tours.import.pointsImported': 'الأماكن المضافة: {count}', 'tours.import.unreadableIn': 'تعذّرت قراءة {name}: الملف تالف أو ليس GPX أو KML أو KMZ', 'tours.import.archiveNoKmlIn': 'لم يُعثر على ملف KML في {name}', 'tours.import.archiveTooLargeIn': '{name} كبير جدًا بعد فك الضغط', diff --git a/shared/src/i18n/az/tours.ts b/shared/src/i18n/az/tours.ts index c64547394..62bd4f92e 100644 --- a/shared/src/i18n/az/tours.ts +++ b/shared/src/i18n/az/tours.ts @@ -956,6 +956,11 @@ const tours: TranslationStrings = { 'tours.import.leftOutAreas.one': '{count} sahə', 'tours.import.leftOutOther': '{count} digər element', 'tours.import.leftOutOther.one': '{count} digər element', + 'tours.import.pointsAsPlaces': 'Nöqtələri məkan kimi əlavə et', + 'tours.import.pointsAsPlacesHint': 'Tək nöqtələr turun bir hissəsi deyil. Onlar əvəzinə səyahətin məkanlarına əlavə olunur.', + 'tours.import.pointsConfirm': 'Məkanları əlavə et', + 'tours.import.pointsFailedIn': '{name} faylındakı nöqtələr məkan kimi əlavə edilə bilmədi.', + 'tours.import.pointsImported': 'Əlavə edilən məkanlar: {count}', 'tours.import.unreadableIn': '{name} oxunmadı: fayl zədələnib və ya GPX, KML, KMZ deyil', 'tours.import.archiveNoKmlIn': '{name} içində KML faylı tapılmadı', 'tours.import.archiveTooLargeIn': '{name} açıldıqdan sonra çox böyükdür', diff --git a/shared/src/i18n/br/tours.ts b/shared/src/i18n/br/tours.ts index ab9dbca6b..288d0b40b 100644 --- a/shared/src/i18n/br/tours.ts +++ b/shared/src/i18n/br/tours.ts @@ -962,6 +962,11 @@ const tours: TranslationStrings = { 'tours.import.leftOutAreas.one': '{count} área', 'tours.import.leftOutOther': '{count} outros itens', 'tours.import.leftOutOther.one': '{count} outro item', + 'tours.import.pointsAsPlaces': 'Adicionar os pontos como lugares', + 'tours.import.pointsAsPlacesHint': 'Pontos avulsos não fazem parte de um passeio. Eles entram nos lugares da viagem.', + 'tours.import.pointsConfirm': 'Adicionar lugares', + 'tours.import.pointsFailedIn': 'Não foi possível adicionar os pontos de {name} como lugares.', + 'tours.import.pointsImported': 'Lugares adicionados: {count}', 'tours.import.unreadableIn': 'Não foi possível ler {name}: o arquivo está danificado ou não é GPX, KML nem KMZ', 'tours.import.archiveNoKmlIn': 'Nenhum arquivo KML encontrado em {name}', 'tours.import.archiveTooLargeIn': '{name} é grande demais depois de descompactado', diff --git a/shared/src/i18n/ca/tours.ts b/shared/src/i18n/ca/tours.ts index a2ab02eae..a76090a1c 100644 --- a/shared/src/i18n/ca/tours.ts +++ b/shared/src/i18n/ca/tours.ts @@ -964,6 +964,11 @@ const tours: TranslationStrings = { 'tours.import.leftOutAreas.one': '{count} àrea', 'tours.import.leftOutOther': '{count} elements més', 'tours.import.leftOutOther.one': '{count} element més', + 'tours.import.pointsAsPlaces': 'Afegeix els punts com a llocs', + 'tours.import.pointsAsPlacesHint': 'Els punts solts no formen part de cap ruta. S’afegeixen als llocs del viatge.', + 'tours.import.pointsConfirm': 'Afegeix llocs', + 'tours.import.pointsFailedIn': 'No s’han pogut afegir els punts de {name} com a llocs.', + 'tours.import.pointsImported': 'Llocs afegits: {count}', 'tours.import.unreadableIn': "No s'ha pogut llegir {name}: el fitxer està malmès o no és GPX, KML ni KMZ", 'tours.import.archiveNoKmlIn': "No s'ha trobat cap fitxer KML a {name}", 'tours.import.archiveTooLargeIn': '{name} és massa gran un cop descomprimit', diff --git a/shared/src/i18n/cs/tours.ts b/shared/src/i18n/cs/tours.ts index f91389d78..19d0adeaa 100644 --- a/shared/src/i18n/cs/tours.ts +++ b/shared/src/i18n/cs/tours.ts @@ -950,6 +950,11 @@ const tours: TranslationStrings = { 'tours.import.leftOutOther': '{count} dalších položek', 'tours.import.leftOutOther.one': '{count} další položka', 'tours.import.leftOutOther.few': '{count} další položky', + 'tours.import.pointsAsPlaces': 'Přidat body jako místa', + 'tours.import.pointsAsPlacesHint': 'Samostatné body nejsou součástí trasy. Přidají se místo toho k místům cesty.', + 'tours.import.pointsConfirm': 'Přidat místa', + 'tours.import.pointsFailedIn': 'Body ze souboru {name} se nepodařilo přidat jako místa.', + 'tours.import.pointsImported': 'Přidaná místa: {count}', 'tours.import.unreadableIn': 'Soubor {name} nelze přečíst: je poškozený nebo to není GPX, KML ani KMZ', 'tours.import.archiveNoKmlIn': 'V souboru {name} není žádný soubor KML', 'tours.import.archiveTooLargeIn': 'Soubor {name} je po rozbalení příliš velký', diff --git a/shared/src/i18n/de/tours.ts b/shared/src/i18n/de/tours.ts index 1d2fdd289..72d7488c6 100644 --- a/shared/src/i18n/de/tours.ts +++ b/shared/src/i18n/de/tours.ts @@ -972,6 +972,11 @@ const tours: TranslationStrings = { 'tours.import.leftOutAreas.one': '{count} Fläche', 'tours.import.leftOutOther': '{count} weitere Elemente', 'tours.import.leftOutOther.one': '{count} weiteres Element', + 'tours.import.pointsAsPlaces': 'Punkte als Orte hinzufügen', + 'tours.import.pointsAsPlacesHint': 'Einzelne Punkte gehören zu keiner Tour. Sie kommen stattdessen zu den Orten der Reise.', + 'tours.import.pointsConfirm': 'Orte hinzufügen', + 'tours.import.pointsFailedIn': 'Die Punkte aus {name} konnten nicht als Orte hinzugefügt werden.', + 'tours.import.pointsImported': 'Orte hinzugefügt: {count}', 'tours.import.unreadableIn': '{name} konnte nicht gelesen werden: Die Datei ist beschädigt oder kein GPX, KML oder KMZ', 'tours.import.archiveNoKmlIn': 'Keine KML-Datei in {name} gefunden', diff --git a/shared/src/i18n/en/tours.ts b/shared/src/i18n/en/tours.ts index 241247b40..134389c73 100644 --- a/shared/src/i18n/en/tours.ts +++ b/shared/src/i18n/en/tours.ts @@ -944,6 +944,11 @@ const tours: TranslationStrings = { 'tours.import.leftOutAreas.one': '{count} area', 'tours.import.leftOutOther': '{count} other items', 'tours.import.leftOutOther.one': '{count} other item', + 'tours.import.pointsAsPlaces': 'Add the points as places', + 'tours.import.pointsAsPlacesHint': 'Single points are not part of a tour. They join the places of the trip instead.', + 'tours.import.pointsConfirm': 'Add places', + 'tours.import.pointsFailedIn': 'The points in {name} could not be added as places.', + 'tours.import.pointsImported': 'Places added: {count}', 'tours.import.unreadableIn': 'Could not read {name}: the file is damaged or not GPX, KML or KMZ', 'tours.import.archiveNoKmlIn': 'No KML file found in {name}', 'tours.import.archiveTooLargeIn': '{name} is too large once unpacked', diff --git a/shared/src/i18n/es/tours.ts b/shared/src/i18n/es/tours.ts index 71aeb34f9..ed7ff0780 100644 --- a/shared/src/i18n/es/tours.ts +++ b/shared/src/i18n/es/tours.ts @@ -961,6 +961,11 @@ const tours: TranslationStrings = { 'tours.import.leftOutAreas.one': '{count} área', 'tours.import.leftOutOther': '{count} elementos más', 'tours.import.leftOutOther.one': '{count} elemento más', + 'tours.import.pointsAsPlaces': 'Añadir los puntos como lugares', + 'tours.import.pointsAsPlacesHint': 'Los puntos sueltos no forman parte de una ruta. Se añaden a los lugares del viaje.', + 'tours.import.pointsConfirm': 'Añadir lugares', + 'tours.import.pointsFailedIn': 'No se pudieron añadir los puntos de {name} como lugares.', + 'tours.import.pointsImported': 'Lugares añadidos: {count}', 'tours.import.unreadableIn': 'No se ha podido leer {name}: el archivo está dañado o no es GPX, KML ni KMZ', 'tours.import.archiveNoKmlIn': 'No hay ningún archivo KML en {name}', 'tours.import.archiveTooLargeIn': '{name} es demasiado grande una vez descomprimido', diff --git a/shared/src/i18n/et/tours.ts b/shared/src/i18n/et/tours.ts index 24cdac66d..e52e143fd 100644 --- a/shared/src/i18n/et/tours.ts +++ b/shared/src/i18n/et/tours.ts @@ -946,6 +946,11 @@ const tours: TranslationStrings = { 'tours.import.leftOutAreas.one': '{count} ala', 'tours.import.leftOutOther': '{count} muud elementi', 'tours.import.leftOutOther.one': '{count} muu element', + 'tours.import.pointsAsPlaces': 'Lisa punktid kohtadena', + 'tours.import.pointsAsPlacesHint': 'Üksikud punktid ei kuulu matka juurde. Need lisatakse hoopis reisi kohtade hulka.', + 'tours.import.pointsConfirm': 'Lisa kohad', + 'tours.import.pointsFailedIn': 'Faili {name} punkte ei õnnestunud kohtadena lisada.', + 'tours.import.pointsImported': 'Lisatud kohti: {count}', 'tours.import.unreadableIn': 'Faili {name} ei saanud lugeda: see on vigane või pole GPX, KML ega KMZ', 'tours.import.archiveNoKmlIn': 'Failis {name} pole KML-faili', 'tours.import.archiveTooLargeIn': 'Fail {name} on lahtipakituna liiga suur', diff --git a/shared/src/i18n/fr/tours.ts b/shared/src/i18n/fr/tours.ts index 409df365c..c73928472 100644 --- a/shared/src/i18n/fr/tours.ts +++ b/shared/src/i18n/fr/tours.ts @@ -979,6 +979,11 @@ const tours: TranslationStrings = { 'tours.import.leftOutAreas.one': '{count} zone', 'tours.import.leftOutOther': '{count} autres éléments', 'tours.import.leftOutOther.one': '{count} autre élément', + 'tours.import.pointsAsPlaces': 'Ajouter les points comme lieux', + 'tours.import.pointsAsPlacesHint': 'Les points isolés ne font pas partie d’un parcours. Ils rejoignent les lieux du voyage.', + 'tours.import.pointsConfirm': 'Ajouter les lieux', + 'tours.import.pointsFailedIn': 'Les points de {name} n’ont pas pu être ajoutés comme lieux.', + 'tours.import.pointsImported': 'Lieux ajoutés : {count}', 'tours.import.unreadableIn': 'Impossible de lire {name} : le fichier est endommagé ou n’est pas un GPX, KML ou KMZ', 'tours.import.archiveNoKmlIn': 'Aucun fichier KML trouvé dans {name}', 'tours.import.archiveTooLargeIn': '{name} est trop volumineux une fois décompressé', diff --git a/shared/src/i18n/gr/tours.ts b/shared/src/i18n/gr/tours.ts index 0034c3e69..89bc86101 100644 --- a/shared/src/i18n/gr/tours.ts +++ b/shared/src/i18n/gr/tours.ts @@ -983,6 +983,11 @@ const tours: TranslationStrings = { 'tours.import.leftOutAreas.one': '{count} περιοχή', 'tours.import.leftOutOther': '{count} άλλα στοιχεία', 'tours.import.leftOutOther.one': '{count} άλλο στοιχείο', + 'tours.import.pointsAsPlaces': 'Προσθήκη των σημείων ως τοποθεσίες', + 'tours.import.pointsAsPlacesHint': 'Τα μεμονωμένα σημεία δεν ανήκουν σε διαδρομή. Προστίθενται στις τοποθεσίες του ταξιδιού.', + 'tours.import.pointsConfirm': 'Προσθήκη τοποθεσιών', + 'tours.import.pointsFailedIn': 'Δεν ήταν δυνατή η προσθήκη των σημείων του {name} ως τοποθεσίες.', + 'tours.import.pointsImported': 'Τοποθεσίες που προστέθηκαν: {count}', 'tours.import.unreadableIn': 'Δεν ήταν δυνατή η ανάγνωση του {name}: το αρχείο είναι κατεστραμμένο ή δεν είναι GPX, KML ή KMZ', 'tours.import.archiveNoKmlIn': 'Δεν βρέθηκε αρχείο KML στο {name}', diff --git a/shared/src/i18n/he/tours.ts b/shared/src/i18n/he/tours.ts index 58fd8fa47..e1a16f9a4 100644 --- a/shared/src/i18n/he/tours.ts +++ b/shared/src/i18n/he/tours.ts @@ -943,6 +943,11 @@ const tours: TranslationStrings = { 'tours.import.leftOutOther': '{count} פריטים אחרים', 'tours.import.leftOutOther.one': 'פריט אחר אחד', 'tours.import.leftOutOther.two': '{count} פריטים אחרים', + 'tours.import.pointsAsPlaces': 'הוספת הנקודות כמקומות', + 'tours.import.pointsAsPlacesHint': 'נקודות בודדות אינן חלק ממסלול. הן מתווספות למקומות של הטיול.', + 'tours.import.pointsConfirm': 'הוספת מקומות', + 'tours.import.pointsFailedIn': 'לא ניתן היה להוסיף את הנקודות מ-{name} כמקומות.', + 'tours.import.pointsImported': 'מקומות שנוספו: {count}', 'tours.import.unreadableIn': 'לא ניתן לקרוא את {name}: הקובץ פגום או שאינו בפורמט GPX, KML או KMZ', 'tours.import.archiveNoKmlIn': 'לא נמצא קובץ KML בתוך {name}', 'tours.import.archiveTooLargeIn': '{name} גדול מדי לאחר חילוץ', diff --git a/shared/src/i18n/hu/tours.ts b/shared/src/i18n/hu/tours.ts index 409061d71..43940643f 100644 --- a/shared/src/i18n/hu/tours.ts +++ b/shared/src/i18n/hu/tours.ts @@ -961,6 +961,11 @@ const tours: TranslationStrings = { 'tours.import.leftOutAreas.one': '{count} terület', 'tours.import.leftOutOther': '{count} egyéb elem', 'tours.import.leftOutOther.one': '{count} egyéb elem', + 'tours.import.pointsAsPlaces': 'Pontok hozzáadása helyekként', + 'tours.import.pointsAsPlacesHint': 'Az önálló pontok nem részei egy túrának. Helyettük az utazás helyei közé kerülnek.', + 'tours.import.pointsConfirm': 'Helyek hozzáadása', + 'tours.import.pointsFailedIn': 'A(z) {name} pontjait nem sikerült helyekként hozzáadni.', + 'tours.import.pointsImported': 'Hozzáadott helyek: {count}', 'tours.import.unreadableIn': 'A(z) {name} nem olvasható: a fájl sérült, vagy nem GPX, KML vagy KMZ', 'tours.import.archiveNoKmlIn': 'A(z) {name} nem tartalmaz KML-fájlt', 'tours.import.archiveTooLargeIn': 'A(z) {name} kicsomagolva túl nagy', diff --git a/shared/src/i18n/id/tours.ts b/shared/src/i18n/id/tours.ts index 67be5a83f..76a74ba6f 100644 --- a/shared/src/i18n/id/tours.ts +++ b/shared/src/i18n/id/tours.ts @@ -952,6 +952,11 @@ const tours: TranslationStrings = { 'tours.import.leftOutPoints': '{count} penanda', 'tours.import.leftOutAreas': '{count} wilayah', 'tours.import.leftOutOther': '{count} item lain', + 'tours.import.pointsAsPlaces': 'Tambahkan titik sebagai tempat', + 'tours.import.pointsAsPlacesHint': 'Titik tunggal bukan bagian dari tur. Titik itu masuk ke tempat-tempat perjalanan.', + 'tours.import.pointsConfirm': 'Tambahkan tempat', + 'tours.import.pointsFailedIn': 'Titik di {name} tidak dapat ditambahkan sebagai tempat.', + 'tours.import.pointsImported': 'Tempat ditambahkan: {count}', 'tours.import.unreadableIn': '{name} tidak dapat dibaca: berkas rusak atau bukan GPX, KML, atau KMZ', 'tours.import.archiveNoKmlIn': 'Tidak ada berkas KML di {name}', 'tours.import.archiveTooLargeIn': '{name} terlalu besar setelah diekstrak', diff --git a/shared/src/i18n/it/tours.ts b/shared/src/i18n/it/tours.ts index b349ee1e8..1040c8da6 100644 --- a/shared/src/i18n/it/tours.ts +++ b/shared/src/i18n/it/tours.ts @@ -968,6 +968,11 @@ const tours: TranslationStrings = { 'tours.import.leftOutAreas.one': '{count} superficie', 'tours.import.leftOutOther': '{count} altri elementi', 'tours.import.leftOutOther.one': '{count} altro elemento', + 'tours.import.pointsAsPlaces': 'Aggiungi i punti come luoghi', + 'tours.import.pointsAsPlacesHint': 'I punti singoli non fanno parte di un percorso. Vengono aggiunti ai luoghi del viaggio.', + 'tours.import.pointsConfirm': 'Aggiungi luoghi', + 'tours.import.pointsFailedIn': 'Non è stato possibile aggiungere i punti di {name} come luoghi.', + 'tours.import.pointsImported': 'Luoghi aggiunti: {count}', 'tours.import.unreadableIn': 'Impossibile leggere {name}: il file è danneggiato o non è GPX, KML o KMZ', 'tours.import.archiveNoKmlIn': 'Nessun file KML trovato in {name}', 'tours.import.archiveTooLargeIn': '{name} è troppo grande una volta decompresso', diff --git a/shared/src/i18n/ja/tours.ts b/shared/src/i18n/ja/tours.ts index dcb1f3a93..888d9760e 100644 --- a/shared/src/i18n/ja/tours.ts +++ b/shared/src/i18n/ja/tours.ts @@ -928,6 +928,11 @@ const tours: TranslationStrings = { 'tours.import.leftOutPoints': 'ピン {count} 個', 'tours.import.leftOutAreas': 'エリア {count} 件', 'tours.import.leftOutOther': 'その他 {count} 件', + 'tours.import.pointsAsPlaces': 'ポイントを場所として追加', + 'tours.import.pointsAsPlacesHint': '単独のポイントはツアーに含まれません。代わりに旅行の場所に追加されます。', + 'tours.import.pointsConfirm': '場所を追加', + 'tours.import.pointsFailedIn': '{name} のポイントを場所として追加できませんでした。', + 'tours.import.pointsImported': '追加した場所: {count}', 'tours.import.unreadableIn': '{name}を読み込めませんでした:ファイルが破損しているか、GPX、KML、KMZではありません', 'tours.import.archiveNoKmlIn': '{name}にKMLファイルが見つかりません', 'tours.import.archiveTooLargeIn': '{name}は展開すると大きすぎます', diff --git a/shared/src/i18n/ko/tours.ts b/shared/src/i18n/ko/tours.ts index 58e8ef350..0d5fe7ae5 100644 --- a/shared/src/i18n/ko/tours.ts +++ b/shared/src/i18n/ko/tours.ts @@ -920,6 +920,11 @@ const tours: TranslationStrings = { 'tours.import.leftOutPoints': '핀 {count}개', 'tours.import.leftOutAreas': '영역 {count}개', 'tours.import.leftOutOther': '기타 항목 {count}개', + 'tours.import.pointsAsPlaces': '포인트를 장소로 추가', + 'tours.import.pointsAsPlacesHint': '단일 포인트는 투어에 포함되지 않습니다. 대신 여행의 장소에 추가됩니다.', + 'tours.import.pointsConfirm': '장소 추가', + 'tours.import.pointsFailedIn': '{name}의 포인트를 장소로 추가하지 못했습니다.', + 'tours.import.pointsImported': '추가된 장소: {count}', 'tours.import.unreadableIn': '{name}을(를) 읽을 수 없습니다: 파일이 손상되었거나 GPX, KML, KMZ가 아닙니다', 'tours.import.archiveNoKmlIn': '{name}에 KML 파일이 없습니다', 'tours.import.archiveTooLargeIn': '{name}은(는) 압축을 풀면 너무 큽니다', diff --git a/shared/src/i18n/nl/tours.ts b/shared/src/i18n/nl/tours.ts index 6de9039de..4cf11d6a7 100644 --- a/shared/src/i18n/nl/tours.ts +++ b/shared/src/i18n/nl/tours.ts @@ -965,6 +965,11 @@ const tours: TranslationStrings = { 'tours.import.leftOutAreas.one': '{count} gebied', 'tours.import.leftOutOther': '{count} andere items', 'tours.import.leftOutOther.one': '{count} ander item', + 'tours.import.pointsAsPlaces': 'Punten als plekken toevoegen', + 'tours.import.pointsAsPlacesHint': 'Losse punten horen niet bij een tocht. Ze komen bij de plekken van de reis.', + 'tours.import.pointsConfirm': 'Plekken toevoegen', + 'tours.import.pointsFailedIn': 'De punten uit {name} konden niet als plekken worden toegevoegd.', + 'tours.import.pointsImported': 'Plekken toegevoegd: {count}', 'tours.import.unreadableIn': '{name} kon niet worden gelezen: het bestand is beschadigd of geen GPX, KML of KMZ', 'tours.import.archiveNoKmlIn': 'Geen KML-bestand gevonden in {name}', 'tours.import.archiveTooLargeIn': '{name} is uitgepakt te groot', diff --git a/shared/src/i18n/pl/tours.ts b/shared/src/i18n/pl/tours.ts index fdce46fc0..73ab1c87a 100644 --- a/shared/src/i18n/pl/tours.ts +++ b/shared/src/i18n/pl/tours.ts @@ -983,6 +983,11 @@ const tours: TranslationStrings = { 'tours.import.leftOutOther.one': '{count} inny element', 'tours.import.leftOutOther.few': '{count} inne elementy', 'tours.import.leftOutOther.many': '{count} innych elementów', + 'tours.import.pointsAsPlaces': 'Dodaj punkty jako miejsca', + 'tours.import.pointsAsPlacesHint': 'Pojedyncze punkty nie należą do trasy. Trafiają do miejsc podróży.', + 'tours.import.pointsConfirm': 'Dodaj miejsca', + 'tours.import.pointsFailedIn': 'Nie udało się dodać punktów z {name} jako miejsc.', + 'tours.import.pointsImported': 'Dodane miejsca: {count}', 'tours.import.unreadableIn': 'Nie można odczytać pliku {name}: jest uszkodzony albo nie jest plikiem GPX, KML ani KMZ', 'tours.import.archiveNoKmlIn': 'W pliku {name} nie ma pliku KML', diff --git a/shared/src/i18n/ru/tours.ts b/shared/src/i18n/ru/tours.ts index cb4106955..d5cb67b18 100644 --- a/shared/src/i18n/ru/tours.ts +++ b/shared/src/i18n/ru/tours.ts @@ -987,6 +987,11 @@ const tours: TranslationStrings = { 'tours.import.leftOutOther.one': '{count} другой элемент', 'tours.import.leftOutOther.few': '{count} других элемента', 'tours.import.leftOutOther.many': '{count} других элементов', + 'tours.import.pointsAsPlaces': 'Добавить точки как места', + 'tours.import.pointsAsPlacesHint': 'Отдельные точки не входят в маршрут. Они добавляются к местам поездки.', + 'tours.import.pointsConfirm': 'Добавить места', + 'tours.import.pointsFailedIn': 'Не удалось добавить точки из {name} как места.', + 'tours.import.pointsImported': 'Добавлено мест: {count}', 'tours.import.unreadableIn': 'Не удалось прочитать {name}: файл повреждён или это не GPX, KML или KMZ', 'tours.import.archiveNoKmlIn': 'В {name} не найден файл KML', 'tours.import.archiveTooLargeIn': '{name} слишком велик после распаковки', diff --git a/shared/src/i18n/sk/tours.ts b/shared/src/i18n/sk/tours.ts index ecfa42787..2a1e27e6d 100644 --- a/shared/src/i18n/sk/tours.ts +++ b/shared/src/i18n/sk/tours.ts @@ -952,6 +952,11 @@ const tours: TranslationStrings = { 'tours.import.leftOutOther': '{count} ďalších položiek', 'tours.import.leftOutOther.one': '{count} ďalšia položka', 'tours.import.leftOutOther.few': '{count} ďalšie položky', + 'tours.import.pointsAsPlaces': 'Pridať body ako miesta', + 'tours.import.pointsAsPlacesHint': 'Samostatné body nie sú súčasťou trasy. Pridajú sa namiesto toho k miestam cesty.', + 'tours.import.pointsConfirm': 'Pridať miesta', + 'tours.import.pointsFailedIn': 'Body zo súboru {name} sa nepodarilo pridať ako miesta.', + 'tours.import.pointsImported': 'Pridané miesta: {count}', 'tours.import.unreadableIn': 'Súbor {name} sa nedá prečítať: je poškodený alebo nie je GPX, KML ani KMZ', 'tours.import.archiveNoKmlIn': 'V súbore {name} nie je žiadny súbor KML', 'tours.import.archiveTooLargeIn': 'Súbor {name} je po rozbalení príliš veľký', diff --git a/shared/src/i18n/sv/tours.ts b/shared/src/i18n/sv/tours.ts index a8acab38e..c57031907 100644 --- a/shared/src/i18n/sv/tours.ts +++ b/shared/src/i18n/sv/tours.ts @@ -943,6 +943,11 @@ const tours: TranslationStrings = { 'tours.import.leftOutAreas.one': '{count} område', 'tours.import.leftOutOther': '{count} andra objekt', 'tours.import.leftOutOther.one': '{count} annat objekt', + 'tours.import.pointsAsPlaces': 'Lägg till punkterna som platser', + 'tours.import.pointsAsPlacesHint': 'Enskilda punkter ingår inte i en tur. De läggs till bland resans platser i stället.', + 'tours.import.pointsConfirm': 'Lägg till platser', + 'tours.import.pointsFailedIn': 'Punkterna i {name} kunde inte läggas till som platser.', + 'tours.import.pointsImported': 'Tillagda platser: {count}', 'tours.import.unreadableIn': 'Det gick inte att läsa {name}: filen är skadad eller inte GPX, KML eller KMZ', 'tours.import.archiveNoKmlIn': 'Ingen KML-fil hittades i {name}', 'tours.import.archiveTooLargeIn': '{name} är för stor när den packats upp', diff --git a/shared/src/i18n/th/tours.ts b/shared/src/i18n/th/tours.ts index cf4b9d551..819d5c6ce 100644 --- a/shared/src/i18n/th/tours.ts +++ b/shared/src/i18n/th/tours.ts @@ -905,6 +905,11 @@ const tours: TranslationStrings = { 'tours.import.leftOutPoints': 'หมุด {count} รายการ', 'tours.import.leftOutAreas': 'พื้นที่ {count} รายการ', 'tours.import.leftOutOther': 'รายการอื่น {count} รายการ', + 'tours.import.pointsAsPlaces': 'เพิ่มจุดเป็นสถานที่', + 'tours.import.pointsAsPlacesHint': 'จุดเดี่ยวไม่ได้เป็นส่วนหนึ่งของทัวร์ แต่จะถูกเพิ่มเข้าไปในสถานที่ของทริปแทน', + 'tours.import.pointsConfirm': 'เพิ่มสถานที่', + 'tours.import.pointsFailedIn': 'ไม่สามารถเพิ่มจุดใน {name} เป็นสถานที่ได้', + 'tours.import.pointsImported': 'เพิ่มสถานที่แล้ว: {count}', 'tours.import.unreadableIn': 'ไม่สามารถอ่าน {name} ได้: ไฟล์เสียหายหรือไม่ใช่ GPX, KML หรือ KMZ', 'tours.import.archiveNoKmlIn': 'ไม่พบไฟล์ KML ใน {name}', 'tours.import.archiveTooLargeIn': '{name} ใหญ่เกินไปเมื่อแตกไฟล์', diff --git a/shared/src/i18n/tr/tours.ts b/shared/src/i18n/tr/tours.ts index 325699370..cc22db4ed 100644 --- a/shared/src/i18n/tr/tours.ts +++ b/shared/src/i18n/tr/tours.ts @@ -954,6 +954,11 @@ const tours: TranslationStrings = { 'tours.import.leftOutAreas.one': '{count} alan', 'tours.import.leftOutOther': '{count} diğer öğe', 'tours.import.leftOutOther.one': '{count} diğer öğe', + 'tours.import.pointsAsPlaces': 'Noktaları yer olarak ekle', + 'tours.import.pointsAsPlacesHint': 'Tekil noktalar bir turun parçası değildir. Bunun yerine seyahatin yerlerine eklenir.', + 'tours.import.pointsConfirm': 'Yerleri ekle', + 'tours.import.pointsFailedIn': '{name} içindeki noktalar yer olarak eklenemedi.', + 'tours.import.pointsImported': 'Eklenen yerler: {count}', 'tours.import.unreadableIn': '{name} okunamadı: dosya bozuk ya da GPX, KML veya KMZ değil', 'tours.import.archiveNoKmlIn': '{name} içinde KML dosyası bulunamadı', 'tours.import.archiveTooLargeIn': '{name} açıldığında çok büyük', diff --git a/shared/src/i18n/uk/tours.ts b/shared/src/i18n/uk/tours.ts index b05d25adb..377f1dcbf 100644 --- a/shared/src/i18n/uk/tours.ts +++ b/shared/src/i18n/uk/tours.ts @@ -978,6 +978,11 @@ const tours: TranslationStrings = { 'tours.import.leftOutOther.one': '{count} інший елемент', 'tours.import.leftOutOther.few': '{count} інші елементи', 'tours.import.leftOutOther.many': '{count} інших елементів', + 'tours.import.pointsAsPlaces': 'Додати точки як місця', + 'tours.import.pointsAsPlacesHint': 'Окремі точки не входять до маршруту. Вони додаються до місць подорожі.', + 'tours.import.pointsConfirm': 'Додати місця', + 'tours.import.pointsFailedIn': 'Не вдалося додати точки з {name} як місця.', + 'tours.import.pointsImported': 'Додано місць: {count}', 'tours.import.unreadableIn': 'Не вдалося прочитати {name}: файл пошкоджено або це не GPX, KML чи KMZ', 'tours.import.archiveNoKmlIn': 'У {name} не знайдено файлу KML', 'tours.import.archiveTooLargeIn': '{name} завеликий після розпакування', diff --git a/shared/src/i18n/vi/tours.ts b/shared/src/i18n/vi/tours.ts index 06befb012..5ccd09945 100644 --- a/shared/src/i18n/vi/tours.ts +++ b/shared/src/i18n/vi/tours.ts @@ -939,6 +939,11 @@ const tours: TranslationStrings = { 'tours.import.leftOutPoints': '{count} ghim', 'tours.import.leftOutAreas': '{count} vùng', 'tours.import.leftOutOther': '{count} mục khác', + 'tours.import.pointsAsPlaces': 'Thêm các điểm làm địa điểm', + 'tours.import.pointsAsPlacesHint': 'Các điểm đơn lẻ không thuộc về chuyến đi bộ. Chúng được thêm vào các địa điểm của chuyến đi.', + 'tours.import.pointsConfirm': 'Thêm địa điểm', + 'tours.import.pointsFailedIn': 'Không thể thêm các điểm trong {name} làm địa điểm.', + 'tours.import.pointsImported': 'Đã thêm địa điểm: {count}', 'tours.import.unreadableIn': 'Không thể đọc {name}: tệp bị hỏng hoặc không phải GPX, KML hay KMZ', 'tours.import.archiveNoKmlIn': 'Không tìm thấy tệp KML trong {name}', 'tours.import.archiveTooLargeIn': '{name} quá lớn sau khi giải nén', diff --git a/shared/src/i18n/zh-TW/tours.ts b/shared/src/i18n/zh-TW/tours.ts index ad2a9d84d..dc3da4e97 100644 --- a/shared/src/i18n/zh-TW/tours.ts +++ b/shared/src/i18n/zh-TW/tours.ts @@ -873,6 +873,11 @@ const tours: TranslationStrings = { 'tours.import.leftOutPoints': '{count} 個圖釘', 'tours.import.leftOutAreas': '{count} 個區域', 'tours.import.leftOutOther': '其他 {count} 項', + 'tours.import.pointsAsPlaces': '將點新增為地點', + 'tours.import.pointsAsPlacesHint': '單一點不屬於路線,而是新增到行程的地點中。', + 'tours.import.pointsConfirm': '新增地點', + 'tours.import.pointsFailedIn': '無法將 {name} 中的點新增為地點。', + 'tours.import.pointsImported': '已新增地點:{count}', 'tours.import.unreadableIn': '無法讀取 {name}:檔案已損毀,或不是 GPX、KML 或 KMZ', 'tours.import.archiveNoKmlIn': '{name} 中沒有 KML 檔案', 'tours.import.archiveTooLargeIn': '{name} 解壓縮後過大', diff --git a/shared/src/i18n/zh/tours.ts b/shared/src/i18n/zh/tours.ts index 8c7e119b9..d03e14da0 100644 --- a/shared/src/i18n/zh/tours.ts +++ b/shared/src/i18n/zh/tours.ts @@ -874,6 +874,11 @@ const tours: TranslationStrings = { 'tours.import.leftOutPoints': '{count} 个图钉', 'tours.import.leftOutAreas': '{count} 个区域', 'tours.import.leftOutOther': '其他 {count} 项', + 'tours.import.pointsAsPlaces': '将点添加为地点', + 'tours.import.pointsAsPlacesHint': '单个点不属于路线,而是添加到行程的地点中。', + 'tours.import.pointsConfirm': '添加地点', + 'tours.import.pointsFailedIn': '无法将 {name} 中的点添加为地点。', + 'tours.import.pointsImported': '已添加地点:{count}', 'tours.import.unreadableIn': '无法读取 {name}:文件已损坏,或不是 GPX、KML 或 KMZ', 'tours.import.archiveNoKmlIn': '{name} 中没有 KML 文件', 'tours.import.archiveTooLargeIn': '{name} 解压后过大', diff --git a/shared/src/tours/tour-gpx-import.schema.ts b/shared/src/tours/tour-gpx-import.schema.ts index 17b1be0d8..af7dfcb54 100644 --- a/shared/src/tours/tour-gpx-import.schema.ts +++ b/shared/src/tours/tour-gpx-import.schema.ts @@ -51,9 +51,9 @@ export type TourGpxPreviewTrack = z.infer; const leftOutCount = z.number().int().nonnegative(); -/** What a KML or KMZ file held besides its lines, counted by kind; none of it is imported. */ +/** What a file held besides its lines, counted by kind; none of it becomes a tour. */ export const tourFileLeftOutSchema = z.object({ - /** Single points (pins). */ + /** Single points: a KML file's pins, a GPX file's waypoints. The client can add them as places. */ points: leftOutCount, /** Polygons. */ areas: leftOutCount, @@ -64,7 +64,7 @@ export type TourFileLeftOut = z.infer; export const tourGpxPreviewResponseSchema = z.object({ tracks: z.array(tourGpxPreviewTrackSchema), - /** Only a KML or KMZ preview counts what it leaves out; a GPX preview never counted its loose waypoints. */ + /** Absent when the file held nothing besides its lines. A GPX file only ever has points here. */ left_out: tourFileLeftOutSchema.optional(), }); export type TourGpxPreviewResponse = z.infer;