diff --git a/client/src/components/Tours/GpxImportTypeSection.tsx b/client/src/components/Tours/GpxImportTypeSection.tsx index 85bbb1e8e..8e30ab01d 100644 --- a/client/src/components/Tours/GpxImportTypeSection.tsx +++ b/client/src/components/Tours/GpxImportTypeSection.tsx @@ -11,6 +11,7 @@ import { useTranslation } from '../../i18n'; import { LABEL, Segmented } from '../shared/dialogParts'; import { DialogSection } from '../shared/DialogShell'; import type { GpxImportChoicesState } from './gpxImportChoices'; +import { RecordedRideFacts } from './horse/RecordedRideFacts'; import { TourTypeSelect } from './planner/TourTypeSelect'; import { RunProfileField } from './run/RunFields'; import { SkiClassificationFields } from './ski/SkiClassificationFields'; @@ -61,7 +62,8 @@ function PaddleImportFields({ choices }: { choices: GpxImportChoicesState }) { /** * The preview's "import as" section: the type, and below it what that type * names: a bike or run profile, a paddle tour's boat and water, a ski track's - * discipline and technique (SKI-02). + * discipline and technique (SKI-02), and for a ride (HOR-02) what the import + * keeps and does not work out. */ export function GpxImportTypeSection({ choices }: { choices: GpxImportChoicesState }) { const { t } = useTranslation(); @@ -86,6 +88,11 @@ export function GpxImportTypeSection({ choices }: { choices: GpxImportChoicesSta {chosen.type === 'run' && } {chosen.type === 'kayak' && } {chosen.type === 'ski' && } + {chosen.type === 'horse' && ( +
+ +
+ )} ); diff --git a/client/src/components/Tours/TourGpxPreviewDialog.tsx b/client/src/components/Tours/TourGpxPreviewDialog.tsx index 105e6baec..57402d2c4 100644 --- a/client/src/components/Tours/TourGpxPreviewDialog.tsx +++ b/client/src/components/Tours/TourGpxPreviewDialog.tsx @@ -64,7 +64,9 @@ function PreviewTrackRow({ name, track }: { name: string; track: GpxPreviewFile[ * map, every track with its measures, the tracks the trip already holds named * as skipped, and the type to import them as; a paddle import names its boat * and water, as a planned paddle tour does, a ski import its discipline - * (SKI-02). Closing it stores nothing. + * (SKI-02); a ride (HOR-02) says before anything is stored that its line stays + * as recorded, gets no riding time and is not checked for access. Closing it + * stores nothing. */ export default function TourGpxPreviewDialog({ gpxImport }: { gpxImport: TourGpxImport }) { const { preview } = gpxImport; diff --git a/client/src/components/Tours/horse/RecordedRideFacts.test.tsx b/client/src/components/Tours/horse/RecordedRideFacts.test.tsx new file mode 100644 index 000000000..aada4c7ea --- /dev/null +++ b/client/src/components/Tours/horse/RecordedRideFacts.test.tsx @@ -0,0 +1,155 @@ +import type { TourListItem } from '@trek/shared'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { fireEvent, render, screen, within } from '../../../../tests/helpers/render'; +import { resetAllStores, seedStore } from '../../../../tests/helpers/store'; +import { hikePlannerDefaults } from '../../../../tests/helpers/tourPlannerStub'; +import { useSettingsStore } from '../../../store/settingsStore'; +import { analyzeRouteGeometry } from '../../../utils/routeGeometry'; +import { TourPlannerRail } from '../planner/TourPlannerPanels'; +import type { TourPlannerController } from '../planner/useTourPlanner'; +import TourGpxPreviewDialog from '../TourGpxPreviewDialog'; +import type { TourGpxImport } from '../useTourGpxImport'; +import { RecordedRideFacts } from './RecordedRideFacts'; + +// FE-HORSE-IMPORT-002 to FE-HORSE-IMPORT-006 (HOR-02: a ride taken from a GPX file) + +vi.mock('../../Map/MapViewAuto', () => ({ MapViewAuto: () =>
})); + +beforeEach(() => { + resetAllStores(); + seedStore(useSettingsStore, { settings: { distance_unit: 'metric' } }); +}); + +const ride: TourListItem = { + place_id: 42, + name: 'Ausritt an der Isar', + tour_type: 'horse', + distance: 1.1, + elevation_gain: null, + elevation_loss: null, + duration: null, + difficulty: null, + wanderer_ref: null, + match_confidence: 0.3, + max_hiking_difficulty: null, + planned: false, + caution: true, + origin: 'gpx', + has_waypoints: false, +}; + +function viewing(tour: TourListItem, line: number[][]): TourPlannerController { + return { + ...hikePlannerDefaults(), + mode: { type: 'view-gpx', placeId: tour.place_id, tour }, + tripId: 1, + canEdit: true, + canAssign: true, + isSaving: false, + hasUnsavedChanges: false, + readOnlyGpxTour: { tour }, + readOnlyGpxAnalysis: analyzeRouteGeometry(JSON.stringify(line)), + returnToNeutral: vi.fn(), + startNewTour: vi.fn(), + newTourConfirmationOpen: false, + cancelNewTour: vi.fn(), + elevationProfileExpanded: true, + toggleElevationProfile: vi.fn(), + routeProfileFocus: null, + setRouteProfileFocus: vi.fn(), + } as unknown as TourPlannerController; +} + +function previewing(): TourGpxImport { + const file = new File([''], 'ride.gpx', { type: 'application/gpx+xml' }); + return { + importing: false, + importFiles: vi.fn(), + pick: vi.fn(), + preview: [ + { + file, + tracks: [ + { + name: 'Ride', + geometry: [ + [48, 11], + [48.01, 11], + ], + point_count: 2, + distance_km: 1.1, + elevation_gain_m: null, + elevation_loss_m: null, + duplicate: false, + }, + ], + }, + ], + confirm: vi.fn().mockResolvedValue(undefined), + cancel: vi.fn(), + }; +} + +describe('a ride taken from a GPX file (HOR-02)', () => { + it('FE-HORSE-IMPORT-002: says the line stays as recorded, gets no riding time and is not checked for access', () => { + render(); + const facts = screen.getByTestId('recorded-ride-facts'); + expect(facts).toHaveTextContent('As recorded'); + expect(facts).toHaveTextContent('No riding time'); + expect(facts).toHaveTextContent('Access not checked'); + // Each reason is the badge's description, reachable without a pointer. + const access = screen.getByText('Access not checked').closest('[tabindex="0"]') as HTMLElement; + expect(access).toHaveAccessibleDescription( + 'The import does not check whether the ways may be ridden. The signs on site count.' + ); + expect(facts).not.toHaveTextContent('No heights in the file'); + }); + + it('FE-HORSE-IMPORT-003: names heights the file did not carry instead of showing zero', () => { + render(); + expect(screen.getByTestId('recorded-ride-facts')).toHaveTextContent('No heights in the file'); + }); + + it('FE-HORSE-IMPORT-004: the preview says so once a ride is chosen, and imports nothing until confirmed', () => { + const gpxImport = previewing(); + render(); + expect(screen.queryByTestId('gpx-preview-horse')).toBeNull(); + + fireEvent.click(screen.getByTestId('tour-type-select')); + fireEvent.click(screen.getByTestId('tour-type-option-horse')); + expect(within(screen.getByTestId('gpx-preview-horse')).getByText('Access not checked')).toBeInTheDocument(); + expect(gpxImport.confirm).not.toHaveBeenCalled(); + + fireEvent.click(screen.getByTestId('gpx-preview-import')); + expect(gpxImport.confirm).toHaveBeenCalledWith({ tour_type: 'horse' }); + }); + + it('FE-HORSE-IMPORT-005: a recorded ride in view carries the card, with the heights its file lacks named', () => { + render( + + ); + const card = screen.getByRole('region', { name: 'Recorded ride' }); + expect(card).toHaveTextContent('No riding time'); + expect(card).toHaveTextContent('No heights in the file'); + // Nothing of a time is shown: an import of a ride gets none worked out. + expect(screen.queryByText('Riding time')).toBeNull(); + }); + + it('FE-HORSE-IMPORT-006: heights the file has need no word, and a recorded hike gets no ride card', () => { + const line = [ + [48, 11, 500], + [48.01, 11, 540], + ]; + const { unmount } = render(); + expect(screen.getByRole('region', { name: 'Recorded ride' })).not.toHaveTextContent('No heights in the file'); + unmount(); + + render(); + expect(screen.queryByRole('region', { name: 'Recorded ride' })).toBeNull(); + }); +}); diff --git a/client/src/components/Tours/horse/RecordedRideFacts.tsx b/client/src/components/Tours/horse/RecordedRideFacts.tsx new file mode 100644 index 000000000..40aab4949 --- /dev/null +++ b/client/src/components/Tours/horse/RecordedRideFacts.tsx @@ -0,0 +1,73 @@ +import { useId, type ReactNode } from 'react'; +import { useTranslation } from '../../../i18n'; +import { SoftPill } from '../../Planner/planParts'; +import { Tooltip } from '../../shared/Tooltip'; +import { RailCard } from '../tourParts'; +import { ACCESS_TONE_DOT } from './horseAccess'; +import { Horseshoe } from './HorseshoeIcon'; + +/** + * HOR-02: what a ride taken from a GPX file is, said as badges with the why in + * their tooltips. Its line stays as the file recorded it, no riding time is + * worked out for it, and nothing checked whether its ways may be ridden: the + * import makes no ride allowed or passable. Heights the file did not carry are + * named as missing rather than shown as zero. + */ + +/** One badge of the row; with a reason, it takes the focus and says the reason in a tooltip. */ +function FactPill({ icon, label, info }: { icon?: ReactNode; label: string; info?: string }) { + const id = useId(); + const pill = {label}; + if (!info) return pill; + return ( + + + {pill} + + {info} + + + + ); +} + +export function RecordedRideFacts({ heightsMissing = false }: { heightsMissing?: boolean }) { + const { t } = useTranslation(); + return ( +
+
+ ); +} + +/** The ride's card in the rail of a recorded ride in view, above its measures. */ +export function RecordedRideCard({ heightsMissing }: { heightsMissing: boolean }) { + const { t } = useTranslation(); + return ( + } title={t('tours.horse.recorded.title')}> + + + ); +} diff --git a/client/src/components/Tours/planner/TourPlannerPanels.tsx b/client/src/components/Tours/planner/TourPlannerPanels.tsx index 6fb3e498a..721324996 100644 --- a/client/src/components/Tours/planner/TourPlannerPanels.tsx +++ b/client/src/components/Tours/planner/TourPlannerPanels.tsx @@ -52,6 +52,7 @@ import { TourPoiSearchCard } from '../poi/TourPoiSearchCard' import { BikeWaysCard } from '../bike/BikeWaysCard' import { RunWaysCard } from '../run/RunWaysCard' import { HorseRoutesCard } from '../horse/HorseRoutesCard' +import { RecordedRideCard } from '../horse/RecordedRideFacts' import { walkTimeFields } from '../walk/walkTimeFields' const STATUS_KEY: Partial> = { @@ -329,6 +330,8 @@ function GpxViewRail({ {tour.tour_type === 'kayak' && ( )} + {/* HOR-02: a recorded ride says what the import did not do for it. */} + {tour.tour_type === 'horse' && } } title={t('tours.planner.inspector.title')}> {analysis ? (
diff --git a/server/tests/e2e/tours-horse-import.e2e.test.ts b/server/tests/e2e/tours-horse-import.e2e.test.ts new file mode 100644 index 000000000..17cac472f --- /dev/null +++ b/server/tests/e2e/tours-horse-import.e2e.test.ts @@ -0,0 +1,269 @@ +/** + * Tours e2e for rides taken from a GPX file (HOR-02): the canonical Tours GPX + * import, previewed and then confirmed as a ride, stored with its line as the + * file recorded it, its origin and only the measures the file carries, opened + * again and put on a day of the trip; a failed import leaves an existing ride + * as it was. Real JwtAuthGuard, AddonGuard and TripAccessGuard against a + * migrated and seeded temp SQLite db, as in tours-horse.e2e.test.ts; the + * permission check is a container spy and the broadcast goes to a + * FakeRealtimeService. + * + * Fixtures: user 1 owns trip 5 with day 3, user 2 is a member of it whom the + * permission spy denies 'place_edit'. + */ +import { DayAssignments } from '../../src/db/entities/DayAssignments.entity'; +import { Days } from '../../src/db/entities/Days.entity'; +import { Places } from '../../src/db/entities/Places.entity'; +import { Tours } from '../../src/db/entities/Tours.entity'; +import { Trips } from '../../src/db/entities/Trips.entity'; +import { AssignmentsModule } from '../../src/nest/assignments/assignments.module'; +import { TrekExceptionFilter } from '../../src/nest/common/trek-exception.filter'; +import { ZodValidationPipe } from '../../src/nest/common/zod-validation.pipe'; +import { JourneyDomainService } from '../../src/nest/journey/journey-domain.service'; +import { MapsService } from '../../src/nest/maps/maps.service'; +import { OverpassElementsClient } from '../../src/nest/overpass/overpass-elements.service'; +import { PermissionsService } from '../../src/nest/permissions/permissions.service'; +import { RealtimeModule } from '../../src/nest/realtime/realtime.module'; +import { RealtimeService } from '../../src/nest/realtime/realtime.service'; +import { ToursModule } from '../../src/nest/tours/tours.module'; +import { createSnapshotTestDb } from '../helpers/db-mock'; +import { countRows, deleteRows, findRow, insertRows } from '../helpers/factories/rows'; +import { setAddonEnabled } from '../helpers/factories/settings'; +import { addTripMember } from '../helpers/factories/trips'; +import { makeUser } from '../helpers/factories/users'; +import { FakeRealtimeService } from '../helpers/fake-realtime'; +import { createTestMikroOrmModule, createTestOrm, type TestOrm } from '../helpers/test-orm'; +import { TestUnitOfWorkModule } from '../helpers/test-uow'; +import { sessionCookie } from './harness'; +import type { NestExpressApplication } from '@nestjs/platform-express'; +import { Test } from '@nestjs/testing'; + +import cookieParser from 'cookie-parser'; +import type { Server } from 'http'; +import request from 'supertest'; +import { afterAll, beforeAll, beforeEach, describe, expect, it, vi, type MockInstance } from 'vitest'; + +vi.mock('../../src/nest/audit/audit-log.logger', () => ({ + LOG_LEVEL: 'error', + logInfo: vi.fn(), + logDebug: vi.fn(), + logError: vi.fn(), + logWarn: vi.fn(), +})); + +const { journeyHooks } = vi.hoisted(() => ({ + journeyHooks: { + reconcileTripSkeletons: vi.fn().mockResolvedValue(undefined), + onPlaceCreated: vi.fn().mockResolvedValue(undefined), + onPlaceUpdated: vi.fn().mockResolvedValue(undefined), + onPlaceDeleted: vi.fn().mockResolvedValue(undefined), + }, +})); + +const db = createSnapshotTestDb(); +const realtime = new FakeRealtimeService(); + +let orm: TestOrm; +let checkPermission: MockInstance; + +const OWNER = 1; +const VIEWER = 2; + +/** A ride as a watch records it: a few points, with heights. */ +const RECORDED = [ + [47, 11, 600], + [47.01, 11.01, 640], + [47.02, 11.03, 610], +]; +const RIDE_GPX = ` +Ausritt an der Isar +${RECORDED.map(([lat, lon, ele]) => `${ele}`).join('\n')} +`; +/** One without heights: the import has nothing to say about them. */ +const FLAT_GPX = ` +Feldweg + +`; + +describe('Rides imported from GPX e2e (real guards + temp SQLite)', () => { + let server: Server; + let app: Awaited>; + + async function build() { + const moduleRef = await Test.createTestingModule({ + imports: [ + await TestUnitOfWorkModule.forRoot(db), + await createTestMikroOrmModule(db), + RealtimeModule, + ToursModule, + AssignmentsModule, + ], + }) + .overrideProvider(RealtimeService) + .useValue(realtime) + .overrideProvider(JourneyDomainService) + .useValue(journeyHooks) + .overrideProvider(MapsService) + .useValue({ pois: vi.fn() }) + .overrideProvider(OverpassElementsClient) + // An import asks OpenStreetMap nothing: any call here would be a bug. + .useValue({ query: vi.fn().mockRejectedValue(new Error('no network in a test')) }) + .compile(); + const nest = moduleRef.createNestApplication(); + nest.use(cookieParser()); + nest.useGlobalPipes(new ZodValidationPipe()); + nest.useGlobalFilters(new TrekExceptionFilter()); + await nest.init(); + return nest; + } + + beforeAll(async () => { + orm = await createTestOrm(db); + // Pinned ids: the session cookies sign for exactly these users. + await makeUser(orm, { id: 1, username: 'owner', email: 'owner@example.test', role: 'user', password_version: 0 }); + await makeUser(orm, { id: 2, username: 'viewer', email: 'viewer@example.test', role: 'user', password_version: 0 }); + await insertRows(orm, Trips, [{ id: 5, user: 1, title: 'Isar' }]); + await addTripMember(orm, 5, 2); + await insertRows(orm, Days, [{ id: 3, trip: 5, day_number: 1, date: '2026-07-10' }]); + app = await build(); + checkPermission = vi.spyOn(app.get(PermissionsService), 'checkPermission'); + server = app.getHttpServer(); + }); + + beforeEach(async () => { + checkPermission.mockImplementation( + (action: string, _role: string, _owner: number, userId: number) => + !((action === 'place_edit' || action === 'day_edit') && userId === VIEWER), + ); + await setAddonEnabled(orm, 'tours', true); + realtime.broadcastMock.mockClear(); + await deleteRows(orm, DayAssignments); + await deleteRows(orm, Places); + }); + + afterAll(async () => { + await app.close(); + await orm.close(); + }); + + const upload = ( + path: 'import/gpx' | 'import/gpx/preview', + xml: string, + fields: Record = {}, + userId = OWNER, + ) => { + let req = request(server).post(`/api/trips/5/tours/${path}`).set('Cookie', sessionCookie(userId)); + for (const [key, value] of Object.entries(fields)) req = req.field(key, value); + return req.attach('file', Buffer.from(xml), { filename: 'ride.gpx', contentType: 'application/gpx+xml' }); + }; + const importRide = async (xml = RIDE_GPX) => { + const res = await upload('import/gpx', xml, { tour_type: 'horse' }); + expect(res.status).toBe(201); + return res.body.tours[0].place_id as number; + }; + + it('HORSE-IMPORT-E2E-001: the preview measures the file and stores nothing', async () => { + const res = await upload('import/gpx/preview', RIDE_GPX); + expect(res.status).toBe(200); + expect(res.body.tracks).toHaveLength(1); + expect(res.body.tracks[0]).toMatchObject({ name: 'Ausritt an der Isar', point_count: 3, duplicate: false }); + expect(res.body.tracks[0].elevation_gain_m).toBeCloseTo(40); + expect(res.body.tracks[0].elevation_loss_m).toBeCloseTo(30); + expect(await countRows(orm, Tours)).toBe(0); + expect(await countRows(orm, Places)).toBe(0); + }); + + it('HORSE-IMPORT-E2E-002: confirmed as a ride, the line stays as recorded with its origin, heights and no riding time', async () => { + const placeId = await importRide(); + const place = await findRow(orm, Places, { id: placeId }); + expect(JSON.parse(String(place?.route_geometry))).toEqual(RECORDED); + expect(await findRow(orm, Tours, { place: placeId })).toMatchObject({ + tour_type: 'horse', + origin: 'gpx', + origin_file_name: 'ride.gpx', + duration: null, + planned_duration_minutes: null, + max_hiking_difficulty: null, + bike_profile: null, + boat_type: null, + }); + expect(realtime.broadcastMock.mock.calls.map((call) => call[1])).toEqual( + expect.arrayContaining(['tours:changed', 'place:created']), + ); + }); + + it('HORSE-IMPORT-E2E-003: opens again as a ride, naming the file it came from, with nothing routed', async () => { + const placeId = await importRide(); + const res = await request(server).get(`/api/trips/5/tours/${placeId}`).set('Cookie', sessionCookie(VIEWER)); + expect(res.status).toBe(200); + expect(res.body.tour).toMatchObject({ tour_type: 'horse', origin: 'gpx', duration: null, caution: false }); + expect(res.body.tour.elevation_gain).toBeCloseTo(40); + expect(res.body.provenance).toMatchObject({ + origin: 'gpx', + source: { kind: 'gpx', fileName: 'ride.gpx' }, + plannedOver: false, + }); + // The two ends of the track stand for its points; no leg was planned or taken from a route. + expect(res.body.waypoints).toEqual([ + { lat: 47, lng: 11, role: 'start', sequence: 0 }, + { lat: 47.02, lng: 11.03, role: 'end', sequence: 1 }, + ]); + }); + + it('HORSE-IMPORT-E2E-004: a file without heights keeps them unknown and says so, never zero', async () => { + const res = await upload('import/gpx', FLAT_GPX, { tour_type: 'horse' }); + expect(res.status).toBe(201); + expect(res.body.caution).toBe(true); + expect(res.body.tours[0]).toMatchObject({ + tour_type: 'horse', + elevation_gain: null, + elevation_loss: null, + match_confidence: 0.3, + }); + expect(res.body.tours[0].distance).toBeGreaterThan(0); + }); + + it('HORSE-IMPORT-E2E-005: goes on a day of the trip like any tour', async () => { + const placeId = await importRide(); + const assigned = await request(server) + .post('/api/trips/5/days/3/assignments') + .set('Cookie', sessionCookie(OWNER)) + .send({ place_id: placeId }); + expect(assigned.status).toBe(201); + const list = await request(server).get('/api/trips/5/tours').set('Cookie', sessionCookie(OWNER)); + expect(list.status).toBe(200); + expect(list.body.tours.find((tour: { place_id: number }) => tour.place_id === placeId)).toMatchObject({ + tour_type: 'horse', + planned: true, + }); + }); + + it('HORSE-IMPORT-E2E-006: a failed import leaves the rides there are as they were', async () => { + const placeId = await importRide(); + const before = await findRow(orm, Tours, { place: placeId }); + const placeBefore = await findRow(orm, Places, { id: placeId }); + + const refusals = [ + // No track in the file. + await upload('import/gpx', '', { tour_type: 'horse' }), + // A field of another type on a ride. + await upload('import/gpx', FLAT_GPX, { tour_type: 'horse', bike_profile: 'road' }), + // A member who may only read. + await upload('import/gpx', FLAT_GPX, { tour_type: 'horse' }, VIEWER), + ]; + expect(refusals.map((res) => res.status)).toEqual([400, 400, 403]); + + expect(await countRows(orm, Tours)).toBe(1); + expect(await countRows(orm, Places)).toBe(1); + expect(await findRow(orm, Tours, { place: placeId })).toEqual(before); + expect(await findRow(orm, Places, { id: placeId })).toEqual(placeBefore); + }); + + it('HORSE-IMPORT-E2E-007: the same recorded ride again is skipped, not stored twice', async () => { + await importRide(); + const again = await upload('import/gpx', RIDE_GPX, { tour_type: 'horse' }); + expect(again.status).toBe(201); + expect(again.body).toEqual({ tours: [], caution: false, skipped: 1 }); + expect(await countRows(orm, Tours)).toBe(1); + }); +}); diff --git a/shared/src/i18n/ar/tours.ts b/shared/src/i18n/ar/tours.ts index a06bf8cec..f1cab1545 100644 --- a/shared/src/i18n/ar/tours.ts +++ b/shared/src/i18n/ar/tours.ts @@ -1095,5 +1095,13 @@ const tours: TranslationStrings = { 'tours.horse.leg.info': 'مقطع من مسار فروسية معلّم من OpenStreetMap. وصول الخيل: {access}', 'tours.horse.leg.manual': 'يدوي، غير مُتحقق منه', 'tours.horse.leg.manualInfo': 'مرسوم باليد، مستقيم. لم يتحقق منه أي مسار فروسية ولا أي مخطط مسارات.', + 'tours.horse.recorded.title': 'جولة مسجّلة', + 'tours.horse.recorded.asRecorded': 'كما سُجّلت', + 'tours.horse.recorded.asRecordedInfo': 'يبقى المسار كما سجّله الملف. لا يُعاد تخطيط أي جزء منه.', + 'tours.horse.recorded.noTime': 'بلا وقت ركوب', + 'tours.horse.recorded.noTimeInfo': 'لا يحسب TREK وقت ركوب للجولة: الإيقاع يحدده الحصان والفارس.', + 'tours.horse.recorded.accessUnchecked': 'الوصول غير مُتحقق منه', + 'tours.horse.recorded.accessUncheckedInfo': + 'لا يتحقق الاستيراد مما إذا كان يُسمح بركوب الطرق. العبرة باللافتات في المكان.', }; export default tours; diff --git a/shared/src/i18n/az/tours.ts b/shared/src/i18n/az/tours.ts index d61282ad4..9693bd0b9 100644 --- a/shared/src/i18n/az/tours.ts +++ b/shared/src/i18n/az/tours.ts @@ -1061,5 +1061,13 @@ const tours: TranslationStrings = { 'tours.horse.leg.info': 'OpenStreetMap-dən nişanlanmış atçılıq marşrutunun hissəsi. At üçün giriş: {access}', 'tours.horse.leg.manual': 'Əl ilə, yoxlanılmayıb', 'tours.horse.leg.manualInfo': 'Əl ilə, düz çəkilib. Nə atçılıq marşrutu, nə də marşrut planlayıcısı yoxlayıb.', + 'tours.horse.recorded.title': 'Qeydə alınmış gəzinti', + 'tours.horse.recorded.asRecorded': 'Qeydə alındığı kimi', + 'tours.horse.recorded.asRecordedInfo': 'Xətt faylın qeydə aldığı kimi qalır. Heç nə yenidən planlanmır.', + 'tours.horse.recorded.noTime': 'Sürmə vaxtı yoxdur', + 'tours.horse.recorded.noTimeInfo': 'TREK at gəzintisi üçün sürmə vaxtı hesablamır: tempi at və atlı müəyyən edir.', + 'tours.horse.recorded.accessUnchecked': 'Giriş yoxlanılmayıb', + 'tours.horse.recorded.accessUncheckedInfo': + 'İdxal yollarda at sürməyə icazə olub-olmadığını yoxlamır. Yerdəki lövhələr əsasdır.', }; export default tours; diff --git a/shared/src/i18n/br/tours.ts b/shared/src/i18n/br/tours.ts index 6a023682e..482cd5ce9 100644 --- a/shared/src/i18n/br/tours.ts +++ b/shared/src/i18n/br/tours.ts @@ -1071,5 +1071,14 @@ const tours: TranslationStrings = { 'tours.horse.leg.info': 'Trecho de uma rota equestre sinalizada do OpenStreetMap. Acesso a cavalo: {access}', 'tours.horse.leg.manual': 'Manual, não verificado', 'tours.horse.leg.manualInfo': 'Traçado à mão, em linha reta. Nenhuma rota equestre e nenhum roteador o verificou.', + 'tours.horse.recorded.title': 'Passeio gravado', + 'tours.horse.recorded.asRecorded': 'Como gravado', + 'tours.horse.recorded.asRecordedInfo': 'A linha fica como o arquivo a gravou. Nada é roteado de novo.', + 'tours.horse.recorded.noTime': 'Sem tempo a cavalo', + 'tours.horse.recorded.noTimeInfo': + 'O TREK não calcula tempo a cavalo para um passeio: o ritmo é do cavalo e do cavaleiro.', + 'tours.horse.recorded.accessUnchecked': 'Acesso não verificado', + 'tours.horse.recorded.accessUncheckedInfo': + 'A importação não verifica se as vias podem ser percorridas a cavalo. Valem as placas no local.', }; export default tours; diff --git a/shared/src/i18n/ca/tours.ts b/shared/src/i18n/ca/tours.ts index 63f75ac88..1a244bff4 100644 --- a/shared/src/i18n/ca/tours.ts +++ b/shared/src/i18n/ca/tours.ts @@ -1074,5 +1074,15 @@ const tours: TranslationStrings = { 'tours.horse.leg.manual': 'Manual, sense comprovar', 'tours.horse.leg.manualInfo': "Traçat a mà, en línia recta. Ni una ruta eqüestre ni un planificador no l'ha comprovat.", + 'tours.horse.recorded.title': 'Ruta enregistrada', + 'tours.horse.recorded.asRecorded': 'Tal com es va enregistrar', + 'tours.horse.recorded.asRecordedInfo': + 'La línia queda tal com la va enregistrar el fitxer. No es torna a calcular res.', + 'tours.horse.recorded.noTime': 'Sense temps a cavall', + 'tours.horse.recorded.noTimeInfo': + 'TREK no calcula temps a cavall per a una ruta: el ritme és del cavall i del genet.', + 'tours.horse.recorded.accessUnchecked': 'Accés sense comprovar', + 'tours.horse.recorded.accessUncheckedInfo': + 'La importació no comprova si es pot anar a cavall pels camins. Valen els senyals sobre el terreny.', }; export default tours; diff --git a/shared/src/i18n/cs/tours.ts b/shared/src/i18n/cs/tours.ts index 6c8c39678..7c3ee6558 100644 --- a/shared/src/i18n/cs/tours.ts +++ b/shared/src/i18n/cs/tours.ts @@ -1060,5 +1060,13 @@ const tours: TranslationStrings = { 'tours.horse.leg.info': 'Úsek značené jezdecké trasy z OpenStreetMap. Přístup pro koně: {access}', 'tours.horse.leg.manual': 'Ručně, neověřeno', 'tours.horse.leg.manualInfo': 'Nakresleno ručně, rovně. Neověřila ho žádná jezdecká trasa ani plánovač.', + 'tours.horse.recorded.title': 'Zaznamenaná vyjížďka', + 'tours.horse.recorded.asRecorded': 'Jak bylo zaznamenáno', + 'tours.horse.recorded.asRecordedInfo': 'Trasa zůstává tak, jak ji soubor zaznamenal. Nic se znovu neplánuje.', + 'tours.horse.recorded.noTime': 'Bez doby jízdy', + 'tours.horse.recorded.noTimeInfo': 'TREK pro vyjížďku nepočítá dobu jízdy: tempo určuje kůň a jezdec.', + 'tours.horse.recorded.accessUnchecked': 'Přístup neověřen', + 'tours.horse.recorded.accessUncheckedInfo': + 'Import neověřuje, zda se po cestách smí jezdit na koni. Platí značení na místě.', }; export default tours; diff --git a/shared/src/i18n/de/tours.ts b/shared/src/i18n/de/tours.ts index 1a8b35c23..584d3a7b3 100644 --- a/shared/src/i18n/de/tours.ts +++ b/shared/src/i18n/de/tours.ts @@ -1077,6 +1077,16 @@ const tours: TranslationStrings = { 'tours.horse.leg.info': 'Abschnitt einer ausgeschilderten Reitroute aus OpenStreetMap. Reitzugang: {access}', 'tours.horse.leg.manual': 'Manuell, ungeprüft', 'tours.horse.leg.manualInfo': 'Von Hand gezogen, gerade. Weder eine Reitroute noch ein Router hat ihn geprüft.', + 'tours.horse.recorded.title': 'Aufgezeichneter Ausritt', + 'tours.horse.recorded.asRecorded': 'Wie aufgezeichnet', + 'tours.horse.recorded.asRecordedInfo': + 'Die Linie bleibt, wie die Datei sie aufgezeichnet hat. Nichts wird neu geplant.', + 'tours.horse.recorded.noTime': 'Keine Reitzeit', + 'tours.horse.recorded.noTimeInfo': + 'TREK berechnet für einen Ausritt keine Reitzeit: Das Tempo bestimmen Pferd und Reiter.', + 'tours.horse.recorded.accessUnchecked': 'Zugang nicht geprüft', + 'tours.horse.recorded.accessUncheckedInfo': + 'Der Import prüft nicht, ob die Wege geritten werden dürfen. Es gilt die Beschilderung vor Ort.', }; export default tours; diff --git a/shared/src/i18n/en/tours.ts b/shared/src/i18n/en/tours.ts index 3a4e0c3de..a6093ce12 100644 --- a/shared/src/i18n/en/tours.ts +++ b/shared/src/i18n/en/tours.ts @@ -1048,5 +1048,14 @@ const tours: TranslationStrings = { 'tours.horse.leg.info': 'A section of a signposted horse route from OpenStreetMap. Riding access: {access}', 'tours.horse.leg.manual': 'Manual, unchecked', 'tours.horse.leg.manualInfo': 'Drawn by hand, straight. No horse route and no router checked it.', + 'tours.horse.recorded.title': 'Recorded ride', + 'tours.horse.recorded.asRecorded': 'As recorded', + 'tours.horse.recorded.asRecordedInfo': 'The line stays as the file recorded it. Nothing is routed again.', + 'tours.horse.recorded.noTime': 'No riding time', + 'tours.horse.recorded.noTimeInfo': + "TREK works out no riding time for a ride: the pace is the horse's and the rider's.", + 'tours.horse.recorded.accessUnchecked': 'Access not checked', + 'tours.horse.recorded.accessUncheckedInfo': + 'The import does not check whether the ways may be ridden. The signs on site count.', }; export default tours; diff --git a/shared/src/i18n/es/tours.ts b/shared/src/i18n/es/tours.ts index 290ab5576..15738b901 100644 --- a/shared/src/i18n/es/tours.ts +++ b/shared/src/i18n/es/tours.ts @@ -1069,5 +1069,14 @@ const tours: TranslationStrings = { 'tours.horse.leg.info': 'Tramo de una ruta ecuestre señalizada de OpenStreetMap. Acceso a caballo: {access}', 'tours.horse.leg.manual': 'Manual, sin comprobar', 'tours.horse.leg.manualInfo': 'Trazado a mano, en línea recta. Ni una ruta ecuestre ni un planificador lo comprobó.', + 'tours.horse.recorded.title': 'Ruta grabada', + 'tours.horse.recorded.asRecorded': 'Tal como se grabó', + 'tours.horse.recorded.asRecordedInfo': 'La línea queda tal como la grabó el archivo. No se vuelve a calcular nada.', + 'tours.horse.recorded.noTime': 'Sin tiempo a caballo', + 'tours.horse.recorded.noTimeInfo': + 'TREK no calcula tiempo a caballo para una ruta: el ritmo es del caballo y del jinete.', + 'tours.horse.recorded.accessUnchecked': 'Acceso sin comprobar', + 'tours.horse.recorded.accessUncheckedInfo': + 'La importación no comprueba si se puede ir a caballo por las vías. Valen las señales sobre el terreno.', }; export default tours; diff --git a/shared/src/i18n/et/tours.ts b/shared/src/i18n/et/tours.ts index 9d3f5dc74..9d4b1ca16 100644 --- a/shared/src/i18n/et/tours.ts +++ b/shared/src/i18n/et/tours.ts @@ -1056,5 +1056,13 @@ const tours: TranslationStrings = { 'tours.horse.leg.manual': 'Käsitsi, kontrollimata', 'tours.horse.leg.manualInfo': 'Käsitsi joonistatud, sirge. Seda ei kontrollinud ükski ratsarada ega marsruudiplaneerija.', + 'tours.horse.recorded.title': 'Salvestatud ratsamatk', + 'tours.horse.recorded.asRecorded': 'Nagu salvestatud', + 'tours.horse.recorded.asRecordedInfo': 'Joon jääb selliseks, nagu fail selle salvestas. Midagi ei planeerita uuesti.', + 'tours.horse.recorded.noTime': 'Ratsutamisaega pole', + 'tours.horse.recorded.noTimeInfo': 'TREK ei arvuta ratsamatkale ratsutamisaega: tempo määravad hobune ja ratsanik.', + 'tours.horse.recorded.accessUnchecked': 'Ligipääs kontrollimata', + 'tours.horse.recorded.accessUncheckedInfo': + 'Import ei kontrolli, kas teedel tohib ratsutada. Kohapeal kehtivad sildid.', }; export default tours; diff --git a/shared/src/i18n/fr/tours.ts b/shared/src/i18n/fr/tours.ts index 1dd7798db..bd91598e1 100644 --- a/shared/src/i18n/fr/tours.ts +++ b/shared/src/i18n/fr/tours.ts @@ -1090,5 +1090,14 @@ const tours: TranslationStrings = { 'tours.horse.leg.manual': 'Manuel, non vérifié', 'tours.horse.leg.manualInfo': "Tracé à la main, en ligne droite. Ni un itinéraire équestre ni un calculateur d'itinéraire ne l'a vérifié.", + 'tours.horse.recorded.title': 'Sortie enregistrée', + 'tours.horse.recorded.asRecorded': 'Tel qu’enregistré', + 'tours.horse.recorded.asRecordedInfo': "Le tracé reste tel que le fichier l'a enregistré. Rien n'est recalculé.", + 'tours.horse.recorded.noTime': 'Pas de temps à cheval', + 'tours.horse.recorded.noTimeInfo': + 'TREK ne calcule aucun temps à cheval pour une sortie : le rythme est celui du cheval et du cavalier.', + 'tours.horse.recorded.accessUnchecked': 'Accès non vérifié', + 'tours.horse.recorded.accessUncheckedInfo': + "L'import ne vérifie pas si les voies peuvent être empruntées à cheval. Les panneaux sur place font foi.", }; export default tours; diff --git a/shared/src/i18n/gr/tours.ts b/shared/src/i18n/gr/tours.ts index fc20d4c77..2385d7058 100644 --- a/shared/src/i18n/gr/tours.ts +++ b/shared/src/i18n/gr/tours.ts @@ -1089,5 +1089,14 @@ const tours: TranslationStrings = { 'tours.horse.leg.info': 'Τμήμα σηματοδοτημένης ιππικής διαδρομής από το OpenStreetMap. Πρόσβαση με άλογο: {access}', 'tours.horse.leg.manual': 'Χειροκίνητο, χωρίς έλεγχο', 'tours.horse.leg.manualInfo': 'Σχεδιασμένο με το χέρι, ευθύ. Ούτε ιππική διαδρομή ούτε δρομολογητής το έλεγξε.', + 'tours.horse.recorded.title': 'Καταγεγραμμένη βόλτα', + 'tours.horse.recorded.asRecorded': 'Όπως καταγράφηκε', + 'tours.horse.recorded.asRecordedInfo': 'Η γραμμή μένει όπως την κατέγραψε το αρχείο. Τίποτα δεν δρομολογείται ξανά.', + 'tours.horse.recorded.noTime': 'Χωρίς χρόνο ιππασίας', + 'tours.horse.recorded.noTimeInfo': + 'Το TREK δεν υπολογίζει χρόνο ιππασίας για μια βόλτα: τον ρυθμό τον ορίζουν το άλογο και ο αναβάτης.', + 'tours.horse.recorded.accessUnchecked': 'Πρόσβαση χωρίς έλεγχο', + 'tours.horse.recorded.accessUncheckedInfo': + 'Η εισαγωγή δεν ελέγχει αν επιτρέπεται η ιππασία στους δρόμους. Ισχύουν οι πινακίδες επί τόπου.', }; export default tours; diff --git a/shared/src/i18n/hu/tours.ts b/shared/src/i18n/hu/tours.ts index f65e9bce0..3a51286c2 100644 --- a/shared/src/i18n/hu/tours.ts +++ b/shared/src/i18n/hu/tours.ts @@ -1071,5 +1071,14 @@ const tours: TranslationStrings = { 'tours.horse.leg.info': 'Jelzett lovas útvonal szakasza az OpenStreetMapből. Lovas hozzáférés: {access}', 'tours.horse.leg.manual': 'Kézi, ellenőrizetlen', 'tours.horse.leg.manualInfo': 'Kézzel, egyenesen húzva. Sem lovas útvonal, sem útvonaltervező nem ellenőrizte.', + 'tours.horse.recorded.title': 'Rögzített lovastúra', + 'tours.horse.recorded.asRecorded': 'Ahogy rögzítették', + 'tours.horse.recorded.asRecordedInfo': 'A vonal úgy marad, ahogy a fájl rögzítette. Semmit nem tervez újra.', + 'tours.horse.recorded.noTime': 'Nincs lovaglási idő', + 'tours.horse.recorded.noTimeInfo': + 'A TREK nem számol lovaglási időt egy lovastúrához: a tempót a ló és a lovas adja.', + 'tours.horse.recorded.accessUnchecked': 'Hozzáférés ellenőrizetlen', + 'tours.horse.recorded.accessUncheckedInfo': + 'Az importálás nem ellenőrzi, hogy szabad-e lovagolni az utakon. A helyszíni táblák a mérvadók.', }; export default tours; diff --git a/shared/src/i18n/id/tours.ts b/shared/src/i18n/id/tours.ts index 93200dd7f..7cdecb267 100644 --- a/shared/src/i18n/id/tours.ts +++ b/shared/src/i18n/id/tours.ts @@ -1053,5 +1053,14 @@ const tours: TranslationStrings = { 'tours.horse.leg.manual': 'Manual, belum diperiksa', 'tours.horse.leg.manualInfo': 'Digambar tangan, lurus. Tidak ada rute berkuda maupun perencana rute yang memeriksanya.', + 'tours.horse.recorded.title': 'Tur berkuda terekam', + 'tours.horse.recorded.asRecorded': 'Sesuai rekaman', + 'tours.horse.recorded.asRecordedInfo': 'Garis tetap seperti yang direkam berkas. Tidak ada yang dirutekan ulang.', + 'tours.horse.recorded.noTime': 'Tanpa waktu berkuda', + 'tours.horse.recorded.noTimeInfo': + 'TREK tidak menghitung waktu berkuda untuk tur berkuda: lajunya milik kuda dan penunggangnya.', + 'tours.horse.recorded.accessUnchecked': 'Akses belum diperiksa', + 'tours.horse.recorded.accessUncheckedInfo': + 'Impor tidak memeriksa apakah jalan boleh dilalui dengan berkuda. Rambu di lokasi yang berlaku.', }; export default tours; diff --git a/shared/src/i18n/it/tours.ts b/shared/src/i18n/it/tours.ts index 864a07804..3e8987adf 100644 --- a/shared/src/i18n/it/tours.ts +++ b/shared/src/i18n/it/tours.ts @@ -1079,5 +1079,14 @@ const tours: TranslationStrings = { 'tours.horse.leg.manual': 'Manuale, non verificato', 'tours.horse.leg.manualInfo': 'Tracciato a mano, dritto. Né un itinerario equestre né un pianificatore lo ha verificato.', + 'tours.horse.recorded.title': 'Escursione registrata', + 'tours.horse.recorded.asRecorded': 'Come registrato', + 'tours.horse.recorded.asRecordedInfo': 'Il tracciato resta come lo ha registrato il file. Nulla viene ricalcolato.', + 'tours.horse.recorded.noTime': 'Nessun tempo a cavallo', + 'tours.horse.recorded.noTimeInfo': + 'TREK non calcola un tempo a cavallo per un’escursione: il ritmo è quello del cavallo e del cavaliere.', + 'tours.horse.recorded.accessUnchecked': 'Accesso non verificato', + 'tours.horse.recorded.accessUncheckedInfo': + 'L’importazione non verifica se le strade si possono percorrere a cavallo. Valgono i cartelli sul posto.', }; export default tours; diff --git a/shared/src/i18n/ja/tours.ts b/shared/src/i18n/ja/tours.ts index bd0e5d21f..3261fe948 100644 --- a/shared/src/i18n/ja/tours.ts +++ b/shared/src/i18n/ja/tours.ts @@ -1024,5 +1024,13 @@ const tours: TranslationStrings = { 'tours.horse.leg.info': 'OpenStreetMap の標識付き乗馬ルートの区間です。乗馬でのアクセス: {access}', 'tours.horse.leg.manual': '手動、未確認', 'tours.horse.leg.manualInfo': '手で直線に引いた区間です。乗馬ルートもルーターも確認していません。', + 'tours.horse.recorded.title': '記録された乗馬', + 'tours.horse.recorded.asRecorded': '記録のまま', + 'tours.horse.recorded.asRecordedInfo': '線はファイルに記録されたままです。ルートを計算し直すことはありません。', + 'tours.horse.recorded.noTime': '乗馬時間なし', + 'tours.horse.recorded.noTimeInfo': 'TREK は乗馬ツアーの乗馬時間を計算しません。ペースは馬と乗り手次第です。', + 'tours.horse.recorded.accessUnchecked': '通行可否は未確認', + 'tours.horse.recorded.accessUncheckedInfo': + 'インポートでは、道を馬で通れるかどうかを確認しません。現地の標識が優先されます。', }; export default tours; diff --git a/shared/src/i18n/ko/tours.ts b/shared/src/i18n/ko/tours.ts index e51b39657..dc767af57 100644 --- a/shared/src/i18n/ko/tours.ts +++ b/shared/src/i18n/ko/tours.ts @@ -1021,5 +1021,14 @@ const tours: TranslationStrings = { 'tours.horse.leg.info': 'OpenStreetMap의 표지가 있는 승마 루트의 구간입니다. 말 통행: {access}', 'tours.horse.leg.manual': '수동, 확인되지 않음', 'tours.horse.leg.manualInfo': '손으로 그은 직선 구간입니다. 승마 루트도 경로 계산기도 확인하지 않았습니다.', + 'tours.horse.recorded.title': '기록된 승마', + 'tours.horse.recorded.asRecorded': '기록된 그대로', + 'tours.horse.recorded.asRecordedInfo': '선은 파일에 기록된 그대로 유지됩니다. 경로를 다시 계산하지 않습니다.', + 'tours.horse.recorded.noTime': '승마 시간 없음', + 'tours.horse.recorded.noTimeInfo': + 'TREK은 승마 투어의 승마 시간을 계산하지 않습니다. 속도는 말과 기수에게 달려 있습니다.', + 'tours.horse.recorded.accessUnchecked': '통행 여부 미확인', + 'tours.horse.recorded.accessUncheckedInfo': + '가져오기는 길을 말로 지나가도 되는지 확인하지 않습니다. 현장 표지판을 따르세요.', }; export default tours; diff --git a/shared/src/i18n/nl/tours.ts b/shared/src/i18n/nl/tours.ts index 17f42fd76..3b2f70ab2 100644 --- a/shared/src/i18n/nl/tours.ts +++ b/shared/src/i18n/nl/tours.ts @@ -1074,5 +1074,14 @@ const tours: TranslationStrings = { 'tours.horse.leg.manual': 'Handmatig, niet gecontroleerd', 'tours.horse.leg.manualInfo': 'Met de hand getrokken, recht. Geen ruiterroute en geen routeplanner heeft het gecontroleerd.', + 'tours.horse.recorded.title': 'Opgenomen rit', + 'tours.horse.recorded.asRecorded': 'Zoals opgenomen', + 'tours.horse.recorded.asRecordedInfo': + 'De lijn blijft zoals het bestand haar heeft opgenomen. Er wordt niets opnieuw gepland.', + 'tours.horse.recorded.noTime': 'Geen rijtijd', + 'tours.horse.recorded.noTimeInfo': 'TREK berekent geen rijtijd voor een rit: het tempo is aan paard en ruiter.', + 'tours.horse.recorded.accessUnchecked': 'Toegang niet gecontroleerd', + 'tours.horse.recorded.accessUncheckedInfo': + 'De import controleert niet of de wegen bereden mogen worden. Ter plekke gelden de borden.', }; export default tours; diff --git a/shared/src/i18n/pl/tours.ts b/shared/src/i18n/pl/tours.ts index c0fe5ac3e..875c79b67 100644 --- a/shared/src/i18n/pl/tours.ts +++ b/shared/src/i18n/pl/tours.ts @@ -1100,5 +1100,13 @@ const tours: TranslationStrings = { 'tours.horse.leg.info': 'Odcinek oznakowanego szlaku konnego z OpenStreetMap. Dostęp dla koni: {access}', 'tours.horse.leg.manual': 'Ręczny, niesprawdzony', 'tours.horse.leg.manualInfo': 'Narysowany ręcznie, prosto. Nie sprawdził go żaden szlak konny ani planer tras.', + 'tours.horse.recorded.title': 'Nagrana przejażdżka', + 'tours.horse.recorded.asRecorded': 'Tak jak nagrano', + 'tours.horse.recorded.asRecordedInfo': 'Linia pozostaje taka, jak zapisał ją plik. Nic nie jest wyznaczane ponownie.', + 'tours.horse.recorded.noTime': 'Bez czasu jazdy', + 'tours.horse.recorded.noTimeInfo': 'TREK nie oblicza czasu jazdy dla przejażdżki: tempo zależy od konia i jeźdźca.', + 'tours.horse.recorded.accessUnchecked': 'Dostęp niesprawdzony', + 'tours.horse.recorded.accessUncheckedInfo': + 'Import nie sprawdza, czy po drogach wolno jeździć konno. Obowiązują znaki na miejscu.', }; export default tours; diff --git a/shared/src/i18n/ru/tours.ts b/shared/src/i18n/ru/tours.ts index 78ec9587d..fac9e4dd9 100644 --- a/shared/src/i18n/ru/tours.ts +++ b/shared/src/i18n/ru/tours.ts @@ -1104,5 +1104,15 @@ const tours: TranslationStrings = { 'tours.horse.leg.info': 'Участок маркированного конного маршрута из OpenStreetMap. Доступ для лошадей: {access}', 'tours.horse.leg.manual': 'Вручную, не проверено', 'tours.horse.leg.manualInfo': 'Нарисован вручную, по прямой. Ни конный маршрут, ни маршрутизатор его не проверяли.', + 'tours.horse.recorded.title': 'Записанная прогулка', + 'tours.horse.recorded.asRecorded': 'Как записано', + 'tours.horse.recorded.asRecordedInfo': + 'Линия остаётся такой, какой её записал файл. Ничего не прокладывается заново.', + 'tours.horse.recorded.noTime': 'Без времени в седле', + 'tours.horse.recorded.noTimeInfo': + 'TREK не рассчитывает время в седле для конной прогулки: темп задают лошадь и всадник.', + 'tours.horse.recorded.accessUnchecked': 'Доступ не проверен', + 'tours.horse.recorded.accessUncheckedInfo': + 'Импорт не проверяет, можно ли ехать по этим дорогам верхом. На месте действуют знаки.', }; export default tours; diff --git a/shared/src/i18n/sk/tours.ts b/shared/src/i18n/sk/tours.ts index 314ef241f..56199e441 100644 --- a/shared/src/i18n/sk/tours.ts +++ b/shared/src/i18n/sk/tours.ts @@ -1063,5 +1063,13 @@ const tours: TranslationStrings = { 'tours.horse.leg.info': 'Úsek značenej jazdeckej trasy z OpenStreetMap. Prístup pre kone: {access}', 'tours.horse.leg.manual': 'Ručne, neoverené', 'tours.horse.leg.manualInfo': 'Nakreslené ručne, rovno. Neoverila ho žiadna jazdecká trasa ani plánovač.', + 'tours.horse.recorded.title': 'Zaznamenaná vychádzka', + 'tours.horse.recorded.asRecorded': 'Ako bolo zaznamenané', + 'tours.horse.recorded.asRecordedInfo': 'Trasa zostáva tak, ako ju súbor zaznamenal. Nič sa znova neplánuje.', + 'tours.horse.recorded.noTime': 'Bez času jazdy', + 'tours.horse.recorded.noTimeInfo': 'TREK pre vychádzku nepočíta čas jazdy: tempo určuje kôň a jazdec.', + 'tours.horse.recorded.accessUnchecked': 'Prístup neoverený', + 'tours.horse.recorded.accessUncheckedInfo': + 'Import neoveruje, či sa po cestách smie jazdiť na koni. Platí značenie na mieste.', }; export default tours; diff --git a/shared/src/i18n/sv/tours.ts b/shared/src/i18n/sv/tours.ts index 475c5032d..729d2ed99 100644 --- a/shared/src/i18n/sv/tours.ts +++ b/shared/src/i18n/sv/tours.ts @@ -1052,5 +1052,14 @@ const tours: TranslationStrings = { 'tours.horse.leg.info': 'Delsträcka av en skyltad ridled från OpenStreetMap. Tillträde för häst: {access}', 'tours.horse.leg.manual': 'Manuell, okontrollerad', 'tours.horse.leg.manualInfo': 'Dragen för hand, rakt. Ingen ridled och ingen ruttplanerare har kontrollerat den.', + 'tours.horse.recorded.title': 'Inspelad ridtur', + 'tours.horse.recorded.asRecorded': 'Som inspelad', + 'tours.horse.recorded.asRecordedInfo': 'Linjen förblir som filen spelade in den. Inget planeras om.', + 'tours.horse.recorded.noTime': 'Ingen ridtid', + 'tours.horse.recorded.noTimeInfo': + 'TREK räknar inte ut någon ridtid för en ridtur: takten bestäms av hästen och ryttaren.', + 'tours.horse.recorded.accessUnchecked': 'Tillträde okontrollerat', + 'tours.horse.recorded.accessUncheckedInfo': + 'Importen kontrollerar inte om vägarna får ridas. Skyltarna på plats gäller.', }; export default tours; diff --git a/shared/src/i18n/th/tours.ts b/shared/src/i18n/th/tours.ts index 122bc37b4..d62a5dd54 100644 --- a/shared/src/i18n/th/tours.ts +++ b/shared/src/i18n/th/tours.ts @@ -1008,5 +1008,13 @@ const tours: TranslationStrings = { 'tours.horse.leg.info': 'ช่วงหนึ่งของเส้นทางขี่ม้าที่มีป้ายบอกจาก OpenStreetMap การเข้าถึงสำหรับม้า: {access}', 'tours.horse.leg.manual': 'วาดเอง ยังไม่ตรวจสอบ', 'tours.horse.leg.manualInfo': 'วาดด้วยมือเป็นเส้นตรง ไม่มีเส้นทางขี่ม้าหรือตัววางเส้นทางใดตรวจสอบ', + 'tours.horse.recorded.title': 'ทริปขี่ม้าที่บันทึกไว้', + 'tours.horse.recorded.asRecorded': 'ตามที่บันทึก', + 'tours.horse.recorded.asRecordedInfo': 'เส้นยังคงเป็นไปตามที่ไฟล์บันทึกไว้ ไม่มีการวางเส้นทางใหม่', + 'tours.horse.recorded.noTime': 'ไม่มีเวลาขี่ม้า', + 'tours.horse.recorded.noTimeInfo': 'TREK ไม่คำนวณเวลาขี่ม้าให้ทริปขี่ม้า จังหวะขึ้นอยู่กับม้าและผู้ขี่', + 'tours.horse.recorded.accessUnchecked': 'ยังไม่ตรวจสอบสิทธิ์ผ่าน', + 'tours.horse.recorded.accessUncheckedInfo': + 'การนำเข้าไม่ได้ตรวจสอบว่าขี่ม้าผ่านเส้นทางได้หรือไม่ ให้ถือป้ายในพื้นที่เป็นหลัก', }; export default tours; diff --git a/shared/src/i18n/tours-locales.spec.ts b/shared/src/i18n/tours-locales.spec.ts index b5983820b..9a32f7cd0 100644 --- a/shared/src/i18n/tours-locales.spec.ts +++ b/shared/src/i18n/tours-locales.spec.ts @@ -20,7 +20,7 @@ const tokens = (value: string) => [...value.matchAll(/\{[^{}]+\}/g)].map((match) // placeholder spec; here only the general keys are. const groups = pluralGroups(Object.keys(english)); const generalKeys = (keys: string[]) => keys.filter((key) => !pluralFormOf(key, groups)); -const GENERAL_KEY_COUNT = 952; +const GENERAL_KEY_COUNT = 959; function literalKeys(source: string): string[] { const tree = ts.createSourceFile('locale.ts', source, ts.ScriptTarget.Latest, true); diff --git a/shared/src/i18n/tr/tours.ts b/shared/src/i18n/tr/tours.ts index 0e1df6378..c37286410 100644 --- a/shared/src/i18n/tr/tours.ts +++ b/shared/src/i18n/tr/tours.ts @@ -1060,5 +1060,13 @@ const tours: TranslationStrings = { 'tours.horse.leg.info': "OpenStreetMap'teki işaretli bir binicilik rotasının bölümü. At erişimi: {access}", 'tours.horse.leg.manual': 'Elle, denetlenmemiş', 'tours.horse.leg.manualInfo': 'Elle, düz çizildi. Ne bir binicilik rotası ne de bir rota planlayıcı denetledi.', + 'tours.horse.recorded.title': 'Kaydedilmiş at gezisi', + 'tours.horse.recorded.asRecorded': 'Kaydedildiği gibi', + 'tours.horse.recorded.asRecordedInfo': 'Çizgi, dosyanın kaydettiği gibi kalır. Hiçbir şey yeniden rotalanmaz.', + 'tours.horse.recorded.noTime': 'Sürüş süresi yok', + 'tours.horse.recorded.noTimeInfo': 'TREK at gezisi için sürüş süresi hesaplamaz: tempoyu at ve binici belirler.', + 'tours.horse.recorded.accessUnchecked': 'Erişim denetlenmedi', + 'tours.horse.recorded.accessUncheckedInfo': + 'İçe aktarma, yollarda ata binilip binilemeyeceğini denetlemez. Yerindeki tabelalar geçerlidir.', }; export default tours; diff --git a/shared/src/i18n/uk/tours.ts b/shared/src/i18n/uk/tours.ts index 32c58de0d..5cc176c74 100644 --- a/shared/src/i18n/uk/tours.ts +++ b/shared/src/i18n/uk/tours.ts @@ -1093,5 +1093,14 @@ const tours: TranslationStrings = { 'tours.horse.leg.info': 'Ділянка маркованого кінного маршруту з OpenStreetMap. Доступ для коней: {access}', 'tours.horse.leg.manual': 'Вручну, не перевірено', 'tours.horse.leg.manualInfo': 'Намальовано вручну, по прямій. Ні кінний маршрут, ні маршрутизатор її не перевіряли.', + 'tours.horse.recorded.title': 'Записана прогулянка', + 'tours.horse.recorded.asRecorded': 'Як записано', + 'tours.horse.recorded.asRecordedInfo': 'Лінія залишається такою, як її записав файл. Нічого не прокладається заново.', + 'tours.horse.recorded.noTime': 'Без часу в сідлі', + 'tours.horse.recorded.noTimeInfo': + 'TREK не розраховує час у сідлі для кінної прогулянки: темп задають кінь і вершник.', + 'tours.horse.recorded.accessUnchecked': 'Доступ не перевірено', + 'tours.horse.recorded.accessUncheckedInfo': + 'Імпорт не перевіряє, чи можна їхати цими дорогами верхи. На місці діють знаки.', }; export default tours; diff --git a/shared/src/i18n/vi/tours.ts b/shared/src/i18n/vi/tours.ts index 25eac445c..f3ef7eb85 100644 --- a/shared/src/i18n/vi/tours.ts +++ b/shared/src/i18n/vi/tours.ts @@ -1044,5 +1044,14 @@ const tours: TranslationStrings = { 'tours.horse.leg.info': 'Một đoạn của tuyến cưỡi ngựa có biển chỉ dẫn từ OpenStreetMap. Quyền đi ngựa: {access}', 'tours.horse.leg.manual': 'Thủ công, chưa kiểm tra', 'tours.horse.leg.manualInfo': 'Vẽ tay, đường thẳng. Không tuyến cưỡi ngựa hay công cụ dẫn đường nào kiểm tra.', + 'tours.horse.recorded.title': 'Chuyến cưỡi ngựa đã ghi', + 'tours.horse.recorded.asRecorded': 'Như đã ghi', + 'tours.horse.recorded.asRecordedInfo': 'Đường đi giữ nguyên như tệp đã ghi. Không có gì được dẫn đường lại.', + 'tours.horse.recorded.noTime': 'Không có thời gian cưỡi ngựa', + 'tours.horse.recorded.noTimeInfo': + 'TREK không tính thời gian cưỡi ngựa cho chuyến cưỡi ngựa: nhịp độ là của ngựa và người cưỡi.', + 'tours.horse.recorded.accessUnchecked': 'Chưa kiểm tra quyền đi', + 'tours.horse.recorded.accessUncheckedInfo': + 'Việc nhập không kiểm tra liệu có được cưỡi ngựa trên các con đường hay không. Biển báo tại chỗ mới là căn cứ.', }; export default tours; diff --git a/shared/src/i18n/zh-TW/tours.ts b/shared/src/i18n/zh-TW/tours.ts index 01e8b47f7..01ff457e5 100644 --- a/shared/src/i18n/zh-TW/tours.ts +++ b/shared/src/i18n/zh-TW/tours.ts @@ -970,5 +970,12 @@ const tours: TranslationStrings = { 'tours.horse.leg.info': '來自 OpenStreetMap 有標示騎馬路線的一段。馬匹通行:{access}', 'tours.horse.leg.manual': '手動,未經檢查', 'tours.horse.leg.manualInfo': '手動繪製的直線。沒有任何騎馬路線或路線規劃器檢查過它。', + 'tours.horse.recorded.title': '已記錄的騎馬遊', + 'tours.horse.recorded.asRecorded': '依記錄保留', + 'tours.horse.recorded.asRecordedInfo': '路線維持檔案記錄時的樣子,不會重新規劃。', + 'tours.horse.recorded.noTime': '無騎乘時間', + 'tours.horse.recorded.noTimeInfo': 'TREK 不為騎馬遊計算騎乘時間:節奏取決於馬和騎手。', + 'tours.horse.recorded.accessUnchecked': '通行未經檢查', + 'tours.horse.recorded.accessUncheckedInfo': '匯入不會檢查這些道路是否允許騎馬通行。以現場標誌為準。', }; export default tours; diff --git a/shared/src/i18n/zh/tours.ts b/shared/src/i18n/zh/tours.ts index cad229d9e..37d5c65f4 100644 --- a/shared/src/i18n/zh/tours.ts +++ b/shared/src/i18n/zh/tours.ts @@ -971,5 +971,12 @@ const tours: TranslationStrings = { 'tours.horse.leg.info': '来自 OpenStreetMap 有标识骑马路线的一段。马匹通行:{access}', 'tours.horse.leg.manual': '手动,未经检查', 'tours.horse.leg.manualInfo': '手动绘制的直线。没有任何骑马路线或路线规划器检查过它。', + 'tours.horse.recorded.title': '已记录的骑马游', + 'tours.horse.recorded.asRecorded': '按记录保留', + 'tours.horse.recorded.asRecordedInfo': '路线保持文件记录时的样子,不会重新规划。', + 'tours.horse.recorded.noTime': '无骑行时间', + 'tours.horse.recorded.noTimeInfo': 'TREK 不为骑马游计算骑行时间:节奏取决于马和骑手。', + 'tours.horse.recorded.accessUnchecked': '通行未经检查', + 'tours.horse.recorded.accessUncheckedInfo': '导入不会检查这些道路是否允许骑马通行。以现场标志为准。', }; export default tours;