mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-21 06:26:26 +00:00
feat(tours): plan ski tours on the piste router in the planner
A ski tour with a discipline now sends its legs to the piste router and takes back runs, lifts and trails: each stretch is coloured by its grade in the area's scheme, links and lift rides down are dotted, and a leg the router cannot plan stays straight and unrouted with its reason and the one change that would plan it. Snowshoe and winter hike legs off the piste data follow summer paths instead, said so. The router's time and heights are used once every leg is planned, and the options, the build and the time are saved with the tour. - skiRouteRepo and api: one request per tour, batched, remembered unless the answer may change, offline legs failed as offline - planner: router options per discipline, leg steps and failure rows with fixes, a piste route summary, draft and reopen keep the options - admin: switch for the piste router under Tours - i18n: new ski keys in all 27 locales, ski info and time texts rewritten
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
import { skiRouteResponseSchema, type SkiRouteInput, type SkiRouteResponse } from '@trek/shared';
|
||||
import apiClient from './client';
|
||||
|
||||
/** The piste router: a ski tour's legs planned on the piste data the server keeps, held to the contract. */
|
||||
export const skiRouteApi = {
|
||||
plan: (tripId: number | string, request: SkiRouteInput, signal?: AbortSignal): Promise<SkiRouteResponse> =>
|
||||
apiClient
|
||||
.post(`/trips/${tripId}/ski/route`, request, { signal })
|
||||
.then((r) => skiRouteResponseSchema.parse(r.data)),
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { AdminFeatureToggleResponse } from '@trek/shared';
|
||||
import apiClient from './client';
|
||||
|
||||
/** An admin's switch for a map the Tours planner offers on its operator's terms. */
|
||||
/** An admin's switch for a map or a service the Tours planner offers on its operator's terms. */
|
||||
function tourBaseMapAdminApi(path: string) {
|
||||
return {
|
||||
get: (): Promise<AdminFeatureToggleResponse> => apiClient.get(path).then((r) => r.data),
|
||||
@@ -14,3 +14,5 @@ function tourBaseMapAdminApi(path: string) {
|
||||
export const hikingMapAdminApi = tourBaseMapAdminApi('/admin/tours-hiking-map');
|
||||
/** BIK-06: CyclOSM for a bike tour. */
|
||||
export const cycleMapAdminApi = tourBaseMapAdminApi('/admin/tours-cycle-map');
|
||||
/** SKI-02: the piste router, which plans ski tours on the Places host's piste data. */
|
||||
export const skiRoutingAdminApi = tourBaseMapAdminApi('/admin/tours-ski-routing');
|
||||
|
||||
@@ -2,7 +2,7 @@ import { act, renderHook, waitFor } from '@testing-library/react';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { adminApi } from '../../../api/client';
|
||||
import { cycleMapAdminApi, hikingMapAdminApi } from '../../../api/tourBaseMaps';
|
||||
import { cycleMapAdminApi, hikingMapAdminApi, skiRoutingAdminApi } from '../../../api/tourBaseMaps';
|
||||
import { wandererAdminApi } from '../../../api/wanderer';
|
||||
import { useAddonStore } from '../../../store/addonStore';
|
||||
import type { Addon } from './addonModel';
|
||||
@@ -40,6 +40,7 @@ beforeEach(() => {
|
||||
vi.spyOn(wandererAdminApi, 'get').mockResolvedValue({ enabled: false });
|
||||
vi.spyOn(hikingMapAdminApi, 'get').mockResolvedValue({ enabled: true });
|
||||
vi.spyOn(cycleMapAdminApi, 'get').mockResolvedValue({ enabled: true });
|
||||
vi.spyOn(skiRoutingAdminApi, 'get').mockResolvedValue({ enabled: true });
|
||||
});
|
||||
afterEach(() => vi.restoreAllMocks());
|
||||
|
||||
@@ -52,7 +53,7 @@ async function loaded() {
|
||||
const enabledOf = (addons: Addon[], id: string) => addons.find((a) => a.id === id)?.enabled;
|
||||
|
||||
/** One of the map switches under Tours, by its key. */
|
||||
function mapSwitch(manager: ReturnType<typeof useAddonManager>, key: 'hiking' | 'cycle') {
|
||||
function mapSwitch(manager: ReturnType<typeof useAddonManager>, key: 'hiking' | 'cycle' | 'ski') {
|
||||
const found = manager.mapSwitches.find((entry) => entry.key === key);
|
||||
if (!found) throw new Error(`no ${key} map switch`);
|
||||
return found;
|
||||
@@ -149,6 +150,16 @@ describe('useAddonManager', () => {
|
||||
expect(mapSwitch(result.current, 'hiking').enabled).toBe(true);
|
||||
});
|
||||
|
||||
it('FE-HOOK-ADDONMGR-010: the piste router switch under Tours loads on, flips and saves on its own (SKI-02)', async () => {
|
||||
const update = vi.spyOn(skiRoutingAdminApi, 'update').mockResolvedValue({ enabled: false });
|
||||
const { result } = await loaded();
|
||||
await waitFor(() => expect(mapSwitch(result.current, 'ski').enabled).toBe(true));
|
||||
await act(() => mapSwitch(result.current, 'ski').toggle());
|
||||
expect(update).toHaveBeenCalledWith(false);
|
||||
expect(mapSwitch(result.current, 'ski').enabled).toBe(false);
|
||||
expect(mapSwitch(result.current, 'cycle').enabled).toBe(true);
|
||||
});
|
||||
|
||||
it('FE-HOOK-ADDONMGR-008: a failed save puts the switch under Tours back and says so', async () => {
|
||||
vi.spyOn(wandererAdminApi, 'update').mockRejectedValue(new Error('500'));
|
||||
const { result } = await loaded();
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Bike, Footprints } from 'lucide-react';
|
||||
import { Bike, Footprints, MountainSnow } from 'lucide-react';
|
||||
import { useEffect, useEffectEvent, useState } from 'react';
|
||||
|
||||
import { adminApi } from '../../../api/client';
|
||||
import { cycleMapAdminApi, hikingMapAdminApi } from '../../../api/tourBaseMaps';
|
||||
import { cycleMapAdminApi, hikingMapAdminApi, skiRoutingAdminApi } from '../../../api/tourBaseMaps';
|
||||
import { wandererAdminApi } from '../../../api/wanderer';
|
||||
import { useTranslation } from '../../../i18n';
|
||||
import { useAddonStore } from '../../../store/addonStore';
|
||||
@@ -24,6 +24,7 @@ export function useAddonManager() {
|
||||
const [toursWanderer, setToursWanderer] = useState(false);
|
||||
const [toursHikingMap, setToursHikingMap] = useState(false);
|
||||
const [toursCycleMap, setToursCycleMap] = useState(false);
|
||||
const [toursSkiRouting, setToursSkiRouting] = useState(false);
|
||||
|
||||
const loadAddons = async () => {
|
||||
try {
|
||||
@@ -48,6 +49,10 @@ export function useAddonManager() {
|
||||
.get()
|
||||
.then((flag) => setToursCycleMap(flag.enabled))
|
||||
.catch(() => toast.error(t('admin.addons.toast.error')));
|
||||
skiRoutingAdminApi
|
||||
.get()
|
||||
.then((flag) => setToursSkiRouting(flag.enabled))
|
||||
.catch(() => toast.error(t('admin.addons.toast.error')));
|
||||
});
|
||||
useEffect(() => {
|
||||
loadOnMount();
|
||||
@@ -94,7 +99,7 @@ export function useAddonManager() {
|
||||
toast.success(t('admin.addons.toast.updated'));
|
||||
};
|
||||
const toggleToursWanderer = () => flipToursSwitch(toursWanderer, setToursWanderer, wandererAdminApi.update);
|
||||
/** The maps the Tours planner offers on their operators' terms, one switch each (MAP-01, BIK-06). */
|
||||
/** The maps and the piste router Tours offers on their operators' terms, one switch each (MAP-01, BIK-06, SKI-02). */
|
||||
const mapSwitches = [
|
||||
{
|
||||
key: 'hiking',
|
||||
@@ -112,6 +117,14 @@ export function useAddonManager() {
|
||||
enabled: toursCycleMap,
|
||||
toggle: () => flipToursSwitch(toursCycleMap, setToursCycleMap, cycleMapAdminApi.update),
|
||||
},
|
||||
{
|
||||
key: 'ski',
|
||||
Icon: MountainSnow,
|
||||
titleKey: 'admin.toursSkiRouting.title',
|
||||
subtitleKey: 'admin.toursSkiRouting.subtitle',
|
||||
enabled: toursSkiRouting,
|
||||
toggle: () => flipToursSwitch(toursSkiRouting, setToursSkiRouting, skiRoutingAdminApi.update),
|
||||
},
|
||||
] as const;
|
||||
|
||||
const asOption = (provider: Addon): ProviderOption => ({
|
||||
|
||||
@@ -63,6 +63,7 @@ import { RunWaysCard } from '../run/RunWaysCard'
|
||||
import { HorseRoutesCard } from '../horse/HorseRoutesCard'
|
||||
import { RecordedRideCard } from '../horse/RecordedRideFacts'
|
||||
import { HikeWaysCard } from '../hike/HikeWaysCard'
|
||||
import { SkiRouteSummary } from '../ski/SkiRouteSummary'
|
||||
import { walkTimeFields } from '../walk/walkTimeFields'
|
||||
|
||||
const STATUS_KEY: Partial<Record<TourPlannerStatus, string>> = {
|
||||
@@ -497,6 +498,8 @@ export function TourPlannerRail({ planner,
|
||||
|
||||
<HorseRoutesCard planner={planner} />
|
||||
|
||||
{planner.isSki && <SkiRouteSummary route={planner.skiRoute} />}
|
||||
|
||||
{planner.tourType === 'hike' && (
|
||||
<HikeWaysCard tripId={planner.tripId} route={planner.route} routeCurrent={planner.routeCurrent}
|
||||
routingLimit={planner.maxHikingDifficulty} />
|
||||
@@ -670,7 +673,7 @@ const ESTIMATE_KEYS = {
|
||||
run: { labelKey: 'tours.run.timeEstimated', shortKey: 'tours.run.timeEstimatedShort', infoLabelKey: 'tours.run.timeEstimatedInfoLabel', infoKey: 'tours.run.timeEstimatedInfo' },
|
||||
// HOR-01: a ride has no estimate; the field says why, and the planned total is the rider's own.
|
||||
horse: { labelKey: 'tours.horse.time', shortKey: 'tours.horse.time', infoLabelKey: 'tours.horse.timeInfoLabel', infoKey: 'tours.horse.timeInfo' },
|
||||
// SKI-02: nor has a ski tour, whose legs are drawn; the field says so.
|
||||
// SKI-02: a ski tour's time is the piste router's, given once every leg is planned; the field says so.
|
||||
ski: { labelKey: 'tours.ski.time', shortKey: 'tours.ski.time', infoLabelKey: 'tours.ski.timeInfoLabel', infoKey: 'tours.ski.timeInfo' },
|
||||
} as const
|
||||
|
||||
|
||||
@@ -136,7 +136,9 @@ describe('TourTypeFields ski and winter (SKI-02)', () => {
|
||||
expect(screen.getByRole('button', { name })).toHaveAttribute('aria-pressed', 'false');
|
||||
}
|
||||
expect(fields).toHaveTextContent('Choose a discipline to save the tour.');
|
||||
expect(fields).toHaveTextContent('Each leg is drawn straight between your points.');
|
||||
expect(fields).toHaveTextContent('Legs follow runs, lifts and trails from OpenStreetMap');
|
||||
// No router options before a discipline is chosen.
|
||||
expect(screen.queryByTestId('tour-planner-ski-options')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('tour-planner-paddle-fields')).not.toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Snowshoeing' }));
|
||||
expect(ski.ski.change).toHaveBeenCalledWith({ discipline: 'snowshoe', technique: null });
|
||||
@@ -144,7 +146,12 @@ describe('TourTypeFields ski and winter (SKI-02)', () => {
|
||||
|
||||
it('asks a cross-country tour for its technique and keeps it dimmed for any other discipline', () => {
|
||||
const crossCountry = skiPlanner({
|
||||
ski: { choice: { discipline: 'cross_country', technique: null }, change: vi.fn(), load: vi.fn() },
|
||||
ski: {
|
||||
choice: { discipline: 'cross_country', technique: null },
|
||||
change: vi.fn(),
|
||||
setOption: vi.fn(),
|
||||
load: vi.fn(),
|
||||
},
|
||||
skiMissing: 'technique',
|
||||
});
|
||||
const { unmount } = render(<TourTypeFields planner={crossCountry} locked={false} />);
|
||||
@@ -155,13 +162,17 @@ describe('TourTypeFields ski and winter (SKI-02)', () => {
|
||||
unmount();
|
||||
|
||||
const alpine = skiPlanner({
|
||||
ski: { choice: { discipline: 'alpine', technique: null }, change: vi.fn(), load: vi.fn() },
|
||||
ski: { choice: { discipline: 'alpine', technique: null }, change: vi.fn(), setOption: vi.fn(), load: vi.fn() },
|
||||
skiMissing: null,
|
||||
});
|
||||
render(<TourTypeFields planner={alpine} locked={false} />);
|
||||
expect(screen.getByTestId('tour-ski-technique')).toBeDisabled();
|
||||
expect(screen.getByRole('button', { name: 'Alpine' })).toHaveAttribute('aria-pressed', 'true');
|
||||
expect(screen.queryByRole('status')).not.toBeInTheDocument();
|
||||
// The piste router's options for the discipline chosen, each change handed to the planner.
|
||||
expect(screen.getByTestId('tour-planner-ski-options')).toHaveTextContent('Hardest runs');
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Sporty' }));
|
||||
expect(alpine.ski.setOption).toHaveBeenCalledWith({ pace: 'sporty' });
|
||||
});
|
||||
|
||||
it('shows no ski fields on any other tour', () => {
|
||||
|
||||
@@ -15,6 +15,7 @@ import { crossesLake } from '../paddle/lakeStyleAdvice';
|
||||
import { PaddleLakeStyleField } from '../paddle/PaddleLakeStyleField';
|
||||
import { RunFields } from '../run/RunFields';
|
||||
import { SkiClassificationFields } from '../ski/SkiClassificationFields';
|
||||
import { SkiRouteOptions } from '../ski/SkiRouteOptions';
|
||||
import { TourNotice } from '../tourParts';
|
||||
import { TourTypeSelect } from './TourTypeSelect';
|
||||
import type { TourPlannerController } from './useTourPlanner';
|
||||
@@ -107,17 +108,21 @@ function BikeProfileField({ planner }: { planner: TourPlannerController }) {
|
||||
|
||||
/**
|
||||
* SKI-02: what a ski tour is, chosen on purpose and nothing preselected: the
|
||||
* discipline and, for cross-country, the technique. A short line says how its
|
||||
* legs are laid: straight between the points, nothing checked against pistes.
|
||||
* discipline and, for cross-country, the technique, then what the piste router
|
||||
* is asked for. A short line says how its legs are laid.
|
||||
*/
|
||||
function SkiFields({ planner }: { planner: TourPlannerController }) {
|
||||
const { t } = useTranslation();
|
||||
const { choice } = planner.ski;
|
||||
return (
|
||||
<div data-testid="tour-planner-ski-fields" className="flex min-w-0 flex-col gap-1.5">
|
||||
<SkiClassificationFields choice={planner.ski.choice} onChange={planner.ski.change} planned />
|
||||
<SkiClassificationFields choice={choice} onChange={planner.ski.change} planned />
|
||||
<span className="block text-content-faint" style={fs(11, 'body')}>
|
||||
{t('tours.ski.drawnFreely')}
|
||||
{t('tours.ski.plannedHint')}
|
||||
</span>
|
||||
{choice.discipline !== null && (
|
||||
<SkiRouteOptions discipline={choice.discipline} options={choice.options} onChange={planner.ski.setOption} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import { LockRow, PendingPortageRow, PortageLegRow } from '../paddle/PaddleWaypo
|
||||
import type { usePaddleRegionLoad } from '../paddle/usePaddleRegionLoad';
|
||||
import { usePaddleWaypointActions } from '../paddle/usePaddleWaypointActions';
|
||||
import { usePortageWay, type usePortageRouting } from '../paddle/usePortageRouting';
|
||||
import { SkiLegFailureRow, SkiLegSteps } from '../ski/SkiLegRows';
|
||||
import type { PaddleLegFailure } from './paddleRouting';
|
||||
import { lineLengthM } from './tourFixedLegs';
|
||||
import type { TourPlannerController } from './useTourPlanner';
|
||||
@@ -175,11 +176,44 @@ function LegFailure({
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* SKI-02: what a ski tour shows under a waypoint for its leg: the steps the
|
||||
* piste router planned, or why it could not plan it and what would.
|
||||
*/
|
||||
function SkiLegExtras({
|
||||
planner,
|
||||
point,
|
||||
index,
|
||||
editable,
|
||||
}: {
|
||||
planner: TourPlannerController;
|
||||
point: WaypointPoint;
|
||||
index: number;
|
||||
editable: boolean;
|
||||
}) {
|
||||
const route = planner.skiRoute;
|
||||
if (!route) return null;
|
||||
const problem = route.problems.find((candidate) => candidate.index === index);
|
||||
if (problem) {
|
||||
return (
|
||||
<SkiLegFailureRow
|
||||
problem={problem}
|
||||
choice={planner.ski.choice}
|
||||
disabled={!editable}
|
||||
onChoice={planner.ski.change}
|
||||
onDrawStraight={() => planner.toggleWaypointFreeLeg(point.id)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
const leg = route.legs[index];
|
||||
return leg?.status === 'ok' ? <SkiLegSteps leg={leg} scheme={route.colourScheme} /> : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The rows under a waypoint for the leg that starts there: the via ferrata it
|
||||
* crosses, a line drawn by hand, a ride's horse route section (HOR-01), and
|
||||
* on a paddle tour what the router found on the way (PAD-11) or why it could
|
||||
* not plan it, plus the portage and lock rows.
|
||||
* not plan it, plus the portage and lock rows, and a ski tour's planned steps.
|
||||
*/
|
||||
export function WaypointLegRows({
|
||||
planner,
|
||||
@@ -211,6 +245,7 @@ export function WaypointLegRows({
|
||||
{point.fixedLeg?.kind === 'free' && <FreeLegRow leg={point.fixedLeg} ride={planner.isHorse} />}
|
||||
{point.fixedLeg?.kind === 'horse_route' && <HorseLegRow leg={point.fixedLeg} />}
|
||||
{routedLegs && <PaddleLegNotes leg={routedLegs.legs[index]} />}
|
||||
{planner.isSki && <SkiLegExtras planner={planner} point={point} index={index} editable={editable} />}
|
||||
{failure && (
|
||||
<LegFailure
|
||||
code={failure.code}
|
||||
|
||||
@@ -1,16 +1,93 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { routeSkiTour, skiLegs, skiRouteKey } from './skiRouting';
|
||||
import type { SkiRouteLeg, SkiRouteSegment } from '@trek/shared';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { skiRouteRepo, type SkiLegAnswer, type SkiPlanAnswer } from '../../../repo/skiRouteRepo';
|
||||
import { legSeconds, routeSkiTour, skiLegs, skiRouteKey } from './skiRouting';
|
||||
import { lineLengthM } from './tourFixedLegs';
|
||||
import { routeWalkingTour } from './tourRouting';
|
||||
|
||||
// FE-TOUR-SKI-ROUTE-001 to FE-TOUR-SKI-ROUTE-004
|
||||
vi.mock('./tourRouting', async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import('./tourRouting')>()),
|
||||
routeWalkingTour: vi.fn(),
|
||||
}));
|
||||
|
||||
// FE-TOUR-SKI-ROUTE-001 to FE-TOUR-SKI-ROUTE-010
|
||||
|
||||
const start = { lat: 47, lng: 11 };
|
||||
const col = { lat: 47.01, lng: 11 };
|
||||
const hut = { lat: 47.01, lng: 11.01 };
|
||||
const touring = { discipline: 'touring' as const, technique: null };
|
||||
const alpine = { discipline: 'alpine' as const, technique: null };
|
||||
const snowshoe = { discipline: 'snowshoe' as const, technique: null };
|
||||
|
||||
function segment(kind: SkiRouteSegment['kind'], from: number, to: number, extra: Partial<SkiRouteSegment> = {}) {
|
||||
return {
|
||||
kind,
|
||||
from,
|
||||
to,
|
||||
name: null,
|
||||
ref: null,
|
||||
difficulty: null,
|
||||
lift_type: null,
|
||||
grooming: [],
|
||||
seconds: 60,
|
||||
wait_seconds: 0,
|
||||
length_m: 100,
|
||||
flags: [],
|
||||
osm_way: null,
|
||||
...extra,
|
||||
} satisfies SkiRouteSegment;
|
||||
}
|
||||
|
||||
/** A planned leg from one point to the next: a lift up, then a red run down. */
|
||||
function okLeg(from: { lat: number; lng: number }, to: { lat: number; lng: number }): SkiRouteLeg {
|
||||
return {
|
||||
status: 'ok',
|
||||
geometry: [
|
||||
[from.lat, from.lng, 1500],
|
||||
[(from.lat + to.lat) / 2, (from.lng + to.lng) / 2, 2000],
|
||||
[to.lat, to.lng, 1600],
|
||||
],
|
||||
segments: [
|
||||
segment('lift', 0, 1, { name: 'Gipfelbahn' }),
|
||||
segment('run', 1, 2, { name: 'Talabfahrt', difficulty: 'intermediate' }),
|
||||
],
|
||||
distance_m: 1800,
|
||||
climb_m: 500,
|
||||
descent_m: 400,
|
||||
seconds: { ski: 300, lift: 420, wait: 60, walk: 20 },
|
||||
snapped_m: { from: 0, to: 0 },
|
||||
area_ids: ['w:1'],
|
||||
why: [],
|
||||
alternatives: [],
|
||||
};
|
||||
}
|
||||
|
||||
function failed(code: Extract<SkiLegAnswer, { status: 'failed' }>['code']): SkiLegAnswer {
|
||||
const hint = { raise_difficulty: 'advanced' as const };
|
||||
return { status: 'failed', code, at: null, hint, detail: { name: 'Schwarze' } } as SkiLegAnswer;
|
||||
}
|
||||
|
||||
const answer = (legs: SkiLegAnswer[], extra: Partial<SkiPlanAnswer> = {}): SkiPlanAnswer => ({
|
||||
legs,
|
||||
build: '2026-11-02',
|
||||
stale: false,
|
||||
areaNames: ['Gletscherwelt'],
|
||||
colourScheme: 'europe',
|
||||
...extra,
|
||||
});
|
||||
|
||||
const planSpy = () => vi.spyOn(skiRouteRepo, 'plan');
|
||||
let plan: ReturnType<typeof planSpy>;
|
||||
|
||||
beforeEach(() => {
|
||||
plan = planSpy();
|
||||
});
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
describe('skiRouting (SKI-02)', () => {
|
||||
it('FE-TOUR-SKI-ROUTE-001: lays a straight free leg between every two points, and keeps a leg the traveller drew', async () => {
|
||||
it('FE-TOUR-SKI-ROUTE-001: lays a straight free leg between every two points, and keeps a leg the traveller drew', () => {
|
||||
const drawn = {
|
||||
kind: 'free' as const,
|
||||
geometry: [
|
||||
@@ -19,8 +96,7 @@ describe('skiRouting (SKI-02)', () => {
|
||||
[47.01, 11.01],
|
||||
] as [number, number][],
|
||||
};
|
||||
const legs = await skiLegs([start, { ...col, fixedLeg: drawn }, hut], touring);
|
||||
expect(legs).toEqual([
|
||||
expect(skiLegs([start, { ...col, fixedLeg: drawn }, hut])).toEqual([
|
||||
{
|
||||
kind: 'free',
|
||||
geometry: [
|
||||
@@ -30,20 +106,17 @@ describe('skiRouting (SKI-02)', () => {
|
||||
},
|
||||
drawn,
|
||||
]);
|
||||
expect(await skiLegs([start], touring)).toEqual([]);
|
||||
expect(skiLegs([start])).toEqual([]);
|
||||
});
|
||||
|
||||
it('FE-TOUR-SKI-ROUTE-002: the line is drawn whole, sampled along each leg for its heights, with no time worked out', async () => {
|
||||
const route = await routeSkiTour([start, col, hut], touring);
|
||||
expect(route).not.toBeNull();
|
||||
it('FE-TOUR-SKI-ROUTE-002: before a discipline is chosen every leg is drawn straight, nothing asked and no time', async () => {
|
||||
const route = await routeSkiTour([start, col, hut], { discipline: null, technique: null }, 7);
|
||||
expect(plan).not.toHaveBeenCalled();
|
||||
expect(route?.durationSeconds).toBeNull();
|
||||
expect(route?.parts).toHaveLength(1);
|
||||
expect(route?.parts[0]?.free).toBe(true);
|
||||
const coordinates = route?.coordinates ?? [];
|
||||
expect(coordinates[0]).toEqual([47, 11]);
|
||||
expect(coordinates[coordinates.length - 1]).toEqual([47.01, 11.01]);
|
||||
expect(route?.parts.every((part) => part.free)).toBe(true);
|
||||
expect(route?.legs).toEqual([null, null]);
|
||||
// Every 25 m a point, so the heights follow the ground between the two ends of each leg.
|
||||
expect(coordinates.length).toBeGreaterThan(40);
|
||||
expect(route?.coordinates.length ?? 0).toBeGreaterThan(40);
|
||||
const straight = lineLengthM([
|
||||
[47, 11],
|
||||
[47.01, 11],
|
||||
@@ -53,18 +126,120 @@ describe('skiRouting (SKI-02)', () => {
|
||||
});
|
||||
|
||||
it('FE-TOUR-SKI-ROUTE-003: fewer than two points, or a request called off, draw nothing', async () => {
|
||||
expect(await routeSkiTour([start], touring)).toBeNull();
|
||||
expect(await routeSkiTour([start], alpine, 7)).toBeNull();
|
||||
const controller = new AbortController();
|
||||
controller.abort();
|
||||
expect(await routeSkiTour([start, col], touring, controller.signal)).toBeNull();
|
||||
plan.mockResolvedValue(answer([okLeg(start, col)]));
|
||||
expect(await routeSkiTour([start, col], alpine, 7, controller.signal)).toBeNull();
|
||||
});
|
||||
|
||||
it('FE-TOUR-SKI-ROUTE-004: no choice of the discipline changes the legs, so none plans the tour again', () => {
|
||||
it('FE-TOUR-SKI-ROUTE-004: the discipline, a cross-country technique and the options plan the legs; nothing before', () => {
|
||||
expect(skiRouteKey({ discipline: null, technique: null })).toBe('');
|
||||
const keys = [
|
||||
skiRouteKey({ discipline: null, technique: null }),
|
||||
skiRouteKey({ discipline: 'alpine', technique: null }),
|
||||
skiRouteKey(alpine),
|
||||
skiRouteKey({ ...alpine, options: { pace: 'sporty' } }),
|
||||
skiRouteKey({ discipline: 'cross_country', technique: 'skating' }),
|
||||
skiRouteKey({ discipline: 'cross_country', technique: 'classic' }),
|
||||
];
|
||||
expect(new Set(keys).size).toBe(1);
|
||||
expect(new Set(keys).size).toBe(4);
|
||||
// A technique left over from cross-country plans an alpine tour the same way.
|
||||
expect(skiRouteKey({ discipline: 'alpine', technique: 'classic' })).toBe(skiRouteKey(alpine));
|
||||
});
|
||||
|
||||
it('FE-TOUR-SKI-ROUTE-005: planned legs follow the router, styled by kind and grade, with its time and heights', async () => {
|
||||
plan.mockResolvedValue(answer([okLeg(start, col), okLeg(col, hut)]));
|
||||
const route = await routeSkiTour([start, col, hut], { ...alpine, options: { pace: 'relaxed' } }, 7);
|
||||
expect(plan).toHaveBeenCalledWith(
|
||||
7,
|
||||
[
|
||||
{ from: start, to: col },
|
||||
{ from: col, to: hut },
|
||||
],
|
||||
{ discipline: 'alpine', technique: null, options: { pace: 'relaxed' } },
|
||||
undefined
|
||||
);
|
||||
expect(route?.durationSeconds).toBe(2 * 800);
|
||||
expect(route?.distanceMeters).toBe(3600);
|
||||
expect(route?.problems).toEqual([]);
|
||||
expect(route?.parts.map((part) => part.ski?.kind)).toEqual(['lift', 'run', 'lift', 'run']);
|
||||
expect(route?.parts[1]?.ski).toEqual({ kind: 'run', difficulty: 'intermediate', scheme: 'europe' });
|
||||
expect(route?.elevations).toHaveLength(5);
|
||||
expect(route?.elevations?.[0]).toEqual([47, 11, 1500]);
|
||||
expect(route?.build).toBe('2026-11-02');
|
||||
expect(route?.areaNames).toEqual(['Gletscherwelt']);
|
||||
});
|
||||
|
||||
it('FE-TOUR-SKI-ROUTE-006: a leg it cannot plan stays straight and unrouted, with its reason, hint and name', async () => {
|
||||
plan.mockResolvedValue(answer([okLeg(start, col), failed('blocked_by_difficulty')]));
|
||||
const route = await routeSkiTour([start, col, hut], alpine, 7);
|
||||
expect(route?.durationSeconds).toBeNull();
|
||||
expect(route?.elevations).toBeNull();
|
||||
expect(route?.problems).toEqual([
|
||||
{
|
||||
index: 1,
|
||||
code: 'blocked_by_difficulty',
|
||||
hint: { raise_difficulty: 'advanced' },
|
||||
name: 'Schwarze',
|
||||
summer: false,
|
||||
},
|
||||
]);
|
||||
const last = route?.parts[route.parts.length - 1];
|
||||
expect(last?.mark).toBe('unrouted');
|
||||
expect(last?.coordinates).toEqual([
|
||||
[47.01, 11],
|
||||
[47.01, 11.01],
|
||||
]);
|
||||
});
|
||||
|
||||
it('FE-TOUR-SKI-ROUTE-007: a snowshoer off the piste data takes summer paths, said so', async () => {
|
||||
plan.mockResolvedValue(answer([failed('not_on_piste')]));
|
||||
const summer = vi.mocked(routeWalkingTour).mockResolvedValueOnce({
|
||||
coordinates: [
|
||||
[47, 11],
|
||||
[47.005, 11.001],
|
||||
[47.01, 11],
|
||||
],
|
||||
parts: [],
|
||||
distanceMeters: 1200,
|
||||
durationSeconds: 900,
|
||||
});
|
||||
const route = await routeSkiTour([start, col], snowshoe, 7);
|
||||
expect(summer).toHaveBeenCalled();
|
||||
expect(route?.problems[0]).toMatchObject({ code: 'not_on_piste', summer: true });
|
||||
expect(route?.parts[0]).toMatchObject({ mark: 'gap', ski: { kind: 'summer' } });
|
||||
expect(route?.durationSeconds).toBeNull();
|
||||
});
|
||||
|
||||
it('FE-TOUR-SKI-ROUTE-008: an area still downloading is asked again until it is in', async () => {
|
||||
vi.useFakeTimers();
|
||||
plan.mockResolvedValueOnce(answer([failed('loading')])).mockResolvedValueOnce(answer([okLeg(start, col)]));
|
||||
const pending = routeSkiTour([start, col], alpine, 7);
|
||||
await vi.advanceTimersByTimeAsync(2_000);
|
||||
const route = await pending;
|
||||
expect(plan).toHaveBeenCalledTimes(2);
|
||||
expect(route?.problems).toEqual([]);
|
||||
});
|
||||
|
||||
it('FE-TOUR-SKI-ROUTE-009: a request that fails outright leaves every asked leg unrouted, a drawn one kept', async () => {
|
||||
plan.mockRejectedValue(new Error('502'));
|
||||
const drawn = {
|
||||
kind: 'free' as const,
|
||||
geometry: [
|
||||
[47.01, 11],
|
||||
[47.01, 11.01],
|
||||
] as [number, number][],
|
||||
};
|
||||
const route = await routeSkiTour([start, { ...col, fixedLeg: drawn }, hut], alpine, 7);
|
||||
expect(plan).toHaveBeenCalledWith(7, [{ from: start, to: col }], expect.anything(), undefined);
|
||||
expect(route?.problems).toEqual([
|
||||
{ index: 0, code: 'places_unavailable', hint: { free_draw: true }, name: null, summer: false },
|
||||
]);
|
||||
expect(route?.parts.some((part) => part.free)).toBe(true);
|
||||
});
|
||||
|
||||
it('FE-TOUR-SKI-ROUTE-010: a leg takes its time on the move and waiting together', () => {
|
||||
const leg = okLeg(start, col);
|
||||
if (leg.status !== 'ok') throw new Error('planned leg expected');
|
||||
expect(legSeconds(leg)).toBe(800);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
import type { TourFixedLeg } from '@trek/shared';
|
||||
import type { SkiColourScheme, SkiFailCode, SkiFailHint, SkiRouteLeg, SkiRoutePoint, TourFixedLeg } from '@trek/shared';
|
||||
import { skiRouteRepo, type SkiLegAnswer, type SkiPlanAnswer } from '../../../repo/skiRouteRepo';
|
||||
import type { SkiChoice } from '../ski/skiChoice';
|
||||
import { densifyLine, lineLengthM } from './tourFixedLegs';
|
||||
import { isTourRoute } from './tourRouteOutcome';
|
||||
import { freeLegBetween } from './tourRouteActions';
|
||||
import { drawnTour, type TourRouteResult } from './tourRouting';
|
||||
import { drawnTour, routeWalkingTour, type TourRoutePart, type TourRouteResult } from './tourRouting';
|
||||
|
||||
/**
|
||||
* SKI-02: how the legs of a planned ski tour are laid. There is no piste
|
||||
* router yet, so every leg is drawn straight between its two points, as a leg
|
||||
* drawn by hand is, and its heights are read off the ground like any free
|
||||
* leg's. No ski time is worked out from it.
|
||||
*
|
||||
* This file is the one place the piste router plugs in: `skiLegs` will ask it
|
||||
* for the legs between the fixed ones and keep a straight leg wherever it
|
||||
* finds none, and `skiRouteKey` will name what of the discipline, technique
|
||||
* and options the legs were planned for, so a change of them plans the tour
|
||||
* again. Nothing else of the planner has to change for it.
|
||||
* SKI-02: how the legs of a planned ski tour are laid. With a discipline
|
||||
* chosen, the legs without a line of their own go to the piste router in one
|
||||
* request, each going on from the one before, and come back along runs, lifts
|
||||
* and trails. A leg the router cannot plan stays a straight stretch marked as
|
||||
* unrouted, with the reason and the one change that would plan it; a
|
||||
* snowshoe or winter hike leg off the piste data follows summer paths
|
||||
* instead, said so. Before a discipline is chosen every leg is drawn straight
|
||||
* and nothing is asked. A time is given only when the router planned every
|
||||
* leg; heights come from the piste data where it has them for every point.
|
||||
*/
|
||||
|
||||
interface SkiLegPoint {
|
||||
@@ -22,39 +24,276 @@ interface SkiLegPoint {
|
||||
fixedLeg?: TourFixedLeg | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* What of the ski choice the legs are laid for. A straight leg depends on its
|
||||
* two points only, so nothing of the choice yet: a discipline changed plans
|
||||
* nothing again.
|
||||
*/
|
||||
export function skiRouteKey(_choice: SkiChoice): string {
|
||||
return '';
|
||||
type SkiLegFailure = SkiFailCode | 'offline';
|
||||
|
||||
export interface SkiLegProblem {
|
||||
/** The leg's place in the tour: the index of the point it starts at. */
|
||||
index: number;
|
||||
code: SkiLegFailure;
|
||||
hint: SkiFailHint | null;
|
||||
name: string | null;
|
||||
/** The leg follows summer paths instead (snowshoe and winter hike). */
|
||||
summer: boolean;
|
||||
}
|
||||
|
||||
export interface SkiTourRoute extends TourRouteResult {
|
||||
/** Per leg: the router's answer, or null for a leg with a line of its own or none asked. */
|
||||
legs: Array<SkiRouteLeg | null>;
|
||||
problems: SkiLegProblem[];
|
||||
/** The line with the piste data's heights, when it has one for every point; else null. */
|
||||
elevations: [number, number, number][] | null;
|
||||
build: string | null;
|
||||
stale: boolean;
|
||||
areaNames: string[];
|
||||
colourScheme: SkiColourScheme;
|
||||
}
|
||||
|
||||
/** How long a leg waits for its ski area to download, asked again every two seconds. */
|
||||
const LOADING_POLL_MS = 2000;
|
||||
const LOADING_TRIES = 15;
|
||||
/** Failures a snowshoer or winter hiker takes summer paths around. */
|
||||
const SUMMER_FALLBACK: ReadonlySet<string> = new Set(['not_on_piste', 'no_area', 'places_unavailable', 'disabled']);
|
||||
const ON_FOOT: ReadonlySet<string> = new Set(['snowshoe', 'winter_hike']);
|
||||
/** Segments walked or ridden down: drawn dotted. */
|
||||
const DOTTED: ReadonlySet<string> = new Set(['link', 'crossing', 'walk', 'lift_down']);
|
||||
|
||||
/**
|
||||
* One leg for each point but the last: the line the point carries (a leg the
|
||||
* traveller drew), else a straight line to the next point.
|
||||
* What of the ski choice the legs are laid for: the discipline, the technique
|
||||
* where it counts, and the options. Nothing while no discipline is chosen, so
|
||||
* picking the first one is the change that plans the tour.
|
||||
*/
|
||||
export async function skiLegs(
|
||||
points: readonly SkiLegPoint[],
|
||||
_choice: SkiChoice,
|
||||
_signal?: AbortSignal
|
||||
): Promise<TourFixedLeg[]> {
|
||||
export function skiRouteKey(choice: SkiChoice): string {
|
||||
if (choice.discipline === null) return '';
|
||||
const technique = choice.discipline === 'cross_country' ? choice.technique : null;
|
||||
return JSON.stringify([choice.discipline, technique, choice.options ?? {}]);
|
||||
}
|
||||
|
||||
/** One leg for each point but the last: the line the point carries, else a straight line to the next point. */
|
||||
export function skiLegs(points: readonly SkiLegPoint[]): TourFixedLeg[] {
|
||||
return points.slice(0, -1).map((point, index) => point.fixedLeg ?? freeLegBetween(point, points[index + 1]));
|
||||
}
|
||||
|
||||
const sleep = (ms: number, signal?: AbortSignal) =>
|
||||
new Promise<void>((resolve) => {
|
||||
const timer = window.setTimeout(resolve, ms);
|
||||
signal?.addEventListener('abort', () => {
|
||||
window.clearTimeout(timer);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
/** The answers for the legs asked, a leg whose area still downloads asked again until it is in or the wait is over. */
|
||||
async function askRouter(
|
||||
tripId: number | string,
|
||||
legs: Array<{ from: SkiRoutePoint; to: SkiRoutePoint }>,
|
||||
choice: SkiChoice,
|
||||
signal?: AbortSignal
|
||||
): Promise<SkiPlanAnswer> {
|
||||
if (choice.discipline === null) throw new Error('no discipline');
|
||||
const ask = {
|
||||
discipline: choice.discipline,
|
||||
technique: choice.discipline === 'cross_country' ? choice.technique : null,
|
||||
options: choice.options ?? {},
|
||||
};
|
||||
let answer = await skiRouteRepo.plan(tripId, legs, ask, signal);
|
||||
for (let tries = 1; tries < LOADING_TRIES && answer.legs.some(isLoading) && !signal?.aborted; tries += 1) {
|
||||
await sleep(LOADING_POLL_MS, signal);
|
||||
answer = await skiRouteRepo.plan(tripId, legs, ask, signal);
|
||||
}
|
||||
// Waited long enough: the area is not to be had now.
|
||||
const legsNow = answer.legs.map((leg): SkiLegAnswer => (isLoading(leg) ? notReachable : leg));
|
||||
return { ...answer, legs: legsNow };
|
||||
}
|
||||
|
||||
const isLoading = (leg: SkiLegAnswer) => leg.status === 'failed' && leg.code === 'loading';
|
||||
const notReachable: SkiLegAnswer = {
|
||||
status: 'failed',
|
||||
code: 'places_unavailable',
|
||||
at: null,
|
||||
hint: { free_draw: true },
|
||||
detail: null,
|
||||
};
|
||||
|
||||
/** The parts of a planned leg, one per stretch, styled by what it is. */
|
||||
function routedParts(leg: Extract<SkiRouteLeg, { status: 'ok' }>, scheme: SkiColourScheme): TourRoutePart[] {
|
||||
return leg.segments.map((segment) => ({
|
||||
coordinates: leg.geometry.slice(segment.from, segment.to + 1).map(([lat, lng]): [number, number] => [lat, lng]),
|
||||
free: false,
|
||||
...(DOTTED.has(segment.kind) ? { mark: 'gap' as const } : {}),
|
||||
ski: { kind: segment.kind, difficulty: segment.difficulty, scheme },
|
||||
}));
|
||||
}
|
||||
|
||||
const straight = (from: SkiLegPoint, to: SkiLegPoint): [number, number][] => [
|
||||
[from.lat, from.lng],
|
||||
[to.lat, to.lng],
|
||||
];
|
||||
|
||||
/** A leg off the piste data on summer paths, for a snowshoer or winter hiker; null when there is none either. */
|
||||
async function summerParts(from: SkiLegPoint, to: SkiLegPoint, signal?: AbortSignal): Promise<TourRoutePart | null> {
|
||||
try {
|
||||
const routed = await routeWalkingTour([from, to], signal);
|
||||
if (!isTourRoute(routed)) return null;
|
||||
return { coordinates: routed.coordinates, free: false, mark: 'gap', ski: { kind: 'summer', difficulty: null } };
|
||||
} catch {
|
||||
// No summer path either: the leg stays straight and unrouted, which says as much.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Neighbouring parts drawn alike as one line. */
|
||||
function mergeParts(parts: TourRoutePart[]): TourRoutePart[] {
|
||||
const merged: TourRoutePart[] = [];
|
||||
for (const part of parts) {
|
||||
const previous = merged[merged.length - 1];
|
||||
const alike =
|
||||
previous &&
|
||||
previous.free === part.free &&
|
||||
previous.mark === part.mark &&
|
||||
JSON.stringify(previous.ski ?? null) === JSON.stringify(part.ski ?? null);
|
||||
if (alike) previous.coordinates = [...previous.coordinates, ...part.coordinates.slice(1)];
|
||||
else merged.push({ ...part, coordinates: [...part.coordinates] });
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
/** The heights of the whole line when the piste data has one for every point of every leg; else null. */
|
||||
function graphHeights(answers: Array<SkiRouteLeg | null>): [number, number, number][] | null {
|
||||
const line: [number, number, number][] = [];
|
||||
for (const leg of answers) {
|
||||
if (leg?.status !== 'ok') return null;
|
||||
const points = line.length === 0 ? leg.geometry : leg.geometry.slice(1);
|
||||
for (const [lat, lng, ele] of points) {
|
||||
if (ele === null) return null;
|
||||
line.push([lat, lng, ele]);
|
||||
}
|
||||
}
|
||||
return line.length > 1 ? line : null;
|
||||
}
|
||||
|
||||
interface LegOutcome {
|
||||
parts: TourRoutePart[];
|
||||
length: number;
|
||||
answer: SkiRouteLeg | null;
|
||||
problem: SkiLegProblem | null;
|
||||
}
|
||||
|
||||
/** One leg of the tour from the router's answer: planned, on summer paths, or straight and unrouted. */
|
||||
async function legOutcome(
|
||||
index: number,
|
||||
points: readonly SkiLegPoint[],
|
||||
answer: SkiLegAnswer,
|
||||
context: { onFoot: boolean; scheme: SkiColourScheme; signal?: AbortSignal }
|
||||
): Promise<LegOutcome> {
|
||||
const from = points[index];
|
||||
const to = points[index + 1];
|
||||
if (answer.status === 'ok') {
|
||||
return { parts: routedParts(answer, context.scheme), length: answer.distance_m, answer, problem: null };
|
||||
}
|
||||
const name = answer.detail?.name ?? null;
|
||||
const summerFirst = context.onFoot && SUMMER_FALLBACK.has(answer.code);
|
||||
const summer = summerFirst ? await summerParts(from, to, context.signal) : null;
|
||||
const problem = { index, code: answer.code, hint: answer.hint, name, summer: summer !== null };
|
||||
if (summer) return { parts: [summer], length: lineLengthM(summer.coordinates), answer: null, problem };
|
||||
const line = straight(from, to);
|
||||
const unrouted: TourRoutePart = { coordinates: line, free: false, mark: 'unrouted' };
|
||||
return { parts: [unrouted], length: lineLengthM(line), answer: null, problem };
|
||||
}
|
||||
|
||||
/** Asks the router for the legs that need it; a server that cannot be asked fails them all, drawn straight. */
|
||||
async function plannedAnswers(
|
||||
points: readonly SkiLegPoint[],
|
||||
choice: SkiChoice,
|
||||
tripId: number | string,
|
||||
signal?: AbortSignal
|
||||
): Promise<{ asked: number[]; plan: SkiPlanAnswer }> {
|
||||
const asked = points.slice(0, -1).flatMap((point, index) => (point.fixedLeg ? [] : [index]));
|
||||
const at = (point: SkiLegPoint): SkiRoutePoint => ({ lat: point.lat, lng: point.lng });
|
||||
const legs = asked.map((index) => ({ from: at(points[index]), to: at(points[index + 1]) }));
|
||||
const none = (answers: SkiLegAnswer[]): SkiPlanAnswer => ({
|
||||
legs: answers,
|
||||
build: null,
|
||||
stale: false,
|
||||
areaNames: [],
|
||||
colourScheme: 'europe',
|
||||
});
|
||||
if (legs.length === 0) return { asked, plan: none([]) };
|
||||
try {
|
||||
return { asked, plan: await askRouter(tripId, legs, choice, signal) };
|
||||
} catch {
|
||||
// The route request failed outright (the server, the network): every asked leg is said to be unrouted.
|
||||
return { asked, plan: none(legs.map(() => notReachable)) };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A ski tour's line from its legs, drawn and never routed: each leg is sampled
|
||||
* along its length so the heights read for it follow the ground. Null for
|
||||
* fewer than two points or a request called off.
|
||||
* A ski tour's line: each leg planned by the piste router, kept as drawn, or
|
||||
* straight where neither is possible. Null for fewer than two points or a
|
||||
* request called off.
|
||||
*/
|
||||
export async function routeSkiTour(
|
||||
points: readonly SkiLegPoint[],
|
||||
choice: SkiChoice,
|
||||
tripId: number | string,
|
||||
signal?: AbortSignal
|
||||
): Promise<TourRouteResult | null> {
|
||||
): Promise<SkiTourRoute | null> {
|
||||
if (points.length < 2) return null;
|
||||
const legs = await skiLegs(points, choice, signal);
|
||||
if (choice.discipline === null) return drawnSkiTour(points);
|
||||
const { asked, plan } = await plannedAnswers(points, choice, tripId, signal);
|
||||
if (signal?.aborted) return null;
|
||||
return drawnTour(points.map((point, index) => ({ ...point, fixedLeg: legs[index] ?? null })));
|
||||
const byLeg = new Map(asked.map((index, at) => [index, plan.legs[at]]));
|
||||
const onFoot = ON_FOOT.has(choice.discipline);
|
||||
const outcomes = await Promise.all(
|
||||
points.slice(0, -1).map(async (point, index): Promise<LegOutcome> => {
|
||||
const answer = byLeg.get(index);
|
||||
if (point.fixedLeg || !answer) {
|
||||
// A leg drawn by hand, sampled along its length so its heights follow the ground.
|
||||
const drawn = (point.fixedLeg ?? freeLegBetween(point, points[index + 1])).geometry;
|
||||
const line = densifyLine(drawn.map(([lat, lng]): [number, number] => [lat, lng]));
|
||||
return { parts: [{ coordinates: line, free: true }], length: lineLengthM(line), answer: null, problem: null };
|
||||
}
|
||||
return legOutcome(index, points, answer, { onFoot, scheme: plan.colourScheme, signal });
|
||||
})
|
||||
);
|
||||
if (signal?.aborted) return null;
|
||||
const parts = mergeParts(outcomes.flatMap((outcome) => outcome.parts));
|
||||
const coordinates = parts.flatMap((part, index) => (index === 0 ? part.coordinates : part.coordinates.slice(1)));
|
||||
const legs = outcomes.map((outcome) => outcome.answer);
|
||||
const everyLegPlanned = legs.every((leg) => leg?.status === 'ok');
|
||||
const seconds = legs.reduce((sum, leg) => sum + (leg?.status === 'ok' ? legSeconds(leg) : 0), 0);
|
||||
return {
|
||||
coordinates,
|
||||
parts,
|
||||
distanceMeters: outcomes.reduce((sum, outcome) => sum + outcome.length, 0),
|
||||
durationSeconds: everyLegPlanned ? Math.round(seconds) : null,
|
||||
legs,
|
||||
problems: outcomes.flatMap((outcome) => (outcome.problem ? [outcome.problem] : [])),
|
||||
elevations: everyLegPlanned ? graphHeights(legs) : null,
|
||||
build: plan.build,
|
||||
stale: plan.stale,
|
||||
areaNames: plan.areaNames,
|
||||
colourScheme: plan.colourScheme,
|
||||
};
|
||||
}
|
||||
|
||||
/** A leg's time on the move and waiting, in seconds. */
|
||||
export function legSeconds(leg: Extract<SkiRouteLeg, { status: 'ok' }>): number {
|
||||
return leg.seconds.ski + leg.seconds.lift + leg.seconds.wait + leg.seconds.walk;
|
||||
}
|
||||
|
||||
/** Every leg drawn straight (or as the traveller drew it), sampled for its heights, nothing asked and no time. */
|
||||
function drawnSkiTour(points: readonly SkiLegPoint[]): SkiTourRoute | null {
|
||||
const legs = skiLegs(points);
|
||||
const drawn = drawnTour(points.map((point, index) => ({ ...point, fixedLeg: legs[index] ?? null })));
|
||||
if (!drawn) return null;
|
||||
return {
|
||||
...drawn,
|
||||
legs: legs.map(() => null),
|
||||
problems: [],
|
||||
elevations: null,
|
||||
build: null,
|
||||
stale: false,
|
||||
areaNames: [],
|
||||
colourScheme: 'europe',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
plannableTourTypeSchema,
|
||||
runProfileSchema,
|
||||
skiDisciplineSchema,
|
||||
skiRouteOptionsSchema,
|
||||
skiTechniqueSchema,
|
||||
type BikeProfile,
|
||||
type HikingPreferences,
|
||||
@@ -108,7 +109,8 @@ function optionalSki(value: SkiChoice | undefined): boolean {
|
||||
if (!value || typeof value !== 'object') return false;
|
||||
return (
|
||||
(value.discipline === null || skiDisciplineSchema.safeParse(value.discipline).success) &&
|
||||
(value.technique === null || skiTechniqueSchema.safeParse(value.technique).success)
|
||||
(value.technique === null || skiTechniqueSchema.safeParse(value.technique).success) &&
|
||||
(value.options == null || skiRouteOptionsSchema.safeParse(value.options).success)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
maxTourMinutesFor,
|
||||
plannableTourTypeSchema,
|
||||
runMinutesAtPace,
|
||||
skiOptionsForDiscipline,
|
||||
tourWebsiteSchema,
|
||||
type BikeProfile,
|
||||
type HikingPreferences,
|
||||
@@ -50,6 +51,8 @@ export interface TourPlannerFactsInput {
|
||||
bikeProfile: BikeProfile;
|
||||
/** SKI-02: a ski tour's discipline and cross-country technique, nothing chosen until the traveller picks. */
|
||||
ski?: SkiChoice;
|
||||
/** The piste data build a ski tour's legs were planned on; null where the piste router planned none. */
|
||||
skiRouteBuild?: string | null;
|
||||
/** RUN-01: the profile a run is routed for, and the pace it is planned at (null while none is entered). */
|
||||
runProfile: RunProfile;
|
||||
runPaceSecondsPerKm: number | null;
|
||||
@@ -98,11 +101,12 @@ function runMinutes({ runPaceSecondsPerKm: pace, routeAnalysis }: TourPlannerFac
|
||||
* weighs the climbs, a hike by the signposts' rule over distance, climb and
|
||||
* descent once the route has its heights, and by the router's own time before that.
|
||||
* A ride (HOR-01) has none: its pace is the horse's and the rider's, so nothing is made up.
|
||||
* Nor has a ski tour (SKI-02): its legs are drawn, not planned along pistes and lifts.
|
||||
* A ski tour (SKI-02) has the piste router's, which it gives only when it planned every leg.
|
||||
*/
|
||||
function movingMinutes(input: TourPlannerFactsInput, routerMinutes: number | null): number | null {
|
||||
const { paddleEstimate, routeAnalysis } = input;
|
||||
if (input.tourType === 'horse' || input.tourType === 'ski') return null;
|
||||
if (input.tourType === 'horse') return null;
|
||||
if (input.tourType === 'ski') return routerMinutes;
|
||||
if (paddleEstimate) return paddleEstimate.waterMinutes === null ? null : paddleEstimate.estimateMinutes;
|
||||
if (input.tourType === 'run') return runMinutes(input);
|
||||
// A bike tour is timed by the router for its profile, a city walk by the router on foot.
|
||||
@@ -164,6 +168,9 @@ function typeFieldsOf(input: TourPlannerFactsInput) {
|
||||
hiking_preferences: isHike ? input.hikingPreferences : null,
|
||||
ski_discipline: ski?.ski_discipline ?? null,
|
||||
ski_technique: ski?.ski_technique ?? null,
|
||||
// What the piste router was asked for and the data it planned on, so the tour reopens as it was planned.
|
||||
ski_options: ski ? skiOptionsForDiscipline(ski.ski_discipline, input.ski?.options ?? {}) : null,
|
||||
ski_route_build: ski ? (input.skiRouteBuild ?? null) : null,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { DEFAULT_SKI_ROUTE_OPTIONS } from '@trek/shared';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { openedTourFields, tourPlannerFacts, type TourPlannerFactsInput } from './tourPlannerFacts';
|
||||
import { usePlannerHistory } from './usePlannerHistory';
|
||||
@@ -58,7 +59,7 @@ describe('tourPlannerFacts', () => {
|
||||
expect(facts.typeFields).toMatchObject({ tour_type: 'hike', max_hiking_difficulty: 3, boat_type: null });
|
||||
});
|
||||
|
||||
it('SKI-02: saves a ski tour with its discipline, untimed, and asks for a discipline and a cross-country technique', () => {
|
||||
it('SKI-02: saves a ski tour with its discipline and router time, and asks for a discipline and a technique', () => {
|
||||
const analysis = { distanceKm: 8, gain: 600, loss: 600 } as TourPlannerFactsInput['routeAnalysis'];
|
||||
const snowshoe = tourPlannerFacts(
|
||||
hike({
|
||||
@@ -76,11 +77,22 @@ describe('tourPlannerFacts', () => {
|
||||
hiking_preferences: null,
|
||||
});
|
||||
expect(snowshoe.skiMissing).toBeNull();
|
||||
// No ski time is worked out: no signposts' rule, no router time, nothing saved.
|
||||
// A ski time is the piste router's, never the signposts' rule: without one, nothing is saved.
|
||||
expect(snowshoe.walkingDurationMinutes).toBeNull();
|
||||
expect(snowshoe.plannedTotalMinutes).toBeNull();
|
||||
expect(snowshoe.savedDurationSeconds).toBeNull();
|
||||
expect(tourPlannerFacts(hike({ tourType: 'ski', durationSeconds: 3_600 })).savedDurationSeconds).toBeNull();
|
||||
expect(snowshoe.typeFields).toMatchObject({ ski_options: DEFAULT_SKI_ROUTE_OPTIONS, ski_route_build: null });
|
||||
const routed = tourPlannerFacts(
|
||||
hike({
|
||||
tourType: 'ski',
|
||||
ski: { discipline: 'alpine', technique: null },
|
||||
durationSeconds: 3_600,
|
||||
skiRouteBuild: '2026-11-02',
|
||||
})
|
||||
);
|
||||
expect(routed.walkingDurationMinutes).toBe(60);
|
||||
expect(routed.savedDurationSeconds).toBe(3_600);
|
||||
expect(routed.typeFields).toMatchObject({ ski_route_build: '2026-11-02' });
|
||||
|
||||
expect(tourPlannerFacts(hike({ tourType: 'ski' })).skiMissing).toBe('discipline');
|
||||
const crossCountry = hike({ tourType: 'ski', ski: { discipline: 'cross_country', technique: null } });
|
||||
@@ -90,6 +102,7 @@ describe('tourPlannerFacts', () => {
|
||||
for (const tourType of ['hike', 'bike', 'walk', 'kayak'] as const) {
|
||||
const other = tourPlannerFacts(hike({ tourType, ski: { discipline: 'alpine', technique: null } }));
|
||||
expect(other.typeFields).toMatchObject({ ski_discipline: null, ski_technique: null });
|
||||
if (tourType !== 'kayak') expect(other.typeFields).toMatchObject({ ski_options: null, ski_route_build: null });
|
||||
expect(other.skiMissing).toBeNull();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// FE-TOUR-ROUTE-STYLE-001 to FE-TOUR-ROUTE-STYLE-003: how the planner map draws the parts of a tour (PAD-03, PAD-11).
|
||||
// FE-TOUR-ROUTE-STYLE-001 to FE-TOUR-ROUTE-STYLE-004: how the planner map draws the parts of a tour (PAD-03, PAD-11, SKI-02).
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { tourRouteStyle } from './tourRouteStyle';
|
||||
import type { TourRoutePart } from './tourRouting';
|
||||
@@ -44,4 +44,20 @@ describe('tourRouteStyle', () => {
|
||||
const style = tourRouteStyle([part(), part({ free: true })], false, PAINT);
|
||||
expect(style).toEqual({ walking: [false, true], colors: null });
|
||||
});
|
||||
|
||||
it('FE-TOUR-ROUTE-STYLE-004: a ski tour colours each stretch by its grade or kind, and dots the links between them', () => {
|
||||
const style = tourRouteStyle(
|
||||
[
|
||||
part({ ski: { kind: 'run', difficulty: 'intermediate', scheme: 'europe' } }),
|
||||
part({ mark: 'gap', ski: { kind: 'link', difficulty: null } }),
|
||||
part({ ski: { kind: 'lift', difficulty: null } }),
|
||||
part({ mark: 'unrouted' }),
|
||||
],
|
||||
false,
|
||||
PAINT
|
||||
);
|
||||
expect(style.walking).toEqual([false, true, false, true]);
|
||||
expect(style.colors?.map((color) => color?.line)).toEqual(['#d51e1e', '#6b7280', '#1f2937', undefined]);
|
||||
expect(style.colors?.[0]?.casing).toBe('white');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { skiPaintOf } from '../ski/skiRouteStyle';
|
||||
import type { TourRoutePart } from './tourRouting';
|
||||
|
||||
/**
|
||||
@@ -41,7 +42,10 @@ export function tourRouteStyle(
|
||||
): TourRouteStyle {
|
||||
if (!parts?.length) return { walking: null, colors: null };
|
||||
const walking = parts.map((part) => part.free || !!part.land || part.mark === 'gap' || part.mark === 'unrouted');
|
||||
const colors = parts.map((part) => (isPaddle ? paddleColor(part, paint) : undefined));
|
||||
// SKI-02: a ski tour's stretches in the colours of their grades and kinds, over the same rim.
|
||||
const colors = parts.map((part) =>
|
||||
part.ski ? { line: skiPaintOf(part.ski), casing: paint.casing } : isPaddle ? paddleColor(part, paint) : undefined
|
||||
);
|
||||
return {
|
||||
walking: walking.some(Boolean) ? walking : null,
|
||||
colors: colors.some(Boolean) ? colors : null,
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
import type { BikeProfile, HikingPreferences, RunProfile, TourFixedLeg } from '@trek/shared'
|
||||
import type {
|
||||
BikeProfile,
|
||||
HikingPreferences,
|
||||
RunProfile,
|
||||
SkiColourScheme,
|
||||
SkiRunDifficulty,
|
||||
SkiSegmentKind,
|
||||
TourFixedLeg,
|
||||
} from '@trek/shared'
|
||||
import type { Waypoint } from '../../../types'
|
||||
import { valhallaBase, valhallaRun, valhallaTurn, type CostingOptions } from '../../Map/valhallaRoute'
|
||||
import { hikingCosting, TOURS_MAX_HIKING_DIFFICULTY } from './hikingCosting'
|
||||
@@ -17,6 +25,8 @@ export interface TourRoutePart {
|
||||
* it could not plan (drawn straight, and nothing checked it).
|
||||
*/
|
||||
mark?: 'rapids' | 'gap' | 'unrouted'
|
||||
/** SKI-02: a stretch of a ski tour the piste router planned, coloured by what it is; `summer` a summer path. */
|
||||
ski?: { kind: SkiSegmentKind | 'summer'; difficulty: SkiRunDifficulty | null; scheme?: SkiColourScheme }
|
||||
}
|
||||
|
||||
export interface TourRouteResult {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { DEFAULT_SKI_ROUTE_OPTIONS } from '@trek/shared';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const { routeWalkingTour, enrichTourElevations, createTour, detailTour, updateTour } = vi.hoisted(() => ({
|
||||
const { routeWalkingTour, enrichTourElevations, createTour, detailTour, updateTour, planSki } = vi.hoisted(() => ({
|
||||
planSki: vi.fn(),
|
||||
routeWalkingTour: vi.fn(),
|
||||
enrichTourElevations: vi.fn(),
|
||||
createTour: vi.fn(),
|
||||
@@ -9,18 +11,19 @@ const { routeWalkingTour, enrichTourElevations, createTour, detailTour, updateTo
|
||||
updateTour: vi.fn(),
|
||||
}));
|
||||
|
||||
// The ski legs are laid by the planner's own drawing; only the routers and the heights are stood in for.
|
||||
// The ski legs are laid by the planner; only the routers, the piste router and the heights are stood in for.
|
||||
vi.mock('./tourRouting', async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import('./tourRouting')>()),
|
||||
routeWalkingTour,
|
||||
enrichTourElevations,
|
||||
}));
|
||||
vi.mock('../../../repo/tourRepo', () => ({ tourRepo: { create: createTour, detail: detailTour, update: updateTour } }));
|
||||
vi.mock('../../../repo/skiRouteRepo', () => ({ skiRouteRepo: { plan: planSki } }));
|
||||
vi.mock('../useTourPermissions', () => ({ useTourPermissions: () => ({ canEdit: true, canAssign: true }) }));
|
||||
|
||||
import { useTourPlanner } from './useTourPlanner';
|
||||
|
||||
// FE-TOUR-SKI-PLAN-001 to FE-TOUR-SKI-PLAN-006
|
||||
// FE-TOUR-SKI-PLAN-001 to FE-TOUR-SKI-PLAN-007
|
||||
|
||||
const savedSki = {
|
||||
place_id: 95,
|
||||
@@ -50,6 +53,50 @@ async function heightsAlong(line: [number, number][]): Promise<[number, number,
|
||||
return line.map(([lat, lng], index): [number, number, number] => [lat, lng, 1600 - index]);
|
||||
}
|
||||
|
||||
type Point = { lat: number; lng: number };
|
||||
|
||||
/** The piste router's answer for every leg asked: a lift up and a run down, with the graph's heights. */
|
||||
async function pisteLegs(_tripId: number, legs: Array<{ from: Point; to: Point }>) {
|
||||
return {
|
||||
legs: legs.map(({ from, to }) => ({
|
||||
status: 'ok' as const,
|
||||
geometry: [
|
||||
[from.lat, from.lng, 1500],
|
||||
[to.lat, to.lng, 1400],
|
||||
],
|
||||
segments: [
|
||||
{
|
||||
kind: 'run' as const,
|
||||
from: 0,
|
||||
to: 1,
|
||||
name: 'Talabfahrt',
|
||||
ref: null,
|
||||
difficulty: 'easy' as const,
|
||||
lift_type: null,
|
||||
grooming: [],
|
||||
seconds: 600,
|
||||
wait_seconds: 0,
|
||||
length_m: 1900,
|
||||
flags: [],
|
||||
osm_way: null,
|
||||
},
|
||||
],
|
||||
distance_m: 1900,
|
||||
climb_m: 0,
|
||||
descent_m: 100,
|
||||
seconds: { ski: 600, lift: 0, wait: 0, walk: 0 },
|
||||
snapped_m: { from: 0, to: 0 },
|
||||
area_ids: ['w:1'],
|
||||
why: [],
|
||||
alternatives: [],
|
||||
})),
|
||||
build: '2026-11-02',
|
||||
stale: false,
|
||||
areaNames: ['Gletscherwelt'],
|
||||
colourScheme: 'europe' as const,
|
||||
};
|
||||
}
|
||||
|
||||
async function settle() {
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(450);
|
||||
@@ -73,6 +120,7 @@ describe('useTourPlanner, a ski and winter tour (SKI-02)', () => {
|
||||
vi.useFakeTimers();
|
||||
localStorage.clear();
|
||||
routeWalkingTour.mockReset();
|
||||
planSki.mockReset().mockImplementation(pisteLegs);
|
||||
enrichTourElevations.mockReset().mockImplementation(heightsAlong);
|
||||
createTour.mockReset();
|
||||
detailTour.mockReset();
|
||||
@@ -81,10 +129,11 @@ describe('useTourPlanner, a ski and winter tour (SKI-02)', () => {
|
||||
|
||||
afterEach(() => vi.useRealTimers());
|
||||
|
||||
it('FE-TOUR-SKI-PLAN-001: draws every leg straight, asks no router, reads the heights, and works out no time', async () => {
|
||||
it('FE-TOUR-SKI-PLAN-001: before a discipline every leg is drawn straight, nothing asked and no time', async () => {
|
||||
const { result } = await plannedSki(81);
|
||||
expect(result.current.isSki).toBe(true);
|
||||
expect(routeWalkingTour).not.toHaveBeenCalled();
|
||||
expect(planSki).not.toHaveBeenCalled();
|
||||
expect(enrichTourElevations).toHaveBeenCalledTimes(1);
|
||||
expect(result.current.status).toBe('ready');
|
||||
expect(result.current.routeParts?.every((part) => part.free)).toBe(true);
|
||||
@@ -108,18 +157,47 @@ describe('useTourPlanner, a ski and winter tour (SKI-02)', () => {
|
||||
expect(result.current.canSave).toBe(true);
|
||||
});
|
||||
|
||||
it('FE-TOUR-SKI-PLAN-003: a discipline changed lays no leg again and reads no height again', async () => {
|
||||
it('FE-TOUR-SKI-PLAN-003: a discipline asks the piste router, whose legs, heights and time the tour takes', async () => {
|
||||
const { result } = await plannedSki(83);
|
||||
act(() => result.current.ski.change({ discipline: 'alpine', technique: null }));
|
||||
await settle();
|
||||
act(() => result.current.ski.change({ discipline: 'snowshoe', technique: null }));
|
||||
await settle();
|
||||
expect(planSki).toHaveBeenCalledWith(
|
||||
83,
|
||||
[{ from: { lat: 47, lng: 11 }, to: { lat: 47.01, lng: 11.02 } }],
|
||||
{ discipline: 'alpine', technique: null, options: {} },
|
||||
expect.anything()
|
||||
);
|
||||
// The piste data has a height for every point: none is read from the terrain again.
|
||||
expect(enrichTourElevations).toHaveBeenCalledTimes(1);
|
||||
expect(result.current.status).toBe('ready');
|
||||
expect(result.current.durationSeconds).toBe(600);
|
||||
expect(result.current.walkingDurationMinutes).toBe(10);
|
||||
expect(result.current.skiRoute?.areaNames).toEqual(['Gletscherwelt']);
|
||||
expect(result.current.routeParts?.[0]?.ski).toEqual({ kind: 'run', difficulty: 'easy', scheme: 'europe' });
|
||||
|
||||
act(() => result.current.ski.change({ discipline: 'snowshoe', technique: null }));
|
||||
await settle();
|
||||
expect(planSki).toHaveBeenCalledTimes(2);
|
||||
expect(result.current.routeCurrent).toBe(true);
|
||||
});
|
||||
|
||||
it('FE-TOUR-SKI-PLAN-004: saves the type, the discipline and no time, nothing of a hike', async () => {
|
||||
it('FE-TOUR-SKI-PLAN-007: an option changed plans the legs again, and a switch to snowboard leaves out surface lifts', async () => {
|
||||
const { result } = await plannedSki(87);
|
||||
act(() => result.current.ski.change({ discipline: 'snowboard', technique: null }));
|
||||
await settle();
|
||||
expect(result.current.ski.choice.options).toEqual({ avoid_lifts: ['drag'] });
|
||||
act(() => result.current.ski.setOption({ pace: 'sporty' }));
|
||||
await settle();
|
||||
expect(planSki).toHaveBeenLastCalledWith(
|
||||
87,
|
||||
expect.anything(),
|
||||
{ discipline: 'snowboard', technique: null, options: { avoid_lifts: ['drag'], pace: 'sporty' } },
|
||||
expect.anything()
|
||||
);
|
||||
expect(planSki).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('FE-TOUR-SKI-PLAN-004: saves the type, the discipline, the router time, options and build, nothing of a hike', async () => {
|
||||
const saved = { ...savedSki, ski_discipline: 'winter_hike', ski_technique: null };
|
||||
createTour.mockResolvedValue({ tour: saved, waypoints: savedPoints });
|
||||
const { result } = await plannedSki(84);
|
||||
@@ -135,7 +213,9 @@ describe('useTourPlanner, a ski and winter tour (SKI-02)', () => {
|
||||
ski_technique: null,
|
||||
max_hiking_difficulty: null,
|
||||
hiking_preferences: null,
|
||||
duration_seconds: null,
|
||||
duration_seconds: 600,
|
||||
ski_options: DEFAULT_SKI_ROUTE_OPTIONS,
|
||||
ski_route_build: '2026-11-02',
|
||||
})
|
||||
);
|
||||
expect(result.current.status).toBe('saved');
|
||||
|
||||
@@ -253,7 +253,7 @@ export function useTourPlanner({ tripId, active = true, onSaved, canEdit: editPe
|
||||
tripId, waypoints, tourType, maxHikingDifficulty, hikingPreferences, bikeProfile, runProfile, paddleBoat, paddleLakeStyle, skiChoice: ski.choice, retryToken,
|
||||
error, setStatus, setError, onCleared: forgetFocus, patchWaypoints,
|
||||
})
|
||||
const { route, routeParts, paddleRoute, enrichedGeometry, routeCurrent, distanceMeters, durationSeconds,
|
||||
const { route, routeParts, paddleRoute, skiRoute, enrichedGeometry, routeCurrent, distanceMeters, durationSeconds,
|
||||
unreachable, interruptRouting, clearRoute } = routing
|
||||
|
||||
const commitWaypoints = useCallback((nextValue: TourPlannerWaypoint[] | ((current: TourPlannerWaypoint[]) => TourPlannerWaypoint[])) => {
|
||||
@@ -535,12 +535,12 @@ export function useTourPlanner({ tripId, active = true, onSaved, canEdit: editPe
|
||||
const stageCount = Math.max(1, stages.stages.length)
|
||||
const facts = useMemo(() => tourPlannerFacts({
|
||||
tourType, maxHikingDifficulty, hikingPreferences, bikeProfile, ski: ski.choice, runProfile, runPaceSecondsPerKm, website,
|
||||
plannedDurationMinutes, breakAdditionalMinutes,
|
||||
skiRouteBuild: skiRoute?.build ?? null, plannedDurationMinutes, breakAdditionalMinutes,
|
||||
paddle: { boatType, waterContext, speedKmh, portageSpeedKmh, lakeStyle },
|
||||
paddleSpeedInvalid, paddleEstimate, routeAnalysis, durationSeconds, stays, stageCount,
|
||||
}), [bikeProfile, boatType, breakAdditionalMinutes, durationSeconds, hikingPreferences, lakeStyle, maxHikingDifficulty, paddleEstimate, paddleSpeedInvalid,
|
||||
plannedDurationMinutes, portageSpeedKmh, routeAnalysis, runPaceSecondsPerKm, runProfile, speedKmh, stageCount, stays, tourType, waterContext,
|
||||
website, ski.choice])
|
||||
website, ski.choice, skiRoute])
|
||||
// CW-02: a city walk's times as the overview says them, from the same derivation as its planned total.
|
||||
const { websiteInvalid, plannedDurationInvalid, breakAdditionalInvalid, paddleIncomplete, walkingDurationMinutes,
|
||||
plannedTotalMinutes, plannedTotalInvalid, savedDurationSeconds, savedBreakAdditionalMinutes, typeFields, stationStays,
|
||||
@@ -920,7 +920,7 @@ export function useTourPlanner({ tripId, active = true, onSaved, canEdit: editPe
|
||||
waypoints, selectedWaypointId, setSelectedWaypointId,
|
||||
addWaypoint, appendFixedLeg, setWaypointPosition, removeWaypoint, moveWaypoint, reorderWaypoint, editWaypoints: commitWaypoints,
|
||||
reverse, returnToStart, isLoop: isClosedLoop(waypoints), toggleWaypointFreeLeg, editPaddlePoints, amendWaypoints, paddleEstimate,
|
||||
...shaping, legVariants, roundHikes, paddleRoute, routeCurrent, stages,
|
||||
...shaping, legVariants, roundHikes, paddleRoute, skiRoute, routeCurrent, stages,
|
||||
undo, redo, canUndo: history.canUndo, canRedo: history.canRedo,
|
||||
status, error, retry, isSaving,
|
||||
route, routeParts, enrichedGeometry, routeAnalysis, distanceMeters, durationSeconds, unreachable,
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { interpolatedHeights } from './useTourRoute';
|
||||
|
||||
// FE-TOUR-ROUTE-HEIGHTS-001 to FE-TOUR-ROUTE-HEIGHTS-002
|
||||
|
||||
describe('interpolatedHeights (SKI-02)', () => {
|
||||
it('FE-TOUR-ROUTE-HEIGHTS-001: fills the points the elevation source missed straight between the known ones', () => {
|
||||
const line: Array<[number, number] | [number, number, number]> = [
|
||||
[47, 11],
|
||||
[47.001, 11, 1000],
|
||||
[47.002, 11],
|
||||
[47.003, 11],
|
||||
[47.004, 11, 1300],
|
||||
[47.005, 11],
|
||||
];
|
||||
expect(interpolatedHeights(line)?.map((point) => point[2])).toEqual([1000, 1000, 1100, 1200, 1300, 1300]);
|
||||
});
|
||||
|
||||
it('FE-TOUR-ROUTE-HEIGHTS-002: a line without one known height has none to estimate from', () => {
|
||||
expect(
|
||||
interpolatedHeights([
|
||||
[47, 11],
|
||||
[47.001, 11],
|
||||
])
|
||||
).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
import { randomId } from '../../../utils/randomId';
|
||||
import type { SkiChoice } from '../ski/skiChoice';
|
||||
import { planPaddleStep, type PaddleTourRoute } from './paddleRouting';
|
||||
import { routeSkiTour, skiRouteKey } from './skiRouting';
|
||||
import { routeSkiTour, skiRouteKey, type SkiTourRoute } from './skiRouting';
|
||||
import {
|
||||
normalizePlannerWaypoints,
|
||||
routeInputsKey,
|
||||
@@ -54,7 +54,7 @@ export interface TourRouteInputs {
|
||||
runProfile: RunProfile;
|
||||
paddleBoat: PaddleBoatType | null;
|
||||
paddleLakeStyle: PaddleLakeStyle;
|
||||
/** SKI-02: a ski tour's discipline and technique, what its legs are laid for. */
|
||||
/** SKI-02: a ski tour's discipline, technique and piste router options, what its legs are laid for. */
|
||||
skiChoice: SkiChoice;
|
||||
/** Raised to plan the same points again. */
|
||||
retryToken: number;
|
||||
@@ -72,6 +72,8 @@ interface PlannedRoute {
|
||||
route: [number, number][] | null;
|
||||
routeParts: TourRoutePart[] | null;
|
||||
paddleRoute: PaddleTourRoute | null;
|
||||
/** SKI-02: a ski tour's legs as the piste router answered them. */
|
||||
skiRoute: SkiTourRoute | null;
|
||||
enrichedGeometry: TourElevationPoint[] | null;
|
||||
distanceMeters: number | null;
|
||||
durationSeconds: number | null;
|
||||
@@ -84,6 +86,7 @@ const NO_ROUTE: PlannedRoute = {
|
||||
route: null,
|
||||
routeParts: null,
|
||||
paddleRoute: null,
|
||||
skiRoute: null,
|
||||
enrichedGeometry: null,
|
||||
distanceMeters: null,
|
||||
durationSeconds: null,
|
||||
@@ -91,11 +94,11 @@ const NO_ROUTE: PlannedRoute = {
|
||||
forRun: null,
|
||||
};
|
||||
|
||||
/** A route planned whole: a paddle route without a failed leg, a route on land with its heights. */
|
||||
/** A route planned whole: a paddle route without a failed leg, a ski tour too, a route on land with its heights. */
|
||||
function routeComplete(planned: PlannedRoute): boolean {
|
||||
return planned.paddleRoute
|
||||
? planned.paddleRoute.failures.length === 0
|
||||
: hasCompleteElevation(planned.enrichedGeometry);
|
||||
if (planned.paddleRoute) return planned.paddleRoute.failures.length === 0;
|
||||
if (planned.skiRoute && planned.skiRoute.problems.length > 0) return false;
|
||||
return hasCompleteElevation(planned.enrichedGeometry);
|
||||
}
|
||||
|
||||
/** One planning run: what it plans from, whether it went stale, and where it writes. */
|
||||
@@ -122,6 +125,7 @@ async function routePaddle({ inputs, plannedKey, runKey, signal, stale, setPlann
|
||||
route: planned.coordinates,
|
||||
routeParts: planned.parts,
|
||||
paddleRoute: planned,
|
||||
skiRoute: null,
|
||||
enrichedGeometry: planned.coordinates.map(([lat, lng]): [number, number, number] => [lat, lng, 0]),
|
||||
distanceMeters: planned.distanceMeters,
|
||||
durationSeconds: null,
|
||||
@@ -135,21 +139,19 @@ async function routePaddle({ inputs, plannedKey, runKey, signal, stale, setPlann
|
||||
|
||||
/**
|
||||
* A route on land for the type of the draft: a bike tour and a run for their
|
||||
* profile, a city walk, a hike within its limit, a ride (HOR-01) put together
|
||||
* from its sections, never routed, and a ski tour (SKI-02) laid by its own
|
||||
* routing, which draws every leg straight until the piste router comes.
|
||||
* profile, a city walk, a hike within its limit, and a ride (HOR-01) put
|
||||
* together from its sections, never routed.
|
||||
*/
|
||||
async function routeLand(inputs: RouteRun['inputs'], signal: AbortSignal) {
|
||||
const { waypoints, tourType, bikeProfile, runProfile, maxHikingDifficulty, hikingPreferences } = inputs;
|
||||
if (tourType === 'horse') return rideTour(waypoints);
|
||||
if (tourType === 'ski') return await routeSkiTour(waypoints, inputs.skiChoice, signal);
|
||||
if (tourType === 'bike') return await routeBikeTour(waypoints, bikeProfile, signal);
|
||||
if (tourType === 'run') return await routeRun(waypoints, runProfile, signal);
|
||||
if (tourType === 'walk') return await routeCityWalk(waypoints, signal);
|
||||
return await routeWalkingTour(waypoints, signal, maxHikingDifficulty, hikingPreferences);
|
||||
}
|
||||
|
||||
// A hike, a run, a city walk, a bike tour, a ride or a ski tour on land, then the heights along it.
|
||||
// A hike, a run, a city walk, a bike tour or a ride on land, then the heights along it.
|
||||
async function routeOnLand({ inputs, plannedKey, runKey, signal, stale, setPlanned, setUnreachable }: RouteRun) {
|
||||
const { setStatus, setError } = inputs;
|
||||
setStatus('routing');
|
||||
@@ -166,6 +168,7 @@ async function routeOnLand({ inputs, plannedKey, runKey, signal, stale, setPlann
|
||||
route: routed.coordinates,
|
||||
routeParts: routed.parts,
|
||||
paddleRoute: null,
|
||||
skiRoute: null,
|
||||
enrichedGeometry: null,
|
||||
distanceMeters: routed.distanceMeters,
|
||||
durationSeconds: routed.durationSeconds,
|
||||
@@ -186,14 +189,68 @@ async function routeOnLand({ inputs, plannedKey, runKey, signal, stale, setPlann
|
||||
setError(null);
|
||||
}
|
||||
|
||||
/** Heights straight between the known ones along the line, where the elevation source has none: an estimate. */
|
||||
export function interpolatedHeights(points: readonly TourElevationPoint[]): [number, number, number][] | null {
|
||||
const known = points.flatMap((point, index) => (point.length === 3 && Number.isFinite(point[2]) ? [index] : []));
|
||||
if (known.length === 0) return null;
|
||||
const height = (k: number) => (points[k] as [number, number, number])[2];
|
||||
return points.map((point, index): [number, number, number] => {
|
||||
const after = known.find((k) => k >= index) ?? (known[known.length - 1] as number);
|
||||
const before = [...known].reverse().find((k) => k <= index) ?? after;
|
||||
const share = after === before ? 0 : (index - before) / (after - before);
|
||||
return [point[0], point[1], height(before) + (height(after) - height(before)) * share];
|
||||
});
|
||||
}
|
||||
|
||||
// SKI-02: a ski tour on the piste router's legs; heights from the piste data where it has them all, else the terrain.
|
||||
async function routeSki({ inputs, plannedKey, runKey, signal, stale, setPlanned }: RouteRun) {
|
||||
const { waypoints, tripId, skiChoice, setStatus, setError } = inputs;
|
||||
setStatus('routing');
|
||||
const planned = await routeSkiTour(waypoints, skiChoice, tripId, signal);
|
||||
if (stale()) return;
|
||||
if (!planned) {
|
||||
setStatus('routing-failed');
|
||||
setError('routing');
|
||||
return;
|
||||
}
|
||||
setPlanned({
|
||||
route: planned.coordinates,
|
||||
routeParts: planned.parts,
|
||||
paddleRoute: null,
|
||||
skiRoute: planned,
|
||||
enrichedGeometry: planned.elevations,
|
||||
distanceMeters: planned.distanceMeters,
|
||||
durationSeconds: planned.durationSeconds,
|
||||
forKey: plannedKey,
|
||||
forRun: runKey,
|
||||
});
|
||||
if (!planned.elevations) {
|
||||
setStatus('enriching-elevation');
|
||||
const enriched = await enrichTourElevations(planned.coordinates, signal);
|
||||
if (stale()) return;
|
||||
// The terrain answered for some points or none: the rest are estimated between the ones known.
|
||||
const heights = hasCompleteElevation(enriched) ? enriched : interpolatedHeights(enriched ?? planned.coordinates);
|
||||
if (!heights) {
|
||||
setStatus('elevation-failed');
|
||||
setError('elevation');
|
||||
return;
|
||||
}
|
||||
setPlanned((current) => ({ ...current, enrichedGeometry: heights }));
|
||||
}
|
||||
setStatus('ready');
|
||||
setError(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Plans a run a moment from now, on the water or on land; a failure that is
|
||||
* still the current run says the route could not be planned. Returns the way
|
||||
* to call it off before it starts.
|
||||
* Plans a run a moment from now, on the water, on the pistes or on land; a
|
||||
* failure that is still the current run says the route could not be planned.
|
||||
* Returns the way to call it off before it starts.
|
||||
*/
|
||||
function scheduleRoute(run: RouteRun): () => void {
|
||||
const timer = window.setTimeout(() => {
|
||||
(run.inputs.tourType === 'kayak' ? routePaddle(run) : routeOnLand(run)).catch(() => {
|
||||
const { tourType } = run.inputs;
|
||||
const plan = tourType === 'kayak' ? routePaddle(run) : tourType === 'ski' ? routeSki(run) : routeOnLand(run);
|
||||
plan.catch(() => {
|
||||
if (run.stale()) return;
|
||||
run.inputs.setStatus('routing-failed');
|
||||
run.inputs.setError('routing');
|
||||
@@ -227,7 +284,7 @@ function plannedKeyOf(waypointKey: string, inputs: TourRouteInputs): string {
|
||||
* or its choices change: a hike on foot within its limit, a bike tour and a
|
||||
* run for their profile (BIK-01, RUN-01), a paddle tour on the water by the
|
||||
* server's router, a ride put together from its horse route sections and
|
||||
* manual legs (HOR-01), a ski tour with its legs drawn (SKI-02). Every tour on
|
||||
* manual legs (HOR-01), a ski tour on the piste router (SKI-02). Every tour on
|
||||
* land then gets its heights; a later
|
||||
* change makes an answer still on its way stale, and a failure keeps the last
|
||||
* route that was planned.
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
import {
|
||||
SKI_DIFFICULTIES,
|
||||
skiLiftClassSchema,
|
||||
type SkiColourScheme,
|
||||
type SkiDifficulty,
|
||||
type SkiRouteOkLeg,
|
||||
type SkiRouteOptionsInput,
|
||||
type SkiRouteSegment,
|
||||
} from '@trek/shared';
|
||||
import { AlertTriangle, CableCar, Footprints, MountainSnow, Spline, Sun } from 'lucide-react';
|
||||
import { useTranslation } from '../../../i18n';
|
||||
import { fs } from '../../shared/DialogShell';
|
||||
import type { SkiLegProblem } from '../planner/skiRouting';
|
||||
import { formatTourDuration } from '../planner/tourDurationInput';
|
||||
import type { SkiChoice } from './skiChoice';
|
||||
import { skiPaintOf } from './skiRouteStyle';
|
||||
|
||||
const ACTION =
|
||||
'inline-flex h-[22px] flex-none items-center gap-1 rounded-full bg-surface-card px-2 font-semibold text-content shadow-sm ring-1 ring-inset ring-edge-faint transition-colors hover:bg-surface-hover disabled:opacity-40';
|
||||
|
||||
/** One change a failed leg's hint suggests, as a change of the ski choice. */
|
||||
interface SkiFix {
|
||||
labelKey: string;
|
||||
apply: (choice: SkiChoice) => SkiChoice;
|
||||
}
|
||||
|
||||
const withOptions = (choice: SkiChoice, patch: Partial<SkiRouteOptionsInput>): SkiChoice => ({
|
||||
...choice,
|
||||
options: { ...choice.options, ...patch },
|
||||
});
|
||||
|
||||
/** The fixes a failed leg's hint offers, each a change of the ski choice that would plan it. */
|
||||
export function skiFixesOf(problem: SkiLegProblem): SkiFix[] {
|
||||
const hint = problem.hint;
|
||||
if (!hint) return [];
|
||||
const fixes: SkiFix[] = [];
|
||||
const level = hint.raise_difficulty;
|
||||
if (level) {
|
||||
const graded = (SKI_DIFFICULTIES as readonly string[]).includes(level);
|
||||
fixes.push({
|
||||
labelKey: 'tours.ski.fix.raiseDifficulty',
|
||||
apply: (choice) =>
|
||||
withOptions(choice, graded ? { max_difficulty: level as SkiDifficulty } : { allow_freeride: true }),
|
||||
});
|
||||
}
|
||||
const lift = skiLiftClassSchema.safeParse(hint.allow_lift);
|
||||
if (lift.success) {
|
||||
fixes.push({
|
||||
labelKey: 'tours.ski.fix.allowLift',
|
||||
apply: (choice) =>
|
||||
withOptions(choice, { avoid_lifts: (choice.options?.avoid_lifts ?? []).filter((c) => c !== lift.data) }),
|
||||
});
|
||||
}
|
||||
if (hint.allow_lift_down) {
|
||||
fixes.push({
|
||||
labelKey: 'tours.ski.fix.liftDown',
|
||||
apply: (choice) => withOptions(choice, { allow_lift_down: true }),
|
||||
});
|
||||
}
|
||||
const technique = hint.technique;
|
||||
if (technique) fixes.push({ labelKey: 'tours.ski.fix.technique', apply: (choice) => ({ ...choice, technique }) });
|
||||
const discipline = hint.discipline;
|
||||
if (discipline) {
|
||||
fixes.push({
|
||||
labelKey: 'tours.ski.fix.discipline',
|
||||
apply: (choice) => ({
|
||||
...choice,
|
||||
discipline,
|
||||
technique: discipline === 'cross_country' ? choice.technique : null,
|
||||
}),
|
||||
});
|
||||
}
|
||||
return fixes;
|
||||
}
|
||||
|
||||
/**
|
||||
* A ski leg the piste router could not plan: why, the run or lift it is about,
|
||||
* and the change that would plan it, applied with one press. A snowshoer's or
|
||||
* winter hiker's leg taken on summer paths instead says so calmly.
|
||||
*/
|
||||
export function SkiLegFailureRow({
|
||||
problem,
|
||||
choice,
|
||||
disabled,
|
||||
onChoice,
|
||||
onDrawStraight,
|
||||
}: {
|
||||
problem: SkiLegProblem;
|
||||
choice: SkiChoice;
|
||||
disabled: boolean;
|
||||
onChoice: (next: SkiChoice) => void;
|
||||
onDrawStraight: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const tone = problem.summer ? 'bg-surface-secondary text-content-secondary' : 'bg-danger-soft text-danger';
|
||||
return (
|
||||
<li data-testid="tour-planner-ski-leg-failure" className="flex min-w-0 py-0.5 ps-[34px]">
|
||||
<div
|
||||
role={problem.summer ? 'status' : 'alert'}
|
||||
className={`flex w-full min-w-0 flex-col gap-1.5 rounded-[10px] ${tone} px-2 py-1.5`}
|
||||
>
|
||||
<span className="flex min-w-0 items-center gap-1.5 font-semibold" style={fs(11.5, 'body')}>
|
||||
{problem.summer ? (
|
||||
<Sun size={12} strokeWidth={2.2} className="flex-none" aria-hidden="true" />
|
||||
) : (
|
||||
<AlertTriangle size={12} strokeWidth={2.2} className="flex-none" aria-hidden="true" />
|
||||
)}
|
||||
<span className="min-w-0">
|
||||
{problem.summer ? t('tours.ski.summerPath') : t(`tours.ski.fail.${problem.code}`)}
|
||||
</span>
|
||||
</span>
|
||||
{problem.name && (
|
||||
<span
|
||||
className="w-fit max-w-full truncate rounded-full bg-surface-card px-2 py-0.5 text-content"
|
||||
style={fs(10.5, 'caption')}
|
||||
>
|
||||
{problem.name}
|
||||
</span>
|
||||
)}
|
||||
<span className="flex flex-wrap gap-1">
|
||||
{skiFixesOf(problem).map((fix) => (
|
||||
<button
|
||||
key={fix.labelKey}
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
onClick={() => onChoice(fix.apply(choice))}
|
||||
className={ACTION}
|
||||
style={fs(10.5)}
|
||||
>
|
||||
{t(fix.labelKey)}
|
||||
</button>
|
||||
))}
|
||||
<button type="button" disabled={disabled} onClick={onDrawStraight} className={ACTION} style={fs(10.5)}>
|
||||
<Spline size={11} strokeWidth={2.2} aria-hidden="true" />
|
||||
{t('tours.paddle.drawStraight')}
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
/** Neighbouring stretches of one way (the same kind, name and grade) as one step. */
|
||||
function stepsOf(segments: readonly SkiRouteSegment[]): SkiRouteSegment[] {
|
||||
const steps: SkiRouteSegment[] = [];
|
||||
for (const segment of segments) {
|
||||
const last = steps[steps.length - 1];
|
||||
const same =
|
||||
last && last.kind === segment.kind && last.name === segment.name && last.difficulty === segment.difficulty;
|
||||
if (same) steps[steps.length - 1] = { ...last, to: segment.to, seconds: last.seconds + segment.seconds };
|
||||
else steps.push(segment);
|
||||
}
|
||||
return steps;
|
||||
}
|
||||
|
||||
const LIFT_KINDS = new Set(['lift', 'rail', 'lift_down']);
|
||||
const FOOT_KINDS = new Set(['walk', 'hike', 'link', 'crossing']);
|
||||
|
||||
/** The icon of a step: a lift, a stretch on foot, or skiing. */
|
||||
function StepIcon({ kind }: { kind: SkiRouteSegment['kind'] }) {
|
||||
const Icon = LIFT_KINDS.has(kind) ? CableCar : FOOT_KINDS.has(kind) ? Footprints : MountainSnow;
|
||||
return <Icon size={11} strokeWidth={2.2} className="flex-none" aria-hidden="true" />;
|
||||
}
|
||||
|
||||
/**
|
||||
* The steps of a planned ski leg, in order: each run in the colour of its
|
||||
* grade, each lift and stretch on foot, with the time on it. A step the map
|
||||
* data names no way for says what kind of way it is.
|
||||
*/
|
||||
export function SkiLegSteps({ leg, scheme }: { leg: SkiRouteOkLeg; scheme: SkiColourScheme }) {
|
||||
const { t } = useTranslation();
|
||||
const steps = stepsOf(leg.segments);
|
||||
return (
|
||||
<li data-testid="tour-planner-ski-leg-steps" className="flex min-w-0 py-0.5 ps-[34px]">
|
||||
<ol className="m-0 flex min-w-0 list-none flex-wrap gap-1 p-0">
|
||||
{steps.map((step, at) => (
|
||||
<li
|
||||
key={`${step.from}-${at}`}
|
||||
className="inline-flex min-w-0 max-w-full items-center gap-1 rounded-full bg-surface-secondary py-0.5 ps-1.5 pe-2 text-content-secondary"
|
||||
style={fs(10.5, 'caption')}
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="h-2 w-2 flex-none rounded-full"
|
||||
style={{ background: skiPaintOf({ kind: step.kind, difficulty: step.difficulty, scheme }) }}
|
||||
/>
|
||||
<StepIcon kind={step.kind} />
|
||||
<span className="min-w-0 truncate">{step.name ?? t(`tours.ski.kind.${step.kind}`)}</span>
|
||||
{step.seconds >= 60 && (
|
||||
<span className="flex-none tabular-nums">{formatTourDuration(Math.round(step.seconds / 60))}</span>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
import {
|
||||
DEFAULT_SKI_ROUTE_OPTIONS,
|
||||
SKI_DIFFICULTIES,
|
||||
SKI_LIFT_CLASSES,
|
||||
SKI_PACES,
|
||||
SKI_ROUTE_PRIORITIES,
|
||||
skiOptionsReadBy,
|
||||
skiUsesLifts,
|
||||
type SkiDifficulty,
|
||||
type SkiDiscipline,
|
||||
type SkiLiftClass,
|
||||
type SkiPace,
|
||||
type SkiRouteOptions as Options,
|
||||
type SkiRouteOptionsInput,
|
||||
type SkiRoutePriority,
|
||||
} from '@trek/shared';
|
||||
import { useTranslation } from '../../../i18n';
|
||||
import ToggleSwitch from '../../Settings/ToggleSwitch';
|
||||
import CustomSelect from '../../shared/CustomSelect';
|
||||
import { LABEL, Segmented } from '../../shared/dialogParts';
|
||||
import { fs } from '../../shared/DialogShell';
|
||||
|
||||
/** The switches of the piste router, each with the key of its label, in the order they are read. */
|
||||
const SWITCHES = [
|
||||
['use_lifts', 'tours.ski.options.useLifts'],
|
||||
['allow_freeride', 'tours.ski.options.freeride'],
|
||||
['allow_lift_down', 'tours.ski.options.liftDown'],
|
||||
['groomed_only', 'tours.ski.options.groomedOnly'],
|
||||
['lit_only', 'tours.ski.options.litOnly'],
|
||||
['touring_uphill_on_pistes', 'tours.ski.options.uphillOnPistes'],
|
||||
['avoid_inferred_links', 'tours.ski.options.avoidLinks'],
|
||||
] as const;
|
||||
|
||||
type SwitchKey = (typeof SWITCHES)[number][0];
|
||||
|
||||
/** One switch set, the rest of the options kept. */
|
||||
function switchPatch(key: SwitchKey, on: boolean): Partial<SkiRouteOptionsInput> {
|
||||
const patch: Partial<SkiRouteOptionsInput> = {};
|
||||
patch[key] = on;
|
||||
return patch;
|
||||
}
|
||||
|
||||
/** Switches that say something only while lifts are ridden. */
|
||||
const LIFT_SWITCHES: ReadonlySet<SwitchKey> = new Set(['allow_lift_down']);
|
||||
|
||||
interface SkiRouteOptionsProps {
|
||||
discipline: SkiDiscipline;
|
||||
options: SkiRouteOptionsInput | null | undefined;
|
||||
onChange: (patch: Partial<SkiRouteOptionsInput>) => void;
|
||||
}
|
||||
|
||||
/** One switch of the piste router on its own row, its label beside it. */
|
||||
function OptionSwitch({ label, on, onToggle }: { label: string; on: boolean; onToggle: () => void }) {
|
||||
return (
|
||||
<div className="flex items-center gap-3">
|
||||
<span aria-hidden="true" className="min-w-0 flex-1 text-content-secondary" style={fs(12, 'body')}>
|
||||
{label}
|
||||
</span>
|
||||
<ToggleSwitch on={on} onToggle={onToggle} label={label} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The lift classes a skier leaves out, as pressed badges: a pressed one is
|
||||
* not ridden. Shown only while lifts are ridden at all.
|
||||
*/
|
||||
function LiftClasses({ avoided, onChange }: { avoided: SkiLiftClass[]; onChange: (next: SkiLiftClass[]) => void }) {
|
||||
const { t } = useTranslation();
|
||||
const flip = (lift: SkiLiftClass) =>
|
||||
onChange(avoided.includes(lift) ? avoided.filter((entry) => entry !== lift) : [...avoided, lift]);
|
||||
return (
|
||||
<div className="min-w-0">
|
||||
<span className={LABEL}>{t('tours.ski.options.avoidLifts')}</span>
|
||||
<div role="group" aria-label={t('tours.ski.options.avoidLifts')} className="flex flex-wrap gap-1.5">
|
||||
{SKI_LIFT_CLASSES.map((lift) => {
|
||||
const left = avoided.includes(lift);
|
||||
const look = left
|
||||
? 'border-edge bg-surface-tertiary text-content-muted line-through'
|
||||
: 'border-edge bg-surface-card text-content-secondary';
|
||||
return (
|
||||
<button
|
||||
key={lift}
|
||||
type="button"
|
||||
aria-pressed={left}
|
||||
onClick={() => flip(lift)}
|
||||
className={`rounded-full border px-2.5 py-1 font-medium ${look}`}
|
||||
style={fs(11.5, 'caption')}
|
||||
>
|
||||
{t(`tours.ski.lift.${lift}`)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* SKI-02: what the piste router is asked for besides the discipline, only the
|
||||
* options the chosen discipline reads: the hardest runs, the pace and what to
|
||||
* weigh most for a downhill skier, groomed and lit trails for cross-country,
|
||||
* the lifts for anyone who rides them. Every change plans the tour again.
|
||||
*/
|
||||
export function SkiRouteOptions({ discipline, options, onChange }: SkiRouteOptionsProps) {
|
||||
const { t } = useTranslation();
|
||||
const read = new Set(skiOptionsReadBy(discipline));
|
||||
const current: Options = { ...DEFAULT_SKI_ROUTE_OPTIONS, ...options };
|
||||
const usesLifts = skiUsesLifts(discipline, current);
|
||||
const switchOn = (key: SwitchKey) => (key === 'use_lifts' ? usesLifts : current[key]);
|
||||
const switches = SWITCHES.filter(([key]) => read.has(key) && (usesLifts || !LIFT_SWITCHES.has(key)));
|
||||
return (
|
||||
<div data-testid="tour-planner-ski-options" className="flex min-w-0 flex-col gap-2.5">
|
||||
{read.has('max_difficulty') && (
|
||||
<div className="min-w-0">
|
||||
<span className={LABEL} id="tour-planner-ski-difficulty-label">
|
||||
{t('tours.ski.options.maxDifficulty')}
|
||||
</span>
|
||||
<CustomSelect
|
||||
id="tour-planner-ski-difficulty"
|
||||
ariaLabel={t('tours.ski.options.maxDifficulty')}
|
||||
size="sm"
|
||||
value={current.max_difficulty}
|
||||
onChange={(value) => onChange({ max_difficulty: value as SkiDifficulty })}
|
||||
options={SKI_DIFFICULTIES.map((level) => ({ value: level, label: t(`tours.ski.difficulty.${level}`) }))}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{read.has('priority') && (
|
||||
<div className="min-w-0">
|
||||
<span className={LABEL}>{t('tours.ski.options.priority')}</span>
|
||||
<Segmented<SkiRoutePriority>
|
||||
value={current.priority}
|
||||
onChange={(priority) => onChange({ priority })}
|
||||
label={t('tours.ski.options.priority')}
|
||||
fill
|
||||
options={SKI_ROUTE_PRIORITIES.map((value) => ({ value, label: t(`tours.ski.priority.${value}`) }))}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{read.has('pace') && (
|
||||
<div className="min-w-0">
|
||||
<span className={LABEL}>{t('tours.ski.options.pace')}</span>
|
||||
<Segmented<SkiPace>
|
||||
value={current.pace}
|
||||
onChange={(pace) => onChange({ pace })}
|
||||
label={t('tours.ski.options.pace')}
|
||||
fill
|
||||
options={SKI_PACES.map((value) => ({ value, label: t(`tours.ski.pace.${value}`) }))}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{switches.map(([key, labelKey]) => (
|
||||
<OptionSwitch
|
||||
key={key}
|
||||
label={t(labelKey)}
|
||||
on={switchOn(key)}
|
||||
onToggle={() => onChange(switchPatch(key, !switchOn(key)))}
|
||||
/>
|
||||
))}
|
||||
{usesLifts && read.has('avoid_lifts') && (
|
||||
<LiftClasses avoided={current.avoid_lifts} onChange={(avoid_lifts) => onChange({ avoid_lifts })} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import type { SkiFlag, SkiRouteOkLeg, SkiWhy } from '@trek/shared';
|
||||
import { Info, MountainSnow } from 'lucide-react';
|
||||
import { useTranslation } from '../../../i18n';
|
||||
import { fs } from '../../shared/DialogShell';
|
||||
import type { SkiTourRoute } from '../planner/skiRouting';
|
||||
import { formatTourDuration } from '../planner/tourDurationInput';
|
||||
import { RailCard, RailFacts } from '../tourParts';
|
||||
|
||||
/** Flags that say a stretch rests on a guess of the map data, not on what is mapped. */
|
||||
const GUESSED: ReadonlySet<SkiFlag> = new Set([
|
||||
'inferred_link',
|
||||
'inferred_crossing',
|
||||
'dir_from_dem',
|
||||
'technique_assumed',
|
||||
'area_derived',
|
||||
'difficulty_unknown',
|
||||
]);
|
||||
|
||||
type Seconds = SkiRouteOkLeg['seconds'];
|
||||
type Avoided = Extract<SkiWhy, { kind: 'avoided_difficulty' }>;
|
||||
|
||||
/** The time of every planned leg, by what it is spent on. */
|
||||
export function skiSecondsOf(route: SkiTourRoute): Seconds {
|
||||
const total: Seconds = { ski: 0, lift: 0, wait: 0, walk: 0 };
|
||||
for (const leg of route.legs) {
|
||||
if (leg?.status !== 'ok') continue;
|
||||
total.ski += leg.seconds.ski;
|
||||
total.lift += leg.seconds.lift;
|
||||
total.wait += leg.seconds.wait;
|
||||
total.walk += leg.seconds.walk;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
/** The harder runs the route kept clear of, and whether any stretch rests on a guess. */
|
||||
function skiWhyOf(route: SkiTourRoute): { avoided: Avoided[]; guessed: boolean } {
|
||||
const why = route.legs.flatMap((leg) => (leg?.status === 'ok' ? leg.why : []));
|
||||
return {
|
||||
avoided: why.flatMap((entry) => (entry.kind === 'avoided_difficulty' ? [entry] : [])),
|
||||
guessed: why.some((entry) => entry.kind === 'flag' && GUESSED.has(entry.flag)),
|
||||
};
|
||||
}
|
||||
|
||||
const minutes = (seconds: number) => formatTourDuration(Math.round(seconds / 60));
|
||||
|
||||
/** A calm line of the card: what the reader should know, never a warning about snow or safety. */
|
||||
function Note({ children }: { children: string }) {
|
||||
return (
|
||||
<p className="m-0 flex min-w-0 items-start gap-1.5 text-content-muted" style={fs(11, 'body')}>
|
||||
<Info size={12} strokeWidth={2} className="mt-0.5 flex-none" aria-hidden="true" />
|
||||
<span className="min-w-0">{children}</span>
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* SKI-02: what the piste router made of a planned ski tour: the time on runs,
|
||||
* on lifts, waiting and on foot, the harder runs it kept clear of, and what
|
||||
* the reader should weigh: legs it could not plan, stretches that rest on a
|
||||
* guess, an older copy of the piste data. The ski areas and the source of the
|
||||
* data close it.
|
||||
*/
|
||||
export function SkiRouteSummary({ route }: { route: SkiTourRoute | null }) {
|
||||
const { t } = useTranslation();
|
||||
if (!route || route.legs.every((leg) => leg === null)) return null;
|
||||
const seconds = skiSecondsOf(route);
|
||||
const { avoided, guessed } = skiWhyOf(route);
|
||||
const level = (entry: Avoided) => t(`tours.ski.difficulty.${entry.level}`);
|
||||
const facts = [
|
||||
{ label: t('tours.ski.summary.ski'), value: minutes(seconds.ski) },
|
||||
{ label: t('tours.ski.summary.lift'), value: minutes(seconds.lift) },
|
||||
{ label: t('tours.ski.summary.wait'), value: minutes(seconds.wait) },
|
||||
{ label: t('tours.ski.summary.walk'), value: minutes(seconds.walk) },
|
||||
];
|
||||
return (
|
||||
<RailCard icon={<MountainSnow size={14} strokeWidth={2} />} title={t('tours.ski.summary.title')} foldId="ski-route">
|
||||
<div data-testid="ski-route-summary" className="flex min-w-0 flex-col gap-3">
|
||||
<RailFacts facts={facts} />
|
||||
{avoided.length > 0 && (
|
||||
<div className="flex min-w-0 flex-col gap-1">
|
||||
<span className="text-content-faint" style={fs(10.5, 'body')}>
|
||||
{t('tours.ski.summary.avoided')}
|
||||
</span>
|
||||
<span className="flex flex-wrap gap-1">
|
||||
{avoided.map((entry, at) => (
|
||||
<span
|
||||
key={`${entry.level}-${entry.name ?? at}`}
|
||||
className="rounded-full bg-surface-secondary px-2 py-0.5 text-content-secondary"
|
||||
style={fs(10.5, 'caption')}
|
||||
>
|
||||
{entry.name ? `${level(entry)}: ${entry.name}` : level(entry)}
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{route.problems.length > 0 && <Note>{t('tours.ski.summary.unplanned')}</Note>}
|
||||
{guessed && <Note>{t('tours.ski.summary.guessed')}</Note>}
|
||||
{route.stale && <Note>{t('tours.ski.summary.stale')}</Note>}
|
||||
<span className="text-content-faint" style={fs(10.5, 'caption')}>
|
||||
{[route.areaNames.join(', '), t('tours.ski.summary.source')].filter(Boolean).join('. ')}
|
||||
</span>
|
||||
</div>
|
||||
</RailCard>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import { DEFAULT_SKI_ROUTE_OPTIONS } from '@trek/shared';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { NO_SKI_CHOICE, skiChoiceChanged, skiChoiceMissing, skiChoiceOf, skiFieldsOf } from './skiChoice';
|
||||
|
||||
// FE-TOUR-SKI-CHOICE-001 to FE-TOUR-SKI-CHOICE-003
|
||||
// FE-TOUR-SKI-CHOICE-001 to FE-TOUR-SKI-CHOICE-004
|
||||
|
||||
describe('ski choice (SKI-02)', () => {
|
||||
it('FE-TOUR-SKI-CHOICE-001: asks for a discipline first, then for a cross-country technique, Unknown counting as one', () => {
|
||||
@@ -32,4 +33,11 @@ describe('ski choice (SKI-02)', () => {
|
||||
const alpine = { discipline: 'alpine' as const, technique: null };
|
||||
expect(skiChoiceChanged({ discipline: 'alpine', technique: 'classic' }, alpine)).toBe(false);
|
||||
});
|
||||
|
||||
it('FE-TOUR-SKI-CHOICE-004: opens with the router options a planned tour kept, and without any where none were kept', () => {
|
||||
const options = { ...DEFAULT_SKI_ROUTE_OPTIONS, pace: 'sporty' as const };
|
||||
const planned = skiChoiceOf({ ski_discipline: 'alpine', ski_technique: null, ski_options: options });
|
||||
expect(planned).toEqual({ discipline: 'alpine', technique: null, options });
|
||||
expect(skiChoiceOf({ ski_discipline: 'alpine', ski_options: null })).not.toHaveProperty('options');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import type { SkiDiscipline, SkiTechnique, TourListItem, TourSkiClassificationRequest } from '@trek/shared';
|
||||
import type {
|
||||
SkiDiscipline,
|
||||
SkiRouteOptionsInput,
|
||||
SkiTechnique,
|
||||
TourListItem,
|
||||
TourSkiClassificationRequest,
|
||||
} from '@trek/shared';
|
||||
|
||||
/**
|
||||
* SKI-02: a ski classification as a form holds it: nothing chosen until the
|
||||
@@ -8,6 +14,8 @@ import type { SkiDiscipline, SkiTechnique, TourListItem, TourSkiClassificationRe
|
||||
export interface SkiChoice {
|
||||
discipline: SkiDiscipline | null;
|
||||
technique: SkiTechnique | null;
|
||||
/** What the piste router is asked for besides the discipline; absent means its defaults. */
|
||||
options?: SkiRouteOptionsInput | null;
|
||||
}
|
||||
|
||||
export const NO_SKI_CHOICE: SkiChoice = { discipline: null, technique: null };
|
||||
@@ -28,9 +36,12 @@ export function skiFieldsOf(
|
||||
: { ski_discipline: choice.discipline };
|
||||
}
|
||||
|
||||
/** The choice a saved ski track opens with. */
|
||||
export function skiChoiceOf(tour: Partial<Pick<TourListItem, 'ski_discipline' | 'ski_technique'>>): SkiChoice {
|
||||
return { discipline: tour.ski_discipline ?? null, technique: tour.ski_technique ?? null };
|
||||
/** The choice a saved ski track opens with, with the router options it was planned with where it keeps them. */
|
||||
export function skiChoiceOf(
|
||||
tour: Partial<Pick<TourListItem, 'ski_discipline' | 'ski_technique' | 'ski_options'>>
|
||||
): SkiChoice {
|
||||
const choice: SkiChoice = { discipline: tour.ski_discipline ?? null, technique: tour.ski_technique ?? null };
|
||||
return tour.ski_options ? { ...choice, options: tour.ski_options } : choice;
|
||||
}
|
||||
|
||||
/** Whether a choice says something else than the saved track does; the technique counts for cross-country only. */
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import type { SkiColourScheme, SkiRunDifficulty, SkiSegmentKind } from '@trek/shared';
|
||||
|
||||
/**
|
||||
* SKI-02: the paint of a ski tour's stretches on the planner map. Runs carry
|
||||
* the colour of their grade as the slopes are marked where they are: in
|
||||
* Europe novice green, easy blue, intermediate red, advanced black, expert
|
||||
* orange and freeride yellow; in North America green for novice and easy,
|
||||
* blue for intermediate, black for advanced and the double black beyond.
|
||||
* Lifts are drawn dark, cross-country trails violet, links and crossings
|
||||
* grey, summer paths amber. Data colours, the same in every theme, as the map
|
||||
* under them is.
|
||||
*/
|
||||
|
||||
const EUROPE: Record<SkiRunDifficulty, string> = {
|
||||
novice: '#2e9e3b',
|
||||
easy: '#2949ca',
|
||||
intermediate: '#d51e1e',
|
||||
advanced: '#202020',
|
||||
expert: '#f37300',
|
||||
freeride: '#e0b800',
|
||||
extreme: '#e0b800',
|
||||
};
|
||||
|
||||
const NORTH_AMERICA: Record<SkiRunDifficulty, string> = {
|
||||
novice: '#2e9e3b',
|
||||
easy: '#2e9e3b',
|
||||
intermediate: '#2949ca',
|
||||
advanced: '#202020',
|
||||
expert: '#000000',
|
||||
freeride: '#000000',
|
||||
extreme: '#000000',
|
||||
};
|
||||
|
||||
const WAYS: Partial<Record<SkiSegmentKind | 'summer', string>> = {
|
||||
lift: '#1f2937',
|
||||
rail: '#1f2937',
|
||||
lift_down: '#1f2937',
|
||||
nordic: '#7c3aed',
|
||||
skitour: '#b45309',
|
||||
hike: '#db2777',
|
||||
link: '#6b7280',
|
||||
crossing: '#6b7280',
|
||||
walk: '#6b7280',
|
||||
summer: '#d97706',
|
||||
};
|
||||
|
||||
/** A run of no known grade, and anything else not named above. */
|
||||
const UNGRADED = '#475569';
|
||||
|
||||
/** The colour a stretch of a ski tour is drawn in. */
|
||||
export function skiPaintOf(ski: {
|
||||
kind: SkiSegmentKind | 'summer';
|
||||
difficulty: SkiRunDifficulty | null;
|
||||
scheme?: SkiColourScheme;
|
||||
}): string {
|
||||
const graded = ski.kind === 'run' || ski.kind === 'area' || ski.kind === 'connection';
|
||||
if (graded && ski.difficulty) return (ski.scheme === 'north_america' ? NORTH_AMERICA : EUROPE)[ski.difficulty];
|
||||
return WAYS[ski.kind] ?? UNGRADED;
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
import type { SkiLiftClass, SkiRouteOkLeg, SkiRouteSegment } from '@trek/shared';
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import type { SkiLegProblem, SkiTourRoute } from '../planner/skiRouting';
|
||||
import { SkiLegFailureRow, SkiLegSteps, skiFixesOf } from './SkiLegRows';
|
||||
import { SkiRouteOptions } from './SkiRouteOptions';
|
||||
import { skiPaintOf } from './skiRouteStyle';
|
||||
import { SkiRouteSummary, skiSecondsOf } from './SkiRouteSummary';
|
||||
|
||||
// FE-TOUR-SKI-UI-001 to FE-TOUR-SKI-UI-009
|
||||
|
||||
function segment(kind: SkiRouteSegment['kind'], extra: Partial<SkiRouteSegment> = {}): SkiRouteSegment {
|
||||
return {
|
||||
kind,
|
||||
from: 0,
|
||||
to: 1,
|
||||
name: null,
|
||||
ref: null,
|
||||
difficulty: null,
|
||||
lift_type: null,
|
||||
grooming: [],
|
||||
seconds: 600,
|
||||
wait_seconds: 0,
|
||||
length_m: 1000,
|
||||
flags: [],
|
||||
osm_way: null,
|
||||
...extra,
|
||||
};
|
||||
}
|
||||
|
||||
function okLeg(extra: Partial<SkiRouteOkLeg> = {}): SkiRouteOkLeg {
|
||||
return {
|
||||
status: 'ok',
|
||||
geometry: [
|
||||
[47, 11, 1500],
|
||||
[47.01, 11, 2000],
|
||||
[47.02, 11, 1400],
|
||||
],
|
||||
segments: [
|
||||
{ ...segment('lift', { name: 'Gipfelbahn' }), from: 0, to: 1 },
|
||||
{ ...segment('run', { difficulty: 'intermediate' }), from: 1, to: 2 },
|
||||
],
|
||||
distance_m: 3000,
|
||||
climb_m: 500,
|
||||
descent_m: 600,
|
||||
seconds: { ski: 600, lift: 480, wait: 120, walk: 0 },
|
||||
snapped_m: { from: 0, to: 0 },
|
||||
area_ids: ['w:1'],
|
||||
why: [],
|
||||
alternatives: [],
|
||||
...extra,
|
||||
};
|
||||
}
|
||||
|
||||
function route(extra: Partial<SkiTourRoute> = {}): SkiTourRoute {
|
||||
return {
|
||||
coordinates: [
|
||||
[47, 11],
|
||||
[47.02, 11],
|
||||
],
|
||||
parts: [],
|
||||
distanceMeters: 3000,
|
||||
durationSeconds: 1200,
|
||||
legs: [okLeg()],
|
||||
problems: [],
|
||||
elevations: null,
|
||||
build: '2026-11-02',
|
||||
stale: false,
|
||||
areaNames: ['Gletscherwelt'],
|
||||
colourScheme: 'europe',
|
||||
...extra,
|
||||
};
|
||||
}
|
||||
|
||||
const problem = (extra: Partial<SkiLegProblem> = {}): SkiLegProblem => ({
|
||||
index: 0,
|
||||
code: 'blocked_by_difficulty',
|
||||
hint: { raise_difficulty: 'advanced' },
|
||||
name: 'Schwarze Wand',
|
||||
summer: false,
|
||||
...extra,
|
||||
});
|
||||
|
||||
describe('SkiRouteOptions (SKI-02)', () => {
|
||||
it('FE-TOUR-SKI-UI-001: a downhill skier sets the hardest runs, the pace, the switches and the lifts to leave out', () => {
|
||||
const onChange = vi.fn();
|
||||
render(<SkiRouteOptions discipline="alpine" options={{ avoid_lifts: ['gondola'] }} onChange={onChange} />);
|
||||
expect(screen.getByRole('button', { name: 'tours.ski.options.maxDifficulty' })).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole('button', { name: 'tours.ski.pace.sporty' }));
|
||||
expect(onChange).toHaveBeenLastCalledWith({ pace: 'sporty' });
|
||||
fireEvent.click(screen.getByRole('button', { name: 'tours.ski.priority.easiest' }));
|
||||
expect(onChange).toHaveBeenLastCalledWith({ priority: 'easiest' });
|
||||
fireEvent.click(screen.getByRole('button', { name: 'tours.ski.options.freeride' }));
|
||||
expect(onChange).toHaveBeenLastCalledWith({ allow_freeride: true });
|
||||
fireEvent.click(screen.getByRole('button', { name: 'tours.ski.options.useLifts' }));
|
||||
expect(onChange).toHaveBeenLastCalledWith({ use_lifts: false });
|
||||
expect(screen.getByRole('button', { name: 'tours.ski.lift.gondola' })).toHaveAttribute('aria-pressed', 'true');
|
||||
fireEvent.click(screen.getByRole('button', { name: 'tours.ski.lift.drag' }));
|
||||
expect(onChange).toHaveBeenLastCalledWith({ avoid_lifts: ['gondola', 'drag'] });
|
||||
fireEvent.click(screen.getByRole('button', { name: 'tours.ski.lift.gondola' }));
|
||||
expect(onChange).toHaveBeenLastCalledWith({ avoid_lifts: [] });
|
||||
});
|
||||
|
||||
it('FE-TOUR-SKI-UI-002: a cross-country skier sees the trail switches only, and no lift classes while riding none', () => {
|
||||
render(<SkiRouteOptions discipline="cross_country" options={null} onChange={vi.fn()} />);
|
||||
expect(screen.getByRole('button', { name: 'tours.ski.options.groomedOnly' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'tours.ski.options.litOnly' })).toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: 'tours.ski.options.maxDifficulty' })).toBeNull();
|
||||
expect(screen.queryByRole('button', { name: 'tours.ski.options.liftDown' })).toBeNull();
|
||||
expect(screen.queryByRole('group', { name: 'tours.ski.options.avoidLifts' })).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('ski leg rows (SKI-02)', () => {
|
||||
it('FE-TOUR-SKI-UI-003: each hint becomes the change of the choice that would plan the leg', () => {
|
||||
const avoided: SkiLiftClass[] = ['drag', 'gondola'];
|
||||
const alpine = { discipline: 'alpine' as const, technique: null, options: { avoid_lifts: avoided } };
|
||||
const [raise, lift] = skiFixesOf(problem({ hint: { raise_difficulty: 'advanced', allow_lift: 'drag' } }));
|
||||
expect(raise?.apply(alpine).options).toMatchObject({ max_difficulty: 'advanced' });
|
||||
expect(lift?.apply(alpine).options).toMatchObject({ avoid_lifts: ['gondola'] });
|
||||
const [freeride] = skiFixesOf(problem({ hint: { raise_difficulty: 'freeride' } }));
|
||||
expect(freeride?.apply(alpine).options).toMatchObject({ allow_freeride: true });
|
||||
const [down, technique, discipline] = skiFixesOf(
|
||||
problem({ hint: { allow_lift_down: true, technique: 'skating', discipline: 'touring' } })
|
||||
);
|
||||
expect(down?.apply(alpine).options).toMatchObject({ allow_lift_down: true });
|
||||
expect(technique?.apply(alpine).technique).toBe('skating');
|
||||
expect(discipline?.apply({ discipline: 'cross_country', technique: 'classic' })).toEqual({
|
||||
discipline: 'touring',
|
||||
technique: null,
|
||||
});
|
||||
expect(skiFixesOf(problem({ hint: null }))).toEqual([]);
|
||||
expect(skiFixesOf(problem({ hint: { allow_lift: 'teleporter' } }))).toEqual([]);
|
||||
});
|
||||
|
||||
it('FE-TOUR-SKI-UI-004: a failed leg says why, names the run, applies its fix and offers to draw it', () => {
|
||||
const onChoice = vi.fn();
|
||||
const onDrawStraight = vi.fn();
|
||||
const choice = { discipline: 'alpine' as const, technique: null };
|
||||
render(
|
||||
<ul>
|
||||
<SkiLegFailureRow
|
||||
problem={problem()}
|
||||
choice={choice}
|
||||
disabled={false}
|
||||
onChoice={onChoice}
|
||||
onDrawStraight={onDrawStraight}
|
||||
/>
|
||||
</ul>
|
||||
);
|
||||
expect(screen.getByRole('alert')).toHaveTextContent('tours.ski.fail.blocked_by_difficulty');
|
||||
expect(screen.getByText('Schwarze Wand')).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole('button', { name: 'tours.ski.fix.raiseDifficulty' }));
|
||||
expect(onChoice).toHaveBeenCalledWith({ ...choice, options: { max_difficulty: 'advanced' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: 'tours.paddle.drawStraight' }));
|
||||
expect(onDrawStraight).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('FE-TOUR-SKI-UI-005: a leg on summer paths is said calmly, not as a failure', () => {
|
||||
render(
|
||||
<ul>
|
||||
<SkiLegFailureRow
|
||||
problem={problem({ code: 'not_on_piste', hint: null, name: null, summer: true })}
|
||||
choice={{ discipline: 'snowshoe', technique: null }}
|
||||
disabled
|
||||
onChoice={vi.fn()}
|
||||
onDrawStraight={vi.fn()}
|
||||
/>
|
||||
</ul>
|
||||
);
|
||||
expect(screen.getByRole('status')).toHaveTextContent('tours.ski.summerPath');
|
||||
expect(screen.queryByRole('alert')).toBeNull();
|
||||
});
|
||||
|
||||
it('FE-TOUR-SKI-UI-006: a planned leg lists its steps, named or by their kind, with the time on each', () => {
|
||||
render(
|
||||
<ul>
|
||||
<SkiLegSteps leg={okLeg()} scheme="europe" />
|
||||
</ul>
|
||||
);
|
||||
const steps = screen.getAllByRole('listitem').filter((item) => item.closest('ol'));
|
||||
expect(steps).toHaveLength(2);
|
||||
expect(steps[0]).toHaveTextContent('Gipfelbahn');
|
||||
expect(steps[1]).toHaveTextContent('tours.ski.kind.run');
|
||||
expect(steps[1]).toHaveTextContent('10');
|
||||
});
|
||||
});
|
||||
|
||||
describe('SkiRouteSummary (SKI-02)', () => {
|
||||
it('FE-TOUR-SKI-UI-007: sums the time by what it is spent on, and says what the reader should weigh', () => {
|
||||
const avoided = { kind: 'avoided_difficulty', level: 'advanced', name: 'Schwarze Wand', saved_seconds: 0 } as const;
|
||||
const guessed = { kind: 'flag', flag: 'inferred_link', count: 1 } as const;
|
||||
const planned = route({
|
||||
legs: [okLeg({ why: [avoided, guessed] }), null],
|
||||
problems: [problem({ index: 1 })],
|
||||
stale: true,
|
||||
});
|
||||
expect(skiSecondsOf(planned)).toEqual({ ski: 600, lift: 480, wait: 120, walk: 0 });
|
||||
render(<SkiRouteSummary route={planned} />);
|
||||
const card = screen.getByTestId('ski-route-summary');
|
||||
expect(card).toHaveTextContent('tours.ski.summary.ski');
|
||||
expect(card).toHaveTextContent('tours.ski.difficulty.advanced: Schwarze Wand');
|
||||
expect(card).toHaveTextContent('tours.ski.summary.unplanned');
|
||||
expect(card).toHaveTextContent('tours.ski.summary.guessed');
|
||||
expect(card).toHaveTextContent('tours.ski.summary.stale');
|
||||
expect(card).toHaveTextContent('Gletscherwelt. tours.ski.summary.source');
|
||||
});
|
||||
|
||||
it('FE-TOUR-SKI-UI-008: nothing while no leg was asked', () => {
|
||||
const { container } = render(<SkiRouteSummary route={route({ legs: [null] })} />);
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
expect(render(<SkiRouteSummary route={null} />).container).toBeEmptyDOMElement();
|
||||
});
|
||||
});
|
||||
|
||||
describe('skiPaintOf (SKI-02)', () => {
|
||||
it('FE-TOUR-SKI-UI-009: runs in the colours of their grade where they are, lifts and the rest by kind', () => {
|
||||
expect(skiPaintOf({ kind: 'run', difficulty: 'intermediate', scheme: 'europe' })).toBe('#d51e1e');
|
||||
expect(skiPaintOf({ kind: 'run', difficulty: 'intermediate', scheme: 'north_america' })).toBe('#2949ca');
|
||||
expect(skiPaintOf({ kind: 'run', difficulty: 'easy' })).toBe('#2949ca');
|
||||
expect(skiPaintOf({ kind: 'lift', difficulty: null })).toBe('#1f2937');
|
||||
expect(skiPaintOf({ kind: 'summer', difficulty: null })).toBe('#d97706');
|
||||
expect(skiPaintOf({ kind: 'run', difficulty: null })).toBe('#475569');
|
||||
});
|
||||
});
|
||||
@@ -3,7 +3,7 @@ import { describe, expect, it, vi } from 'vitest';
|
||||
import { NO_SKI_CHOICE } from './skiChoice';
|
||||
import { useSkiFields } from './useSkiFields';
|
||||
|
||||
// FE-TOUR-SKI-FIELDS-001 to FE-TOUR-SKI-FIELDS-002
|
||||
// FE-TOUR-SKI-FIELDS-001 to FE-TOUR-SKI-FIELDS-004
|
||||
|
||||
describe('useSkiFields (SKI-02)', () => {
|
||||
it('FE-TOUR-SKI-FIELDS-001: a change is an edit only when it says something else and the planner allows it', () => {
|
||||
@@ -27,4 +27,28 @@ describe('useSkiFields (SKI-02)', () => {
|
||||
expect(result.current.choice).toEqual({ discipline: 'cross_country', technique: 'classic' });
|
||||
expect(onEdit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('FE-TOUR-SKI-FIELDS-003: an option changed is an edit that keeps the rest, the same option again none', () => {
|
||||
const onEdit = vi.fn(() => true);
|
||||
const { result } = renderHook(() => useSkiFields({ discipline: 'alpine', technique: null }, onEdit));
|
||||
act(() => result.current.setOption({ pace: 'sporty' }));
|
||||
act(() => result.current.setOption({ max_difficulty: 'advanced' }));
|
||||
expect(result.current.choice.options).toEqual({ pace: 'sporty', max_difficulty: 'advanced' });
|
||||
act(() => result.current.setOption({ pace: 'sporty' }));
|
||||
expect(onEdit).toHaveBeenCalledTimes(2);
|
||||
// Another discipline keeps the options chosen.
|
||||
act(() => result.current.change({ discipline: 'touring', technique: null }));
|
||||
expect(result.current.choice.options).toEqual({ pace: 'sporty', max_difficulty: 'advanced' });
|
||||
});
|
||||
|
||||
it('FE-TOUR-SKI-FIELDS-004: a switch to snowboard leaves out surface lifts, unless lift classes were chosen', () => {
|
||||
const onEdit = vi.fn(() => true);
|
||||
const { result } = renderHook(() => useSkiFields({ discipline: 'alpine', technique: null }, onEdit));
|
||||
act(() => result.current.change({ discipline: 'snowboard', technique: null }));
|
||||
expect(result.current.choice.options).toEqual({ avoid_lifts: ['drag'] });
|
||||
act(() => result.current.setOption({ avoid_lifts: [] }));
|
||||
act(() => result.current.change({ discipline: 'alpine', technique: null }));
|
||||
act(() => result.current.change({ discipline: 'snowboard', technique: null }));
|
||||
expect(result.current.choice.options).toEqual({ avoid_lifts: [] });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,25 +1,52 @@
|
||||
import type { SkiRouteOptionsInput } from '@trek/shared';
|
||||
import { useCallback, useState } from 'react';
|
||||
import type { SkiChoice } from './skiChoice';
|
||||
|
||||
/**
|
||||
* SKI-02: a planned ski tour's discipline and cross-country technique, fields
|
||||
* of the draft like a paddle tour's boat: nothing chosen until the traveller
|
||||
* picks, and no step of the history. A technique stays remembered while
|
||||
* another discipline is tried. `onEdit` tells the planner the draft changed,
|
||||
* and says whether it may.
|
||||
* The options a choice carries on to the next discipline. A snowboarder is
|
||||
* spared surface lifts unless they said otherwise: switching to snowboard
|
||||
* leaves them out where no lift class was chosen yet.
|
||||
*/
|
||||
function carriedOptions(previous: SkiChoice, next: SkiChoice): SkiRouteOptionsInput | null | undefined {
|
||||
const options = next.options !== undefined ? next.options : previous.options;
|
||||
const toBoard = next.discipline === 'snowboard' && previous.discipline !== 'snowboard';
|
||||
if (toBoard && options?.avoid_lifts === undefined) return { ...options, avoid_lifts: ['drag'] };
|
||||
return options;
|
||||
}
|
||||
|
||||
const sameChoice = (a: SkiChoice, b: SkiChoice) =>
|
||||
a.discipline === b.discipline &&
|
||||
a.technique === b.technique &&
|
||||
JSON.stringify(a.options ?? null) === JSON.stringify(b.options ?? null);
|
||||
|
||||
/**
|
||||
* SKI-02: a planned ski tour's discipline, cross-country technique and piste
|
||||
* router options, fields of the draft like a paddle tour's boat: nothing
|
||||
* chosen until the traveller picks, and no step of the history. A technique
|
||||
* stays remembered while another discipline is tried, and so do the options.
|
||||
* `onEdit` tells the planner the draft changed, and says whether it may.
|
||||
*/
|
||||
export function useSkiFields(initial: SkiChoice, onEdit: () => boolean) {
|
||||
const [choice, setChoice] = useState<SkiChoice>(initial);
|
||||
const change = useCallback(
|
||||
(next: SkiChoice) => {
|
||||
if (next.discipline === choice.discipline && next.technique === choice.technique) return;
|
||||
if (onEdit()) setChoice(next);
|
||||
const options = carriedOptions(choice, next);
|
||||
const merged: SkiChoice = { discipline: next.discipline, technique: next.technique };
|
||||
if (options !== undefined) merged.options = options;
|
||||
if (sameChoice(merged, choice)) return;
|
||||
if (onEdit()) setChoice(merged);
|
||||
},
|
||||
[choice, onEdit]
|
||||
);
|
||||
/** One option of the piste router changed, the rest kept. */
|
||||
const setOption = useCallback(
|
||||
(patch: Partial<SkiRouteOptionsInput>) => change({ ...choice, options: { ...choice.options, ...patch } }),
|
||||
[change, choice]
|
||||
);
|
||||
return {
|
||||
choice,
|
||||
change,
|
||||
setOption,
|
||||
/** Loaded from a tour, a draft or an import, or cleared; not an edit. */
|
||||
load: setChoice,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
// FE-REPO-SKIROUTE-001 to FE-REPO-SKIROUTE-005: ski legs planned on the piste data, online only, remembered per request.
|
||||
// FE-API-SKIROUTE-001 to FE-API-SKIROUTE-002: the call behind them, held to the @trek/shared contract.
|
||||
import {
|
||||
MAX_SKI_ROUTE_LEGS,
|
||||
skiRouteRequestSchema,
|
||||
skiRouteResponseSchema,
|
||||
type SkiRouteLeg,
|
||||
type SkiRoutePoint,
|
||||
type SkiRouteResponse,
|
||||
} from '@trek/shared';
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { contractHandler } from '../../tests/helpers/msw/contract';
|
||||
import { server } from '../../tests/helpers/msw/server';
|
||||
import { skiRouteApi } from '../api/skiRoute';
|
||||
import { isEffectivelyOffline } from '../sync/networkMode';
|
||||
import { skiRouteRepo, type SkiRouteAsk } from './skiRouteRepo';
|
||||
|
||||
vi.mock('../sync/networkMode', () => ({ isEffectivelyOffline: vi.fn(() => false) }));
|
||||
vi.mock('../api/skiRoute', () => ({ skiRouteApi: { plan: vi.fn() } }));
|
||||
|
||||
const ALPINE: SkiRouteAsk = { discipline: 'alpine', technique: null, options: {} };
|
||||
const A = { lat: 47.1, lng: 11.1 };
|
||||
const B = { lat: 47.11, lng: 11.12 };
|
||||
const C = { lat: 47.12, lng: 11.14 };
|
||||
|
||||
/** A planned leg: one easy run straight from one point to the next. */
|
||||
function okLeg(from: SkiRoutePoint, to: SkiRoutePoint): SkiRouteLeg {
|
||||
return {
|
||||
status: 'ok',
|
||||
geometry: [
|
||||
[from.lat, from.lng, 1800],
|
||||
[to.lat, to.lng, 1500],
|
||||
],
|
||||
segments: [
|
||||
{
|
||||
kind: 'run',
|
||||
from: 0,
|
||||
to: 1,
|
||||
name: 'Talabfahrt',
|
||||
ref: null,
|
||||
difficulty: 'easy',
|
||||
lift_type: null,
|
||||
grooming: [],
|
||||
seconds: 300,
|
||||
wait_seconds: 0,
|
||||
length_m: 1500,
|
||||
flags: [],
|
||||
osm_way: null,
|
||||
},
|
||||
],
|
||||
distance_m: 1500,
|
||||
climb_m: 0,
|
||||
descent_m: 300,
|
||||
seconds: { ski: 300, lift: 0, wait: 0, walk: 0 },
|
||||
snapped_m: { from: 0, to: 0 },
|
||||
area_ids: ['w:1'],
|
||||
why: [],
|
||||
alternatives: [],
|
||||
};
|
||||
}
|
||||
|
||||
function response(legs: SkiRouteLeg[], extra: Partial<SkiRouteResponse> = {}): SkiRouteResponse {
|
||||
return {
|
||||
legs,
|
||||
build: '2026-11-02',
|
||||
stale: false,
|
||||
area_names: ['Gletscherwelt'],
|
||||
colour_scheme: 'europe',
|
||||
attribution: '© OpenStreetMap contributors',
|
||||
...extra,
|
||||
};
|
||||
}
|
||||
|
||||
function answerAll() {
|
||||
vi.mocked(skiRouteApi.plan).mockImplementation(async (_tripId, request) =>
|
||||
response(request.legs.map(({ from, to }) => okLeg(from, to)))
|
||||
);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.mocked(isEffectivelyOffline).mockReturnValue(false);
|
||||
vi.mocked(skiRouteApi.plan).mockReset();
|
||||
answerAll();
|
||||
});
|
||||
|
||||
describe('skiRouteRepo (SKI-02)', () => {
|
||||
it('FE-REPO-SKIROUTE-001: asks for the legs of a tour in one request and hands on the build, areas and colours', async () => {
|
||||
const answer = await skiRouteRepo.plan(
|
||||
201,
|
||||
[
|
||||
{ from: A, to: B },
|
||||
{ from: B, to: C },
|
||||
],
|
||||
{ discipline: 'cross_country', technique: 'skating', options: { lit_only: true } }
|
||||
);
|
||||
expect(skiRouteApi.plan).toHaveBeenCalledTimes(1);
|
||||
expect(vi.mocked(skiRouteApi.plan).mock.calls[0]?.[1]).toEqual({
|
||||
legs: [
|
||||
{ from: A, to: B },
|
||||
{ from: B, to: C },
|
||||
],
|
||||
discipline: 'cross_country',
|
||||
technique: 'skating',
|
||||
options: { lit_only: true },
|
||||
});
|
||||
expect(answer.legs.map((leg) => leg.status)).toEqual(['ok', 'ok']);
|
||||
expect(answer).toMatchObject({ build: '2026-11-02', stale: false, areaNames: ['Gletscherwelt'] });
|
||||
expect(answer.colourScheme).toBe('europe');
|
||||
});
|
||||
|
||||
it('FE-REPO-SKIROUTE-002: the same request again comes from memory, another option asks again', async () => {
|
||||
await skiRouteRepo.plan(202, [{ from: A, to: B }], ALPINE);
|
||||
await skiRouteRepo.plan(202, [{ from: A, to: B }], ALPINE);
|
||||
expect(skiRouteApi.plan).toHaveBeenCalledTimes(1);
|
||||
await skiRouteRepo.plan(202, [{ from: A, to: B }], { ...ALPINE, options: { pace: 'sporty' } });
|
||||
expect(skiRouteApi.plan).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('FE-REPO-SKIROUTE-003: an answer that may change a moment later is not remembered', async () => {
|
||||
vi.mocked(skiRouteApi.plan).mockResolvedValueOnce(
|
||||
response([{ status: 'failed', code: 'loading', at: null, hint: null, detail: null }], { build: null })
|
||||
);
|
||||
const first = await skiRouteRepo.plan(203, [{ from: A, to: B }], ALPINE);
|
||||
expect(first.legs[0]).toMatchObject({ status: 'failed', code: 'loading' });
|
||||
const again = await skiRouteRepo.plan(203, [{ from: A, to: B }], ALPINE);
|
||||
expect(again.legs[0]?.status).toBe('ok');
|
||||
expect(skiRouteApi.plan).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('FE-REPO-SKIROUTE-004: offline every leg comes back failed as offline and nothing is sent', async () => {
|
||||
vi.mocked(isEffectivelyOffline).mockReturnValue(true);
|
||||
const answer = await skiRouteRepo.plan(204, [{ from: A, to: B }], ALPINE);
|
||||
expect(answer.legs).toEqual([{ status: 'failed', code: 'offline', at: null, hint: null, detail: null }]);
|
||||
expect(answer.build).toBeNull();
|
||||
expect(skiRouteApi.plan).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('FE-REPO-SKIROUTE-005: more legs than one request takes go in batches, merged in order, a stale copy said so', async () => {
|
||||
vi.mocked(skiRouteApi.plan).mockImplementation(async (_tripId, request) => {
|
||||
const legs = request.legs.map(({ from, to }) => okLeg(from, to));
|
||||
return response(legs, { stale: legs.length < MAX_SKI_ROUTE_LEGS, area_names: [`Area ${legs.length}`] });
|
||||
});
|
||||
const many = Array.from({ length: MAX_SKI_ROUTE_LEGS + 2 }, (_, index) => ({
|
||||
from: { lat: 47 + index * 0.001, lng: 11 },
|
||||
to: { lat: 47 + (index + 1) * 0.001, lng: 11 },
|
||||
}));
|
||||
const answer = await skiRouteRepo.plan(205, many, ALPINE);
|
||||
expect(skiRouteApi.plan).toHaveBeenCalledTimes(2);
|
||||
expect(answer.legs).toHaveLength(MAX_SKI_ROUTE_LEGS + 2);
|
||||
expect(answer.stale).toBe(true);
|
||||
expect(answer.areaNames).toEqual([`Area ${MAX_SKI_ROUTE_LEGS}`, 'Area 2']);
|
||||
});
|
||||
});
|
||||
|
||||
/** The real call; the repo tests above run against a mock of this module. */
|
||||
async function realApi() {
|
||||
const actual = await vi.importActual<typeof import('../api/skiRoute')>('../api/skiRoute');
|
||||
return actual.skiRouteApi;
|
||||
}
|
||||
|
||||
describe('skiRouteApi', () => {
|
||||
it('FE-API-SKIROUTE-001: posts the legs to the trip and answers the parsed reply', async () => {
|
||||
const api = await realApi();
|
||||
server.use(
|
||||
contractHandler(
|
||||
'post',
|
||||
'/api/trips/7/ski/route',
|
||||
{ request: skiRouteRequestSchema, response: skiRouteResponseSchema },
|
||||
({ body }) => response(body.legs.map(({ from, to }) => okLeg(from, to)))
|
||||
)
|
||||
);
|
||||
const answer = await api.plan(7, { legs: [{ from: A, to: B }], discipline: 'touring' });
|
||||
expect(answer.legs[0]).toMatchObject({ status: 'ok', distance_m: 1500 });
|
||||
expect(answer.build).toBe('2026-11-02');
|
||||
});
|
||||
|
||||
it('FE-API-SKIROUTE-002: an answer off the contract is refused instead of handed on', async () => {
|
||||
const api = await realApi();
|
||||
server.use(http.post('/api/trips/7/ski/route', () => HttpResponse.json({ legs: [{ status: 'maybe' }] })));
|
||||
await expect(api.plan(7, { legs: [{ from: A, to: B }], discipline: 'alpine' })).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,105 @@
|
||||
import {
|
||||
MAX_SKI_ROUTE_LEGS,
|
||||
type SkiColourScheme,
|
||||
type SkiDiscipline,
|
||||
type SkiRouteLeg,
|
||||
type SkiRoutePoint,
|
||||
type SkiRouteOptionsInput,
|
||||
type SkiRouteResponse,
|
||||
type SkiTechnique,
|
||||
} from '@trek/shared';
|
||||
import { skiRouteApi } from '../api/skiRoute';
|
||||
import { isEffectivelyOffline } from '../sync/networkMode';
|
||||
|
||||
/** What the legs of a ski tour are planned for: the discipline, a cross-country technique and the options. */
|
||||
export interface SkiRouteAsk {
|
||||
discipline: SkiDiscipline;
|
||||
technique: SkiTechnique | null;
|
||||
options: SkiRouteOptionsInput;
|
||||
}
|
||||
|
||||
/** A leg the piste router could not be asked for: offline. */
|
||||
export type SkiLegAnswer = SkiRouteLeg | { status: 'failed'; code: 'offline'; at: null; hint: null; detail: null };
|
||||
|
||||
export interface SkiPlanAnswer {
|
||||
legs: SkiLegAnswer[];
|
||||
build: string | null;
|
||||
stale: boolean;
|
||||
areaNames: string[];
|
||||
colourScheme: SkiColourScheme;
|
||||
}
|
||||
|
||||
const OFFLINE: SkiLegAnswer = { status: 'failed', code: 'offline', at: null, hint: null, detail: null };
|
||||
|
||||
/** How many planned requests stay remembered, so an undo or a redo plans nothing again. */
|
||||
const REMEMBERED = 200;
|
||||
const remembered = new Map<string, SkiPlanAnswer>();
|
||||
|
||||
/** Answers that may change by asking again a moment later are not remembered. */
|
||||
const PASSING: ReadonlySet<string> = new Set(['loading', 'busy', 'timeout', 'places_unavailable', 'offline']);
|
||||
|
||||
function keyOf(
|
||||
tripId: number | string,
|
||||
legs: ReadonlyArray<{ from: SkiRoutePoint; to: SkiRoutePoint }>,
|
||||
ask: SkiRouteAsk
|
||||
): string {
|
||||
const points = legs.map(({ from, to }) => [from.lat, from.lng, to.lat, to.lng].map((v) => v.toFixed(6)).join(','));
|
||||
return [tripId, ask.discipline, ask.technique ?? '', JSON.stringify(ask.options), ...points].join('|');
|
||||
}
|
||||
|
||||
function remember(key: string, answer: SkiPlanAnswer): void {
|
||||
if (answer.legs.some((leg) => leg.status === 'failed' && PASSING.has(leg.code))) return;
|
||||
remembered.delete(key);
|
||||
remembered.set(key, answer);
|
||||
if (remembered.size > REMEMBERED) remembered.delete(remembered.keys().next().value as string);
|
||||
}
|
||||
|
||||
function merged(answers: SkiRouteResponse[]): SkiPlanAnswer {
|
||||
return {
|
||||
legs: answers.flatMap((answer) => answer.legs),
|
||||
build: answers.find((answer) => answer.build)?.build ?? null,
|
||||
stale: answers.some((answer) => answer.stale),
|
||||
areaNames: [...new Set(answers.flatMap((answer) => answer.area_names))],
|
||||
colourScheme: answers[0]?.colour_scheme ?? 'europe',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Legs of a ski tour planned on the piste data, online only on purpose: the
|
||||
* area graphs live on the server, and no clicked point goes further than it.
|
||||
* The legs of a tour go in one request, each going on from the one before; a
|
||||
* request asked before comes from memory, so an undo plans nothing again.
|
||||
* Offline every leg comes back failed as `offline`, and the planner keeps the
|
||||
* stretch visibly unrouted.
|
||||
*/
|
||||
export const skiRouteRepo = {
|
||||
async plan(
|
||||
tripId: number | string,
|
||||
legs: ReadonlyArray<{ from: SkiRoutePoint; to: SkiRoutePoint }>,
|
||||
ask: SkiRouteAsk,
|
||||
signal?: AbortSignal
|
||||
): Promise<SkiPlanAnswer> {
|
||||
const key = keyOf(tripId, legs, ask);
|
||||
const known = remembered.get(key);
|
||||
if (known) return known;
|
||||
if (isEffectivelyOffline()) {
|
||||
return { legs: legs.map(() => OFFLINE), build: null, stale: false, areaNames: [], colourScheme: 'europe' };
|
||||
}
|
||||
const batches: Array<Array<{ from: SkiRoutePoint; to: SkiRoutePoint }>> = [];
|
||||
for (let start = 0; start < legs.length; start += MAX_SKI_ROUTE_LEGS) {
|
||||
batches.push(legs.slice(start, start + MAX_SKI_ROUTE_LEGS));
|
||||
}
|
||||
const answers = await Promise.all(
|
||||
batches.map((batch) =>
|
||||
skiRouteApi.plan(
|
||||
tripId,
|
||||
{ legs: batch, discipline: ask.discipline, technique: ask.technique, options: ask.options },
|
||||
signal
|
||||
)
|
||||
)
|
||||
);
|
||||
const answer = merged(answers);
|
||||
remember(key, answer);
|
||||
return answer;
|
||||
},
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import apiClient from '../api/client';
|
||||
import { cycleMapAdminApi, hikingMapAdminApi } from '../api/tourBaseMaps';
|
||||
import { cycleMapAdminApi, hikingMapAdminApi, skiRoutingAdminApi } from '../api/tourBaseMaps';
|
||||
import { tourTrailGradeApi } from '../api/tourTrailGrade';
|
||||
import { OnlineOnlyError } from './onlineOnly';
|
||||
import { tourTrailGradeRepo } from './tourTrailGradeRepo';
|
||||
@@ -42,5 +42,7 @@ describe('tourBaseMaps admin api (MAP-01, BIK-06)', () => {
|
||||
expect(put).toHaveBeenCalledWith('/admin/tours-cycle-map', { enabled: false });
|
||||
await hikingMapAdminApi.update(true);
|
||||
expect(put).toHaveBeenLastCalledWith('/admin/tours-hiking-map', { enabled: true });
|
||||
await skiRoutingAdminApi.update(false);
|
||||
expect(put).toHaveBeenLastCalledWith('/admin/tours-ski-routing', { enabled: false });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -94,6 +94,15 @@ export const adminHandlers = [
|
||||
return HttpResponse.json({ enabled: body.enabled });
|
||||
}),
|
||||
|
||||
http.get('/api/admin/tours-ski-routing', () => {
|
||||
return HttpResponse.json({ enabled: true });
|
||||
}),
|
||||
|
||||
http.put('/api/admin/tours-ski-routing', async ({ request }) => {
|
||||
const body = await request.json() as Record<string, unknown>;
|
||||
return HttpResponse.json({ enabled: body.enabled });
|
||||
}),
|
||||
|
||||
http.get('/api/admin/addons', () => {
|
||||
return HttpResponse.json({ addons: [] });
|
||||
}),
|
||||
|
||||
@@ -16,7 +16,8 @@ export function hikePlannerDefaults() {
|
||||
setBikeProfile: vi.fn(),
|
||||
// SKI-02: no ski tour, so no discipline chosen nor asked for.
|
||||
isSki: false,
|
||||
ski: { choice: { discipline: null, technique: null }, change: vi.fn(), load: vi.fn() },
|
||||
ski: { choice: { discipline: null, technique: null }, change: vi.fn(), setOption: vi.fn(), load: vi.fn() },
|
||||
skiRoute: null,
|
||||
skiMissing: null,
|
||||
// RUN-01: no run, so no pace asked for.
|
||||
isRun: false,
|
||||
|
||||
@@ -723,6 +723,9 @@ const admin: TranslationStrings = {
|
||||
'admin.toursCycleMap.title': 'خريطة الدراجات (CyclOSM)',
|
||||
'admin.toursCycleMap.subtitle':
|
||||
'يعرض CyclOSM كخريطة أساس لجولات الدراجات في مخطط الجولات. تقدمها OpenStreetMap France للاستخدام المعتدل؛ والتثبيت ذو الاستخدام الكثيف أو التجاري يوقفها أو يتفق مع المشغّل أولًا',
|
||||
'admin.toursSkiRouting.title': 'موجّه المنحدرات',
|
||||
'admin.toursSkiRouting.subtitle':
|
||||
'يخطط جولات التزلج على المنحدرات والمصاعد والمسارات ببيانات المنحدرات من خادم Places. عند إيقافه تُرسم مراحل جولة التزلج مستقيمة',
|
||||
'admin.addons.noAddons': 'لا توجد إضافات متاحة',
|
||||
'admin.weather.title': 'بيانات الطقس',
|
||||
'admin.weather.badge': 'منذ 24 مارس 2026',
|
||||
|
||||
@@ -422,17 +422,89 @@ const tours: TranslationStrings = {
|
||||
'tours.ski.technique.unknown': 'غير معروف',
|
||||
'tours.ski.chooseDiscipline': 'اختر نوعًا لاستيراد المسار كتزلج.',
|
||||
'tours.ski.chooseDisciplineToSave': 'اختر نوعًا لحفظ الجولة.',
|
||||
'tours.ski.drawnFreely':
|
||||
'تُرسم كل مرحلة بخط مستقيم بين نقاطك. لا شيء يتحقق منها مقابل المنحدرات أو المصاعد أو المسارات.',
|
||||
'tours.ski.plannedHint':
|
||||
'تتبع المراحل المنحدرات والمصاعد والمسارات من OpenStreetMap حيث تصل بيانات المنحدرات، وتسير مستقيمة في غير ذلك. لا شيء هنا يقول إن كانت مفتوحة.',
|
||||
'tours.ski.options.maxDifficulty': 'أصعب المنحدرات',
|
||||
'tours.ski.options.priority': 'التفضيل',
|
||||
'tours.ski.options.pace': 'الإيقاع',
|
||||
'tours.ski.options.useLifts': 'استخدام المصاعد',
|
||||
'tours.ski.options.freeride': 'السماح بمسارات التزلج الحر',
|
||||
'tours.ski.options.liftDown': 'النزول بالمصعد عند الحاجة',
|
||||
'tours.ski.options.groomedOnly': 'المسارات المجهزة فقط',
|
||||
'tours.ski.options.litOnly': 'المسارات المضاءة فقط',
|
||||
'tours.ski.options.uphillOnPistes': 'الصعود على حافة المنحدر',
|
||||
'tours.ski.options.avoidLinks': 'تجنب الوصلات المفترضة',
|
||||
'tours.ski.options.avoidLifts': 'استبعاد',
|
||||
'tours.ski.difficulty.novice': 'مبتدئ',
|
||||
'tours.ski.difficulty.easy': 'سهل',
|
||||
'tours.ski.difficulty.intermediate': 'متوسط',
|
||||
'tours.ski.difficulty.advanced': 'صعب',
|
||||
'tours.ski.difficulty.expert': 'خبير',
|
||||
'tours.ski.difficulty.freeride': 'تزلج حر',
|
||||
'tours.ski.difficulty.extreme': 'شديد الصعوبة',
|
||||
'tours.ski.lift.drag': 'مصاعد السحب',
|
||||
'tours.ski.lift.open_chair': 'كراسي معلقة مكشوفة',
|
||||
'tours.ski.lift.gondola': 'عربات الجندول',
|
||||
'tours.ski.lift.cable_car': 'التلفريك',
|
||||
'tours.ski.lift.rail': 'القطارات الجبلية والقطارات',
|
||||
'tours.ski.priority.fastest': 'الأسرع',
|
||||
'tours.ski.priority.most_skiing': 'أكثر تزلجًا',
|
||||
'tours.ski.priority.easiest': 'الأسهل',
|
||||
'tours.ski.pace.relaxed': 'هادئ',
|
||||
'tours.ski.pace.normal': 'عادي',
|
||||
'tours.ski.pace.sporty': 'رياضي',
|
||||
'tours.ski.kind.run': 'منحدر',
|
||||
'tours.ski.kind.lift': 'مصعد',
|
||||
'tours.ski.kind.rail': 'قطار جبلي',
|
||||
'tours.ski.kind.lift_down': 'نزول بالمصعد',
|
||||
'tours.ski.kind.nordic': 'مسار تزلج ريفي',
|
||||
'tours.ski.kind.skitour': 'مسار تزلج جبلي',
|
||||
'tours.ski.kind.hike': 'ممر شتوي',
|
||||
'tours.ski.kind.connection': 'منحدر رابط',
|
||||
'tours.ski.kind.area': 'سفح مفتوح',
|
||||
'tours.ski.kind.link': 'وصلة غير مرسومة',
|
||||
'tours.ski.kind.crossing': 'عبور',
|
||||
'tours.ski.kind.walk': 'سيرًا على الأقدام',
|
||||
'tours.ski.fail.not_on_piste': 'هذه النقطة بعيدة جدًا عن أي منحدر أو مسار أو مصعد يمكنك استخدامه.',
|
||||
'tours.ski.fail.no_area': 'لا تغطي أي منطقة تزلج في بيانات المنحدرات هذه النقطة.',
|
||||
'tours.ski.fail.loading': 'ما زالت بيانات المنحدرات لهذه المنطقة قيد التنزيل.',
|
||||
'tours.ski.fail.places_unavailable': 'لا يمكن الوصول إلى بيانات المنحدرات الآن.',
|
||||
'tours.ski.fail.disabled': 'توجيه المنحدرات متوقف.',
|
||||
'tours.ski.fail.blocked_by_difficulty': 'لا يربط هذه النقاط إلا منحدرات أصعب من حدك.',
|
||||
'tours.ski.fail.blocked_by_lift_avoid': 'لا يربط هذه النقاط إلا مصعد استبعدته.',
|
||||
'tours.ski.fail.blocked_by_technique': 'لا يربط هذه النقاط إلا مسارات مجهزة للتقنية الأخرى.',
|
||||
'tours.ski.fail.needs_ride_down': 'تحتاج طريق العودة إلى النزول بالمصعد.',
|
||||
'tours.ski.fail.uphill_only': 'لا يربط هذه النقاط إلا طريق صاعد، ولا يوجد مصعد.',
|
||||
'tours.ski.fail.disconnected': 'لا شيء في بيانات المنحدرات يربط هذه النقاط.',
|
||||
'tours.ski.fail.cross_area': 'تقع هذه النقاط في منطقتي تزلج غير متصلتين.',
|
||||
'tours.ski.fail.busy': 'موجّه المنحدرات مشغول. حاول مرة أخرى بعد قليل.',
|
||||
'tours.ski.fail.timeout': 'استغرق تخطيط هذه المرحلة وقتًا طويلًا.',
|
||||
'tours.ski.fail.offline': 'أنت غير متصل، لذا تُرسم هذه المرحلة مستقيمة حاليًا.',
|
||||
'tours.ski.fix.raiseDifficulty': 'السماح بمنحدرات أصعب',
|
||||
'tours.ski.fix.allowLift': 'السماح بهذا المصعد',
|
||||
'tours.ski.fix.liftDown': 'النزول بالمصعد',
|
||||
'tours.ski.fix.technique': 'تغيير التقنية',
|
||||
'tours.ski.fix.discipline': 'تغيير النوع',
|
||||
'tours.ski.summerPath': 'خارج بيانات المنحدرات: عبر ممرات صيفية قد تكون تحت الثلج.',
|
||||
'tours.ski.summary.title': 'مسار المنحدرات',
|
||||
'tours.ski.summary.ski': 'التزلج',
|
||||
'tours.ski.summary.lift': 'في المصاعد',
|
||||
'tours.ski.summary.wait': 'الانتظار',
|
||||
'tours.ski.summary.walk': 'سيرًا على الأقدام',
|
||||
'tours.ski.summary.avoided': 'تم تجنبه',
|
||||
'tours.ski.summary.unplanned': 'تعذّر تخطيط بعض المراحل، لذا لا يوجد وقت تزلج بعد.',
|
||||
'tours.ski.summary.guessed': 'تعتمد بعض الأجزاء على تخمين من بيانات الخريطة، مثل وصلة أو اتجاه غير مرسوم.',
|
||||
'tours.ski.summary.stale': 'خُطط على نسخة أقدم من بيانات المنحدرات، لأن النسخة الحالية غير متاحة.',
|
||||
'tours.ski.summary.source': 'بيانات المنحدرات © مساهمو OpenStreetMap',
|
||||
'tours.ski.chooseTechnique': 'اختر الأسلوب، أو غير معروف.',
|
||||
'tours.ski.infoLabel': 'ما يفعله TREK بجولات ومسارات التزلج',
|
||||
'tours.ski.info':
|
||||
'في الجولة المخطَّطة تُرسم كل مرحلة بخط مستقيم بين نقاطها، ويبقى المسار المستورد كما سُجّل. لا يُوجَّه أيٌّ منهما على المنحدرات أو المصاعد أو المسارات. تأتي الارتفاعات من التضاريس أو من الملف. لا يحسب TREK وقتًا للتزلج، ولا شيء هنا، ومنه ألوان الانحدار في العرض ثلاثي الأبعاد، يقول شيئًا عن الصعوبة أو الثلج أو الانهيارات أو ما إذا كانت المسارات مفتوحة.',
|
||||
'تتبع الجولة المخططة المنحدرات والمصاعد والمسارات من OpenStreetMap حيث تصل بيانات المنحدرات، وتصل نقاطها بخط مستقيم في غير ذلك. يبقى المسار المستورد كما سُجّل. تأتي الارتفاعات من بيانات المنحدرات أو التضاريس أو الملف. لا شيء هنا، ولا ألوان الانحدار في العرض ثلاثي الأبعاد، يقول شيئًا عن الثلج أو الانهيارات الثلجية أو ما إذا كانت المنحدرات والمصاعد والمسارات مفتوحة.',
|
||||
'tours.ski.time': 'وقت التزلج',
|
||||
'tours.ski.timeUnknown': 'غير محسوب',
|
||||
'tours.ski.timeInfoLabel': 'لماذا لا يوجد وقت تزلج',
|
||||
'tours.ski.timeInfoLabel': 'كيف يُحسب وقت التزلج',
|
||||
'tours.ski.timeInfo':
|
||||
'لا يحسب TREK وقت التزلج: المراحل مرسومة بخطوط مستقيمة، لا مخططة على المنحدرات أو المصاعد أو المسارات. أدخل بنفسك إجمالي الوقت المخطط إن أردت.',
|
||||
'يجمع موجّه المنحدرات الوقت على المنحدرات وفي المصاعد وفي الانتظار عند المحطات السفلية وسيرًا على الأقدام، حسب نوع التزلج وإيقاعك. لا يعطي وقتًا إلا بعد تخطيط كل مرحلة. أدخل إجماليًا مخططًا بنفسك إن أردت.',
|
||||
'tours.ski.classifyTitle': 'تصنيف مسار التزلج',
|
||||
'tours.ski.saved': 'تم حفظ التصنيف',
|
||||
'tours.ski.saveFailed': 'تعذّر حفظ التصنيف.',
|
||||
|
||||
@@ -756,6 +756,9 @@ const admin: TranslationStrings = {
|
||||
'admin.toursCycleMap.title': 'Velosiped xəritəsi (CyclOSM)',
|
||||
'admin.toursCycleMap.subtitle':
|
||||
'Tur planlayıcısında velosiped turları üçün CyclOSM-i əsas xəritə kimi təklif edir. OpenStreetMap France onu orta istifadə üçün təqdim edir; ağır və ya kommersiya quraşdırması onu söndürür və ya əvvəlcə operatorla razılaşır',
|
||||
'admin.toursSkiRouting.title': 'Pist marşrutlaşdırıcısı',
|
||||
'admin.toursSkiRouting.subtitle':
|
||||
'Places serverinin pist məlumatları ilə xizək turlarını pistlər, liftlər və cığırlar boyunca planlaşdırır. Söndürüldükdə xizək turunun mərhələləri düz çəkilir',
|
||||
'admin.addons.noAddons': 'Heç bir əlavə mövcud deyil',
|
||||
'admin.weather.title': 'Hava məlumatları',
|
||||
'admin.weather.badge': '24 mart 2026-cı ildən',
|
||||
|
||||
@@ -411,17 +411,91 @@ const tours: TranslationStrings = {
|
||||
'tours.ski.technique.unknown': 'Naməlum',
|
||||
'tours.ski.chooseDiscipline': 'İzi xizək kimi idxal etmək üçün növ seç.',
|
||||
'tours.ski.chooseDisciplineToSave': 'Turu saxlamaq üçün növ seç.',
|
||||
'tours.ski.drawnFreely':
|
||||
'Hər hissə nöqtələrin arasında düz xətlə çəkilir. Heç nə onu trassalar, liftlər və ya cığırlarla yoxlamır.',
|
||||
'tours.ski.plannedHint':
|
||||
'Mərhələlər pist məlumatlarının çatdığı yerdə OpenStreetMap pistlərini, liftlərini və cığırlarını izləyir, çatmadığı yerdə düz gedir. Burada heç nə onların açıq olub-olmadığını demir.',
|
||||
'tours.ski.options.maxDifficulty': 'Ən çətin pistlər',
|
||||
'tours.ski.options.priority': 'Üstünlük ver',
|
||||
'tours.ski.options.pace': 'Temp',
|
||||
'tours.ski.options.useLifts': 'Liftlərdən istifadə et',
|
||||
'tours.ski.options.freeride': 'Freeride marşrutlarına icazə ver',
|
||||
'tours.ski.options.liftDown': 'Lazım olsa liftlə en',
|
||||
'tours.ski.options.groomedOnly': 'Yalnız hazırlanmış cığırlar',
|
||||
'tours.ski.options.litOnly': 'Yalnız işıqlandırılmış cığırlar',
|
||||
'tours.ski.options.uphillOnPistes': 'Pistin kənarı ilə qalx',
|
||||
'tours.ski.options.avoidLinks': 'Təxmin edilən əlaqələrdən qaç',
|
||||
'tours.ski.options.avoidLifts': 'Çıxar',
|
||||
'tours.ski.difficulty.novice': 'Yeni başlayan',
|
||||
'tours.ski.difficulty.easy': 'Asan',
|
||||
'tours.ski.difficulty.intermediate': 'Orta',
|
||||
'tours.ski.difficulty.advanced': 'Çətin',
|
||||
'tours.ski.difficulty.expert': 'Ekspert',
|
||||
'tours.ski.difficulty.freeride': 'Fririd',
|
||||
'tours.ski.difficulty.extreme': 'Ekstremal',
|
||||
'tours.ski.lift.drag': 'Dartma liftlər',
|
||||
'tours.ski.lift.open_chair': 'Açıq kreslo liftlər',
|
||||
'tours.ski.lift.gondola': 'Qondollar',
|
||||
'tours.ski.lift.cable_car': 'Kanat yolları',
|
||||
'tours.ski.lift.rail': 'Funikulyorlar və qatarlar',
|
||||
'tours.ski.priority.fastest': 'Ən sürətli',
|
||||
'tours.ski.priority.most_skiing': 'Ən çox xizək',
|
||||
'tours.ski.priority.easiest': 'Ən asan',
|
||||
'tours.ski.pace.relaxed': 'Sakit',
|
||||
'tours.ski.pace.normal': 'Normal', // same-as-en
|
||||
'tours.ski.pace.sporty': 'İdmanlı',
|
||||
'tours.ski.kind.run': 'Pist',
|
||||
'tours.ski.kind.lift': 'Lift', // same-as-en
|
||||
'tours.ski.kind.rail': 'Funikulyor',
|
||||
'tours.ski.kind.lift_down': 'Liftlə eniş',
|
||||
'tours.ski.kind.nordic': 'Qaçış xizəyi cığırı',
|
||||
'tours.ski.kind.skitour': 'Xizək turu marşrutu',
|
||||
'tours.ski.kind.hike': 'Qış cığırı',
|
||||
'tours.ski.kind.connection': 'Birləşdirici pist',
|
||||
'tours.ski.kind.area': 'Açıq yamac',
|
||||
'tours.ski.kind.link': 'Xəritədə olmayan əlaqə',
|
||||
'tours.ski.kind.crossing': 'Keçid',
|
||||
'tours.ski.kind.walk': 'Piyada',
|
||||
'tours.ski.fail.not_on_piste': 'Bu nöqtə istifadə edə biləcəyin hər pistdən, cığırdan və ya liftdən çox uzaqdır.',
|
||||
'tours.ski.fail.no_area': 'Pist məlumatlarında heç bir xizək mərkəzi bu nöqtəni əhatə etmir.',
|
||||
'tours.ski.fail.loading': 'Bu ərazinin pist məlumatları hələ yüklənir.',
|
||||
'tours.ski.fail.places_unavailable': 'Pist məlumatlarına hazırda çatmaq olmur.',
|
||||
'tours.ski.fail.disabled': 'Pist marşrutlaşdırması söndürülüb.',
|
||||
'tours.ski.fail.blocked_by_difficulty': 'Bu nöqtələri yalnız limitindən çətin pistlər birləşdirir.',
|
||||
'tours.ski.fail.blocked_by_lift_avoid': 'Bu nöqtələri yalnız çıxardığın lift birləşdirir.',
|
||||
'tours.ski.fail.blocked_by_technique': 'Bu nöqtələri yalnız digər texnika üçün hazırlanmış cığırlar birləşdirir.',
|
||||
'tours.ski.fail.needs_ride_down': 'Geri yol liftlə enməyi tələb edir.',
|
||||
'tours.ski.fail.uphill_only': 'Bu nöqtələri yalnız yuxarı gedən yol birləşdirir və minmək üçün lift yoxdur.',
|
||||
'tours.ski.fail.disconnected': 'Pist məlumatlarında bu nöqtələri heç nə birləşdirmir.',
|
||||
'tours.ski.fail.cross_area': 'Bu nöqtələr bir-birinə bağlı olmayan iki xizək mərkəzindədir.',
|
||||
'tours.ski.fail.busy': 'Pist marşrutlaşdırıcısı məşğuldur. Bir azdan yenidən cəhd et.',
|
||||
'tours.ski.fail.timeout': 'Bu mərhələnin planlaşdırılması çox uzun çəkdi.',
|
||||
'tours.ski.fail.offline': 'Oflayn rejimdəsən, ona görə bu mərhələ hələlik düz çəkilir.',
|
||||
'tours.ski.fix.raiseDifficulty': 'Daha çətin pistlərə icazə ver',
|
||||
'tours.ski.fix.allowLift': 'Bu liftə icazə ver',
|
||||
'tours.ski.fix.liftDown': 'Liftlə en',
|
||||
'tours.ski.fix.technique': 'Texnikanı dəyiş',
|
||||
'tours.ski.fix.discipline': 'Növü dəyiş',
|
||||
'tours.ski.summerPath': 'Pist məlumatlarından kənarda: qar altında ola bilən yay cığırları ilə.',
|
||||
'tours.ski.summary.title': 'Pist marşrutu',
|
||||
'tours.ski.summary.ski': 'Xizək sürmə',
|
||||
'tours.ski.summary.lift': 'Liftlərdə',
|
||||
'tours.ski.summary.wait': 'Gözləmə',
|
||||
'tours.ski.summary.walk': 'Piyada',
|
||||
'tours.ski.summary.avoided': 'Qaçınıldı',
|
||||
'tours.ski.summary.unplanned': 'Bəzi mərhələləri planlaşdırmaq olmadı, ona görə hələ xizək vaxtı yoxdur.',
|
||||
'tours.ski.summary.guessed':
|
||||
'Bəzi hissələr xəritə məlumatlarından təxminə əsaslanır, məsələn xəritədə olmayan əlaqə və ya istiqamət.',
|
||||
'tours.ski.summary.stale':
|
||||
'Cari nüsxəyə çatmaq mümkün olmadığı üçün pist məlumatlarının köhnə nüsxəsi ilə planlaşdırılıb.',
|
||||
'tours.ski.summary.source': 'Pist məlumatları © OpenStreetMap iştirakçıları',
|
||||
'tours.ski.chooseTechnique': 'Texnikanı və ya Naməlum seç.',
|
||||
'tours.ski.infoLabel': 'TREK xizək turları və izləri ilə nə edir',
|
||||
'tours.ski.info':
|
||||
'Planlaşdırılmış turda hər hissə nöqtələri arasında düz xətlə çəkilir, idxal olunmuş iz isə qeydə alındığı kimi qalır. Heç biri trassalar, liftlər və ya cığırlar boyunca marşrutlanmır. Hündürlüklər relyefdən və ya fayldan gəlir. TREK xizək vaxtı hesablamır və burada heç nə, 3D-dəki meyil rəngləri də daxil olmaqla, çətinlik, qar, uçqun və ya trassaların açıq olub-olmaması barədə heç nə demir.',
|
||||
'Planlaşdırılmış tur pist məlumatlarının çatdığı yerdə OpenStreetMap pistlərini, liftlərini və cığırlarını izləyir, çatmadığı yerdə nöqtələrini düz birləşdirir. İdxal edilmiş iz yazıldığı kimi qalır. Hündürlüklər pist məlumatlarından, relyefdən və ya fayldan gəlir. Burada heç nə, 3D meyil rəngləri də daxil, qar, uçqunlar və ya pistlərin, liftlərin və cığırların açıq olub-olmadığı barədə heç nə demir.',
|
||||
'tours.ski.time': 'Xizək vaxtı',
|
||||
'tours.ski.timeUnknown': 'Hesablanmayıb',
|
||||
'tours.ski.timeInfoLabel': 'Niyə xizək vaxtı yoxdur',
|
||||
'tours.ski.timeInfoLabel': 'Xizək vaxtı necə hesablanır',
|
||||
'tours.ski.timeInfo':
|
||||
'TREK xizək vaxtını hesablamır: hissələr düz çəkilir, trassalar, liftlər və ya cığırlar boyunca planlaşdırılmır. İstəsən, planlaşdırılan ümumi vaxtı özün daxil et.',
|
||||
'Pist marşrutlaşdırıcısı növünə və tempinə uyğun olaraq pistlərdə, liftlərdə, aşağı stansiyalarda gözləməyə və piyada keçən vaxtı toplayır. Vaxtı yalnız hər mərhələ planlaşdırıldıqda verir. İstəsən, planlaşdırılan ümumi vaxtı özün daxil et.',
|
||||
'tours.ski.classifyTitle': 'Xizək izini təsnif et',
|
||||
'tours.ski.saved': 'Təsnifat saxlanıldı',
|
||||
'tours.ski.saveFailed': 'Təsnifatı saxlamaq alınmadı.',
|
||||
|
||||
@@ -664,6 +664,9 @@ const admin: TranslationStrings = {
|
||||
'admin.toursCycleMap.title': 'Mapa de ciclismo (CyclOSM)',
|
||||
'admin.toursCycleMap.subtitle':
|
||||
'Oferece o CyclOSM como mapa base para passeios de bicicleta no planejador. O OpenStreetMap France o fornece para uso moderado; uma instalação de uso intenso ou comercial o desativa ou faz antes um acordo com o operador',
|
||||
'admin.toursSkiRouting.title': 'Roteador de pistas',
|
||||
'admin.toursSkiRouting.subtitle':
|
||||
'Planeja passeios de esqui por pistas, teleféricos e trilhas com os dados de pistas do servidor Places. Desativado, os trechos de um passeio de esqui são desenhados em linha reta',
|
||||
'admin.addons.noAddons': 'Nenhum complemento disponível',
|
||||
'admin.addons.integrationHint': 'Serviços de backend e integrações de API sem página dedicada',
|
||||
'admin.weather.title': 'Dados meteorológicos',
|
||||
|
||||
@@ -417,17 +417,92 @@ const tours: TranslationStrings = {
|
||||
'tours.ski.technique.unknown': 'Desconhecida',
|
||||
'tours.ski.chooseDiscipline': 'Escolha uma modalidade para importar a trilha como esqui.',
|
||||
'tours.ski.chooseDisciplineToSave': 'Escolha uma modalidade para salvar o passeio.',
|
||||
'tours.ski.drawnFreely':
|
||||
'Cada trecho é desenhado em linha reta entre seus pontos. Nada o confere com pistas, teleféricos ou trilhas.',
|
||||
'tours.ski.plannedHint':
|
||||
'Os trechos seguem pistas, teleféricos e trilhas do OpenStreetMap onde os dados de pistas chegam, e em linha reta onde não chegam. Nada aqui diz se estão abertos.',
|
||||
'tours.ski.options.maxDifficulty': 'Pistas mais difíceis',
|
||||
'tours.ski.options.priority': 'Preferir',
|
||||
'tours.ski.options.pace': 'Ritmo',
|
||||
'tours.ski.options.useLifts': 'Usar teleféricos',
|
||||
'tours.ski.options.freeride': 'Permitir rotas freeride',
|
||||
'tours.ski.options.liftDown': 'Descer de teleférico se preciso',
|
||||
'tours.ski.options.groomedOnly': 'Só trilhas preparadas',
|
||||
'tours.ski.options.litOnly': 'Só trilhas iluminadas',
|
||||
'tours.ski.options.uphillOnPistes': 'Subir pela borda da pista',
|
||||
'tours.ski.options.avoidLinks': 'Evitar ligações supostas',
|
||||
'tours.ski.options.avoidLifts': 'Excluir',
|
||||
'tours.ski.difficulty.novice': 'Iniciante',
|
||||
'tours.ski.difficulty.easy': 'Fácil',
|
||||
'tours.ski.difficulty.intermediate': 'Intermediária',
|
||||
'tours.ski.difficulty.advanced': 'Difícil',
|
||||
'tours.ski.difficulty.expert': 'Especialista',
|
||||
'tours.ski.difficulty.freeride': 'Freeride', // same-as-en
|
||||
'tours.ski.difficulty.extreme': 'Extrema',
|
||||
'tours.ski.lift.drag': 'Teleskis',
|
||||
'tours.ski.lift.open_chair': 'Cadeirinhas abertas',
|
||||
'tours.ski.lift.gondola': 'Gôndolas',
|
||||
'tours.ski.lift.cable_car': 'Bondinhos',
|
||||
'tours.ski.lift.rail': 'Funiculares e trens',
|
||||
'tours.ski.priority.fastest': 'Mais rápida',
|
||||
'tours.ski.priority.most_skiing': 'Mais esqui',
|
||||
'tours.ski.priority.easiest': 'Mais fácil',
|
||||
'tours.ski.pace.relaxed': 'Tranquilo',
|
||||
'tours.ski.pace.normal': 'Normal', // same-as-en
|
||||
'tours.ski.pace.sporty': 'Esportivo',
|
||||
'tours.ski.kind.run': 'Pista',
|
||||
'tours.ski.kind.lift': 'Teleférico',
|
||||
'tours.ski.kind.rail': 'Funicular', // same-as-en
|
||||
'tours.ski.kind.lift_down': 'Descida de teleférico',
|
||||
'tours.ski.kind.nordic': 'Trilha de esqui cross-country',
|
||||
'tours.ski.kind.skitour': 'Rota de esqui de travessia',
|
||||
'tours.ski.kind.hike': 'Caminho de inverno',
|
||||
'tours.ski.kind.connection': 'Pista de ligação',
|
||||
'tours.ski.kind.area': 'Encosta aberta',
|
||||
'tours.ski.kind.link': 'Ligação não mapeada',
|
||||
'tours.ski.kind.crossing': 'Travessia',
|
||||
'tours.ski.kind.walk': 'A pé',
|
||||
'tours.ski.fail.not_on_piste':
|
||||
'Este ponto está longe demais de qualquer pista, trilha ou teleférico que você possa usar.',
|
||||
'tours.ski.fail.no_area': 'Nenhuma estação nos dados de pistas cobre este ponto.',
|
||||
'tours.ski.fail.loading': 'Os dados de pistas desta área ainda estão sendo baixados.',
|
||||
'tours.ski.fail.places_unavailable': 'Não é possível acessar os dados de pistas agora.',
|
||||
'tours.ski.fail.disabled': 'O roteamento por pistas está desativado.',
|
||||
'tours.ski.fail.blocked_by_difficulty': 'Só pistas acima do seu limite ligam estes pontos.',
|
||||
'tours.ski.fail.blocked_by_lift_avoid': 'Só um teleférico que você excluiu liga estes pontos.',
|
||||
'tours.ski.fail.blocked_by_technique': 'Só trilhas preparadas para a outra técnica ligam estes pontos.',
|
||||
'tours.ski.fail.needs_ride_down': 'A volta precisa de uma descida de teleférico.',
|
||||
'tours.ski.fail.uphill_only': 'Só uma subida liga estes pontos, e não há teleférico para pegar.',
|
||||
'tours.ski.fail.disconnected': 'Nada nos dados de pistas liga estes pontos.',
|
||||
'tours.ski.fail.cross_area': 'Estes pontos ficam em duas estações que não se ligam.',
|
||||
'tours.ski.fail.busy': 'O roteador de pistas está ocupado. Tente de novo em instantes.',
|
||||
'tours.ski.fail.timeout': 'Planejar este trecho demorou demais.',
|
||||
'tours.ski.fail.offline': 'Você está offline, então por enquanto este trecho é desenhado em linha reta.',
|
||||
'tours.ski.fix.raiseDifficulty': 'Permitir pistas mais difíceis',
|
||||
'tours.ski.fix.allowLift': 'Permitir este teleférico',
|
||||
'tours.ski.fix.liftDown': 'Descer de teleférico',
|
||||
'tours.ski.fix.technique': 'Trocar de técnica',
|
||||
'tours.ski.fix.discipline': 'Trocar de disciplina',
|
||||
'tours.ski.summerPath': 'Fora dos dados de pistas: por caminhos de verão, que podem estar sob a neve.',
|
||||
'tours.ski.summary.title': 'Rota pelas pistas',
|
||||
'tours.ski.summary.ski': 'Esquiando',
|
||||
'tours.ski.summary.lift': 'Em teleféricos',
|
||||
'tours.ski.summary.wait': 'Esperando',
|
||||
'tours.ski.summary.walk': 'A pé',
|
||||
'tours.ski.summary.avoided': 'Evitadas',
|
||||
'tours.ski.summary.unplanned': 'Alguns trechos não puderam ser planejados, então ainda não há tempo de esqui.',
|
||||
'tours.ski.summary.guessed':
|
||||
'Alguns trechos se baseiam em uma suposição dos dados do mapa, como uma ligação ou um sentido não mapeados.',
|
||||
'tours.ski.summary.stale':
|
||||
'Planejado com uma cópia mais antiga dos dados de pistas, pois a atual não está acessível.',
|
||||
'tours.ski.summary.source': 'Dados de pistas © colaboradores do OpenStreetMap',
|
||||
'tours.ski.chooseTechnique': 'Escolha a técnica, ou Desconhecida.',
|
||||
'tours.ski.infoLabel': 'O que o TREK faz com passeios e trilhas de esqui',
|
||||
'tours.ski.info':
|
||||
'Um passeio planejado tem cada trecho desenhado em linha reta entre seus pontos, e uma trilha importada fica como foi gravada. Nenhum dos dois é roteado por pistas, teleféricos ou trilhas. As altitudes vêm do terreno ou do arquivo. O TREK não calcula tempo de esqui, e nada aqui, nem as cores de inclinação em 3D, diz algo sobre dificuldade, neve, avalanches ou se pistas e trilhas estão abertas.',
|
||||
'Um passeio planejado segue pistas, teleféricos e trilhas do OpenStreetMap onde os dados de pistas chegam, e liga seus pontos em linha reta onde não chegam. Um trajeto importado fica como foi gravado. As altitudes vêm dos dados de pistas, do terreno ou do arquivo. Nada aqui, nem mesmo as cores de inclinação em 3D, diz algo sobre neve, avalanches ou se pistas, teleféricos e trilhas estão abertos.',
|
||||
'tours.ski.time': 'Tempo de esqui',
|
||||
'tours.ski.timeUnknown': 'Não calculado',
|
||||
'tours.ski.timeInfoLabel': 'Por que não há tempo de esqui',
|
||||
'tours.ski.timeInfoLabel': 'Como o tempo de esqui é calculado',
|
||||
'tours.ski.timeInfo':
|
||||
'O TREK não calcula tempo de esqui: os trechos são desenhados em linha reta, não planejados por pistas, teleféricos ou trilhas. Se quiser, informe você mesmo um total planejado.',
|
||||
'O roteador de pistas soma o tempo nas pistas, nos teleféricos, esperando nas estações de base e a pé, conforme sua disciplina e ritmo. Ele só dá um tempo quando todos os trechos estão planejados. Informe você mesmo um total previsto, se quiser um.',
|
||||
'tours.ski.classifyTitle': 'Classificar trilha de esqui',
|
||||
'tours.ski.saved': 'Classificação salva',
|
||||
'tours.ski.saveFailed': 'Não foi possível salvar a classificação.',
|
||||
|
||||
@@ -273,6 +273,9 @@ const admin: TranslationStrings = {
|
||||
'admin.toursCycleMap.title': 'Mapa ciclista (CyclOSM)',
|
||||
'admin.toursCycleMap.subtitle':
|
||||
"Ofereix CyclOSM com a mapa base per a rutes en bici al planificador. OpenStreetMap France el serveix per a un ús moderat; una instal·lació d'ús intens o comercial el desactiva o primer arriba a un acord amb l'operador",
|
||||
'admin.toursSkiRouting.title': 'Encaminador de pistes',
|
||||
'admin.toursSkiRouting.subtitle':
|
||||
'Planifica rutes d’esquí per pistes, remuntadors i circuits amb les dades de pistes del servidor Places. Desactivat, els trams d’una ruta d’esquí es dibuixen en línia recta',
|
||||
'admin.addons.noAddons': 'No hi ha complements disponibles',
|
||||
'admin.weather.title': 'Dades meteorològiques',
|
||||
'admin.weather.badge': 'Des del 24 de març de 2026',
|
||||
|
||||
@@ -418,17 +418,92 @@ const tours: TranslationStrings = {
|
||||
'tours.ski.technique.unknown': 'Desconeguda',
|
||||
'tours.ski.chooseDiscipline': 'Tria una disciplina per importar la traça com a esquí.',
|
||||
'tours.ski.chooseDisciplineToSave': 'Tria una disciplina per desar la ruta.',
|
||||
'tours.ski.drawnFreely':
|
||||
'Cada tram es dibuixa en línia recta entre els teus punts. Res no el comprova amb pistes, remuntadors o senders.',
|
||||
'tours.ski.plannedHint':
|
||||
'Els trams segueixen pistes, remuntadors i circuits d’OpenStreetMap on arriben les dades de pistes, i van en línia recta on no. Res d’aquí diu si són oberts.',
|
||||
'tours.ski.options.maxDifficulty': 'Pistes més difícils',
|
||||
'tours.ski.options.priority': 'Preferir',
|
||||
'tours.ski.options.pace': 'Ritme',
|
||||
'tours.ski.options.useLifts': 'Fer servir remuntadors',
|
||||
'tours.ski.options.freeride': 'Permetre rutes freeride',
|
||||
'tours.ski.options.liftDown': 'Baixar amb remuntador si cal',
|
||||
'tours.ski.options.groomedOnly': 'Només circuits trepitjats',
|
||||
'tours.ski.options.litOnly': 'Només circuits il·luminats',
|
||||
'tours.ski.options.uphillOnPistes': 'Pujar pel marge de la pista',
|
||||
'tours.ski.options.avoidLinks': 'Evitar enllaços suposats',
|
||||
'tours.ski.options.avoidLifts': 'Excloure',
|
||||
'tours.ski.difficulty.novice': 'Principiant',
|
||||
'tours.ski.difficulty.easy': 'Fàcil',
|
||||
'tours.ski.difficulty.intermediate': 'Intermèdia',
|
||||
'tours.ski.difficulty.advanced': 'Difícil',
|
||||
'tours.ski.difficulty.expert': 'Expert', // same-as-en
|
||||
'tours.ski.difficulty.freeride': 'Freeride', // same-as-en
|
||||
'tours.ski.difficulty.extreme': 'Extrema',
|
||||
'tours.ski.lift.drag': 'Teleesquís',
|
||||
'tours.ski.lift.open_chair': 'Telecadires oberts',
|
||||
'tours.ski.lift.gondola': 'Telecabines',
|
||||
'tours.ski.lift.cable_car': 'Telefèrics',
|
||||
'tours.ski.lift.rail': 'Funiculars i trens',
|
||||
'tours.ski.priority.fastest': 'Més ràpida',
|
||||
'tours.ski.priority.most_skiing': 'Més esquí',
|
||||
'tours.ski.priority.easiest': 'Més fàcil',
|
||||
'tours.ski.pace.relaxed': 'Tranquil',
|
||||
'tours.ski.pace.normal': 'Normal', // same-as-en
|
||||
'tours.ski.pace.sporty': 'Esportiu',
|
||||
'tours.ski.kind.run': 'Pista',
|
||||
'tours.ski.kind.lift': 'Remuntador',
|
||||
'tours.ski.kind.rail': 'Funicular', // same-as-en
|
||||
'tours.ski.kind.lift_down': 'Baixada amb remuntador',
|
||||
'tours.ski.kind.nordic': 'Circuit de fons',
|
||||
'tours.ski.kind.skitour': 'Ruta d’esquí de muntanya',
|
||||
'tours.ski.kind.hike': 'Camí d’hivern',
|
||||
'tours.ski.kind.connection': 'Pista d’enllaç',
|
||||
'tours.ski.kind.area': 'Vessant obert',
|
||||
'tours.ski.kind.link': 'Enllaç sense cartografiar',
|
||||
'tours.ski.kind.crossing': 'Travessa',
|
||||
'tours.ski.kind.walk': 'A peu',
|
||||
'tours.ski.fail.not_on_piste':
|
||||
'Aquest punt és massa lluny de qualsevol pista, circuit o remuntador que puguis fer servir.',
|
||||
'tours.ski.fail.no_area': 'Cap estació de les dades de pistes cobreix aquest punt.',
|
||||
'tours.ski.fail.loading': 'Les dades de pistes d’aquesta zona encara s’estan baixant.',
|
||||
'tours.ski.fail.places_unavailable': 'Ara no es pot accedir a les dades de pistes.',
|
||||
'tours.ski.fail.disabled': 'L’encaminament per pistes està desactivat.',
|
||||
'tours.ski.fail.blocked_by_difficulty': 'Només pistes per sobre del teu límit uneixen aquests punts.',
|
||||
'tours.ski.fail.blocked_by_lift_avoid': 'Només un remuntador que has exclòs uneix aquests punts.',
|
||||
'tours.ski.fail.blocked_by_technique': 'Només circuits trepitjats per a l’altra tècnica uneixen aquests punts.',
|
||||
'tours.ski.fail.needs_ride_down': 'La tornada necessita baixar amb remuntador.',
|
||||
'tours.ski.fail.uphill_only': 'Només una pujada uneix aquests punts i no hi ha remuntador per agafar.',
|
||||
'tours.ski.fail.disconnected': 'Res de les dades de pistes uneix aquests punts.',
|
||||
'tours.ski.fail.cross_area': 'Aquests punts són en dues estacions que no s’uneixen.',
|
||||
'tours.ski.fail.busy': 'L’encaminador de pistes està ocupat. Torna-ho a provar d’aquí a un moment.',
|
||||
'tours.ski.fail.timeout': 'Planificar aquest tram ha trigat massa.',
|
||||
'tours.ski.fail.offline': 'Estàs sense connexió, així que de moment aquest tram es dibuixa en línia recta.',
|
||||
'tours.ski.fix.raiseDifficulty': 'Permetre pistes més difícils',
|
||||
'tours.ski.fix.allowLift': 'Permetre aquest remuntador',
|
||||
'tours.ski.fix.liftDown': 'Baixar amb remuntador',
|
||||
'tours.ski.fix.technique': 'Canviar de tècnica',
|
||||
'tours.ski.fix.discipline': 'Canviar de disciplina',
|
||||
'tours.ski.summerPath': 'Fora de les dades de pistes: per camins d’estiu, que poden ser sota la neu.',
|
||||
'tours.ski.summary.title': 'Ruta per pistes',
|
||||
'tours.ski.summary.ski': 'Esquiant',
|
||||
'tours.ski.summary.lift': 'En remuntadors',
|
||||
'tours.ski.summary.wait': 'Esperant',
|
||||
'tours.ski.summary.walk': 'A peu',
|
||||
'tours.ski.summary.avoided': 'Evitades',
|
||||
'tours.ski.summary.unplanned': 'Alguns trams no s’han pogut planificar, així que encara no hi ha temps d’esquí.',
|
||||
'tours.ski.summary.guessed':
|
||||
'Alguns passos es basen en una suposició de les dades del mapa, com un enllaç o un sentit sense cartografiar.',
|
||||
'tours.ski.summary.stale':
|
||||
'Planificat amb una còpia anterior de les dades de pistes, perquè l’actual no és accessible.',
|
||||
'tours.ski.summary.source': 'Dades de pistes © col·laboradors d’OpenStreetMap',
|
||||
'tours.ski.chooseTechnique': 'Tria la tècnica, o Desconeguda.',
|
||||
'tours.ski.infoLabel': 'Què fa TREK amb les rutes i traces d’esquí',
|
||||
'tours.ski.info':
|
||||
'Una ruta planificada té cada tram dibuixat en línia recta entre els seus punts, i una traça importada es manté tal com es va gravar. Cap de les dues no s’enruta per pistes, remuntadors o senders. Les altituds vénen del terreny o del fitxer. TREK no calcula cap temps d’esquí, i res d’aquí, ni els colors de pendent en 3D, diu res de dificultat, neu, allaus o si les pistes estan obertes.',
|
||||
'Una ruta planificada segueix pistes, remuntadors i circuits d’OpenStreetMap on arriben les dades de pistes, i uneix els seus punts en línia recta on no. Un track importat es conserva tal com es va enregistrar. Les altituds vénen de les dades de pistes, del terreny o del fitxer. Res d’aquí, ni tan sols els colors de pendent en 3D, diu res sobre la neu, les allaus o si pistes, remuntadors i circuits són oberts.',
|
||||
'tours.ski.time': 'Temps d’esquí',
|
||||
'tours.ski.timeUnknown': 'Sense calcular',
|
||||
'tours.ski.timeInfoLabel': 'Per què no hi ha temps d’esquí',
|
||||
'tours.ski.timeInfoLabel': 'Com es calcula el temps d’esquí',
|
||||
'tours.ski.timeInfo':
|
||||
'TREK no calcula cap temps d’esquí: els trams es dibuixen rectes, no es planifiquen per pistes, remuntadors o senders. Si vols, introdueix tu mateix un total previst.',
|
||||
'L’encaminador de pistes suma el temps a les pistes, als remuntadors, esperant a les estacions de baix i a peu, segons la disciplina i el ritme. Només dona un temps quan tots els trams estan planificats. Introdueix tu un total previst si en vols un.',
|
||||
'tours.ski.classifyTitle': 'Classificar la traça d’esquí',
|
||||
'tours.ski.saved': 'Classificació desada',
|
||||
'tours.ski.saveFailed': 'No s’ha pogut desar la classificació.',
|
||||
|
||||
@@ -667,6 +667,9 @@ const admin: TranslationStrings = {
|
||||
'admin.toursCycleMap.title': 'Cyklomapa (CyclOSM)',
|
||||
'admin.toursCycleMap.subtitle':
|
||||
'Nabízí CyclOSM jako podkladovou mapu pro cyklotúry v plánovači. OpenStreetMap France ji poskytuje pro přiměřené používání; silně využívaná nebo komerční instalace ji vypne, nebo se nejprve dohodne s provozovatelem',
|
||||
'admin.toursSkiRouting.title': 'Router sjezdovek',
|
||||
'admin.toursSkiRouting.subtitle':
|
||||
'Plánuje lyžařské túry po sjezdovkách, lanovkách a stopách na datech sjezdovek serveru Places. Vypnutý kreslí úseky lyžařské túry rovně',
|
||||
'admin.addons.noAddons': 'Žádné doplňky nejsou k dispozici',
|
||||
'admin.addons.catalog.mcp.name': 'MCP',
|
||||
'admin.addons.catalog.mcp.description': 'Model Context Protocol pro integraci AI asistentů',
|
||||
|
||||
@@ -410,17 +410,91 @@ const tours: TranslationStrings = {
|
||||
'tours.ski.technique.unknown': 'Neznámá',
|
||||
'tours.ski.chooseDiscipline': 'Vyberte disciplínu pro import trasy jako lyže.',
|
||||
'tours.ski.chooseDisciplineToSave': 'Vyberte disciplínu, abyste mohli túru uložit.',
|
||||
'tours.ski.drawnFreely':
|
||||
'Každý úsek se kreslí rovně mezi vašimi body. Nic ho neporovnává se sjezdovkami, vleky ani stopami.',
|
||||
'tours.ski.plannedHint':
|
||||
'Úseky vedou po sjezdovkách, lanovkách a stopách z OpenStreetMap, kam sahají data sjezdovek, jinde rovně. Nic tu neříká, zda jsou otevřené.',
|
||||
'tours.ski.options.maxDifficulty': 'Nejtěžší sjezdovky',
|
||||
'tours.ski.options.priority': 'Upřednostnit',
|
||||
'tours.ski.options.pace': 'Tempo',
|
||||
'tours.ski.options.useLifts': 'Jezdit lanovkami',
|
||||
'tours.ski.options.freeride': 'Povolit freeride trasy',
|
||||
'tours.ski.options.liftDown': 'Podle potřeby sjet lanovkou',
|
||||
'tours.ski.options.groomedOnly': 'Jen upravené stopy',
|
||||
'tours.ski.options.litOnly': 'Jen osvětlené stopy',
|
||||
'tours.ski.options.uphillOnPistes': 'Stoupat po okraji sjezdovky',
|
||||
'tours.ski.options.avoidLinks': 'Vyhnout se domnělým spojkám',
|
||||
'tours.ski.options.avoidLifts': 'Vynechat',
|
||||
'tours.ski.difficulty.novice': 'Začátečník',
|
||||
'tours.ski.difficulty.easy': 'Lehká',
|
||||
'tours.ski.difficulty.intermediate': 'Střední',
|
||||
'tours.ski.difficulty.advanced': 'Těžká',
|
||||
'tours.ski.difficulty.expert': 'Expertní',
|
||||
'tours.ski.difficulty.freeride': 'Freeride', // same-as-en
|
||||
'tours.ski.difficulty.extreme': 'Extrémní',
|
||||
'tours.ski.lift.drag': 'Vleky',
|
||||
'tours.ski.lift.open_chair': 'Otevřené sedačky',
|
||||
'tours.ski.lift.gondola': 'Kabinkové lanovky',
|
||||
'tours.ski.lift.cable_car': 'Kyvadlové lanovky',
|
||||
'tours.ski.lift.rail': 'Pozemní lanovky a vlaky',
|
||||
'tours.ski.priority.fastest': 'Nejrychlejší',
|
||||
'tours.ski.priority.most_skiing': 'Nejvíc lyžování',
|
||||
'tours.ski.priority.easiest': 'Nejlehčí',
|
||||
'tours.ski.pace.relaxed': 'Pohodové',
|
||||
'tours.ski.pace.normal': 'Normální',
|
||||
'tours.ski.pace.sporty': 'Sportovní',
|
||||
'tours.ski.kind.run': 'Sjezdovka',
|
||||
'tours.ski.kind.lift': 'Lanovka',
|
||||
'tours.ski.kind.rail': 'Pozemní lanovka',
|
||||
'tours.ski.kind.lift_down': 'Sjezd lanovkou',
|
||||
'tours.ski.kind.nordic': 'Běžecká stopa',
|
||||
'tours.ski.kind.skitour': 'Skialpová trasa',
|
||||
'tours.ski.kind.hike': 'Zimní cesta',
|
||||
'tours.ski.kind.connection': 'Spojovací sjezdovka',
|
||||
'tours.ski.kind.area': 'Otevřený svah',
|
||||
'tours.ski.kind.link': 'Nezmapovaná spojka',
|
||||
'tours.ski.kind.crossing': 'Přejezd',
|
||||
'tours.ski.kind.walk': 'Pěšky',
|
||||
'tours.ski.fail.not_on_piste':
|
||||
'Tento bod je příliš daleko od jakékoli sjezdovky, stopy nebo lanovky, kterou můžeš použít.',
|
||||
'tours.ski.fail.no_area': 'Tento bod nepokrývá žádné lyžařské středisko v datech sjezdovek.',
|
||||
'tours.ski.fail.loading': 'Data sjezdovek pro tuto oblast se ještě stahují.',
|
||||
'tours.ski.fail.places_unavailable': 'Data sjezdovek teď nejsou dostupná.',
|
||||
'tours.ski.fail.disabled': 'Plánování po sjezdovkách je vypnuté.',
|
||||
'tours.ski.fail.blocked_by_difficulty': 'Tyto body spojují jen sjezdovky nad tvým limitem.',
|
||||
'tours.ski.fail.blocked_by_lift_avoid': 'Tyto body spojuje jen lanovka, kterou jsi vynechal.',
|
||||
'tours.ski.fail.blocked_by_technique': 'Tyto body spojují jen stopy upravené pro druhou techniku.',
|
||||
'tours.ski.fail.needs_ride_down': 'Cesta zpět vyžaduje sjezd lanovkou.',
|
||||
'tours.ski.fail.uphill_only': 'Tyto body spojuje jen cesta nahoru a není tu lanovka.',
|
||||
'tours.ski.fail.disconnected': 'Nic v datech sjezdovek tyto body nespojuje.',
|
||||
'tours.ski.fail.cross_area': 'Tyto body leží ve dvou nespojených střediscích.',
|
||||
'tours.ski.fail.busy': 'Router sjezdovek je vytížený. Zkus to za chvíli znovu.',
|
||||
'tours.ski.fail.timeout': 'Plánování tohoto úseku trvalo příliš dlouho.',
|
||||
'tours.ski.fail.offline': 'Jsi offline, proto se tento úsek zatím kreslí rovně.',
|
||||
'tours.ski.fix.raiseDifficulty': 'Povolit těžší sjezdovky',
|
||||
'tours.ski.fix.allowLift': 'Povolit tuto lanovku',
|
||||
'tours.ski.fix.liftDown': 'Sjet lanovkou',
|
||||
'tours.ski.fix.technique': 'Změnit techniku',
|
||||
'tours.ski.fix.discipline': 'Změnit disciplínu',
|
||||
'tours.ski.summerPath': 'Mimo data sjezdovek: po letních cestách, které mohou být pod sněhem.',
|
||||
'tours.ski.summary.title': 'Trasa po sjezdovkách',
|
||||
'tours.ski.summary.ski': 'Lyžování',
|
||||
'tours.ski.summary.lift': 'Na lanovkách',
|
||||
'tours.ski.summary.wait': 'Čekání',
|
||||
'tours.ski.summary.walk': 'Pěšky',
|
||||
'tours.ski.summary.avoided': 'Vynecháno',
|
||||
'tours.ski.summary.unplanned': 'Některé úseky se nepodařilo naplánovat, proto zatím není čas lyžování.',
|
||||
'tours.ski.summary.guessed':
|
||||
'Některé části stojí na odhadu z mapových dat, například na nezmapované spojce nebo směru.',
|
||||
'tours.ski.summary.stale': 'Naplánováno na starší kopii dat sjezdovek, protože aktuální není dostupná.',
|
||||
'tours.ski.summary.source': 'Data sjezdovek © přispěvatelé OpenStreetMap',
|
||||
'tours.ski.chooseTechnique': 'Vyberte techniku, nebo Neznámá.',
|
||||
'tours.ski.infoLabel': 'Co TREK dělá s lyžařskými túrami a trasami',
|
||||
'tours.ski.info':
|
||||
'Naplánovaná túra má každý úsek nakreslený rovně mezi svými body, importovaná trasa zůstává tak, jak byla zaznamenána. Ani jedna se nevede po sjezdovkách, vlecích ani stopách. Výšky pocházejí z terénu nebo ze souboru. TREK nepočítá žádný čas na lyžích a nic tady, ani barvy sklonu ve 3D, neříká nic o obtížnosti, sněhu, lavinách ani o tom, zda jsou sjezdovky a stopy otevřené.',
|
||||
'Naplánovaná túra vede po sjezdovkách, lanovkách a stopách z OpenStreetMap, kam sahají data sjezdovek, jinde spojuje své body rovně. Importovaná stopa zůstává tak, jak byla nahrána. Výšky pocházejí z dat sjezdovek, terénu nebo souboru. Nic tu, ani barvy sklonu ve 3D, neříká nic o sněhu, lavinách ani o tom, zda jsou sjezdovky, lanovky a stopy otevřené.',
|
||||
'tours.ski.time': 'Čas na lyžích',
|
||||
'tours.ski.timeUnknown': 'Nepočítá se',
|
||||
'tours.ski.timeInfoLabel': 'Proč tu není čas na lyžích',
|
||||
'tours.ski.timeInfoLabel': 'Jak se počítá čas lyžování',
|
||||
'tours.ski.timeInfo':
|
||||
'TREK čas na lyžích nepočítá: úseky jsou nakreslené rovně, ne naplánované po sjezdovkách, vlecích nebo stopách. Pokud chcete, zadejte plánovaný celkový čas sami.',
|
||||
'Router sjezdovek sčítá čas na sjezdovkách, na lanovkách, čekáním u dolních stanic a pěšky, podle disciplíny a tempa. Čas uvede až tehdy, když je naplánovaný každý úsek. Zadej si plánovaný celkový čas sám, pokud ho chceš.',
|
||||
'tours.ski.classifyTitle': 'Zařadit lyžařskou trasu',
|
||||
'tours.ski.saved': 'Zařazení uloženo',
|
||||
'tours.ski.saveFailed': 'Zařazení se nepodařilo uložit.',
|
||||
|
||||
@@ -672,6 +672,9 @@ const admin: TranslationStrings = {
|
||||
'admin.toursCycleMap.title': 'Fahrradkarte (CyclOSM)',
|
||||
'admin.toursCycleMap.subtitle':
|
||||
'Bietet CyclOSM als Grundkarte für Radtouren im Tourplaner an. OpenStreetMap France stellt sie für moderate Nutzung bereit; eine stark genutzte oder kommerzielle Installation schaltet sie ab oder trifft vorher eine Vereinbarung mit dem Betreiber',
|
||||
'admin.toursSkiRouting.title': 'Pistenrouter',
|
||||
'admin.toursSkiRouting.subtitle':
|
||||
'Plant Skitouren entlang von Pisten, Liften und Loipen auf den Pistendaten des Places-Servers. Ausgeschaltet werden die Abschnitte einer Skitour gerade gezeichnet',
|
||||
'admin.addons.noAddons': 'Keine Addons verfügbar',
|
||||
'admin.weather.title': 'Wetterdaten',
|
||||
'admin.weather.badge': 'Seit 24. März 2026',
|
||||
|
||||
@@ -420,17 +420,91 @@ const tours: TranslationStrings = {
|
||||
'tours.ski.technique.unknown': 'Unbekannt',
|
||||
'tours.ski.chooseDiscipline': 'Wähle eine Disziplin, um die Strecke als Ski zu importieren.',
|
||||
'tours.ski.chooseDisciplineToSave': 'Wähle eine Disziplin, um die Tour zu speichern.',
|
||||
'tours.ski.drawnFreely':
|
||||
'Jeder Abschnitt wird gerade zwischen deinen Punkten gezeichnet. Nichts gleicht ihn mit Pisten, Liften oder Loipen ab.',
|
||||
'tours.ski.plannedHint':
|
||||
'Abschnitte folgen Pisten, Liften und Loipen aus OpenStreetMap, wo die Pistendaten reichen, und verlaufen sonst gerade. Nichts hier sagt, ob sie geöffnet sind.',
|
||||
'tours.ski.options.maxDifficulty': 'Schwerste Pisten',
|
||||
'tours.ski.options.priority': 'Bevorzugen',
|
||||
'tours.ski.options.pace': 'Tempo',
|
||||
'tours.ski.options.useLifts': 'Lifte nutzen',
|
||||
'tours.ski.options.freeride': 'Freeride-Routen erlauben',
|
||||
'tours.ski.options.liftDown': 'Bei Bedarf mit dem Lift ins Tal',
|
||||
'tours.ski.options.groomedOnly': 'Nur gespurte Loipen',
|
||||
'tours.ski.options.litOnly': 'Nur beleuchtete Loipen',
|
||||
'tours.ski.options.uphillOnPistes': 'Am Pistenrand aufsteigen',
|
||||
'tours.ski.options.avoidLinks': 'Vermutete Verbindungen meiden',
|
||||
'tours.ski.options.avoidLifts': 'Auslassen',
|
||||
'tours.ski.difficulty.novice': 'Anfänger',
|
||||
'tours.ski.difficulty.easy': 'Leicht',
|
||||
'tours.ski.difficulty.intermediate': 'Mittel',
|
||||
'tours.ski.difficulty.advanced': 'Schwer',
|
||||
'tours.ski.difficulty.expert': 'Experte',
|
||||
'tours.ski.difficulty.freeride': 'Freeride', // same-as-en
|
||||
'tours.ski.difficulty.extreme': 'Extrem',
|
||||
'tours.ski.lift.drag': 'Schlepplifte',
|
||||
'tours.ski.lift.open_chair': 'Offene Sessellifte',
|
||||
'tours.ski.lift.gondola': 'Gondelbahnen',
|
||||
'tours.ski.lift.cable_car': 'Seilbahnen',
|
||||
'tours.ski.lift.rail': 'Standseilbahnen und Züge',
|
||||
'tours.ski.priority.fastest': 'Am schnellsten',
|
||||
'tours.ski.priority.most_skiing': 'Am meisten Skifahren',
|
||||
'tours.ski.priority.easiest': 'Am leichtesten',
|
||||
'tours.ski.pace.relaxed': 'Gemütlich',
|
||||
'tours.ski.pace.normal': 'Normal', // same-as-en
|
||||
'tours.ski.pace.sporty': 'Sportlich',
|
||||
'tours.ski.kind.run': 'Piste',
|
||||
'tours.ski.kind.lift': 'Lift', // same-as-en
|
||||
'tours.ski.kind.rail': 'Standseilbahn',
|
||||
'tours.ski.kind.lift_down': 'Talfahrt im Lift',
|
||||
'tours.ski.kind.nordic': 'Loipe',
|
||||
'tours.ski.kind.skitour': 'Skitourenroute',
|
||||
'tours.ski.kind.hike': 'Winterweg',
|
||||
'tours.ski.kind.connection': 'Verbindungspiste',
|
||||
'tours.ski.kind.area': 'Offener Hang',
|
||||
'tours.ski.kind.link': 'Nicht kartierte Verbindung',
|
||||
'tours.ski.kind.crossing': 'Querung',
|
||||
'tours.ski.kind.walk': 'Zu Fuß',
|
||||
'tours.ski.fail.not_on_piste':
|
||||
'Dieser Punkt liegt zu weit von jeder Piste, Loipe oder jedem Lift entfernt, die du nutzen kannst.',
|
||||
'tours.ski.fail.no_area': 'Kein Skigebiet in den Pistendaten deckt diesen Punkt ab.',
|
||||
'tours.ski.fail.loading': 'Die Pistendaten für dieses Gebiet werden noch heruntergeladen.',
|
||||
'tours.ski.fail.places_unavailable': 'Die Pistendaten sind gerade nicht erreichbar.',
|
||||
'tours.ski.fail.disabled': 'Das Pistenrouting ist ausgeschaltet.',
|
||||
'tours.ski.fail.blocked_by_difficulty': 'Nur Pisten über deinem Limit verbinden diese Punkte.',
|
||||
'tours.ski.fail.blocked_by_lift_avoid': 'Nur ein Lift, den du ausgelassen hast, verbindet diese Punkte.',
|
||||
'tours.ski.fail.blocked_by_technique': 'Nur Loipen für die andere Technik verbinden diese Punkte.',
|
||||
'tours.ski.fail.needs_ride_down': 'Der Rückweg braucht eine Talfahrt im Lift.',
|
||||
'tours.ski.fail.uphill_only': 'Nur ein Weg bergauf verbindet diese Punkte, und es gibt keinen Lift.',
|
||||
'tours.ski.fail.disconnected': 'Nichts in den Pistendaten verbindet diese Punkte.',
|
||||
'tours.ski.fail.cross_area': 'Diese Punkte liegen in zwei Skigebieten, die nicht verbunden sind.',
|
||||
'tours.ski.fail.busy': 'Der Pistenrouter ist ausgelastet. Versuch es gleich noch einmal.',
|
||||
'tours.ski.fail.timeout': 'Die Planung dieses Abschnitts hat zu lange gedauert.',
|
||||
'tours.ski.fail.offline': 'Du bist offline, daher wird dieser Abschnitt vorerst gerade gezeichnet.',
|
||||
'tours.ski.fix.raiseDifficulty': 'Schwerere Pisten erlauben',
|
||||
'tours.ski.fix.allowLift': 'Diesen Lift erlauben',
|
||||
'tours.ski.fix.liftDown': 'Mit dem Lift ins Tal',
|
||||
'tours.ski.fix.technique': 'Technik wechseln',
|
||||
'tours.ski.fix.discipline': 'Disziplin wechseln',
|
||||
'tours.ski.summerPath': 'Außerhalb der Pistendaten: auf Sommerwegen geführt, die unter Schnee liegen können.',
|
||||
'tours.ski.summary.title': 'Pistenroute',
|
||||
'tours.ski.summary.ski': 'Skifahren',
|
||||
'tours.ski.summary.lift': 'Im Lift',
|
||||
'tours.ski.summary.wait': 'Warten',
|
||||
'tours.ski.summary.walk': 'Zu Fuß',
|
||||
'tours.ski.summary.avoided': 'Gemieden',
|
||||
'tours.ski.summary.unplanned': 'Einige Abschnitte ließen sich nicht planen, daher gibt es noch keine Skizeit.',
|
||||
'tours.ski.summary.guessed':
|
||||
'Einige Stücke beruhen auf einer Vermutung aus den Kartendaten, etwa einer Verbindung oder Richtung, die nicht kartiert ist.',
|
||||
'tours.ski.summary.stale': 'Auf einer älteren Kopie der Pistendaten geplant, weil die aktuelle nicht erreichbar ist.',
|
||||
'tours.ski.summary.source': 'Pistendaten © OpenStreetMap-Mitwirkende',
|
||||
'tours.ski.chooseTechnique': 'Wähle die Technik oder Unbekannt.',
|
||||
'tours.ski.infoLabel': 'Was TREK mit Wintertouren und Skistrecken macht',
|
||||
'tours.ski.info':
|
||||
'Bei einer geplanten Tour ist jeder Abschnitt gerade zwischen ihren Punkten gezeichnet, eine importierte Strecke bleibt wie aufgezeichnet. Keine von beiden wird über Pisten, Lifte oder Loipen geroutet. Die Höhen stammen aus dem Gelände oder aus der Datei. TREK berechnet keine Skizeit, und nichts hier, auch nicht die Steigungsfarben in 3D, sagt etwas über Schwierigkeit, Schnee, Lawinen oder geöffnete Pisten und Loipen.',
|
||||
'Eine geplante Tour folgt Pisten, Liften und Loipen aus OpenStreetMap, wo die Pistendaten reichen, und verläuft sonst gerade zwischen ihren Punkten. Ein importierter Track bleibt, wie er aufgezeichnet wurde. Höhen stammen aus den Pistendaten, dem Gelände oder der Datei. Nichts hier, auch nicht die Neigungsfarben in 3D, sagt etwas über Schnee, Lawinen oder darüber, ob Pisten, Lifte und Loipen geöffnet sind.',
|
||||
'tours.ski.time': 'Skizeit',
|
||||
'tours.ski.timeUnknown': 'Nicht berechnet',
|
||||
'tours.ski.timeInfoLabel': 'Warum es keine Skizeit gibt',
|
||||
'tours.ski.timeInfoLabel': 'Wie die Skizeit berechnet wird',
|
||||
'tours.ski.timeInfo':
|
||||
'TREK berechnet keine Skizeit: Die Abschnitte sind gerade gezeichnet, nicht über Pisten, Lifte oder Loipen geplant. Trag bei Bedarf selbst eine geplante Gesamtzeit ein.',
|
||||
'Der Pistenrouter zählt die Zeit auf Pisten, in Liften, beim Warten an den Talstationen und zu Fuß zusammen, passend zu Disziplin und Tempo. Eine Zeit gibt es erst, wenn jeder Abschnitt geplant ist. Trag selbst eine geplante Gesamtzeit ein, wenn du eine möchtest.',
|
||||
'tours.ski.classifyTitle': 'Skistrecke einordnen',
|
||||
'tours.ski.saved': 'Einordnung gespeichert',
|
||||
'tours.ski.saveFailed': 'Die Einordnung konnte nicht gespeichert werden.',
|
||||
|
||||
@@ -716,6 +716,9 @@ const admin: TranslationStrings = {
|
||||
'admin.toursCycleMap.title': 'Cycling map (CyclOSM)',
|
||||
'admin.toursCycleMap.subtitle':
|
||||
'Offers CyclOSM as a base map for bike tours in the tour planner. OpenStreetMap France serves it for moderate use; a heavy or commercial install switches it off or reaches an agreement with the operator first',
|
||||
'admin.toursSkiRouting.title': 'Piste router',
|
||||
'admin.toursSkiRouting.subtitle':
|
||||
'Plans ski tours along runs, lifts and trails on the piste data of the Places server. Switched off, the legs of a ski tour are drawn straight',
|
||||
'admin.addons.noAddons': 'No addons available',
|
||||
'admin.weather.title': 'Weather Data',
|
||||
'admin.weather.badge': 'Since March 24, 2026',
|
||||
|
||||
@@ -410,17 +410,90 @@ const tours: TranslationStrings = {
|
||||
'tours.ski.technique.unknown': 'Unknown',
|
||||
'tours.ski.chooseDiscipline': 'Choose a discipline to import the track as ski.',
|
||||
'tours.ski.chooseDisciplineToSave': 'Choose a discipline to save the tour.',
|
||||
'tours.ski.drawnFreely':
|
||||
'Each leg is drawn straight between your points. Nothing checks it against pistes, lifts or trails.',
|
||||
'tours.ski.plannedHint':
|
||||
'Legs follow runs, lifts and trails from OpenStreetMap where the piste data reaches, and run straight where it does not. Nothing here says whether they are open.',
|
||||
'tours.ski.options.maxDifficulty': 'Hardest runs',
|
||||
'tours.ski.options.priority': 'Prefer',
|
||||
'tours.ski.options.pace': 'Pace',
|
||||
'tours.ski.options.useLifts': 'Ride lifts',
|
||||
'tours.ski.options.freeride': 'Allow freeride routes',
|
||||
'tours.ski.options.liftDown': 'Ride a lift down where needed',
|
||||
'tours.ski.options.groomedOnly': 'Groomed trails only',
|
||||
'tours.ski.options.litOnly': 'Lit trails only',
|
||||
'tours.ski.options.uphillOnPistes': 'Skin up at the edge of pistes',
|
||||
'tours.ski.options.avoidLinks': 'Avoid guessed links',
|
||||
'tours.ski.options.avoidLifts': 'Leave out',
|
||||
'tours.ski.difficulty.novice': 'Novice',
|
||||
'tours.ski.difficulty.easy': 'Easy',
|
||||
'tours.ski.difficulty.intermediate': 'Intermediate',
|
||||
'tours.ski.difficulty.advanced': 'Advanced',
|
||||
'tours.ski.difficulty.expert': 'Expert',
|
||||
'tours.ski.difficulty.freeride': 'Freeride',
|
||||
'tours.ski.difficulty.extreme': 'Extreme',
|
||||
'tours.ski.lift.drag': 'Surface lifts',
|
||||
'tours.ski.lift.open_chair': 'Open chairlifts',
|
||||
'tours.ski.lift.gondola': 'Gondolas',
|
||||
'tours.ski.lift.cable_car': 'Cable cars',
|
||||
'tours.ski.lift.rail': 'Funiculars and trains',
|
||||
'tours.ski.priority.fastest': 'Fastest',
|
||||
'tours.ski.priority.most_skiing': 'Most skiing',
|
||||
'tours.ski.priority.easiest': 'Easiest',
|
||||
'tours.ski.pace.relaxed': 'Relaxed',
|
||||
'tours.ski.pace.normal': 'Normal',
|
||||
'tours.ski.pace.sporty': 'Sporty',
|
||||
'tours.ski.kind.run': 'Run',
|
||||
'tours.ski.kind.lift': 'Lift',
|
||||
'tours.ski.kind.rail': 'Funicular',
|
||||
'tours.ski.kind.lift_down': 'Lift down',
|
||||
'tours.ski.kind.nordic': 'Cross-country trail',
|
||||
'tours.ski.kind.skitour': 'Ski touring route',
|
||||
'tours.ski.kind.hike': 'Winter path',
|
||||
'tours.ski.kind.connection': 'Connecting run',
|
||||
'tours.ski.kind.area': 'Open slope',
|
||||
'tours.ski.kind.link': 'Unmapped link',
|
||||
'tours.ski.kind.crossing': 'Crossing',
|
||||
'tours.ski.kind.walk': 'On foot',
|
||||
'tours.ski.fail.not_on_piste': 'This point is too far from any run, trail or lift you can use.',
|
||||
'tours.ski.fail.no_area': 'No ski area in the piste data covers this point.',
|
||||
'tours.ski.fail.loading': 'The piste data for this area is still being downloaded.',
|
||||
'tours.ski.fail.places_unavailable': 'The piste data cannot be reached right now.',
|
||||
'tours.ski.fail.disabled': 'Piste routing is switched off.',
|
||||
'tours.ski.fail.blocked_by_difficulty': 'Only runs harder than your limit join these points.',
|
||||
'tours.ski.fail.blocked_by_lift_avoid': 'Only a lift you left out joins these points.',
|
||||
'tours.ski.fail.blocked_by_technique': 'Only trails groomed for the other technique join these points.',
|
||||
'tours.ski.fail.needs_ride_down': 'The way back needs a lift down.',
|
||||
'tours.ski.fail.uphill_only': 'Only a way up joins these points, and there is no lift to take.',
|
||||
'tours.ski.fail.disconnected': 'Nothing in the piste data joins these points.',
|
||||
'tours.ski.fail.cross_area': 'These points lie in two ski areas that are not joined.',
|
||||
'tours.ski.fail.busy': 'The piste router is busy. Try again in a moment.',
|
||||
'tours.ski.fail.timeout': 'Planning this leg took too long.',
|
||||
'tours.ski.fail.offline': 'You are offline, so this leg is drawn straight for now.',
|
||||
'tours.ski.fix.raiseDifficulty': 'Allow harder runs',
|
||||
'tours.ski.fix.allowLift': 'Allow this lift',
|
||||
'tours.ski.fix.liftDown': 'Ride the lift down',
|
||||
'tours.ski.fix.technique': 'Switch technique',
|
||||
'tours.ski.fix.discipline': 'Switch discipline',
|
||||
'tours.ski.summerPath': 'Outside the piste data: taken on summer paths, which may lie under snow.',
|
||||
'tours.ski.summary.title': 'Piste route',
|
||||
'tours.ski.summary.ski': 'Skiing',
|
||||
'tours.ski.summary.lift': 'On lifts',
|
||||
'tours.ski.summary.wait': 'Waiting',
|
||||
'tours.ski.summary.walk': 'On foot',
|
||||
'tours.ski.summary.avoided': 'Kept clear of',
|
||||
'tours.ski.summary.unplanned': 'Some legs could not be planned, so there is no ski time yet.',
|
||||
'tours.ski.summary.guessed':
|
||||
'Some stretches rest on a guess of the map data, such as a link or a direction that is not mapped.',
|
||||
'tours.ski.summary.stale': 'Planned on an older copy of the piste data, as the current one cannot be reached.',
|
||||
'tours.ski.summary.source': 'Piste data © OpenStreetMap contributors',
|
||||
'tours.ski.chooseTechnique': 'Choose the technique, or Unknown.',
|
||||
'tours.ski.infoLabel': 'What TREK does with ski tours and tracks',
|
||||
'tours.ski.info':
|
||||
'A planned tour has each leg drawn straight between its points, an imported track is kept as recorded. Neither is routed along pistes, lifts or trails. Heights come from the terrain or the file. TREK works out no ski time, and nothing here, the gradient colours in 3D included, says anything about difficulty, snow, avalanches or whether runs and trails are open.',
|
||||
'A planned tour follows runs, lifts and trails from OpenStreetMap where the piste data reaches, and runs straight between its points where it does not. An imported track is kept as recorded. Heights come from the piste data, the terrain or the file. Nothing here, the gradient colours in 3D included, says anything about snow, avalanches or whether runs, lifts and trails are open.',
|
||||
'tours.ski.time': 'Ski time',
|
||||
'tours.ski.timeUnknown': 'Not worked out',
|
||||
'tours.ski.timeInfoLabel': 'Why there is no ski time',
|
||||
'tours.ski.timeInfoLabel': 'How the ski time is worked out',
|
||||
'tours.ski.timeInfo':
|
||||
'TREK works out no ski time: the legs are drawn straight, not planned along pistes, lifts or trails. Enter a planned total yourself if you want one.',
|
||||
'The piste router adds up the time on runs, on lifts, waiting at the bottom stations and on foot, for your discipline and pace. It gives a time only once every leg is planned. Enter a planned total yourself if you want one.',
|
||||
'tours.ski.classifyTitle': 'Classify ski track',
|
||||
'tours.ski.saved': 'Classification saved',
|
||||
'tours.ski.saveFailed': 'The classification could not be saved.',
|
||||
|
||||
@@ -655,6 +655,9 @@ const admin: TranslationStrings = {
|
||||
'admin.toursCycleMap.title': 'Mapa ciclista (CyclOSM)',
|
||||
'admin.toursCycleMap.subtitle':
|
||||
'Ofrece CyclOSM como mapa base para rutas en bici en el planificador. OpenStreetMap France lo sirve para un uso moderado; una instalación de uso intenso o comercial lo desactiva o llega antes a un acuerdo con el operador',
|
||||
'admin.toursSkiRouting.title': 'Enrutador de pistas',
|
||||
'admin.toursSkiRouting.subtitle':
|
||||
'Planifica rutas de esquí por pistas, remontes y circuitos con los datos de pistas del servidor Places. Desactivado, los tramos de una ruta de esquí se dibujan en línea recta',
|
||||
'admin.addons.noAddons': 'No hay complementos disponibles',
|
||||
'admin.weather.title': 'Datos meteorológicos',
|
||||
'admin.weather.badge': 'Desde el 24 de marzo de 2026',
|
||||
|
||||
@@ -417,17 +417,92 @@ const tours: TranslationStrings = {
|
||||
'tours.ski.technique.unknown': 'Desconocida',
|
||||
'tours.ski.chooseDiscipline': 'Elige una disciplina para importar la ruta como esquí.',
|
||||
'tours.ski.chooseDisciplineToSave': 'Elige una disciplina para guardar la ruta.',
|
||||
'tours.ski.drawnFreely':
|
||||
'Cada tramo se dibuja en línea recta entre tus puntos. Nada lo comprueba con pistas, remontes ni senderos.',
|
||||
'tours.ski.plannedHint':
|
||||
'Los tramos siguen pistas, remontes y circuitos de OpenStreetMap donde llegan los datos de pistas, y van en línea recta donde no. Nada aquí dice si están abiertos.',
|
||||
'tours.ski.options.maxDifficulty': 'Pistas más difíciles',
|
||||
'tours.ski.options.priority': 'Preferir',
|
||||
'tours.ski.options.pace': 'Ritmo',
|
||||
'tours.ski.options.useLifts': 'Usar remontes',
|
||||
'tours.ski.options.freeride': 'Permitir rutas freeride',
|
||||
'tours.ski.options.liftDown': 'Bajar en remonte si hace falta',
|
||||
'tours.ski.options.groomedOnly': 'Solo circuitos pisados',
|
||||
'tours.ski.options.litOnly': 'Solo circuitos iluminados',
|
||||
'tours.ski.options.uphillOnPistes': 'Subir por el borde de la pista',
|
||||
'tours.ski.options.avoidLinks': 'Evitar enlaces supuestos',
|
||||
'tours.ski.options.avoidLifts': 'Excluir',
|
||||
'tours.ski.difficulty.novice': 'Principiante',
|
||||
'tours.ski.difficulty.easy': 'Fácil',
|
||||
'tours.ski.difficulty.intermediate': 'Intermedia',
|
||||
'tours.ski.difficulty.advanced': 'Difícil',
|
||||
'tours.ski.difficulty.expert': 'Experto',
|
||||
'tours.ski.difficulty.freeride': 'Freeride', // same-as-en
|
||||
'tours.ski.difficulty.extreme': 'Extrema',
|
||||
'tours.ski.lift.drag': 'Telearrastres',
|
||||
'tours.ski.lift.open_chair': 'Telesillas abiertos',
|
||||
'tours.ski.lift.gondola': 'Telecabinas',
|
||||
'tours.ski.lift.cable_car': 'Teleféricos',
|
||||
'tours.ski.lift.rail': 'Funiculares y trenes',
|
||||
'tours.ski.priority.fastest': 'Más rápida',
|
||||
'tours.ski.priority.most_skiing': 'Más esquí',
|
||||
'tours.ski.priority.easiest': 'Más fácil',
|
||||
'tours.ski.pace.relaxed': 'Tranquilo',
|
||||
'tours.ski.pace.normal': 'Normal', // same-as-en
|
||||
'tours.ski.pace.sporty': 'Deportivo',
|
||||
'tours.ski.kind.run': 'Pista',
|
||||
'tours.ski.kind.lift': 'Remonte',
|
||||
'tours.ski.kind.rail': 'Funicular', // same-as-en
|
||||
'tours.ski.kind.lift_down': 'Bajada en remonte',
|
||||
'tours.ski.kind.nordic': 'Circuito de fondo',
|
||||
'tours.ski.kind.skitour': 'Ruta de esquí de travesía',
|
||||
'tours.ski.kind.hike': 'Camino invernal',
|
||||
'tours.ski.kind.connection': 'Pista de enlace',
|
||||
'tours.ski.kind.area': 'Ladera abierta',
|
||||
'tours.ski.kind.link': 'Enlace sin cartografiar',
|
||||
'tours.ski.kind.crossing': 'Cruce',
|
||||
'tours.ski.kind.walk': 'A pie',
|
||||
'tours.ski.fail.not_on_piste':
|
||||
'Este punto está demasiado lejos de cualquier pista, circuito o remonte que puedas usar.',
|
||||
'tours.ski.fail.no_area': 'Ninguna estación de los datos de pistas cubre este punto.',
|
||||
'tours.ski.fail.loading': 'Los datos de pistas de esta zona aún se están descargando.',
|
||||
'tours.ski.fail.places_unavailable': 'Ahora no se puede acceder a los datos de pistas.',
|
||||
'tours.ski.fail.disabled': 'El enrutado por pistas está desactivado.',
|
||||
'tours.ski.fail.blocked_by_difficulty': 'Solo pistas por encima de tu límite unen estos puntos.',
|
||||
'tours.ski.fail.blocked_by_lift_avoid': 'Solo un remonte que excluiste une estos puntos.',
|
||||
'tours.ski.fail.blocked_by_technique': 'Solo circuitos pisados para la otra técnica unen estos puntos.',
|
||||
'tours.ski.fail.needs_ride_down': 'La vuelta necesita bajar en remonte.',
|
||||
'tours.ski.fail.uphill_only': 'Solo una subida une estos puntos y no hay remonte que tomar.',
|
||||
'tours.ski.fail.disconnected': 'Nada en los datos de pistas une estos puntos.',
|
||||
'tours.ski.fail.cross_area': 'Estos puntos están en dos estaciones que no se unen.',
|
||||
'tours.ski.fail.busy': 'El enrutador de pistas está ocupado. Inténtalo de nuevo en un momento.',
|
||||
'tours.ski.fail.timeout': 'Planificar este tramo tardó demasiado.',
|
||||
'tours.ski.fail.offline': 'Estás sin conexión, así que de momento este tramo se dibuja en línea recta.',
|
||||
'tours.ski.fix.raiseDifficulty': 'Permitir pistas más difíciles',
|
||||
'tours.ski.fix.allowLift': 'Permitir este remonte',
|
||||
'tours.ski.fix.liftDown': 'Bajar en remonte',
|
||||
'tours.ski.fix.technique': 'Cambiar técnica',
|
||||
'tours.ski.fix.discipline': 'Cambiar disciplina',
|
||||
'tours.ski.summerPath': 'Fuera de los datos de pistas: por caminos de verano, que pueden estar bajo la nieve.',
|
||||
'tours.ski.summary.title': 'Ruta por pistas',
|
||||
'tours.ski.summary.ski': 'Esquiando',
|
||||
'tours.ski.summary.lift': 'En remontes',
|
||||
'tours.ski.summary.wait': 'Esperando',
|
||||
'tours.ski.summary.walk': 'A pie',
|
||||
'tours.ski.summary.avoided': 'Evitadas',
|
||||
'tours.ski.summary.unplanned': 'Algunos tramos no se pudieron planificar, así que aún no hay tiempo de esquí.',
|
||||
'tours.ski.summary.guessed':
|
||||
'Algunos pasos se basan en una suposición de los datos del mapa, como un enlace o un sentido sin cartografiar.',
|
||||
'tours.ski.summary.stale':
|
||||
'Planificado con una copia anterior de los datos de pistas, porque la actual no está accesible.',
|
||||
'tours.ski.summary.source': 'Datos de pistas © colaboradores de OpenStreetMap',
|
||||
'tours.ski.chooseTechnique': 'Elige la técnica, o Desconocida.',
|
||||
'tours.ski.infoLabel': 'Qué hace TREK con rutas y trazas de esquí',
|
||||
'tours.ski.info':
|
||||
'Una ruta planificada tiene cada tramo dibujado en línea recta entre sus puntos, y un recorrido importado se conserva tal como se grabó. Ninguno se enruta por pistas, remontes ni senderos. Las alturas vienen del terreno o del archivo. TREK no calcula ningún tiempo de esquí, y nada de aquí, ni los colores de pendiente en 3D, dice nada sobre dificultad, nieve, aludes o si las pistas están abiertas.',
|
||||
'Una ruta planificada sigue pistas, remontes y circuitos de OpenStreetMap donde llegan los datos de pistas, y une sus puntos en línea recta donde no. Un track importado se conserva tal como se grabó. Las altitudes vienen de los datos de pistas, del terreno o del archivo. Nada aquí, ni siquiera los colores de pendiente en 3D, dice nada sobre la nieve, los aludes o si pistas, remontes y circuitos están abiertos.',
|
||||
'tours.ski.time': 'Tiempo de esquí',
|
||||
'tours.ski.timeUnknown': 'Sin calcular',
|
||||
'tours.ski.timeInfoLabel': 'Por qué no hay tiempo de esquí',
|
||||
'tours.ski.timeInfoLabel': 'Cómo se calcula el tiempo de esquí',
|
||||
'tours.ski.timeInfo':
|
||||
'TREK no calcula ningún tiempo de esquí: los tramos se dibujan rectos, no se planifican por pistas, remontes ni senderos. Si quieres, introduce tú mismo un total previsto.',
|
||||
'El enrutador de pistas suma el tiempo en pistas, en remontes, esperando en las estaciones inferiores y a pie, según tu disciplina y ritmo. Solo da un tiempo cuando cada tramo está planificado. Introduce tú un total previsto si quieres uno.',
|
||||
'tours.ski.classifyTitle': 'Clasificar ruta de esquí',
|
||||
'tours.ski.saved': 'Clasificación guardada',
|
||||
'tours.ski.saveFailed': 'No se pudo guardar la clasificación.',
|
||||
|
||||
@@ -736,6 +736,9 @@ const admin: TranslationStrings = {
|
||||
'admin.toursCycleMap.title': 'Rattakaart (CyclOSM)',
|
||||
'admin.toursCycleMap.subtitle':
|
||||
'Pakub CyclOSM-i rattamatkade aluskaardina matkaplaneerijas. OpenStreetMap France pakub seda mõõdukaks kasutuseks; tugeva kasutusega või äriline paigaldus lülitab selle välja või lepib enne operaatoriga kokku',
|
||||
'admin.toursSkiRouting.title': 'Nõlvamarsruutija',
|
||||
'admin.toursSkiRouting.subtitle':
|
||||
'Planeerib suusamatku nõlvade, tõstukite ja radade järgi Places-serveri nõlvaandmetel. Väljalülitatuna joonistatakse suusamatka lõigud otse',
|
||||
'admin.addons.noAddons': 'Lisamooduleid pole saadaval',
|
||||
'admin.weather.title': 'Ilmaandmed',
|
||||
'admin.weather.badge': 'Alates 24. märtsist 2026',
|
||||
|
||||
@@ -412,17 +412,90 @@ const tours: TranslationStrings = {
|
||||
'tours.ski.technique.unknown': 'Teadmata',
|
||||
'tours.ski.chooseDiscipline': 'Vali ala, et importida rada suusatamisena.',
|
||||
'tours.ski.chooseDisciplineToSave': 'Vali ala, et matk salvestada.',
|
||||
'tours.ski.drawnFreely':
|
||||
'Iga lõik joonistatakse sinu punktide vahele sirgelt. Miski ei võrdle seda nõlvade, tõstukite ega radadega.',
|
||||
'tours.ski.plannedHint':
|
||||
'Lõigud järgivad OpenStreetMapi nõlvu, tõstukeid ja radu seal, kuhu nõlvaandmed ulatuvad, mujal kulgevad otse. Miski siin ei ütle, kas need on avatud.',
|
||||
'tours.ski.options.maxDifficulty': 'Raskeimad nõlvad',
|
||||
'tours.ski.options.priority': 'Eelista',
|
||||
'tours.ski.options.pace': 'Tempo',
|
||||
'tours.ski.options.useLifts': 'Kasuta tõstukeid',
|
||||
'tours.ski.options.freeride': 'Luba vabalaskumise marsruudid',
|
||||
'tours.ski.options.liftDown': 'Vajadusel sõida tõstukiga alla',
|
||||
'tours.ski.options.groomedOnly': 'Ainult hooldatud rajad',
|
||||
'tours.ski.options.litOnly': 'Ainult valgustatud rajad',
|
||||
'tours.ski.options.uphillOnPistes': 'Tõuse nõlva servas',
|
||||
'tours.ski.options.avoidLinks': 'Väldi oletatud ühendusi',
|
||||
'tours.ski.options.avoidLifts': 'Jäta välja',
|
||||
'tours.ski.difficulty.novice': 'Algaja',
|
||||
'tours.ski.difficulty.easy': 'Lihtne',
|
||||
'tours.ski.difficulty.intermediate': 'Keskmine',
|
||||
'tours.ski.difficulty.advanced': 'Raske',
|
||||
'tours.ski.difficulty.expert': 'Ekspert',
|
||||
'tours.ski.difficulty.freeride': 'Vabalaskumine',
|
||||
'tours.ski.difficulty.extreme': 'Äärmuslik',
|
||||
'tours.ski.lift.drag': 'Pukseerliftid',
|
||||
'tours.ski.lift.open_chair': 'Avatud toolliftid',
|
||||
'tours.ski.lift.gondola': 'Gondliftid',
|
||||
'tours.ski.lift.cable_car': 'Köisraudteed',
|
||||
'tours.ski.lift.rail': 'Funikulöörid ja rongid',
|
||||
'tours.ski.priority.fastest': 'Kiireim',
|
||||
'tours.ski.priority.most_skiing': 'Kõige rohkem suusatamist',
|
||||
'tours.ski.priority.easiest': 'Lihtsaim',
|
||||
'tours.ski.pace.relaxed': 'Rahulik',
|
||||
'tours.ski.pace.normal': 'Tavaline',
|
||||
'tours.ski.pace.sporty': 'Sportlik',
|
||||
'tours.ski.kind.run': 'Nõlv',
|
||||
'tours.ski.kind.lift': 'Tõstuk',
|
||||
'tours.ski.kind.rail': 'Funikulöör',
|
||||
'tours.ski.kind.lift_down': 'Tõstukiga alla',
|
||||
'tours.ski.kind.nordic': 'Murdmaarada',
|
||||
'tours.ski.kind.skitour': 'Matkasuusamarsruut',
|
||||
'tours.ski.kind.hike': 'Talvine rada',
|
||||
'tours.ski.kind.connection': 'Ühendusnõlv',
|
||||
'tours.ski.kind.area': 'Avatud nõlv',
|
||||
'tours.ski.kind.link': 'Kaardistamata ühendus',
|
||||
'tours.ski.kind.crossing': 'Ületus',
|
||||
'tours.ski.kind.walk': 'Jalgsi',
|
||||
'tours.ski.fail.not_on_piste': 'See punkt on liiga kaugel igast nõlvast, rajast või tõstukist, mida saad kasutada.',
|
||||
'tours.ski.fail.no_area': 'Ükski suusakeskus nõlvaandmetes ei kata seda punkti.',
|
||||
'tours.ski.fail.loading': 'Selle piirkonna nõlvaandmed alles laaditakse alla.',
|
||||
'tours.ski.fail.places_unavailable': 'Nõlvaandmed pole praegu kättesaadavad.',
|
||||
'tours.ski.fail.disabled': 'Nõlvamarsruutimine on välja lülitatud.',
|
||||
'tours.ski.fail.blocked_by_difficulty': 'Neid punkte ühendavad ainult sinu piirist raskemad nõlvad.',
|
||||
'tours.ski.fail.blocked_by_lift_avoid': 'Neid punkte ühendab ainult tõstuk, mille välja jätsid.',
|
||||
'tours.ski.fail.blocked_by_technique': 'Neid punkte ühendavad ainult teise tehnika jaoks hooldatud rajad.',
|
||||
'tours.ski.fail.needs_ride_down': 'Tagasitee vajab tõstukiga allasõitu.',
|
||||
'tours.ski.fail.uphill_only': 'Neid punkte ühendab ainult tõus ja tõstukit pole.',
|
||||
'tours.ski.fail.disconnected': 'Miski nõlvaandmetes ei ühenda neid punkte.',
|
||||
'tours.ski.fail.cross_area': 'Need punktid on kahes ühendamata suusakeskuses.',
|
||||
'tours.ski.fail.busy': 'Nõlvamarsruutija on hõivatud. Proovi kohe uuesti.',
|
||||
'tours.ski.fail.timeout': 'Selle lõigu planeerimine võttis liiga kaua aega.',
|
||||
'tours.ski.fail.offline': 'Oled võrguühenduseta, seega joonistatakse see lõik praegu otse.',
|
||||
'tours.ski.fix.raiseDifficulty': 'Luba raskemad nõlvad',
|
||||
'tours.ski.fix.allowLift': 'Luba see tõstuk',
|
||||
'tours.ski.fix.liftDown': 'Sõida tõstukiga alla',
|
||||
'tours.ski.fix.technique': 'Vaheta tehnikat',
|
||||
'tours.ski.fix.discipline': 'Vaheta ala',
|
||||
'tours.ski.summerPath': 'Väljaspool nõlvaandmeid: suveradu mööda, mis võivad olla lume all.',
|
||||
'tours.ski.summary.title': 'Nõlvamarsruut',
|
||||
'tours.ski.summary.ski': 'Suusatamine',
|
||||
'tours.ski.summary.lift': 'Tõstukites',
|
||||
'tours.ski.summary.wait': 'Ootamine',
|
||||
'tours.ski.summary.walk': 'Jalgsi',
|
||||
'tours.ski.summary.avoided': 'Välditud',
|
||||
'tours.ski.summary.unplanned': 'Mõnda lõiku ei õnnestunud planeerida, seega suusaaega veel pole.',
|
||||
'tours.ski.summary.guessed':
|
||||
'Mõned lõigud põhinevad kaardiandmete oletusel, näiteks kaardistamata ühendusel või suunal.',
|
||||
'tours.ski.summary.stale': 'Planeeritud nõlvaandmete vanemal koopial, sest praegune pole kättesaadav.',
|
||||
'tours.ski.summary.source': 'Nõlvaandmed © OpenStreetMapi kaastöötajad',
|
||||
'tours.ski.chooseTechnique': 'Vali tehnika või Teadmata.',
|
||||
'tours.ski.infoLabel': 'Mida TREK suusamatkade ja -radadega teeb',
|
||||
'tours.ski.info':
|
||||
'Planeeritud matkal joonistatakse iga lõik punktide vahele sirgelt, imporditud rada jääb nii, nagu salvestati. Kumbagi ei marsruudita mööda nõlvu, tõstukeid ega radu. Kõrgused tulevad maastikust või failist. TREK ei arvuta suusaaega ning miski siin, ka 3D kallakuvärvid, ei ütle midagi raskuse, lume, laviinide ega nõlvade ja radade avatuse kohta.',
|
||||
'Planeeritud matk järgib OpenStreetMapi nõlvu, tõstukeid ja radu seal, kuhu nõlvaandmed ulatuvad, mujal ühendab oma punkte otse. Imporditud rada jääb nii, nagu see salvestati. Kõrgused tulevad nõlvaandmetest, maastikust või failist. Miski siin, ka mitte 3D kaldevärvid, ei ütle midagi lume, laviinide ega selle kohta, kas nõlvad, tõstukid ja rajad on avatud.',
|
||||
'tours.ski.time': 'Suusaaeg',
|
||||
'tours.ski.timeUnknown': 'Arvutamata',
|
||||
'tours.ski.timeInfoLabel': 'Miks suusaaega pole',
|
||||
'tours.ski.timeInfoLabel': 'Kuidas suusaaeg arvutatakse',
|
||||
'tours.ski.timeInfo':
|
||||
'TREK ei arvuta suusaaega: lõigud on joonistatud sirgelt, mitte planeeritud mööda nõlvu, tõstukeid ega radu. Soovi korral sisesta kavandatud koguaeg ise.',
|
||||
'Nõlvamarsruutija liidab kokku aja nõlvadel, tõstukites, alajaamades ootamisel ja jalgsi, vastavalt su alale ja tempole. Aja annab see alles siis, kui iga lõik on planeeritud. Sisesta ise planeeritud koguaeg, kui seda soovid.',
|
||||
'tours.ski.classifyTitle': 'Liigita suusarada',
|
||||
'tours.ski.saved': 'Liigitus salvestatud',
|
||||
'tours.ski.saveFailed': 'Liigitust ei õnnestunud salvestada.',
|
||||
|
||||
@@ -677,6 +677,9 @@ const admin: TranslationStrings = {
|
||||
'admin.toursCycleMap.title': 'Carte vélo (CyclOSM)',
|
||||
'admin.toursCycleMap.subtitle':
|
||||
"Propose CyclOSM comme fond de carte pour les sorties vélo dans le planificateur. OpenStreetMap France le sert pour un usage modéré ; une installation à fort trafic ou commerciale le désactive ou conclut d'abord un accord avec l'opérateur",
|
||||
'admin.toursSkiRouting.title': 'Routeur de pistes',
|
||||
'admin.toursSkiRouting.subtitle':
|
||||
'Planifie les sorties à ski le long des pistes, remontées et itinéraires avec les données de pistes du serveur Places. Désactivé, les tronçons d’une sortie à ski sont tracés en ligne droite',
|
||||
'admin.addons.noAddons': 'Aucune extension disponible',
|
||||
'admin.weather.title': 'Données météo',
|
||||
'admin.weather.badge': 'Depuis le 24 mars 2026',
|
||||
|
||||
@@ -423,17 +423,93 @@ const tours: TranslationStrings = {
|
||||
'tours.ski.technique.unknown': 'Inconnue',
|
||||
'tours.ski.chooseDiscipline': 'Choisissez une discipline pour importer la trace en ski.',
|
||||
'tours.ski.chooseDisciplineToSave': 'Choisissez une discipline pour enregistrer la sortie.',
|
||||
'tours.ski.drawnFreely':
|
||||
'Chaque tronçon est tracé en ligne droite entre vos points. Rien ne le vérifie par rapport aux pistes, remontées ou sentiers.',
|
||||
'tours.ski.plannedHint':
|
||||
'Les tronçons suivent les pistes, remontées et itinéraires d’OpenStreetMap là où les données de pistes vont, et en ligne droite ailleurs. Rien ici n’indique s’ils sont ouverts.',
|
||||
'tours.ski.options.maxDifficulty': 'Pistes les plus difficiles',
|
||||
'tours.ski.options.priority': 'Privilégier',
|
||||
'tours.ski.options.pace': 'Rythme',
|
||||
'tours.ski.options.useLifts': 'Prendre les remontées',
|
||||
'tours.ski.options.freeride': 'Autoriser les itinéraires freeride',
|
||||
'tours.ski.options.liftDown': 'Redescendre en remontée si besoin',
|
||||
'tours.ski.options.groomedOnly': 'Pistes damées uniquement',
|
||||
'tours.ski.options.litOnly': 'Pistes éclairées uniquement',
|
||||
'tours.ski.options.uphillOnPistes': 'Monter en bord de piste',
|
||||
'tours.ski.options.avoidLinks': 'Éviter les liaisons supposées',
|
||||
'tours.ski.options.avoidLifts': 'Exclure',
|
||||
'tours.ski.difficulty.novice': 'Débutant',
|
||||
'tours.ski.difficulty.easy': 'Facile',
|
||||
'tours.ski.difficulty.intermediate': 'Intermédiaire',
|
||||
'tours.ski.difficulty.advanced': 'Difficile',
|
||||
'tours.ski.difficulty.expert': 'Expert', // same-as-en
|
||||
'tours.ski.difficulty.freeride': 'Freeride', // same-as-en
|
||||
'tours.ski.difficulty.extreme': 'Extrême',
|
||||
'tours.ski.lift.drag': 'Téléskis',
|
||||
'tours.ski.lift.open_chair': 'Télésièges ouverts',
|
||||
'tours.ski.lift.gondola': 'Télécabines',
|
||||
'tours.ski.lift.cable_car': 'Téléphériques',
|
||||
'tours.ski.lift.rail': 'Funiculaires et trains',
|
||||
'tours.ski.priority.fastest': 'Le plus rapide',
|
||||
'tours.ski.priority.most_skiing': 'Le plus de ski',
|
||||
'tours.ski.priority.easiest': 'Le plus facile',
|
||||
'tours.ski.pace.relaxed': 'Tranquille',
|
||||
'tours.ski.pace.normal': 'Normal', // same-as-en
|
||||
'tours.ski.pace.sporty': 'Sportif',
|
||||
'tours.ski.kind.run': 'Piste',
|
||||
'tours.ski.kind.lift': 'Remontée',
|
||||
'tours.ski.kind.rail': 'Funiculaire',
|
||||
'tours.ski.kind.lift_down': 'Descente en remontée',
|
||||
'tours.ski.kind.nordic': 'Piste de fond',
|
||||
'tours.ski.kind.skitour': 'Itinéraire de ski de rando',
|
||||
'tours.ski.kind.hike': 'Sentier d’hiver',
|
||||
'tours.ski.kind.connection': 'Piste de liaison',
|
||||
'tours.ski.kind.area': 'Pente ouverte',
|
||||
'tours.ski.kind.link': 'Liaison non cartographiée',
|
||||
'tours.ski.kind.crossing': 'Traversée',
|
||||
'tours.ski.kind.walk': 'À pied',
|
||||
'tours.ski.fail.not_on_piste':
|
||||
'Ce point est trop loin de toute piste, de tout itinéraire ou de toute remontée utilisable.',
|
||||
'tours.ski.fail.no_area': 'Aucun domaine skiable des données de pistes ne couvre ce point.',
|
||||
'tours.ski.fail.loading': 'Les données de pistes de ce domaine sont encore en téléchargement.',
|
||||
'tours.ski.fail.places_unavailable': 'Les données de pistes sont injoignables pour le moment.',
|
||||
'tours.ski.fail.disabled': 'Le routage sur pistes est désactivé.',
|
||||
'tours.ski.fail.blocked_by_difficulty': 'Seules des pistes au-delà de votre limite relient ces points.',
|
||||
'tours.ski.fail.blocked_by_lift_avoid': 'Seule une remontée exclue relie ces points.',
|
||||
'tours.ski.fail.blocked_by_technique': 'Seules des pistes damées pour l’autre technique relient ces points.',
|
||||
'tours.ski.fail.needs_ride_down': 'Le retour demande de redescendre en remontée.',
|
||||
'tours.ski.fail.uphill_only': 'Seule une montée relie ces points, sans remontée à prendre.',
|
||||
'tours.ski.fail.disconnected': 'Rien dans les données de pistes ne relie ces points.',
|
||||
'tours.ski.fail.cross_area': 'Ces points sont dans deux domaines skiables non reliés.',
|
||||
'tours.ski.fail.busy': 'Le routeur de pistes est occupé. Réessayez dans un instant.',
|
||||
'tours.ski.fail.timeout': 'La planification de ce tronçon a pris trop de temps.',
|
||||
'tours.ski.fail.offline': 'Vous êtes hors ligne : ce tronçon est tracé en ligne droite pour l’instant.',
|
||||
'tours.ski.fix.raiseDifficulty': 'Autoriser des pistes plus difficiles',
|
||||
'tours.ski.fix.allowLift': 'Autoriser cette remontée',
|
||||
'tours.ski.fix.liftDown': 'Redescendre en remontée',
|
||||
'tours.ski.fix.technique': 'Changer de technique',
|
||||
'tours.ski.fix.discipline': 'Changer de discipline',
|
||||
'tours.ski.summerPath': 'Hors des données de pistes : tracé sur des chemins d’été, parfois sous la neige.',
|
||||
'tours.ski.summary.title': 'Itinéraire sur pistes',
|
||||
'tours.ski.summary.ski': 'Ski',
|
||||
'tours.ski.summary.lift': 'En remontée',
|
||||
'tours.ski.summary.wait': 'Attente',
|
||||
'tours.ski.summary.walk': 'À pied',
|
||||
'tours.ski.summary.avoided': 'Évité',
|
||||
'tours.ski.summary.unplanned':
|
||||
'Certains tronçons n’ont pas pu être planifiés, il n’y a donc pas encore de temps de ski.',
|
||||
'tours.ski.summary.guessed':
|
||||
'Certains passages reposent sur une supposition des données cartographiques, comme une liaison ou un sens non cartographiés.',
|
||||
'tours.ski.summary.stale':
|
||||
'Planifié sur une copie plus ancienne des données de pistes, la version actuelle étant injoignable.',
|
||||
'tours.ski.summary.source': 'Données de pistes © contributeurs OpenStreetMap',
|
||||
'tours.ski.chooseTechnique': 'Choisissez la technique, ou Inconnue.',
|
||||
'tours.ski.infoLabel': 'Ce que TREK fait des sorties et traces de ski',
|
||||
'tours.ski.info':
|
||||
'Une sortie planifiée a chaque tronçon tracé en ligne droite entre ses points, une trace importée est conservée telle qu’enregistrée. Aucune n’est calculée le long des pistes, remontées ou sentiers. Les altitudes viennent du terrain ou du fichier. TREK ne calcule aucun temps de ski, et rien ici, pas même les couleurs de pente en 3D, ne dit quoi que ce soit sur la difficulté, la neige, les avalanches ou l’ouverture des pistes.',
|
||||
'Un itinéraire planifié suit les pistes, remontées et itinéraires d’OpenStreetMap là où les données de pistes vont, et relie ses points en ligne droite ailleurs. Une trace importée reste telle qu’enregistrée. Les altitudes viennent des données de pistes, du terrain ou du fichier. Rien ici, pas même les couleurs de pente en 3D, ne dit quoi que ce soit sur la neige, les avalanches ou l’ouverture des pistes, remontées et itinéraires.',
|
||||
'tours.ski.time': 'Temps de ski',
|
||||
'tours.ski.timeUnknown': 'Non calculé',
|
||||
'tours.ski.timeInfoLabel': 'Pourquoi il n’y a pas de temps de ski',
|
||||
'tours.ski.timeInfoLabel': 'Comment le temps de ski est calculé',
|
||||
'tours.ski.timeInfo':
|
||||
'TREK ne calcule aucun temps de ski : les tronçons sont tracés en ligne droite, pas planifiés le long des pistes, remontées ou sentiers. Saisissez vous-même une durée totale prévue si besoin.',
|
||||
'Le routeur de pistes additionne le temps sur les pistes, dans les remontées, en attente aux gares de départ et à pied, selon votre discipline et votre rythme. Il ne donne un temps qu’une fois chaque tronçon planifié. Saisissez vous-même une durée totale si vous en voulez une.',
|
||||
'tours.ski.classifyTitle': 'Classer la trace de ski',
|
||||
'tours.ski.saved': 'Classement enregistré',
|
||||
'tours.ski.saveFailed': 'Le classement n’a pas pu être enregistré.',
|
||||
|
||||
@@ -740,6 +740,9 @@ const admin: TranslationStrings = {
|
||||
'admin.toursCycleMap.title': 'Ποδηλατικός χάρτης (CyclOSM)',
|
||||
'admin.toursCycleMap.subtitle':
|
||||
'Προσφέρει το CyclOSM ως βασικό χάρτη για ποδηλατικές διαδρομές στον σχεδιαστή. Το OpenStreetMap France το παρέχει για μέτρια χρήση· μια εγκατάσταση με μεγάλη ή εμπορική χρήση το απενεργοποιεί ή συμφωνεί πρώτα με τον πάροχο',
|
||||
'admin.toursSkiRouting.title': 'Δρομολογητής πιστών',
|
||||
'admin.toursSkiRouting.subtitle':
|
||||
'Σχεδιάζει διαδρομές σκι κατά μήκος πιστών, αναβατήρων και διαδρομών με τα δεδομένα πιστών του διακομιστή Places. Όταν είναι ανενεργός, τα τμήματα μιας διαδρομής σκι σχεδιάζονται ευθεία',
|
||||
'admin.addons.noAddons': 'Δεν υπάρχουν διαθέσιμα πρόσθετα',
|
||||
'admin.weather.title': 'Δεδομένα Καιρού',
|
||||
'admin.weather.badge': 'Από 24 Μαρτίου 2026',
|
||||
|
||||
@@ -425,17 +425,92 @@ const tours: TranslationStrings = {
|
||||
'tours.ski.technique.unknown': 'Άγνωστη',
|
||||
'tours.ski.chooseDiscipline': 'Επιλέξτε αγώνισμα για εισαγωγή της διαδρομής ως σκι.',
|
||||
'tours.ski.chooseDisciplineToSave': 'Επιλέξτε αγώνισμα για να αποθηκεύσετε τη διαδρομή.',
|
||||
'tours.ski.drawnFreely':
|
||||
'Κάθε σκέλος σχεδιάζεται ευθεία ανάμεσα στα σημεία σας. Τίποτα δεν το ελέγχει με πίστες, αναβατήρες ή μονοπάτια.',
|
||||
'tours.ski.plannedHint':
|
||||
'Τα τμήματα ακολουθούν πίστες, αναβατήρες και διαδρομές του OpenStreetMap όπου φτάνουν τα δεδομένα πιστών, και αλλού πάνε ευθεία. Τίποτα εδώ δεν λέει αν είναι ανοιχτά.',
|
||||
'tours.ski.options.maxDifficulty': 'Δυσκολότερες πίστες',
|
||||
'tours.ski.options.priority': 'Προτίμηση',
|
||||
'tours.ski.options.pace': 'Ρυθμός',
|
||||
'tours.ski.options.useLifts': 'Χρήση αναβατήρων',
|
||||
'tours.ski.options.freeride': 'Να επιτρέπονται διαδρομές freeride',
|
||||
'tours.ski.options.liftDown': 'Κατάβαση με αναβατήρα όταν χρειάζεται',
|
||||
'tours.ski.options.groomedOnly': 'Μόνο περιποιημένες διαδρομές',
|
||||
'tours.ski.options.litOnly': 'Μόνο φωτισμένες διαδρομές',
|
||||
'tours.ski.options.uphillOnPistes': 'Ανάβαση στην άκρη της πίστας',
|
||||
'tours.ski.options.avoidLinks': 'Αποφυγή εικαζόμενων συνδέσεων',
|
||||
'tours.ski.options.avoidLifts': 'Εξαίρεση',
|
||||
'tours.ski.difficulty.novice': 'Αρχάριος',
|
||||
'tours.ski.difficulty.easy': 'Εύκολη',
|
||||
'tours.ski.difficulty.intermediate': 'Μέτρια',
|
||||
'tours.ski.difficulty.advanced': 'Δύσκολη',
|
||||
'tours.ski.difficulty.expert': 'Για έμπειρους',
|
||||
'tours.ski.difficulty.freeride': 'Ελεύθερο σκι',
|
||||
'tours.ski.difficulty.extreme': 'Ακραία',
|
||||
'tours.ski.lift.drag': 'Συρόμενοι αναβατήρες',
|
||||
'tours.ski.lift.open_chair': 'Ανοιχτά λιφτ με καθίσματα',
|
||||
'tours.ski.lift.gondola': 'Γόνδολες',
|
||||
'tours.ski.lift.cable_car': 'Τελεφερίκ',
|
||||
'tours.ski.lift.rail': 'Οδοντωτοί και τρένα',
|
||||
'tours.ski.priority.fastest': 'Ταχύτερη',
|
||||
'tours.ski.priority.most_skiing': 'Περισσότερο σκι',
|
||||
'tours.ski.priority.easiest': 'Ευκολότερη',
|
||||
'tours.ski.pace.relaxed': 'Χαλαρός',
|
||||
'tours.ski.pace.normal': 'Κανονικός',
|
||||
'tours.ski.pace.sporty': 'Αθλητικός',
|
||||
'tours.ski.kind.run': 'Πίστα',
|
||||
'tours.ski.kind.lift': 'Αναβατήρας',
|
||||
'tours.ski.kind.rail': 'Οδοντωτός',
|
||||
'tours.ski.kind.lift_down': 'Κατάβαση με αναβατήρα',
|
||||
'tours.ski.kind.nordic': 'Διαδρομή αντοχής',
|
||||
'tours.ski.kind.skitour': 'Διαδρομή ορειβατικού σκι',
|
||||
'tours.ski.kind.hike': 'Χειμερινό μονοπάτι',
|
||||
'tours.ski.kind.connection': 'Συνδετική πίστα',
|
||||
'tours.ski.kind.area': 'Ανοιχτή πλαγιά',
|
||||
'tours.ski.kind.link': 'Μη χαρτογραφημένη σύνδεση',
|
||||
'tours.ski.kind.crossing': 'Διάσχιση',
|
||||
'tours.ski.kind.walk': 'Με τα πόδια',
|
||||
'tours.ski.fail.not_on_piste':
|
||||
'Αυτό το σημείο απέχει πολύ από κάθε πίστα, διαδρομή ή αναβατήρα που μπορείς να χρησιμοποιήσεις.',
|
||||
'tours.ski.fail.no_area': 'Κανένα χιονοδρομικό στα δεδομένα πιστών δεν καλύπτει αυτό το σημείο.',
|
||||
'tours.ski.fail.loading': 'Τα δεδομένα πιστών αυτής της περιοχής κατεβαίνουν ακόμη.',
|
||||
'tours.ski.fail.places_unavailable': 'Τα δεδομένα πιστών δεν είναι διαθέσιμα αυτή τη στιγμή.',
|
||||
'tours.ski.fail.disabled': 'Η δρομολόγηση σε πίστες είναι απενεργοποιημένη.',
|
||||
'tours.ski.fail.blocked_by_difficulty': 'Μόνο πίστες πάνω από το όριό σου ενώνουν αυτά τα σημεία.',
|
||||
'tours.ski.fail.blocked_by_lift_avoid': 'Μόνο ένας αναβατήρας που εξαίρεσες ενώνει αυτά τα σημεία.',
|
||||
'tours.ski.fail.blocked_by_technique': 'Μόνο διαδρομές για την άλλη τεχνική ενώνουν αυτά τα σημεία.',
|
||||
'tours.ski.fail.needs_ride_down': 'Η επιστροφή χρειάζεται κατάβαση με αναβατήρα.',
|
||||
'tours.ski.fail.uphill_only': 'Μόνο μια ανηφόρα ενώνει αυτά τα σημεία και δεν υπάρχει αναβατήρας.',
|
||||
'tours.ski.fail.disconnected': 'Τίποτα στα δεδομένα πιστών δεν ενώνει αυτά τα σημεία.',
|
||||
'tours.ski.fail.cross_area': 'Αυτά τα σημεία βρίσκονται σε δύο χιονοδρομικά που δεν συνδέονται.',
|
||||
'tours.ski.fail.busy': 'Ο δρομολογητής πιστών είναι απασχολημένος. Δοκίμασε ξανά σε λίγο.',
|
||||
'tours.ski.fail.timeout': 'Ο σχεδιασμός αυτού του τμήματος άργησε πολύ.',
|
||||
'tours.ski.fail.offline': 'Είσαι εκτός σύνδεσης, οπότε αυτό το τμήμα σχεδιάζεται προς το παρόν ευθεία.',
|
||||
'tours.ski.fix.raiseDifficulty': 'Να επιτρέπονται δυσκολότερες πίστες',
|
||||
'tours.ski.fix.allowLift': 'Να επιτρέπεται αυτός ο αναβατήρας',
|
||||
'tours.ski.fix.liftDown': 'Κατάβαση με τον αναβατήρα',
|
||||
'tours.ski.fix.technique': 'Αλλαγή τεχνικής',
|
||||
'tours.ski.fix.discipline': 'Αλλαγή αθλήματος',
|
||||
'tours.ski.summerPath': 'Εκτός δεδομένων πιστών: από καλοκαιρινά μονοπάτια, που μπορεί να είναι κάτω από χιόνι.',
|
||||
'tours.ski.summary.title': 'Διαδρομή σε πίστες',
|
||||
'tours.ski.summary.ski': 'Σκι',
|
||||
'tours.ski.summary.lift': 'Σε αναβατήρες',
|
||||
'tours.ski.summary.wait': 'Αναμονή',
|
||||
'tours.ski.summary.walk': 'Με τα πόδια',
|
||||
'tours.ski.summary.avoided': 'Αποφεύχθηκαν',
|
||||
'tours.ski.summary.unplanned': 'Ορισμένα τμήματα δεν σχεδιάστηκαν, οπότε δεν υπάρχει ακόμη χρόνος σκι.',
|
||||
'tours.ski.summary.guessed':
|
||||
'Ορισμένα κομμάτια στηρίζονται σε εικασία των δεδομένων χάρτη, όπως μια σύνδεση ή κατεύθυνση που δεν έχει χαρτογραφηθεί.',
|
||||
'tours.ski.summary.stale':
|
||||
'Σχεδιάστηκε σε παλαιότερο αντίγραφο των δεδομένων πιστών, επειδή το τρέχον δεν είναι διαθέσιμο.',
|
||||
'tours.ski.summary.source': 'Δεδομένα πιστών © συντελεστές του OpenStreetMap',
|
||||
'tours.ski.chooseTechnique': 'Επιλέξτε τεχνική ή Άγνωστη.',
|
||||
'tours.ski.infoLabel': 'Τι κάνει το TREK με διαδρομές και ίχνη σκι',
|
||||
'tours.ski.info':
|
||||
'Σε μια σχεδιασμένη διαδρομή κάθε σκέλος σχεδιάζεται ευθεία ανάμεσα στα σημεία της, ενώ ένα εισαγόμενο ίχνος μένει όπως καταγράφηκε. Κανένα δεν δρομολογείται σε πίστες, αναβατήρες ή μονοπάτια. Τα υψόμετρα προέρχονται από το έδαφος ή το αρχείο. Το TREK δεν υπολογίζει χρόνο σκι, και τίποτα εδώ, ούτε τα χρώματα κλίσης στο 3D, δεν λέει κάτι για δυσκολία, χιόνι, χιονοστιβάδες ή αν οι πίστες είναι ανοιχτές.',
|
||||
'Μια σχεδιασμένη διαδρομή ακολουθεί πίστες, αναβατήρες και διαδρομές του OpenStreetMap όπου φτάνουν τα δεδομένα πιστών, και αλλού ενώνει τα σημεία της ευθεία. Μια εισαγόμενη διαδρομή μένει όπως καταγράφηκε. Τα υψόμετρα προέρχονται από τα δεδομένα πιστών, το έδαφος ή το αρχείο. Τίποτα εδώ, ούτε τα χρώματα κλίσης στο 3D, δεν λέει κάτι για χιόνι, χιονοστιβάδες ή αν πίστες, αναβατήρες και διαδρομές είναι ανοιχτά.',
|
||||
'tours.ski.time': 'Χρόνος σκι',
|
||||
'tours.ski.timeUnknown': 'Δεν υπολογίστηκε',
|
||||
'tours.ski.timeInfoLabel': 'Γιατί δεν υπάρχει χρόνος σκι',
|
||||
'tours.ski.timeInfoLabel': 'Πώς υπολογίζεται ο χρόνος σκι',
|
||||
'tours.ski.timeInfo':
|
||||
'Το TREK δεν υπολογίζει χρόνο σκι: τα σκέλη σχεδιάζονται ευθεία, όχι κατά μήκος πιστών, αναβατήρων ή μονοπατιών. Αν θέλετε, δώστε μόνοι σας έναν προγραμματισμένο συνολικό χρόνο.',
|
||||
'Ο δρομολογητής πιστών αθροίζει τον χρόνο στις πίστες, στους αναβατήρες, στην αναμονή στους κάτω σταθμούς και με τα πόδια, για το άθλημα και τον ρυθμό σου. Δίνει χρόνο μόνο όταν έχει σχεδιαστεί κάθε τμήμα. Βάλε μόνος σου συνολικό χρόνο, αν θέλεις.',
|
||||
'tours.ski.classifyTitle': 'Κατάταξη διαδρομής σκι',
|
||||
'tours.ski.saved': 'Η κατάταξη αποθηκεύτηκε',
|
||||
'tours.ski.saveFailed': 'Η κατάταξη δεν αποθηκεύτηκε.',
|
||||
|
||||
@@ -678,6 +678,9 @@ const admin: TranslationStrings = {
|
||||
'admin.toursCycleMap.title': 'Kerékpáros térkép (CyclOSM)',
|
||||
'admin.toursCycleMap.subtitle':
|
||||
'A CyclOSM-et kínálja alaptérképként a kerékpártúrákhoz a túratervezőben. Az OpenStreetMap France mérsékelt használatra biztosítja; egy erősen használt vagy kereskedelmi telepítés kikapcsolja, vagy előbb megállapodik az üzemeltetővel',
|
||||
'admin.toursSkiRouting.title': 'Pályaútvonal-tervező',
|
||||
'admin.toursSkiRouting.subtitle':
|
||||
'Sítúrákat tervez pályák, felvonók és nyomvonalak mentén a Places-szerver pályaadatain. Kikapcsolva a sítúra szakaszai egyenesen rajzolódnak',
|
||||
'admin.addons.noAddons': 'Nincsenek elérhető bővítmények',
|
||||
'admin.weather.title': 'Időjárás adatok',
|
||||
'admin.weather.badge': '2026. március 24. óta',
|
||||
|
||||
@@ -417,17 +417,90 @@ const tours: TranslationStrings = {
|
||||
'tours.ski.technique.unknown': 'Ismeretlen',
|
||||
'tours.ski.chooseDiscipline': 'Válassz szakágat, hogy sínyomvonalként importáld.',
|
||||
'tours.ski.chooseDisciplineToSave': 'Válassz szakágat a túra mentéséhez.',
|
||||
'tours.ski.drawnFreely':
|
||||
'Minden szakaszt egyenesen rajzolunk a pontjaid közé. Semmi sem veti össze pályákkal, felvonókkal vagy nyomvonalakkal.',
|
||||
'tours.ski.plannedHint':
|
||||
'A szakaszok OpenStreetMap-pályákat, felvonókat és nyomvonalakat követnek, ameddig a pályaadatok érnek, máshol egyenesen haladnak. Semmi sem mutatja itt, hogy nyitva vannak-e.',
|
||||
'tours.ski.options.maxDifficulty': 'Legnehezebb pályák',
|
||||
'tours.ski.options.priority': 'Előnyben',
|
||||
'tours.ski.options.pace': 'Tempó',
|
||||
'tours.ski.options.useLifts': 'Felvonók használata',
|
||||
'tours.ski.options.freeride': 'Freeride-útvonalak engedése',
|
||||
'tours.ski.options.liftDown': 'Szükség esetén lefelé felvonóval',
|
||||
'tours.ski.options.groomedOnly': 'Csak ápolt nyomvonalak',
|
||||
'tours.ski.options.litOnly': 'Csak kivilágított nyomvonalak',
|
||||
'tours.ski.options.uphillOnPistes': 'Felfelé a pálya szélén',
|
||||
'tours.ski.options.avoidLinks': 'Feltételezett összeköttetések kerülése',
|
||||
'tours.ski.options.avoidLifts': 'Kihagyás',
|
||||
'tours.ski.difficulty.novice': 'Kezdő',
|
||||
'tours.ski.difficulty.easy': 'Könnyű',
|
||||
'tours.ski.difficulty.intermediate': 'Közepes',
|
||||
'tours.ski.difficulty.advanced': 'Nehéz',
|
||||
'tours.ski.difficulty.expert': 'Szakértő',
|
||||
'tours.ski.difficulty.freeride': 'Freeride', // same-as-en
|
||||
'tours.ski.difficulty.extreme': 'Extrém',
|
||||
'tours.ski.lift.drag': 'Húzóliftek',
|
||||
'tours.ski.lift.open_chair': 'Nyitott ülőliftek',
|
||||
'tours.ski.lift.gondola': 'Kabinos felvonók',
|
||||
'tours.ski.lift.cable_car': 'Kötélpályák',
|
||||
'tours.ski.lift.rail': 'Siklók és vonatok',
|
||||
'tours.ski.priority.fastest': 'Leggyorsabb',
|
||||
'tours.ski.priority.most_skiing': 'Legtöbb síelés',
|
||||
'tours.ski.priority.easiest': 'Legkönnyebb',
|
||||
'tours.ski.pace.relaxed': 'Laza',
|
||||
'tours.ski.pace.normal': 'Átlagos',
|
||||
'tours.ski.pace.sporty': 'Sportos',
|
||||
'tours.ski.kind.run': 'Pálya',
|
||||
'tours.ski.kind.lift': 'Felvonó',
|
||||
'tours.ski.kind.rail': 'Sikló',
|
||||
'tours.ski.kind.lift_down': 'Lefelé felvonóval',
|
||||
'tours.ski.kind.nordic': 'Sífutónyom',
|
||||
'tours.ski.kind.skitour': 'Sítúra-útvonal',
|
||||
'tours.ski.kind.hike': 'Téli ösvény',
|
||||
'tours.ski.kind.connection': 'Összekötő pálya',
|
||||
'tours.ski.kind.area': 'Nyílt lejtő',
|
||||
'tours.ski.kind.link': 'Nem térképezett összeköttetés',
|
||||
'tours.ski.kind.crossing': 'Átkelés',
|
||||
'tours.ski.kind.walk': 'Gyalog',
|
||||
'tours.ski.fail.not_on_piste': 'Ez a pont túl messze van minden használható pályától, nyomvonaltól vagy felvonótól.',
|
||||
'tours.ski.fail.no_area': 'A pályaadatokban egy síterep sem fedi le ezt a pontot.',
|
||||
'tours.ski.fail.loading': 'A terület pályaadatai még töltődnek.',
|
||||
'tours.ski.fail.places_unavailable': 'A pályaadatok most nem érhetők el.',
|
||||
'tours.ski.fail.disabled': 'A pályaútvonal-tervezés ki van kapcsolva.',
|
||||
'tours.ski.fail.blocked_by_difficulty': 'Ezeket a pontokat csak a határodnál nehezebb pályák kötik össze.',
|
||||
'tours.ski.fail.blocked_by_lift_avoid': 'Ezeket a pontokat csak egy kihagyott felvonó köti össze.',
|
||||
'tours.ski.fail.blocked_by_technique': 'Ezeket a pontokat csak a másik technikára ápolt nyomvonalak kötik össze.',
|
||||
'tours.ski.fail.needs_ride_down': 'A visszaúthoz lefelé felvonó kell.',
|
||||
'tours.ski.fail.uphill_only': 'Ezeket a pontokat csak felfelé vezető út köti össze, felvonó pedig nincs.',
|
||||
'tours.ski.fail.disconnected': 'A pályaadatokban semmi sem köti össze ezeket a pontokat.',
|
||||
'tours.ski.fail.cross_area': 'Ezek a pontok két, egymással nem összekötött síterepen vannak.',
|
||||
'tours.ski.fail.busy': 'A pályaútvonal-tervező foglalt. Próbáld újra egy kis idő múlva.',
|
||||
'tours.ski.fail.timeout': 'A szakasz tervezése túl sokáig tartott.',
|
||||
'tours.ski.fail.offline': 'Offline vagy, ezért ez a szakasz egyelőre egyenesen rajzolódik.',
|
||||
'tours.ski.fix.raiseDifficulty': 'Nehezebb pályák engedése',
|
||||
'tours.ski.fix.allowLift': 'Felvonó engedése',
|
||||
'tours.ski.fix.liftDown': 'Lefelé felvonóval',
|
||||
'tours.ski.fix.technique': 'Technika váltása',
|
||||
'tours.ski.fix.discipline': 'Sportág váltása',
|
||||
'tours.ski.summerPath': 'A pályaadatokon kívül: nyári ösvényeken, amelyek hó alatt lehetnek.',
|
||||
'tours.ski.summary.title': 'Pályaútvonal',
|
||||
'tours.ski.summary.ski': 'Síelés',
|
||||
'tours.ski.summary.lift': 'Felvonón',
|
||||
'tours.ski.summary.wait': 'Várakozás',
|
||||
'tours.ski.summary.walk': 'Gyalog',
|
||||
'tours.ski.summary.avoided': 'Elkerülve',
|
||||
'tours.ski.summary.unplanned': 'Néhány szakaszt nem sikerült megtervezni, így még nincs síidő.',
|
||||
'tours.ski.summary.guessed':
|
||||
'Egyes részek a térképadatok becslésén alapulnak, például nem térképezett összeköttetésen vagy irányon.',
|
||||
'tours.ski.summary.stale': 'A pályaadatok egy régebbi másolatán tervezve, mert a jelenlegi nem érhető el.',
|
||||
'tours.ski.summary.source': 'Pályaadatok © OpenStreetMap közreműködők',
|
||||
'tours.ski.chooseTechnique': 'Válaszd ki a technikát, vagy az Ismeretlent.',
|
||||
'tours.ski.infoLabel': 'Mit kezd a TREK a sí- és téli túrákkal és a sínyomvonalakkal',
|
||||
'tours.ski.info':
|
||||
'Egy tervezett túra minden szakasza egyenesen fut a pontjai között, egy importált nyomvonal pedig úgy marad, ahogy rögzítették. Egyiket sem vezetjük pályákon, felvonókon vagy nyomvonalakon. A magasságok a terepből vagy a fájlból származnak. A TREK nem számol síidőt, és semmi itt, a 3D lejtőszínei sem, nem mond semmit a nehézségről, a hóról, a lavinákról vagy arról, hogy nyitva vannak-e a pályák.',
|
||||
'A tervezett túra OpenStreetMap-pályákat, felvonókat és nyomvonalakat követ, ameddig a pályaadatok érnek, máshol egyenesen köti össze a pontjait. Az importált nyomvonal úgy marad, ahogy rögzítették. A magasságok a pályaadatokból, a terepből vagy a fájlból származnak. Semmi sem, a 3D lejtőszínek sem, mond itt semmit a hóról, lavinákról vagy arról, hogy a pályák, felvonók és nyomvonalak nyitva vannak-e.',
|
||||
'tours.ski.time': 'Síidő',
|
||||
'tours.ski.timeUnknown': 'Nincs kiszámolva',
|
||||
'tours.ski.timeInfoLabel': 'Miért nincs síidő',
|
||||
'tours.ski.timeInfoLabel': 'Hogyan számoljuk a síidőt',
|
||||
'tours.ski.timeInfo':
|
||||
'A TREK nem számol síidőt: a szakaszok egyenesek, nem pályák, felvonók vagy nyomvonalak mentén tervezettek. Ha szeretnéd, add meg magad a tervezett teljes időt.',
|
||||
'A pályaútvonal-tervező összeadja a pályákon, felvonókon, a völgyállomásokon várakozással és gyalog töltött időt, a sportágad és tempód szerint. Időt csak akkor ad, ha minden szakasz meg van tervezve. Adj meg magad tervezett összidőt, ha szeretnél.',
|
||||
'tours.ski.classifyTitle': 'Sínyomvonal besorolása',
|
||||
'tours.ski.saved': 'Besorolás mentve',
|
||||
'tours.ski.saveFailed': 'A besorolást nem sikerült menteni.',
|
||||
|
||||
@@ -716,6 +716,9 @@ const admin: TranslationStrings = {
|
||||
'admin.toursCycleMap.title': 'Peta sepeda (CyclOSM)',
|
||||
'admin.toursCycleMap.subtitle':
|
||||
'Menawarkan CyclOSM sebagai peta dasar untuk tur sepeda di perencana tur. OpenStreetMap France menyediakannya untuk penggunaan wajar; instalasi dengan penggunaan berat atau komersial mematikannya atau terlebih dahulu membuat kesepakatan dengan operator',
|
||||
'admin.toursSkiRouting.title': 'Router lintasan',
|
||||
'admin.toursSkiRouting.subtitle':
|
||||
'Merencanakan tur ski di sepanjang lintasan, lift, dan jalur dengan data lintasan dari server Places. Saat dimatikan, ruas tur ski digambar lurus',
|
||||
'admin.addons.noAddons': 'Tidak ada addon yang tersedia',
|
||||
'admin.weather.title': 'Data Cuaca',
|
||||
'admin.weather.badge': 'Sejak 24 Maret 2026',
|
||||
|
||||
@@ -412,17 +412,93 @@ const tours: TranslationStrings = {
|
||||
'tours.ski.technique.unknown': 'Tidak diketahui',
|
||||
'tours.ski.chooseDiscipline': 'Pilih disiplin untuk mengimpor jalur sebagai ski.',
|
||||
'tours.ski.chooseDisciplineToSave': 'Pilih disiplin untuk menyimpan tur.',
|
||||
'tours.ski.drawnFreely':
|
||||
'Setiap ruas digambar lurus di antara titik-titikmu. Tidak ada yang memeriksanya terhadap lintasan, lift, atau jalur.',
|
||||
'tours.ski.plannedHint':
|
||||
'Ruas mengikuti lintasan, lift, dan jalur dari OpenStreetMap sejauh data lintasan menjangkau, dan lurus di tempat lain. Tidak ada di sini yang menyatakan apakah semuanya buka.',
|
||||
'tours.ski.options.maxDifficulty': 'Lintasan tersulit',
|
||||
'tours.ski.options.priority': 'Utamakan',
|
||||
'tours.ski.options.pace': 'Kecepatan',
|
||||
'tours.ski.options.useLifts': 'Naik lift',
|
||||
'tours.ski.options.freeride': 'Izinkan rute freeride',
|
||||
'tours.ski.options.liftDown': 'Turun dengan lift bila perlu',
|
||||
'tours.ski.options.groomedOnly': 'Hanya jalur yang dirawat',
|
||||
'tours.ski.options.litOnly': 'Hanya jalur berlampu',
|
||||
'tours.ski.options.uphillOnPistes': 'Naik di tepi lintasan',
|
||||
'tours.ski.options.avoidLinks': 'Hindari sambungan yang hanya ditebak',
|
||||
'tours.ski.options.avoidLifts': 'Kecualikan',
|
||||
'tours.ski.difficulty.novice': 'Pemula',
|
||||
'tours.ski.difficulty.easy': 'Mudah',
|
||||
'tours.ski.difficulty.intermediate': 'Menengah',
|
||||
'tours.ski.difficulty.advanced': 'Sulit',
|
||||
'tours.ski.difficulty.expert': 'Ahli',
|
||||
'tours.ski.difficulty.freeride': 'Freeride', // same-as-en
|
||||
'tours.ski.difficulty.extreme': 'Ekstrem',
|
||||
'tours.ski.lift.drag': 'Lift seret',
|
||||
'tours.ski.lift.open_chair': 'Kursi gantung terbuka',
|
||||
'tours.ski.lift.gondola': 'Kabin gondola',
|
||||
'tours.ski.lift.cable_car': 'Kereta gantung',
|
||||
'tours.ski.lift.rail': 'Funikular dan kereta',
|
||||
'tours.ski.priority.fastest': 'Tercepat',
|
||||
'tours.ski.priority.most_skiing': 'Paling banyak ski',
|
||||
'tours.ski.priority.easiest': 'Termudah',
|
||||
'tours.ski.pace.relaxed': 'Santai',
|
||||
'tours.ski.pace.normal': 'Normal', // same-as-en
|
||||
'tours.ski.pace.sporty': 'Sportif',
|
||||
'tours.ski.kind.run': 'Lintasan',
|
||||
'tours.ski.kind.lift': 'Lift', // same-as-en
|
||||
'tours.ski.kind.rail': 'Funikular',
|
||||
'tours.ski.kind.lift_down': 'Turun dengan lift',
|
||||
'tours.ski.kind.nordic': 'Jalur ski lintas alam',
|
||||
'tours.ski.kind.skitour': 'Rute ski touring',
|
||||
'tours.ski.kind.hike': 'Jalan setapak musim dingin',
|
||||
'tours.ski.kind.connection': 'Lintasan penghubung',
|
||||
'tours.ski.kind.area': 'Lereng terbuka',
|
||||
'tours.ski.kind.link': 'Sambungan tak terpetakan',
|
||||
'tours.ski.kind.crossing': 'Penyeberangan',
|
||||
'tours.ski.kind.walk': 'Jalan kaki',
|
||||
'tours.ski.fail.not_on_piste':
|
||||
'Titik ini terlalu jauh dari lintasan, jalur, atau lift mana pun yang bisa kamu gunakan.',
|
||||
'tours.ski.fail.no_area': 'Tidak ada area ski dalam data lintasan yang mencakup titik ini.',
|
||||
'tours.ski.fail.loading': 'Data lintasan untuk area ini masih diunduh.',
|
||||
'tours.ski.fail.places_unavailable': 'Data lintasan tidak dapat dijangkau sekarang.',
|
||||
'tours.ski.fail.disabled': 'Perutean lintasan dimatikan.',
|
||||
'tours.ski.fail.blocked_by_difficulty': 'Hanya lintasan di atas batasmu yang menghubungkan titik-titik ini.',
|
||||
'tours.ski.fail.blocked_by_lift_avoid': 'Hanya lift yang kamu kecualikan yang menghubungkan titik-titik ini.',
|
||||
'tours.ski.fail.blocked_by_technique':
|
||||
'Hanya jalur yang dirawat untuk teknik lain yang menghubungkan titik-titik ini.',
|
||||
'tours.ski.fail.needs_ride_down': 'Jalan kembali perlu turun dengan lift.',
|
||||
'tours.ski.fail.uphill_only': 'Hanya jalan menanjak yang menghubungkan titik-titik ini, dan tidak ada lift.',
|
||||
'tours.ski.fail.disconnected': 'Tidak ada dalam data lintasan yang menghubungkan titik-titik ini.',
|
||||
'tours.ski.fail.cross_area': 'Titik-titik ini berada di dua area ski yang tidak terhubung.',
|
||||
'tours.ski.fail.busy': 'Router lintasan sedang sibuk. Coba lagi sebentar lagi.',
|
||||
'tours.ski.fail.timeout': 'Merencanakan ruas ini terlalu lama.',
|
||||
'tours.ski.fail.offline': 'Kamu sedang offline, jadi ruas ini digambar lurus untuk sementara.',
|
||||
'tours.ski.fix.raiseDifficulty': 'Izinkan lintasan lebih sulit',
|
||||
'tours.ski.fix.allowLift': 'Izinkan lift ini',
|
||||
'tours.ski.fix.liftDown': 'Turun dengan lift',
|
||||
'tours.ski.fix.technique': 'Ganti teknik',
|
||||
'tours.ski.fix.discipline': 'Ganti disiplin',
|
||||
'tours.ski.summerPath': 'Di luar data lintasan: lewat jalur musim panas, yang mungkin tertutup salju.',
|
||||
'tours.ski.summary.title': 'Rute lintasan',
|
||||
'tours.ski.summary.ski': 'Bermain ski',
|
||||
'tours.ski.summary.lift': 'Di lift',
|
||||
'tours.ski.summary.wait': 'Menunggu',
|
||||
'tours.ski.summary.walk': 'Jalan kaki',
|
||||
'tours.ski.summary.avoided': 'Dihindari',
|
||||
'tours.ski.summary.unplanned': 'Beberapa ruas tidak dapat direncanakan, jadi belum ada waktu ski.',
|
||||
'tours.ski.summary.guessed':
|
||||
'Beberapa bagian bergantung pada tebakan dari data peta, seperti sambungan atau arah yang tidak terpetakan.',
|
||||
'tours.ski.summary.stale':
|
||||
'Direncanakan pada salinan data lintasan yang lebih lama, karena yang terbaru tidak dapat dijangkau.',
|
||||
'tours.ski.summary.source': 'Data lintasan © kontributor OpenStreetMap',
|
||||
'tours.ski.chooseTechnique': 'Pilih teknik, atau Tidak diketahui.',
|
||||
'tours.ski.infoLabel': 'Apa yang TREK lakukan dengan tur dan jalur ski',
|
||||
'tours.ski.info':
|
||||
'Tur yang direncanakan menggambar setiap ruas lurus di antara titik-titiknya, jalur yang diimpor tetap seperti yang direkam. Keduanya tidak dirutekan lewat lintasan, lift, atau jalur. Ketinggian berasal dari medan atau berkas. TREK tidak menghitung waktu ski, dan tidak ada apa pun di sini, termasuk warna kemiringan dalam 3D, yang mengatakan sesuatu tentang kesulitan, salju, longsoran, atau apakah lintasan sedang dibuka.',
|
||||
'Tur yang direncanakan mengikuti lintasan, lift, dan jalur dari OpenStreetMap sejauh data lintasan menjangkau, dan menghubungkan titiknya secara lurus di tempat lain. Trek yang diimpor tetap seperti saat direkam. Ketinggian berasal dari data lintasan, medan, atau file. Tidak ada di sini, termasuk warna kemiringan dalam 3D, yang menyatakan apa pun tentang salju, longsoran, atau apakah lintasan, lift, dan jalur buka.',
|
||||
'tours.ski.time': 'Waktu ski',
|
||||
'tours.ski.timeUnknown': 'Tidak dihitung',
|
||||
'tours.ski.timeInfoLabel': 'Mengapa tidak ada waktu ski',
|
||||
'tours.ski.timeInfoLabel': 'Cara waktu ski dihitung',
|
||||
'tours.ski.timeInfo':
|
||||
'TREK tidak menghitung waktu ski: ruas digambar lurus, tidak direncanakan lewat lintasan, lift, atau jalur. Jika mau, isi sendiri total waktu yang direncanakan.',
|
||||
'Router lintasan menjumlahkan waktu di lintasan, di lift, menunggu di stasiun bawah, dan berjalan kaki, sesuai disiplin dan kecepatanmu. Waktu hanya diberikan setelah setiap ruas direncanakan. Masukkan sendiri total yang direncanakan jika kamu mau.',
|
||||
'tours.ski.classifyTitle': 'Klasifikasikan jalur ski',
|
||||
'tours.ski.saved': 'Klasifikasi disimpan',
|
||||
'tours.ski.saveFailed': 'Klasifikasi tidak dapat disimpan.',
|
||||
|
||||
@@ -672,6 +672,9 @@ const admin: TranslationStrings = {
|
||||
'admin.toursCycleMap.title': 'Mappa ciclabile (CyclOSM)',
|
||||
'admin.toursCycleMap.subtitle':
|
||||
"Offre CyclOSM come mappa di base per i giri in bici nel pianificatore. OpenStreetMap France la fornisce per un uso moderato; un'installazione molto usata o commerciale la disattiva o prende prima un accordo con il gestore",
|
||||
'admin.toursSkiRouting.title': 'Router delle piste',
|
||||
'admin.toursSkiRouting.subtitle':
|
||||
'Pianifica i giri con gli sci lungo piste, impianti e tracciati con i dati delle piste del server Places. Disattivato, i tratti di un giro con gli sci sono disegnati in linea retta',
|
||||
'admin.addons.noAddons': 'Nessun modulo disponibile',
|
||||
'admin.weather.title': 'Dati meteo',
|
||||
'admin.weather.badge': 'Dal 24 marzo 2026',
|
||||
|
||||
@@ -420,17 +420,91 @@ const tours: TranslationStrings = {
|
||||
'tours.ski.technique.unknown': 'Sconosciuta',
|
||||
'tours.ski.chooseDiscipline': 'Scegli una disciplina per importare la traccia come sci.',
|
||||
'tours.ski.chooseDisciplineToSave': 'Scegli una disciplina per salvare il tour.',
|
||||
'tours.ski.drawnFreely':
|
||||
'Ogni tratto è disegnato in linea retta tra i tuoi punti. Nulla lo confronta con piste, impianti o sentieri.',
|
||||
'tours.ski.plannedHint':
|
||||
'I tratti seguono piste, impianti e tracciati di OpenStreetMap dove arrivano i dati delle piste, altrimenti vanno in linea retta. Nulla qui dice se sono aperti.',
|
||||
'tours.ski.options.maxDifficulty': 'Piste più difficili',
|
||||
'tours.ski.options.priority': 'Preferisci',
|
||||
'tours.ski.options.pace': 'Ritmo',
|
||||
'tours.ski.options.useLifts': 'Usa gli impianti',
|
||||
'tours.ski.options.freeride': 'Consenti percorsi freeride',
|
||||
'tours.ski.options.liftDown': 'Scendi in impianto se serve',
|
||||
'tours.ski.options.groomedOnly': 'Solo piste battute',
|
||||
'tours.ski.options.litOnly': 'Solo piste illuminate',
|
||||
'tours.ski.options.uphillOnPistes': 'Sali a bordo pista',
|
||||
'tours.ski.options.avoidLinks': 'Evita collegamenti ipotizzati',
|
||||
'tours.ski.options.avoidLifts': 'Escludi',
|
||||
'tours.ski.difficulty.novice': 'Principiante',
|
||||
'tours.ski.difficulty.easy': 'Facile',
|
||||
'tours.ski.difficulty.intermediate': 'Intermedia',
|
||||
'tours.ski.difficulty.advanced': 'Difficile',
|
||||
'tours.ski.difficulty.expert': 'Esperto',
|
||||
'tours.ski.difficulty.freeride': 'Freeride', // same-as-en
|
||||
'tours.ski.difficulty.extreme': 'Estrema',
|
||||
'tours.ski.lift.drag': 'Skilift',
|
||||
'tours.ski.lift.open_chair': 'Seggiovie aperte',
|
||||
'tours.ski.lift.gondola': 'Cabinovie',
|
||||
'tours.ski.lift.cable_car': 'Funivie',
|
||||
'tours.ski.lift.rail': 'Funicolari e treni',
|
||||
'tours.ski.priority.fastest': 'Più veloce',
|
||||
'tours.ski.priority.most_skiing': 'Più sci',
|
||||
'tours.ski.priority.easiest': 'Più facile',
|
||||
'tours.ski.pace.relaxed': 'Tranquillo',
|
||||
'tours.ski.pace.normal': 'Normale',
|
||||
'tours.ski.pace.sporty': 'Sportivo',
|
||||
'tours.ski.kind.run': 'Pista',
|
||||
'tours.ski.kind.lift': 'Impianto',
|
||||
'tours.ski.kind.rail': 'Funicolare',
|
||||
'tours.ski.kind.lift_down': 'Discesa in impianto',
|
||||
'tours.ski.kind.nordic': 'Pista da fondo',
|
||||
'tours.ski.kind.skitour': 'Itinerario scialpinistico',
|
||||
'tours.ski.kind.hike': 'Sentiero invernale',
|
||||
'tours.ski.kind.connection': 'Pista di collegamento',
|
||||
'tours.ski.kind.area': 'Pendio aperto',
|
||||
'tours.ski.kind.link': 'Collegamento non mappato',
|
||||
'tours.ski.kind.crossing': 'Attraversamento',
|
||||
'tours.ski.kind.walk': 'A piedi',
|
||||
'tours.ski.fail.not_on_piste': 'Questo punto è troppo lontano da piste, tracciati o impianti che puoi usare.',
|
||||
'tours.ski.fail.no_area': 'Nessun comprensorio nei dati delle piste copre questo punto.',
|
||||
'tours.ski.fail.loading': 'I dati delle piste di questa zona sono ancora in download.',
|
||||
'tours.ski.fail.places_unavailable': 'I dati delle piste non sono raggiungibili ora.',
|
||||
'tours.ski.fail.disabled': 'Il routing sulle piste è disattivato.',
|
||||
'tours.ski.fail.blocked_by_difficulty': 'Solo piste oltre il tuo limite uniscono questi punti.',
|
||||
'tours.ski.fail.blocked_by_lift_avoid': 'Solo un impianto che hai escluso unisce questi punti.',
|
||||
'tours.ski.fail.blocked_by_technique': 'Solo piste battute per l’altra tecnica uniscono questi punti.',
|
||||
'tours.ski.fail.needs_ride_down': 'Il ritorno richiede una discesa in impianto.',
|
||||
'tours.ski.fail.uphill_only': 'Solo una salita unisce questi punti e non c’è un impianto da prendere.',
|
||||
'tours.ski.fail.disconnected': 'Nulla nei dati delle piste unisce questi punti.',
|
||||
'tours.ski.fail.cross_area': 'Questi punti sono in due comprensori non collegati.',
|
||||
'tours.ski.fail.busy': 'Il router delle piste è occupato. Riprova tra poco.',
|
||||
'tours.ski.fail.timeout': 'La pianificazione di questo tratto ha richiesto troppo tempo.',
|
||||
'tours.ski.fail.offline': 'Sei offline, quindi per ora questo tratto è disegnato in linea retta.',
|
||||
'tours.ski.fix.raiseDifficulty': 'Consenti piste più difficili',
|
||||
'tours.ski.fix.allowLift': 'Consenti questo impianto',
|
||||
'tours.ski.fix.liftDown': 'Scendi in impianto',
|
||||
'tours.ski.fix.technique': 'Cambia tecnica',
|
||||
'tours.ski.fix.discipline': 'Cambia disciplina',
|
||||
'tours.ski.summerPath': 'Fuori dai dati delle piste: su sentieri estivi, che possono essere sotto la neve.',
|
||||
'tours.ski.summary.title': 'Percorso sulle piste',
|
||||
'tours.ski.summary.ski': 'Sciando',
|
||||
'tours.ski.summary.lift': 'In impianto',
|
||||
'tours.ski.summary.wait': 'Attesa',
|
||||
'tours.ski.summary.walk': 'A piedi',
|
||||
'tours.ski.summary.avoided': 'Evitate',
|
||||
'tours.ski.summary.unplanned': 'Alcuni tratti non si sono potuti pianificare, quindi non c’è ancora un tempo di sci.',
|
||||
'tours.ski.summary.guessed':
|
||||
'Alcuni passaggi si basano su un’ipotesi dei dati della mappa, come un collegamento o un senso non mappati.',
|
||||
'tours.ski.summary.stale':
|
||||
'Pianificato su una copia più vecchia dei dati delle piste, perché quella attuale non è raggiungibile.',
|
||||
'tours.ski.summary.source': 'Dati delle piste © contributori di OpenStreetMap',
|
||||
'tours.ski.chooseTechnique': 'Scegli la tecnica, oppure Sconosciuta.',
|
||||
'tours.ski.infoLabel': 'Cosa fa TREK con tour e tracce di sci',
|
||||
'tours.ski.info':
|
||||
'Un tour pianificato ha ogni tratto disegnato in linea retta tra i suoi punti, una traccia importata resta come registrata. Nessuno dei due viene instradato lungo piste, impianti o sentieri. Le quote vengono dal terreno o dal file. TREK non calcola alcun tempo di sci, e nulla qui, nemmeno i colori della pendenza in 3D, dice qualcosa su difficoltà, neve, valanghe o apertura di piste e anelli.',
|
||||
'Un giro pianificato segue piste, impianti e tracciati di OpenStreetMap dove arrivano i dati delle piste, altrimenti unisce i suoi punti in linea retta. Una traccia importata resta come è stata registrata. Le quote vengono dai dati delle piste, dal terreno o dal file. Nulla qui, nemmeno i colori di pendenza in 3D, dice qualcosa su neve, valanghe o apertura di piste, impianti e tracciati.',
|
||||
'tours.ski.time': 'Tempo di sci',
|
||||
'tours.ski.timeUnknown': 'Non calcolato',
|
||||
'tours.ski.timeInfoLabel': 'Perché non c’è un tempo di sci',
|
||||
'tours.ski.timeInfoLabel': 'Come si calcola il tempo di sci',
|
||||
'tours.ski.timeInfo':
|
||||
'TREK non calcola alcun tempo di sci: i tratti sono disegnati in linea retta, non pianificati lungo piste, impianti o sentieri. Se vuoi, inserisci tu un totale previsto.',
|
||||
'Il router delle piste somma il tempo su piste, impianti, attese alle stazioni a valle e a piedi, secondo disciplina e ritmo. Dà un tempo solo quando ogni tratto è pianificato. Inserisci tu un totale previsto, se ne vuoi uno.',
|
||||
'tours.ski.classifyTitle': 'Classifica traccia di sci',
|
||||
'tours.ski.saved': 'Classificazione salvata',
|
||||
'tours.ski.saveFailed': 'Impossibile salvare la classificazione.',
|
||||
|
||||
@@ -679,6 +679,8 @@ const admin: TranslationStrings = {
|
||||
'admin.toursCycleMap.title': 'サイクリング地図(CyclOSM)',
|
||||
'admin.toursCycleMap.subtitle':
|
||||
'ツアープランナーで自転車ツアーのベース地図として CyclOSM を提供します。OpenStreetMap France が適度な利用向けに配信しています。利用の多い、または商用のインストールでは無効にするか、事前に運営者と合意してください',
|
||||
'admin.toursSkiRouting.title': 'ゲレンデルーター',
|
||||
'admin.toursSkiRouting.subtitle': 'Places サーバーのゲレンデデータで、コース、リフト、トレイルに沿ってスキーツアーを計画します。オフにすると、スキーツアーの区間は直線で描かれます',
|
||||
'admin.addons.noAddons': '利用可能なアドオンはありません',
|
||||
'admin.weather.title': '天気データ',
|
||||
'admin.weather.badge': '2026年3月24日以降',
|
||||
|
||||
@@ -402,16 +402,88 @@ const tours: TranslationStrings = {
|
||||
'tours.ski.technique.unknown': '不明',
|
||||
'tours.ski.chooseDiscipline': 'スキーとして取り込むには種目を選んでください。',
|
||||
'tours.ski.chooseDisciplineToSave': '保存するには種目を選んでください。',
|
||||
'tours.ski.drawnFreely': '各区間はポイントの間を直線で描きます。コース、リフト、トレイルとの照合は行いません。',
|
||||
'tours.ski.plannedHint': 'ゲレンデデータが届く範囲では OpenStreetMap のコース、リフト、トレイルに沿い、それ以外は直線で結びます。営業中かどうかはここでは示しません。',
|
||||
'tours.ski.options.maxDifficulty': '最も難しいコース',
|
||||
'tours.ski.options.priority': '優先',
|
||||
'tours.ski.options.pace': 'ペース',
|
||||
'tours.ski.options.useLifts': 'リフトを使う',
|
||||
'tours.ski.options.freeride': 'フリーライドルートを許可',
|
||||
'tours.ski.options.liftDown': '必要ならリフトで下る',
|
||||
'tours.ski.options.groomedOnly': '圧雪済みトレイルのみ',
|
||||
'tours.ski.options.litOnly': 'ライトアップされたトレイルのみ',
|
||||
'tours.ski.options.uphillOnPistes': 'コース脇を登る',
|
||||
'tours.ski.options.avoidLinks': '推定の接続を避ける',
|
||||
'tours.ski.options.avoidLifts': '除外',
|
||||
'tours.ski.difficulty.novice': '初心者',
|
||||
'tours.ski.difficulty.easy': '初級',
|
||||
'tours.ski.difficulty.intermediate': '中級',
|
||||
'tours.ski.difficulty.advanced': '上級',
|
||||
'tours.ski.difficulty.expert': 'エキスパート',
|
||||
'tours.ski.difficulty.freeride': 'フリーライド',
|
||||
'tours.ski.difficulty.extreme': 'エクストリーム',
|
||||
'tours.ski.lift.drag': 'Tバー・ロープトウ',
|
||||
'tours.ski.lift.open_chair': 'オープンチェアリフト',
|
||||
'tours.ski.lift.gondola': 'ゴンドラ',
|
||||
'tours.ski.lift.cable_car': 'ロープウェイ',
|
||||
'tours.ski.lift.rail': 'ケーブルカーと鉄道',
|
||||
'tours.ski.priority.fastest': '最速',
|
||||
'tours.ski.priority.most_skiing': 'たくさん滑る',
|
||||
'tours.ski.priority.easiest': '最も易しい',
|
||||
'tours.ski.pace.relaxed': 'ゆったり',
|
||||
'tours.ski.pace.normal': '普通',
|
||||
'tours.ski.pace.sporty': 'スポーティ',
|
||||
'tours.ski.kind.run': 'コース',
|
||||
'tours.ski.kind.lift': 'リフト',
|
||||
'tours.ski.kind.rail': 'ケーブルカー',
|
||||
'tours.ski.kind.lift_down': 'リフトで下山',
|
||||
'tours.ski.kind.nordic': 'クロスカントリートレイル',
|
||||
'tours.ski.kind.skitour': '山スキールート',
|
||||
'tours.ski.kind.hike': '冬道',
|
||||
'tours.ski.kind.connection': '連絡コース',
|
||||
'tours.ski.kind.area': 'オープンスロープ',
|
||||
'tours.ski.kind.link': '地図にない接続',
|
||||
'tours.ski.kind.crossing': '横断',
|
||||
'tours.ski.kind.walk': '徒歩',
|
||||
'tours.ski.fail.not_on_piste': 'このポイントは利用できるコース、トレイル、リフトから遠すぎます。',
|
||||
'tours.ski.fail.no_area': 'ゲレンデデータのどのスキー場もこのポイントを含んでいません。',
|
||||
'tours.ski.fail.loading': 'このエリアのゲレンデデータをまだダウンロード中です。',
|
||||
'tours.ski.fail.places_unavailable': '今はゲレンデデータにアクセスできません。',
|
||||
'tours.ski.fail.disabled': 'ゲレンデルーティングはオフです。',
|
||||
'tours.ski.fail.blocked_by_difficulty': 'これらのポイントをつなぐのは、上限より難しいコースだけです。',
|
||||
'tours.ski.fail.blocked_by_lift_avoid': 'これらのポイントをつなぐのは、除外したリフトだけです。',
|
||||
'tours.ski.fail.blocked_by_technique': 'これらのポイントをつなぐのは、もう一方の走法用のトレイルだけです。',
|
||||
'tours.ski.fail.needs_ride_down': '戻るにはリフトで下る必要があります。',
|
||||
'tours.ski.fail.uphill_only': 'これらのポイントをつなぐのは登りだけで、乗れるリフトがありません。',
|
||||
'tours.ski.fail.disconnected': 'ゲレンデデータにはこれらのポイントをつなぐものがありません。',
|
||||
'tours.ski.fail.cross_area': 'これらのポイントはつながっていない二つのスキー場にあります。',
|
||||
'tours.ski.fail.busy': 'ゲレンデルーターが混み合っています。少ししてからもう一度お試しください。',
|
||||
'tours.ski.fail.timeout': 'この区間の計画に時間がかかりすぎました。',
|
||||
'tours.ski.fail.offline': 'オフラインのため、この区間は今のところ直線で描かれます。',
|
||||
'tours.ski.fix.raiseDifficulty': 'より難しいコースを許可',
|
||||
'tours.ski.fix.allowLift': 'このリフトを許可',
|
||||
'tours.ski.fix.liftDown': 'リフトで下る',
|
||||
'tours.ski.fix.technique': '走法を切り替える',
|
||||
'tours.ski.fix.discipline': '種目を切り替える',
|
||||
'tours.ski.summerPath': 'ゲレンデデータの外: 夏道を通ります。雪の下にあることがあります。',
|
||||
'tours.ski.summary.title': 'ゲレンデルート',
|
||||
'tours.ski.summary.ski': '滑走',
|
||||
'tours.ski.summary.lift': 'リフト乗車',
|
||||
'tours.ski.summary.wait': '待ち時間',
|
||||
'tours.ski.summary.walk': '徒歩',
|
||||
'tours.ski.summary.avoided': '回避したコース',
|
||||
'tours.ski.summary.unplanned': '計画できなかった区間があるため、まだスキー時間はありません。',
|
||||
'tours.ski.summary.guessed': '一部の区間は、地図にない接続や方向など、地図データからの推定に基づいています。',
|
||||
'tours.ski.summary.stale': '最新のゲレンデデータにアクセスできないため、古いコピーで計画しました。',
|
||||
'tours.ski.summary.source': 'ゲレンデデータ © OpenStreetMap 貢献者',
|
||||
'tours.ski.chooseTechnique': '走法を選ぶか、不明を選んでください。',
|
||||
'tours.ski.infoLabel': 'TREK がスキーツアーとトラックで行うこと',
|
||||
'tours.ski.info':
|
||||
'計画したツアーは各区間をポイントの間の直線で描き、取り込んだトラックは記録されたまま保持します。どちらもコース、リフト、トレイルに沿ったルート検索は行いません。標高は地形またはファイルの値です。TREK はスキーの所要時間を計算せず、3D の勾配の色を含めここにあるものは、難易度、積雪、雪崩、コースの営業状況について何も示しません。',
|
||||
'計画したツアーは、ゲレンデデータが届く範囲では OpenStreetMap のコース、リフト、トレイルに沿い、それ以外はポイント間を直線で結びます。インポートしたトラックは記録どおりに残ります。標高はゲレンデデータ、地形、またはファイルから取得します。3D の傾斜色を含め、ここにある情報は雪、雪崩、コースやリフト、トレイルの営業状況について何も示しません。',
|
||||
'tours.ski.time': 'スキー時間',
|
||||
'tours.ski.timeUnknown': '計算なし',
|
||||
'tours.ski.timeInfoLabel': 'スキー時間がない理由',
|
||||
'tours.ski.timeInfoLabel': 'スキー時間の求め方',
|
||||
'tours.ski.timeInfo':
|
||||
'TREK はスキー時間を計算しません。区間は直線で描かれ、コース、リフト、トレイルに沿って計画されていません。必要なら予定の合計時間をご自身で入力してください。',
|
||||
'ゲレンデルーターは、種目とペースに合わせて、コース、リフト、山麓駅での待ち時間、徒歩の時間を合計します。すべての区間が計画されたときだけ時間を出します。必要なら予定合計を自分で入力してください。',
|
||||
'tours.ski.classifyTitle': 'スキーのトラックを分類',
|
||||
'tours.ski.saved': '分類を保存しました',
|
||||
'tours.ski.saveFailed': '分類を保存できませんでした。',
|
||||
|
||||
@@ -677,6 +677,8 @@ const admin: TranslationStrings = {
|
||||
'admin.toursCycleMap.title': '자전거 지도 (CyclOSM)',
|
||||
'admin.toursCycleMap.subtitle':
|
||||
'투어 플래너에서 자전거 투어의 기본 지도로 CyclOSM을 제공합니다. OpenStreetMap France가 적당한 사용을 위해 제공하며, 사용량이 많거나 상업적인 설치는 이를 끄거나 먼저 운영자와 합의합니다',
|
||||
'admin.toursSkiRouting.title': '슬로프 라우터',
|
||||
'admin.toursSkiRouting.subtitle': 'Places 서버의 슬로프 데이터로 슬로프, 리프트, 트레일을 따라 스키 투어를 계획합니다. 끄면 스키 투어 구간이 직선으로 그려집니다',
|
||||
'admin.addons.noAddons': '사용 가능한 애드온이 없습니다',
|
||||
'admin.weather.title': '날씨 데이터',
|
||||
'admin.weather.badge': '2026년 3월 24일부터',
|
||||
|
||||
@@ -400,16 +400,89 @@ const tours: TranslationStrings = {
|
||||
'tours.ski.technique.unknown': '알 수 없음',
|
||||
'tours.ski.chooseDiscipline': '트랙을 스키로 가져오려면 종목을 선택하세요.',
|
||||
'tours.ski.chooseDisciplineToSave': '투어를 저장하려면 종목을 선택하세요.',
|
||||
'tours.ski.drawnFreely': '각 구간은 지점 사이를 직선으로 그립니다. 슬로프, 리프트, 트레일과 대조하지 않습니다.',
|
||||
'tours.ski.plannedHint':
|
||||
'슬로프 데이터가 닿는 곳에서는 OpenStreetMap의 슬로프, 리프트, 트레일을 따르고, 그 밖에서는 직선으로 이어집니다. 운영 여부는 여기서 알 수 없습니다.',
|
||||
'tours.ski.options.maxDifficulty': '가장 어려운 슬로프',
|
||||
'tours.ski.options.priority': '우선',
|
||||
'tours.ski.options.pace': '속도',
|
||||
'tours.ski.options.useLifts': '리프트 이용',
|
||||
'tours.ski.options.freeride': '프리라이드 경로 허용',
|
||||
'tours.ski.options.liftDown': '필요하면 리프트로 하산',
|
||||
'tours.ski.options.groomedOnly': '정설된 트레일만',
|
||||
'tours.ski.options.litOnly': '조명 있는 트레일만',
|
||||
'tours.ski.options.uphillOnPistes': '슬로프 가장자리로 오르기',
|
||||
'tours.ski.options.avoidLinks': '추정 연결 피하기',
|
||||
'tours.ski.options.avoidLifts': '제외',
|
||||
'tours.ski.difficulty.novice': '입문',
|
||||
'tours.ski.difficulty.easy': '초급',
|
||||
'tours.ski.difficulty.intermediate': '중급',
|
||||
'tours.ski.difficulty.advanced': '상급',
|
||||
'tours.ski.difficulty.expert': '최상급',
|
||||
'tours.ski.difficulty.freeride': '프리라이드',
|
||||
'tours.ski.difficulty.extreme': '익스트림',
|
||||
'tours.ski.lift.drag': '견인 리프트',
|
||||
'tours.ski.lift.open_chair': '오픈 체어리프트',
|
||||
'tours.ski.lift.gondola': '곤돌라',
|
||||
'tours.ski.lift.cable_car': '케이블카',
|
||||
'tours.ski.lift.rail': '푸니쿨라와 기차',
|
||||
'tours.ski.priority.fastest': '가장 빠름',
|
||||
'tours.ski.priority.most_skiing': '가장 많이 타기',
|
||||
'tours.ski.priority.easiest': '가장 쉬움',
|
||||
'tours.ski.pace.relaxed': '여유롭게',
|
||||
'tours.ski.pace.normal': '보통',
|
||||
'tours.ski.pace.sporty': '스포티하게',
|
||||
'tours.ski.kind.run': '슬로프',
|
||||
'tours.ski.kind.lift': '리프트',
|
||||
'tours.ski.kind.rail': '푸니쿨라',
|
||||
'tours.ski.kind.lift_down': '리프트 하산',
|
||||
'tours.ski.kind.nordic': '크로스컨트리 트레일',
|
||||
'tours.ski.kind.skitour': '스키 투어 경로',
|
||||
'tours.ski.kind.hike': '겨울 길',
|
||||
'tours.ski.kind.connection': '연결 슬로프',
|
||||
'tours.ski.kind.area': '열린 사면',
|
||||
'tours.ski.kind.link': '지도에 없는 연결',
|
||||
'tours.ski.kind.crossing': '횡단',
|
||||
'tours.ski.kind.walk': '도보',
|
||||
'tours.ski.fail.not_on_piste': '이 지점은 이용할 수 있는 슬로프, 트레일, 리프트에서 너무 멉니다.',
|
||||
'tours.ski.fail.no_area': '슬로프 데이터의 어떤 스키장도 이 지점을 포함하지 않습니다.',
|
||||
'tours.ski.fail.loading': '이 지역의 슬로프 데이터를 아직 내려받는 중입니다.',
|
||||
'tours.ski.fail.places_unavailable': '지금은 슬로프 데이터에 연결할 수 없습니다.',
|
||||
'tours.ski.fail.disabled': '슬로프 라우팅이 꺼져 있습니다.',
|
||||
'tours.ski.fail.blocked_by_difficulty': '이 지점들은 한도보다 어려운 슬로프로만 이어집니다.',
|
||||
'tours.ski.fail.blocked_by_lift_avoid': '이 지점들은 제외한 리프트로만 이어집니다.',
|
||||
'tours.ski.fail.blocked_by_technique': '이 지점들은 다른 주법용 트레일로만 이어집니다.',
|
||||
'tours.ski.fail.needs_ride_down': '돌아가려면 리프트로 하산해야 합니다.',
|
||||
'tours.ski.fail.uphill_only': '이 지점들은 오르막으로만 이어지고 탈 리프트가 없습니다.',
|
||||
'tours.ski.fail.disconnected': '슬로프 데이터에 이 지점들을 잇는 것이 없습니다.',
|
||||
'tours.ski.fail.cross_area': '이 지점들은 연결되지 않은 두 스키장에 있습니다.',
|
||||
'tours.ski.fail.busy': '슬로프 라우터가 바쁩니다. 잠시 후 다시 시도하세요.',
|
||||
'tours.ski.fail.timeout': '이 구간을 계획하는 데 너무 오래 걸렸습니다.',
|
||||
'tours.ski.fail.offline': '오프라인 상태라 이 구간은 지금은 직선으로 그려집니다.',
|
||||
'tours.ski.fix.raiseDifficulty': '더 어려운 슬로프 허용',
|
||||
'tours.ski.fix.allowLift': '이 리프트 허용',
|
||||
'tours.ski.fix.liftDown': '리프트로 하산',
|
||||
'tours.ski.fix.technique': '주법 바꾸기',
|
||||
'tours.ski.fix.discipline': '종목 바꾸기',
|
||||
'tours.ski.summerPath': '슬로프 데이터 밖: 눈에 덮였을 수 있는 여름 길을 따릅니다.',
|
||||
'tours.ski.summary.title': '슬로프 경로',
|
||||
'tours.ski.summary.ski': '스키',
|
||||
'tours.ski.summary.lift': '리프트 탑승',
|
||||
'tours.ski.summary.wait': '대기',
|
||||
'tours.ski.summary.walk': '도보',
|
||||
'tours.ski.summary.avoided': '피한 슬로프',
|
||||
'tours.ski.summary.unplanned': '일부 구간을 계획하지 못해 아직 스키 시간이 없습니다.',
|
||||
'tours.ski.summary.guessed': '일부 구간은 지도에 없는 연결이나 방향처럼 지도 데이터의 추정에 기반합니다.',
|
||||
'tours.ski.summary.stale': '현재 슬로프 데이터에 연결할 수 없어 이전 사본으로 계획했습니다.',
|
||||
'tours.ski.summary.source': '슬로프 데이터 © OpenStreetMap 기여자',
|
||||
'tours.ski.chooseTechnique': '주법을 선택하거나 알 수 없음을 선택하세요.',
|
||||
'tours.ski.infoLabel': 'TREK이 스키 투어와 트랙으로 하는 일',
|
||||
'tours.ski.info':
|
||||
'계획한 투어는 각 구간을 지점 사이의 직선으로 그리고, 가져온 트랙은 기록된 그대로 유지합니다. 어느 쪽도 슬로프, 리프트, 트레일을 따라 경로를 계산하지 않습니다. 고도는 지형 또는 파일에서 가져옵니다. TREK은 스키 시간을 계산하지 않으며, 3D 경사 색상을 포함해 여기의 어떤 것도 난이도, 눈, 눈사태, 슬로프 운영 여부에 대해 알려 주지 않습니다.',
|
||||
'계획한 투어는 슬로프 데이터가 닿는 곳에서는 OpenStreetMap의 슬로프, 리프트, 트레일을 따르고, 그 밖에서는 지점 사이를 직선으로 잇습니다. 가져온 트랙은 기록된 그대로 유지됩니다. 고도는 슬로프 데이터, 지형 또는 파일에서 가져옵니다. 3D 경사 색상을 포함해 여기의 어떤 정보도 눈, 눈사태, 슬로프와 리프트, 트레일의 운영 여부에 대해 말하지 않습니다.',
|
||||
'tours.ski.time': '스키 시간',
|
||||
'tours.ski.timeUnknown': '계산 안 함',
|
||||
'tours.ski.timeInfoLabel': '스키 시간이 없는 이유',
|
||||
'tours.ski.timeInfoLabel': '스키 시간 계산 방법',
|
||||
'tours.ski.timeInfo':
|
||||
'TREK은 스키 시간을 계산하지 않습니다. 구간은 직선으로 그려지며 슬로프, 리프트, 트레일을 따라 계획되지 않습니다. 원하면 예정 총 시간을 직접 입력하세요.',
|
||||
'슬로프 라우터는 종목과 속도에 맞춰 슬로프, 리프트, 하부 승강장 대기, 도보 시간을 합산합니다. 모든 구간이 계획된 뒤에만 시간을 제공합니다. 원하면 예정 총 시간을 직접 입력하세요.',
|
||||
'tours.ski.classifyTitle': '스키 트랙 분류',
|
||||
'tours.ski.saved': '분류를 저장했습니다',
|
||||
'tours.ski.saveFailed': '분류를 저장하지 못했습니다.',
|
||||
|
||||
@@ -671,6 +671,9 @@ const admin: TranslationStrings = {
|
||||
'admin.toursCycleMap.title': 'Fietskaart (CyclOSM)',
|
||||
'admin.toursCycleMap.subtitle':
|
||||
'Biedt CyclOSM aan als basiskaart voor fietstochten in de tourplanner. OpenStreetMap France levert hem voor gematigd gebruik; een intensief gebruikte of commerciële installatie zet hem uit of maakt eerst afspraken met de beheerder',
|
||||
'admin.toursSkiRouting.title': 'Pisterouter',
|
||||
'admin.toursSkiRouting.subtitle':
|
||||
'Plant skitochten langs pistes, liften en loipes op de pistedata van de Places-server. Uitgeschakeld worden de etappes van een skitocht recht getekend',
|
||||
'admin.addons.noAddons': 'Geen add-ons beschikbaar',
|
||||
'admin.weather.title': 'Weergegevens',
|
||||
'admin.weather.badge': 'Sinds 24 maart 2026',
|
||||
|
||||
@@ -419,17 +419,90 @@ const tours: TranslationStrings = {
|
||||
'tours.ski.technique.unknown': 'Onbekend',
|
||||
'tours.ski.chooseDiscipline': 'Kies een discipline om het spoor als ski te importeren.',
|
||||
'tours.ski.chooseDisciplineToSave': 'Kies een discipline om de tocht op te slaan.',
|
||||
'tours.ski.drawnFreely':
|
||||
'Elk deel wordt recht tussen je punten getekend. Niets toetst het aan pistes, liften of paden.',
|
||||
'tours.ski.plannedHint':
|
||||
'Etappes volgen pistes, liften en loipes uit OpenStreetMap waar de pistedata reiken, en lopen elders recht. Niets hier zegt of ze open zijn.',
|
||||
'tours.ski.options.maxDifficulty': 'Zwaarste pistes',
|
||||
'tours.ski.options.priority': 'Voorkeur',
|
||||
'tours.ski.options.pace': 'Tempo',
|
||||
'tours.ski.options.useLifts': 'Liften gebruiken',
|
||||
'tours.ski.options.freeride': 'Freerideroutes toestaan',
|
||||
'tours.ski.options.liftDown': 'Zo nodig met de lift naar beneden',
|
||||
'tours.ski.options.groomedOnly': 'Alleen geprepareerde loipes',
|
||||
'tours.ski.options.litOnly': 'Alleen verlichte loipes',
|
||||
'tours.ski.options.uphillOnPistes': 'Langs de pisterand omhoog',
|
||||
'tours.ski.options.avoidLinks': 'Vermoede verbindingen mijden',
|
||||
'tours.ski.options.avoidLifts': 'Weglaten',
|
||||
'tours.ski.difficulty.novice': 'Beginner',
|
||||
'tours.ski.difficulty.easy': 'Makkelijk',
|
||||
'tours.ski.difficulty.intermediate': 'Gemiddeld',
|
||||
'tours.ski.difficulty.advanced': 'Moeilijk',
|
||||
'tours.ski.difficulty.expert': 'Expert', // same-as-en
|
||||
'tours.ski.difficulty.freeride': 'Freeride', // same-as-en
|
||||
'tours.ski.difficulty.extreme': 'Extreem',
|
||||
'tours.ski.lift.drag': 'Sleepliften',
|
||||
'tours.ski.lift.open_chair': 'Open stoeltjesliften',
|
||||
'tours.ski.lift.gondola': 'Gondels',
|
||||
'tours.ski.lift.cable_car': 'Kabelbanen',
|
||||
'tours.ski.lift.rail': 'Kabelspoorwegen en treinen',
|
||||
'tours.ski.priority.fastest': 'Snelst',
|
||||
'tours.ski.priority.most_skiing': 'Meeste skiën',
|
||||
'tours.ski.priority.easiest': 'Makkelijkst',
|
||||
'tours.ski.pace.relaxed': 'Rustig',
|
||||
'tours.ski.pace.normal': 'Normaal',
|
||||
'tours.ski.pace.sporty': 'Sportief',
|
||||
'tours.ski.kind.run': 'Piste',
|
||||
'tours.ski.kind.lift': 'Lift', // same-as-en
|
||||
'tours.ski.kind.rail': 'Kabelspoorweg',
|
||||
'tours.ski.kind.lift_down': 'Met de lift omlaag',
|
||||
'tours.ski.kind.nordic': 'Loipe',
|
||||
'tours.ski.kind.skitour': 'Toerskiroute',
|
||||
'tours.ski.kind.hike': 'Winterpad',
|
||||
'tours.ski.kind.connection': 'Verbindingspiste',
|
||||
'tours.ski.kind.area': 'Open helling',
|
||||
'tours.ski.kind.link': 'Niet-gekarteerde verbinding',
|
||||
'tours.ski.kind.crossing': 'Oversteek',
|
||||
'tours.ski.kind.walk': 'Te voet',
|
||||
'tours.ski.fail.not_on_piste': 'Dit punt ligt te ver van elke piste, loipe of lift die je kunt gebruiken.',
|
||||
'tours.ski.fail.no_area': 'Geen skigebied in de pistedata dekt dit punt.',
|
||||
'tours.ski.fail.loading': 'De pistedata voor dit gebied worden nog gedownload.',
|
||||
'tours.ski.fail.places_unavailable': 'De pistedata zijn nu niet bereikbaar.',
|
||||
'tours.ski.fail.disabled': 'Pisteroutering staat uit.',
|
||||
'tours.ski.fail.blocked_by_difficulty': 'Alleen pistes boven je limiet verbinden deze punten.',
|
||||
'tours.ski.fail.blocked_by_lift_avoid': 'Alleen een lift die je hebt weggelaten verbindt deze punten.',
|
||||
'tours.ski.fail.blocked_by_technique': 'Alleen loipes voor de andere techniek verbinden deze punten.',
|
||||
'tours.ski.fail.needs_ride_down': 'De terugweg vraagt een liftrit omlaag.',
|
||||
'tours.ski.fail.uphill_only': 'Alleen een weg omhoog verbindt deze punten, en er is geen lift.',
|
||||
'tours.ski.fail.disconnected': 'Niets in de pistedata verbindt deze punten.',
|
||||
'tours.ski.fail.cross_area': 'Deze punten liggen in twee skigebieden die niet verbonden zijn.',
|
||||
'tours.ski.fail.busy': 'De pisterouter is bezet. Probeer het zo opnieuw.',
|
||||
'tours.ski.fail.timeout': 'Het plannen van deze etappe duurde te lang.',
|
||||
'tours.ski.fail.offline': 'Je bent offline, dus deze etappe wordt voorlopig recht getekend.',
|
||||
'tours.ski.fix.raiseDifficulty': 'Zwaardere pistes toestaan',
|
||||
'tours.ski.fix.allowLift': 'Deze lift toestaan',
|
||||
'tours.ski.fix.liftDown': 'Met de lift omlaag',
|
||||
'tours.ski.fix.technique': 'Techniek wisselen',
|
||||
'tours.ski.fix.discipline': 'Discipline wisselen',
|
||||
'tours.ski.summerPath': 'Buiten de pistedata: over zomerpaden, die onder sneeuw kunnen liggen.',
|
||||
'tours.ski.summary.title': 'Pisteroute',
|
||||
'tours.ski.summary.ski': 'Skiën',
|
||||
'tours.ski.summary.lift': 'In liften',
|
||||
'tours.ski.summary.wait': 'Wachten',
|
||||
'tours.ski.summary.walk': 'Te voet',
|
||||
'tours.ski.summary.avoided': 'Vermeden',
|
||||
'tours.ski.summary.unplanned': 'Sommige etappes konden niet worden gepland, dus er is nog geen skitijd.',
|
||||
'tours.ski.summary.guessed':
|
||||
'Sommige stukken berusten op een gok uit de kaartdata, zoals een verbinding of richting die niet in kaart is gebracht.',
|
||||
'tours.ski.summary.stale': 'Gepland op een oudere kopie van de pistedata, omdat de huidige niet bereikbaar is.',
|
||||
'tours.ski.summary.source': 'Pistedata © OpenStreetMap-bijdragers',
|
||||
'tours.ski.chooseTechnique': 'Kies de techniek, of Onbekend.',
|
||||
'tours.ski.infoLabel': 'Wat TREK met skitochten en skisporen doet',
|
||||
'tours.ski.info':
|
||||
'Bij een geplande tocht is elk deel recht tussen de punten getekend, een geïmporteerd spoor blijft zoals opgenomen. Geen van beide wordt over pistes, liften of paden gerouteerd. Hoogtes komen uit het terrein of het bestand. TREK berekent geen skitijd, en niets hier, ook de hellingkleuren in 3D niet, zegt iets over moeilijkheid, sneeuw, lawines of of pistes en loipes open zijn.',
|
||||
'Een geplande tocht volgt pistes, liften en loipes uit OpenStreetMap waar de pistedata reiken, en loopt elders recht tussen zijn punten. Een geïmporteerde track blijft zoals hij is opgenomen. Hoogtes komen uit de pistedata, het terrein of het bestand. Niets hier, ook niet de hellingkleuren in 3D, zegt iets over sneeuw, lawines of of pistes, liften en loipes open zijn.',
|
||||
'tours.ski.time': 'Skitijd',
|
||||
'tours.ski.timeUnknown': 'Niet berekend',
|
||||
'tours.ski.timeInfoLabel': 'Waarom er geen skitijd is',
|
||||
'tours.ski.timeInfoLabel': 'Hoe de skitijd wordt berekend',
|
||||
'tours.ski.timeInfo':
|
||||
'TREK berekent geen skitijd: de delen zijn recht getekend, niet over pistes, liften of paden gepland. Vul zelf een geplande totale tijd in als je die wilt.',
|
||||
'De pisterouter telt de tijd op pistes, in liften, wachtend bij de dalstations en te voet bij elkaar, voor je discipline en tempo. Hij geeft pas een tijd als elke etappe gepland is. Vul zelf een geplande totaaltijd in als je die wilt.',
|
||||
'tours.ski.classifyTitle': 'Skispoor indelen',
|
||||
'tours.ski.saved': 'Indeling opgeslagen',
|
||||
'tours.ski.saveFailed': 'De indeling kon niet worden opgeslagen.',
|
||||
|
||||
@@ -691,6 +691,9 @@ const admin: TranslationStrings = {
|
||||
'admin.toursCycleMap.title': 'Mapa rowerowa (CyclOSM)',
|
||||
'admin.toursCycleMap.subtitle':
|
||||
'Udostępnia CyclOSM jako mapę bazową dla wycieczek rowerowych w planerze. OpenStreetMap France udostępnia ją do umiarkowanego użytku; instalacja intensywnie używana lub komercyjna wyłącza ją albo najpierw zawiera porozumienie z operatorem',
|
||||
'admin.toursSkiRouting.title': 'Router tras narciarskich',
|
||||
'admin.toursSkiRouting.subtitle':
|
||||
'Planuje wycieczki narciarskie trasami, wyciągami i szlakami na danych tras serwera Places. Po wyłączeniu odcinki wycieczki narciarskiej są rysowane prosto',
|
||||
'admin.addons.noAddons': 'Brak dostępnych dodatków',
|
||||
'admin.weather.title': 'Dane pogodowe',
|
||||
'admin.weather.badge': 'Od 24 marca 2026',
|
||||
|
||||
@@ -426,17 +426,91 @@ const tours: TranslationStrings = {
|
||||
'tours.ski.technique.unknown': 'Nieznana',
|
||||
'tours.ski.chooseDiscipline': 'Wybierz dyscyplinę, aby zaimportować ślad jako narty.',
|
||||
'tours.ski.chooseDisciplineToSave': 'Wybierz dyscyplinę, aby zapisać trasę.',
|
||||
'tours.ski.drawnFreely':
|
||||
'Każdy odcinek jest rysowany prosto między twoimi punktami. Nic nie sprawdza go względem tras, wyciągów ani szlaków.',
|
||||
'tours.ski.plannedHint':
|
||||
'Odcinki biegną trasami, wyciągami i szlakami z OpenStreetMap tam, gdzie sięgają dane tras, a gdzie indziej prosto. Nic tu nie mówi, czy są otwarte.',
|
||||
'tours.ski.options.maxDifficulty': 'Najtrudniejsze trasy',
|
||||
'tours.ski.options.priority': 'Preferuj',
|
||||
'tours.ski.options.pace': 'Tempo',
|
||||
'tours.ski.options.useLifts': 'Korzystaj z wyciągów',
|
||||
'tours.ski.options.freeride': 'Zezwól na trasy freeride',
|
||||
'tours.ski.options.liftDown': 'W razie potrzeby zjedź wyciągiem',
|
||||
'tours.ski.options.groomedOnly': 'Tylko przygotowane trasy',
|
||||
'tours.ski.options.litOnly': 'Tylko oświetlone trasy',
|
||||
'tours.ski.options.uphillOnPistes': 'Podchodź skrajem trasy',
|
||||
'tours.ski.options.avoidLinks': 'Unikaj domniemanych połączeń',
|
||||
'tours.ski.options.avoidLifts': 'Pomiń',
|
||||
'tours.ski.difficulty.novice': 'Początkujący',
|
||||
'tours.ski.difficulty.easy': 'Łatwa',
|
||||
'tours.ski.difficulty.intermediate': 'Średnia',
|
||||
'tours.ski.difficulty.advanced': 'Trudna',
|
||||
'tours.ski.difficulty.expert': 'Ekspercka',
|
||||
'tours.ski.difficulty.freeride': 'Freeride', // same-as-en
|
||||
'tours.ski.difficulty.extreme': 'Ekstremalna',
|
||||
'tours.ski.lift.drag': 'Wyciągi orczykowe',
|
||||
'tours.ski.lift.open_chair': 'Otwarte kolejki krzesełkowe',
|
||||
'tours.ski.lift.gondola': 'Gondole',
|
||||
'tours.ski.lift.cable_car': 'Koleje linowe',
|
||||
'tours.ski.lift.rail': 'Koleje szynowo-linowe i pociągi',
|
||||
'tours.ski.priority.fastest': 'Najszybciej',
|
||||
'tours.ski.priority.most_skiing': 'Najwięcej jazdy',
|
||||
'tours.ski.priority.easiest': 'Najłatwiej',
|
||||
'tours.ski.pace.relaxed': 'Spokojne',
|
||||
'tours.ski.pace.normal': 'Normalne',
|
||||
'tours.ski.pace.sporty': 'Sportowe',
|
||||
'tours.ski.kind.run': 'Trasa',
|
||||
'tours.ski.kind.lift': 'Wyciąg',
|
||||
'tours.ski.kind.rail': 'Kolej szynowo-linowa',
|
||||
'tours.ski.kind.lift_down': 'Zjazd wyciągiem',
|
||||
'tours.ski.kind.nordic': 'Trasa biegowa',
|
||||
'tours.ski.kind.skitour': 'Trasa skiturowa',
|
||||
'tours.ski.kind.hike': 'Ścieżka zimowa',
|
||||
'tours.ski.kind.connection': 'Trasa łącznikowa',
|
||||
'tours.ski.kind.area': 'Otwarty stok',
|
||||
'tours.ski.kind.link': 'Niezmapowane połączenie',
|
||||
'tours.ski.kind.crossing': 'Przejście',
|
||||
'tours.ski.kind.walk': 'Pieszo',
|
||||
'tours.ski.fail.not_on_piste':
|
||||
'Ten punkt leży za daleko od każdej trasy, szlaku lub wyciągu, z którego możesz korzystać.',
|
||||
'tours.ski.fail.no_area': 'Żaden ośrodek w danych tras nie obejmuje tego punktu.',
|
||||
'tours.ski.fail.loading': 'Dane tras dla tego obszaru wciąż się pobierają.',
|
||||
'tours.ski.fail.places_unavailable': 'Dane tras są teraz nieosiągalne.',
|
||||
'tours.ski.fail.disabled': 'Wyznaczanie tras narciarskich jest wyłączone.',
|
||||
'tours.ski.fail.blocked_by_difficulty': 'Te punkty łączą tylko trasy trudniejsze niż twój limit.',
|
||||
'tours.ski.fail.blocked_by_lift_avoid': 'Te punkty łączy tylko pominięty przez ciebie wyciąg.',
|
||||
'tours.ski.fail.blocked_by_technique': 'Te punkty łączą tylko trasy przygotowane dla drugiej techniki.',
|
||||
'tours.ski.fail.needs_ride_down': 'Powrót wymaga zjazdu wyciągiem.',
|
||||
'tours.ski.fail.uphill_only': 'Te punkty łączy tylko droga w górę, a nie ma wyciągu.',
|
||||
'tours.ski.fail.disconnected': 'Nic w danych tras nie łączy tych punktów.',
|
||||
'tours.ski.fail.cross_area': 'Te punkty leżą w dwóch niepołączonych ośrodkach.',
|
||||
'tours.ski.fail.busy': 'Router tras jest zajęty. Spróbuj ponownie za chwilę.',
|
||||
'tours.ski.fail.timeout': 'Planowanie tego odcinka trwało zbyt długo.',
|
||||
'tours.ski.fail.offline': 'Jesteś offline, więc ten odcinek jest na razie rysowany prosto.',
|
||||
'tours.ski.fix.raiseDifficulty': 'Zezwól na trudniejsze trasy',
|
||||
'tours.ski.fix.allowLift': 'Zezwól na ten wyciąg',
|
||||
'tours.ski.fix.liftDown': 'Zjedź wyciągiem',
|
||||
'tours.ski.fix.technique': 'Zmień technikę',
|
||||
'tours.ski.fix.discipline': 'Zmień dyscyplinę',
|
||||
'tours.ski.summerPath': 'Poza danymi tras: letnimi ścieżkami, które mogą leżeć pod śniegiem.',
|
||||
'tours.ski.summary.title': 'Trasa narciarska',
|
||||
'tours.ski.summary.ski': 'Jazda',
|
||||
'tours.ski.summary.lift': 'Na wyciągach',
|
||||
'tours.ski.summary.wait': 'Oczekiwanie',
|
||||
'tours.ski.summary.walk': 'Pieszo',
|
||||
'tours.ski.summary.avoided': 'Ominięto',
|
||||
'tours.ski.summary.unplanned': 'Niektórych odcinków nie udało się zaplanować, więc nie ma jeszcze czasu jazdy.',
|
||||
'tours.ski.summary.guessed':
|
||||
'Niektóre fragmenty opierają się na domysłach z danych mapy, np. niezmapowanym połączeniu lub kierunku.',
|
||||
'tours.ski.summary.stale': 'Zaplanowano na starszej kopii danych tras, bo aktualna jest nieosiągalna.',
|
||||
'tours.ski.summary.source': 'Dane tras © współtwórcy OpenStreetMap',
|
||||
'tours.ski.chooseTechnique': 'Wybierz technikę lub Nieznana.',
|
||||
'tours.ski.infoLabel': 'Co TREK robi z wycieczkami i śladami narciarskimi',
|
||||
'tours.ski.info':
|
||||
'Zaplanowana trasa ma każdy odcinek narysowany prosto między swoimi punktami, a zaimportowany ślad pozostaje taki, jak został nagrany. Żaden nie jest wyznaczany po trasach, wyciągach ani szlakach. Wysokości pochodzą z terenu lub z pliku. TREK nie wylicza czasu na nartach i nic tutaj, także kolory nachylenia w 3D, nie mówi nic o trudności, śniegu, lawinach ani o tym, czy trasy są otwarte.',
|
||||
'Zaplanowana trasa biegnie trasami zjazdowymi, wyciągami i szlakami z OpenStreetMap tam, gdzie sięgają dane tras, a gdzie indziej łączy punkty prostą linią. Zaimportowany ślad zostaje taki, jak go nagrano. Wysokości pochodzą z danych tras, terenu lub pliku. Nic tu, także kolory nachylenia w 3D, nie mówi niczego o śniegu, lawinach ani o tym, czy trasy, wyciągi i szlaki są otwarte.',
|
||||
'tours.ski.time': 'Czas na nartach',
|
||||
'tours.ski.timeUnknown': 'Nie wyliczono',
|
||||
'tours.ski.timeInfoLabel': 'Dlaczego nie ma czasu na nartach',
|
||||
'tours.ski.timeInfoLabel': 'Jak liczony jest czas jazdy na nartach',
|
||||
'tours.ski.timeInfo':
|
||||
'TREK nie wylicza czasu na nartach: odcinki są narysowane prosto, a nie zaplanowane po trasach, wyciągach czy szlakach. Jeśli chcesz, wpisz sam planowany czas łączny.',
|
||||
'Router tras sumuje czas na trasach, na wyciągach, w oczekiwaniu przy dolnych stacjach i pieszo, dla twojej dyscypliny i tempa. Podaje czas dopiero, gdy każdy odcinek jest zaplanowany. Wpisz sam planowany czas łączny, jeśli go chcesz.',
|
||||
'tours.ski.classifyTitle': 'Sklasyfikuj ślad narciarski',
|
||||
'tours.ski.saved': 'Klasyfikację zapisano',
|
||||
'tours.ski.saveFailed': 'Nie udało się zapisać klasyfikacji.',
|
||||
|
||||
@@ -696,6 +696,9 @@ const admin: TranslationStrings = {
|
||||
'admin.toursCycleMap.title': 'Велокарта (CyclOSM)',
|
||||
'admin.toursCycleMap.subtitle':
|
||||
'Предлагает CyclOSM как базовую карту для велопоездок в планировщике. OpenStreetMap France предоставляет её для умеренного использования; нагруженная или коммерческая установка отключает её или сначала договаривается с оператором',
|
||||
'admin.toursSkiRouting.title': 'Маршрутизатор трасс',
|
||||
'admin.toursSkiRouting.subtitle':
|
||||
'Планирует лыжные маршруты по трассам, подъёмникам и лыжням на данных трасс сервера Places. Если выключен, участки лыжного маршрута рисуются прямыми',
|
||||
'admin.addons.noAddons': 'Нет доступных дополнений',
|
||||
'admin.weather.title': 'Данные о погоде',
|
||||
'admin.weather.badge': 'С 24 марта 2026',
|
||||
|
||||
@@ -430,17 +430,90 @@ const tours: TranslationStrings = {
|
||||
'tours.ski.technique.unknown': 'Неизвестно',
|
||||
'tours.ski.chooseDiscipline': 'Выберите дисциплину, чтобы импортировать трек как лыжи.',
|
||||
'tours.ski.chooseDisciplineToSave': 'Выберите дисциплину, чтобы сохранить маршрут.',
|
||||
'tours.ski.drawnFreely':
|
||||
'Каждый участок рисуется прямой между вашими точками. Ничто не сверяет его с трассами, подъёмниками или тропами.',
|
||||
'tours.ski.plannedHint':
|
||||
'Участки идут по трассам, подъёмникам и лыжням из OpenStreetMap там, где есть данные трасс, а в остальных местах прямо. Ничто здесь не говорит, открыты ли они.',
|
||||
'tours.ski.options.maxDifficulty': 'Самые сложные трассы',
|
||||
'tours.ski.options.priority': 'Предпочитать',
|
||||
'tours.ski.options.pace': 'Темп',
|
||||
'tours.ski.options.useLifts': 'Ездить на подъёмниках',
|
||||
'tours.ski.options.freeride': 'Разрешить фрирайд-маршруты',
|
||||
'tours.ski.options.liftDown': 'При необходимости спускаться на подъёмнике',
|
||||
'tours.ski.options.groomedOnly': 'Только подготовленные лыжни',
|
||||
'tours.ski.options.litOnly': 'Только освещённые лыжни',
|
||||
'tours.ski.options.uphillOnPistes': 'Подниматься по краю трассы',
|
||||
'tours.ski.options.avoidLinks': 'Избегать предполагаемых связок',
|
||||
'tours.ski.options.avoidLifts': 'Исключить',
|
||||
'tours.ski.difficulty.novice': 'Новичок',
|
||||
'tours.ski.difficulty.easy': 'Лёгкая',
|
||||
'tours.ski.difficulty.intermediate': 'Средняя',
|
||||
'tours.ski.difficulty.advanced': 'Сложная',
|
||||
'tours.ski.difficulty.expert': 'Экспертная',
|
||||
'tours.ski.difficulty.freeride': 'Фрирайд',
|
||||
'tours.ski.difficulty.extreme': 'Экстремальная',
|
||||
'tours.ski.lift.drag': 'Бугельные подъёмники',
|
||||
'tours.ski.lift.open_chair': 'Открытые кресельные',
|
||||
'tours.ski.lift.gondola': 'Гондолы',
|
||||
'tours.ski.lift.cable_car': 'Канатные дороги',
|
||||
'tours.ski.lift.rail': 'Фуникулёры и поезда',
|
||||
'tours.ski.priority.fastest': 'Быстрее всего',
|
||||
'tours.ski.priority.most_skiing': 'Больше катания',
|
||||
'tours.ski.priority.easiest': 'Проще всего',
|
||||
'tours.ski.pace.relaxed': 'Спокойный',
|
||||
'tours.ski.pace.normal': 'Обычный',
|
||||
'tours.ski.pace.sporty': 'Спортивный',
|
||||
'tours.ski.kind.run': 'Трасса',
|
||||
'tours.ski.kind.lift': 'Подъёмник',
|
||||
'tours.ski.kind.rail': 'Фуникулёр',
|
||||
'tours.ski.kind.lift_down': 'Спуск на подъёмнике',
|
||||
'tours.ski.kind.nordic': 'Лыжня',
|
||||
'tours.ski.kind.skitour': 'Маршрут ски-тура',
|
||||
'tours.ski.kind.hike': 'Зимняя тропа',
|
||||
'tours.ski.kind.connection': 'Соединительная трасса',
|
||||
'tours.ski.kind.area': 'Открытый склон',
|
||||
'tours.ski.kind.link': 'Неотмеченная связка',
|
||||
'tours.ski.kind.crossing': 'Переход',
|
||||
'tours.ski.kind.walk': 'Пешком',
|
||||
'tours.ski.fail.not_on_piste': 'Эта точка слишком далеко от любой доступной вам трассы, лыжни или подъёмника.',
|
||||
'tours.ski.fail.no_area': 'Ни один курорт в данных трасс не охватывает эту точку.',
|
||||
'tours.ski.fail.loading': 'Данные трасс для этого района ещё загружаются.',
|
||||
'tours.ski.fail.places_unavailable': 'Данные трасс сейчас недоступны.',
|
||||
'tours.ski.fail.disabled': 'Маршрутизация по трассам выключена.',
|
||||
'tours.ski.fail.blocked_by_difficulty': 'Эти точки соединяют только трассы сложнее вашего предела.',
|
||||
'tours.ski.fail.blocked_by_lift_avoid': 'Эти точки соединяет только исключённый вами подъёмник.',
|
||||
'tours.ski.fail.blocked_by_technique': 'Эти точки соединяют только лыжни для другой техники.',
|
||||
'tours.ski.fail.needs_ride_down': 'Обратный путь требует спуска на подъёмнике.',
|
||||
'tours.ski.fail.uphill_only': 'Эти точки соединяет только путь вверх, а подъёмника нет.',
|
||||
'tours.ski.fail.disconnected': 'Ничто в данных трасс не соединяет эти точки.',
|
||||
'tours.ski.fail.cross_area': 'Эти точки лежат на двух несвязанных курортах.',
|
||||
'tours.ski.fail.busy': 'Маршрутизатор трасс занят. Попробуйте чуть позже.',
|
||||
'tours.ski.fail.timeout': 'Планирование этого участка заняло слишком много времени.',
|
||||
'tours.ski.fail.offline': 'Вы офлайн, поэтому этот участок пока нарисован прямым.',
|
||||
'tours.ski.fix.raiseDifficulty': 'Разрешить более сложные трассы',
|
||||
'tours.ski.fix.allowLift': 'Разрешить этот подъёмник',
|
||||
'tours.ski.fix.liftDown': 'Спуститься на подъёмнике',
|
||||
'tours.ski.fix.technique': 'Сменить технику',
|
||||
'tours.ski.fix.discipline': 'Сменить дисциплину',
|
||||
'tours.ski.summerPath': 'Вне данных трасс: по летним тропам, которые могут быть под снегом.',
|
||||
'tours.ski.summary.title': 'Маршрут по трассам',
|
||||
'tours.ski.summary.ski': 'Катание',
|
||||
'tours.ski.summary.lift': 'На подъёмниках',
|
||||
'tours.ski.summary.wait': 'Ожидание',
|
||||
'tours.ski.summary.walk': 'Пешком',
|
||||
'tours.ski.summary.avoided': 'Обойдено',
|
||||
'tours.ski.summary.unplanned': 'Некоторые участки не удалось спланировать, поэтому времени катания пока нет.',
|
||||
'tours.ski.summary.guessed':
|
||||
'Некоторые отрезки опираются на догадку по картографическим данным, например на неотмеченную связку или направление.',
|
||||
'tours.ski.summary.stale': 'Спланировано по более старой копии данных трасс, так как текущая недоступна.',
|
||||
'tours.ski.summary.source': 'Данные трасс © участники OpenStreetMap',
|
||||
'tours.ski.chooseTechnique': 'Выберите технику или «Неизвестно».',
|
||||
'tours.ski.infoLabel': 'Что TREK делает с лыжными маршрутами и треками',
|
||||
'tours.ski.info':
|
||||
'В запланированном маршруте каждый участок нарисован прямой между его точками, импортированный трек остаётся таким, как записан. Ни один из них не прокладывается по трассам, подъёмникам или тропам. Высоты берутся из рельефа или из файла. TREK не рассчитывает время на лыжах, и ничто здесь, включая цвета уклона в 3D, не говорит о сложности, снеге, лавинах или о том, открыты ли трассы и лыжни.',
|
||||
'Запланированный маршрут идёт по трассам, подъёмникам и лыжням из OpenStreetMap там, где есть данные трасс, а в остальных местах соединяет точки прямо. Импортированный трек остаётся таким, каким был записан. Высоты берутся из данных трасс, рельефа или файла. Ничто здесь, включая цвета уклона в 3D, ничего не говорит о снеге, лавинах или о том, открыты ли трассы, подъёмники и лыжни.',
|
||||
'tours.ski.time': 'Время на лыжах',
|
||||
'tours.ski.timeUnknown': 'Не рассчитано',
|
||||
'tours.ski.timeInfoLabel': 'Почему нет времени на лыжах',
|
||||
'tours.ski.timeInfoLabel': 'Как считается время катания',
|
||||
'tours.ski.timeInfo':
|
||||
'TREK не рассчитывает время на лыжах: участки нарисованы прямыми, а не проложены по трассам, подъёмникам или тропам. При желании укажите общее время сами.',
|
||||
'Маршрутизатор трасс складывает время на трассах, на подъёмниках, в ожидании у нижних станций и пешком, с учётом дисциплины и темпа. Время он даёт, только когда спланирован каждый участок. Введите плановое общее время сами, если оно нужно.',
|
||||
'tours.ski.classifyTitle': 'Классифицировать лыжный трек',
|
||||
'tours.ski.saved': 'Классификация сохранена',
|
||||
'tours.ski.saveFailed': 'Не удалось сохранить классификацию.',
|
||||
|
||||
@@ -743,6 +743,9 @@ const admin: TranslationStrings = {
|
||||
'admin.toursCycleMap.title': 'Cyklomapa (CyclOSM)',
|
||||
'admin.toursCycleMap.subtitle':
|
||||
'Ponúka CyclOSM ako podkladovú mapu pre cyklotúry v plánovači. OpenStreetMap France ju poskytuje na primerané používanie; silne využívaná alebo komerčná inštalácia ju vypne, alebo sa najprv dohodne s prevádzkovateľom',
|
||||
'admin.toursSkiRouting.title': 'Router zjazdoviek',
|
||||
'admin.toursSkiRouting.subtitle':
|
||||
'Plánuje lyžiarske túry po zjazdovkách, lanovkách a stopách na dátach zjazdoviek servera Places. Vypnutý kreslí úseky lyžiarskej túry rovno',
|
||||
'admin.addons.noAddons': 'Žiadne doplnky nie sú dostupné',
|
||||
'admin.weather.title': 'Dáta o počasí',
|
||||
'admin.weather.badge': 'Od 24. marca 2026',
|
||||
|
||||
@@ -411,17 +411,91 @@ const tours: TranslationStrings = {
|
||||
'tours.ski.technique.unknown': 'Neznáma',
|
||||
'tours.ski.chooseDiscipline': 'Vyberte disciplínu na import trasy ako lyže.',
|
||||
'tours.ski.chooseDisciplineToSave': 'Vyberte disciplínu, aby ste mohli túru uložiť.',
|
||||
'tours.ski.drawnFreely':
|
||||
'Každý úsek sa kreslí rovno medzi vašimi bodmi. Nič ho neporovnáva so zjazdovkami, vlekmi ani stopami.',
|
||||
'tours.ski.plannedHint':
|
||||
'Úseky vedú po zjazdovkách, lanovkách a stopách z OpenStreetMap, kam siahajú dáta zjazdoviek, inde rovno. Nič tu nehovorí, či sú otvorené.',
|
||||
'tours.ski.options.maxDifficulty': 'Najťažšie zjazdovky',
|
||||
'tours.ski.options.priority': 'Uprednostniť',
|
||||
'tours.ski.options.pace': 'Tempo',
|
||||
'tours.ski.options.useLifts': 'Jazdiť lanovkami',
|
||||
'tours.ski.options.freeride': 'Povoliť freeride trasy',
|
||||
'tours.ski.options.liftDown': 'Podľa potreby zísť lanovkou',
|
||||
'tours.ski.options.groomedOnly': 'Len upravené stopy',
|
||||
'tours.ski.options.litOnly': 'Len osvetlené stopy',
|
||||
'tours.ski.options.uphillOnPistes': 'Stúpať po okraji zjazdovky',
|
||||
'tours.ski.options.avoidLinks': 'Vyhnúť sa domnelým spojkám',
|
||||
'tours.ski.options.avoidLifts': 'Vynechať',
|
||||
'tours.ski.difficulty.novice': 'Začiatočník',
|
||||
'tours.ski.difficulty.easy': 'Ľahká',
|
||||
'tours.ski.difficulty.intermediate': 'Stredná',
|
||||
'tours.ski.difficulty.advanced': 'Ťažká',
|
||||
'tours.ski.difficulty.expert': 'Expertná',
|
||||
'tours.ski.difficulty.freeride': 'Freeride', // same-as-en
|
||||
'tours.ski.difficulty.extreme': 'Extrémna',
|
||||
'tours.ski.lift.drag': 'Vleky',
|
||||
'tours.ski.lift.open_chair': 'Otvorené sedačky',
|
||||
'tours.ski.lift.gondola': 'Kabínkové lanovky',
|
||||
'tours.ski.lift.cable_car': 'Kyvadlové lanovky',
|
||||
'tours.ski.lift.rail': 'Pozemné lanovky a vlaky',
|
||||
'tours.ski.priority.fastest': 'Najrýchlejšia',
|
||||
'tours.ski.priority.most_skiing': 'Najviac lyžovania',
|
||||
'tours.ski.priority.easiest': 'Najľahšia',
|
||||
'tours.ski.pace.relaxed': 'Pohodové',
|
||||
'tours.ski.pace.normal': 'Normálne',
|
||||
'tours.ski.pace.sporty': 'Športové',
|
||||
'tours.ski.kind.run': 'Zjazdovka',
|
||||
'tours.ski.kind.lift': 'Lanovka',
|
||||
'tours.ski.kind.rail': 'Pozemná lanovka',
|
||||
'tours.ski.kind.lift_down': 'Zjazd lanovkou',
|
||||
'tours.ski.kind.nordic': 'Bežecká stopa',
|
||||
'tours.ski.kind.skitour': 'Skialpová trasa',
|
||||
'tours.ski.kind.hike': 'Zimný chodník',
|
||||
'tours.ski.kind.connection': 'Spojovacia zjazdovka',
|
||||
'tours.ski.kind.area': 'Otvorený svah',
|
||||
'tours.ski.kind.link': 'Nezmapovaná spojka',
|
||||
'tours.ski.kind.crossing': 'Prejazd',
|
||||
'tours.ski.kind.walk': 'Pešo',
|
||||
'tours.ski.fail.not_on_piste':
|
||||
'Tento bod je príliš ďaleko od akejkoľvek zjazdovky, stopy alebo lanovky, ktorú môžeš použiť.',
|
||||
'tours.ski.fail.no_area': 'Tento bod nepokrýva žiadne lyžiarske stredisko v dátach zjazdoviek.',
|
||||
'tours.ski.fail.loading': 'Dáta zjazdoviek pre túto oblasť sa ešte sťahujú.',
|
||||
'tours.ski.fail.places_unavailable': 'Dáta zjazdoviek teraz nie sú dostupné.',
|
||||
'tours.ski.fail.disabled': 'Plánovanie po zjazdovkách je vypnuté.',
|
||||
'tours.ski.fail.blocked_by_difficulty': 'Tieto body spájajú len zjazdovky nad tvojím limitom.',
|
||||
'tours.ski.fail.blocked_by_lift_avoid': 'Tieto body spája len lanovka, ktorú si vynechal.',
|
||||
'tours.ski.fail.blocked_by_technique': 'Tieto body spájajú len stopy upravené pre druhú techniku.',
|
||||
'tours.ski.fail.needs_ride_down': 'Cesta späť vyžaduje zjazd lanovkou.',
|
||||
'tours.ski.fail.uphill_only': 'Tieto body spája len cesta nahor a nie je tu lanovka.',
|
||||
'tours.ski.fail.disconnected': 'Nič v dátach zjazdoviek tieto body nespája.',
|
||||
'tours.ski.fail.cross_area': 'Tieto body ležia v dvoch nespojených strediskách.',
|
||||
'tours.ski.fail.busy': 'Router zjazdoviek je vyťažený. Skús to o chvíľu znova.',
|
||||
'tours.ski.fail.timeout': 'Plánovanie tohto úseku trvalo príliš dlho.',
|
||||
'tours.ski.fail.offline': 'Si offline, preto sa tento úsek zatiaľ kreslí rovno.',
|
||||
'tours.ski.fix.raiseDifficulty': 'Povoliť ťažšie zjazdovky',
|
||||
'tours.ski.fix.allowLift': 'Povoliť túto lanovku',
|
||||
'tours.ski.fix.liftDown': 'Zísť lanovkou',
|
||||
'tours.ski.fix.technique': 'Zmeniť techniku',
|
||||
'tours.ski.fix.discipline': 'Zmeniť disciplínu',
|
||||
'tours.ski.summerPath': 'Mimo dát zjazdoviek: po letných chodníkoch, ktoré môžu byť pod snehom.',
|
||||
'tours.ski.summary.title': 'Trasa po zjazdovkách',
|
||||
'tours.ski.summary.ski': 'Lyžovanie',
|
||||
'tours.ski.summary.lift': 'Na lanovkách',
|
||||
'tours.ski.summary.wait': 'Čakanie',
|
||||
'tours.ski.summary.walk': 'Pešo',
|
||||
'tours.ski.summary.avoided': 'Vynechané',
|
||||
'tours.ski.summary.unplanned': 'Niektoré úseky sa nepodarilo naplánovať, preto zatiaľ nie je čas lyžovania.',
|
||||
'tours.ski.summary.guessed':
|
||||
'Niektoré časti stoja na odhade z mapových dát, napríklad na nezmapovanej spojke alebo smere.',
|
||||
'tours.ski.summary.stale': 'Naplánované na staršej kópii dát zjazdoviek, lebo aktuálna nie je dostupná.',
|
||||
'tours.ski.summary.source': 'Dáta zjazdoviek © prispievatelia OpenStreetMap',
|
||||
'tours.ski.chooseTechnique': 'Vyberte techniku, alebo Neznáma.',
|
||||
'tours.ski.infoLabel': 'Čo TREK robí s lyžiarskymi túrami a trasami',
|
||||
'tours.ski.info':
|
||||
'Naplánovaná túra má každý úsek nakreslený rovno medzi svojimi bodmi, importovaná trasa zostáva tak, ako bola zaznamenaná. Ani jedna nevedie po zjazdovkách, vlekoch ani stopách. Výšky pochádzajú z terénu alebo zo súboru. TREK nepočíta žiadny čas na lyžiach a nič tu, ani farby sklonu v 3D, nehovorí nič o náročnosti, snehu, lavínach ani o tom, či sú zjazdovky a stopy otvorené.',
|
||||
'Naplánovaná túra vedie po zjazdovkách, lanovkách a stopách z OpenStreetMap, kam siahajú dáta zjazdoviek, inde spája svoje body rovno. Importovaná stopa zostáva tak, ako bola nahraná. Výšky pochádzajú z dát zjazdoviek, terénu alebo súboru. Nič tu, ani farby sklonu v 3D, nehovorí nič o snehu, lavínach ani o tom, či sú zjazdovky, lanovky a stopy otvorené.',
|
||||
'tours.ski.time': 'Čas na lyžiach',
|
||||
'tours.ski.timeUnknown': 'Nepočíta sa',
|
||||
'tours.ski.timeInfoLabel': 'Prečo tu nie je čas na lyžiach',
|
||||
'tours.ski.timeInfoLabel': 'Ako sa počíta čas lyžovania',
|
||||
'tours.ski.timeInfo':
|
||||
'TREK čas na lyžiach nepočíta: úseky sú nakreslené rovno, nie naplánované po zjazdovkách, vlekoch alebo stopách. Ak chcete, zadajte plánovaný celkový čas sami.',
|
||||
'Router zjazdoviek sčíta čas na zjazdovkách, na lanovkách, čakaním pri dolných staniciach a pešo, podľa disciplíny a tempa. Čas uvedie až vtedy, keď je naplánovaný každý úsek. Zadaj si plánovaný celkový čas sám, ak ho chceš.',
|
||||
'tours.ski.classifyTitle': 'Zaradiť lyžiarsku trasu',
|
||||
'tours.ski.saved': 'Zaradenie uložené',
|
||||
'tours.ski.saveFailed': 'Zaradenie sa nepodarilo uložiť.',
|
||||
|
||||
@@ -738,6 +738,9 @@ const admin: TranslationStrings = {
|
||||
'admin.toursCycleMap.title': 'Cykelkarta (CyclOSM)',
|
||||
'admin.toursCycleMap.subtitle':
|
||||
'Erbjuder CyclOSM som baskarta för cykelturer i turplaneraren. OpenStreetMap France tillhandahåller den för måttlig användning; en tungt använd eller kommersiell installation stänger av den eller kommer först överens med operatören',
|
||||
'admin.toursSkiRouting.title': 'Pistrouter',
|
||||
'admin.toursSkiRouting.subtitle':
|
||||
'Planerar skidturer längs backar, liftar och spår på pistdatan från Places-servern. Avstängd ritas en skidturs etapper raka',
|
||||
'admin.addons.noAddons': 'Inga tillägg tillgängliga',
|
||||
'admin.weather.title': 'Väderdata',
|
||||
'admin.weather.badge': 'Sedan den 24 mars 2026',
|
||||
|
||||
@@ -409,17 +409,90 @@ const tours: TranslationStrings = {
|
||||
'tours.ski.technique.unknown': 'Okänd',
|
||||
'tours.ski.chooseDiscipline': 'Välj en gren för att importera spåret som skidor.',
|
||||
'tours.ski.chooseDisciplineToSave': 'Välj en gren för att spara turen.',
|
||||
'tours.ski.drawnFreely':
|
||||
'Varje etapp ritas rakt mellan dina punkter. Inget stämmer av den mot backar, liftar eller leder.',
|
||||
'tours.ski.plannedHint':
|
||||
'Etapperna följer pister, liftar och spår från OpenStreetMap där pistdatan når, och går rakt annars. Inget här säger om de är öppna.',
|
||||
'tours.ski.options.maxDifficulty': 'Svåraste backar',
|
||||
'tours.ski.options.priority': 'Föredra',
|
||||
'tours.ski.options.pace': 'Takt',
|
||||
'tours.ski.options.useLifts': 'Åk liftar',
|
||||
'tours.ski.options.freeride': 'Tillåt friåkningsvägar',
|
||||
'tours.ski.options.liftDown': 'Åk lift ner vid behov',
|
||||
'tours.ski.options.groomedOnly': 'Bara preparerade spår',
|
||||
'tours.ski.options.litOnly': 'Bara upplysta spår',
|
||||
'tours.ski.options.uphillOnPistes': 'Gå upp längs pistkanten',
|
||||
'tours.ski.options.avoidLinks': 'Undvik gissade länkar',
|
||||
'tours.ski.options.avoidLifts': 'Uteslut',
|
||||
'tours.ski.difficulty.novice': 'Nybörjare',
|
||||
'tours.ski.difficulty.easy': 'Lätt',
|
||||
'tours.ski.difficulty.intermediate': 'Medel',
|
||||
'tours.ski.difficulty.advanced': 'Svår',
|
||||
'tours.ski.difficulty.expert': 'Expert', // same-as-en
|
||||
'tours.ski.difficulty.freeride': 'Friåkning',
|
||||
'tours.ski.difficulty.extreme': 'Extrem',
|
||||
'tours.ski.lift.drag': 'Släpliftar',
|
||||
'tours.ski.lift.open_chair': 'Öppna stolliftar',
|
||||
'tours.ski.lift.gondola': 'Gondoler',
|
||||
'tours.ski.lift.cable_car': 'Linbanor',
|
||||
'tours.ski.lift.rail': 'Bergbanor och tåg',
|
||||
'tours.ski.priority.fastest': 'Snabbast',
|
||||
'tours.ski.priority.most_skiing': 'Mest åkning',
|
||||
'tours.ski.priority.easiest': 'Lättast',
|
||||
'tours.ski.pace.relaxed': 'Lugnt',
|
||||
'tours.ski.pace.normal': 'Normalt',
|
||||
'tours.ski.pace.sporty': 'Sportigt',
|
||||
'tours.ski.kind.run': 'Backe',
|
||||
'tours.ski.kind.lift': 'Lift', // same-as-en
|
||||
'tours.ski.kind.rail': 'Bergbana',
|
||||
'tours.ski.kind.lift_down': 'Lift ner',
|
||||
'tours.ski.kind.nordic': 'Längdspår',
|
||||
'tours.ski.kind.skitour': 'Topptursled',
|
||||
'tours.ski.kind.hike': 'Vinterled',
|
||||
'tours.ski.kind.connection': 'Förbindelsebacke',
|
||||
'tours.ski.kind.area': 'Öppen sluttning',
|
||||
'tours.ski.kind.link': 'Ej kartlagd länk',
|
||||
'tours.ski.kind.crossing': 'Korsning',
|
||||
'tours.ski.kind.walk': 'Till fots',
|
||||
'tours.ski.fail.not_on_piste': 'Den här punkten ligger för långt från alla backar, spår och liftar du kan använda.',
|
||||
'tours.ski.fail.no_area': 'Inget skidområde i pistdatan täcker den här punkten.',
|
||||
'tours.ski.fail.loading': 'Pistdatan för det här området laddas fortfarande ner.',
|
||||
'tours.ski.fail.places_unavailable': 'Pistdatan går inte att nå just nu.',
|
||||
'tours.ski.fail.disabled': 'Pistroutning är avstängd.',
|
||||
'tours.ski.fail.blocked_by_difficulty': 'Bara backar över din gräns förbinder de här punkterna.',
|
||||
'tours.ski.fail.blocked_by_lift_avoid': 'Bara en lift du uteslöt förbinder de här punkterna.',
|
||||
'tours.ski.fail.blocked_by_technique': 'Bara spår preparerade för den andra tekniken förbinder de här punkterna.',
|
||||
'tours.ski.fail.needs_ride_down': 'Vägen tillbaka kräver en liftresa ner.',
|
||||
'tours.ski.fail.uphill_only': 'Bara en väg uppåt förbinder punkterna, och det finns ingen lift att ta.',
|
||||
'tours.ski.fail.disconnected': 'Inget i pistdatan förbinder de här punkterna.',
|
||||
'tours.ski.fail.cross_area': 'Punkterna ligger i två skidområden som inte hänger ihop.',
|
||||
'tours.ski.fail.busy': 'Pistroutern är upptagen. Försök igen om en stund.',
|
||||
'tours.ski.fail.timeout': 'Det tog för lång tid att planera den här etappen.',
|
||||
'tours.ski.fail.offline': 'Du är offline, så den här etappen ritas rak tills vidare.',
|
||||
'tours.ski.fix.raiseDifficulty': 'Tillåt svårare backar',
|
||||
'tours.ski.fix.allowLift': 'Tillåt den här liften',
|
||||
'tours.ski.fix.liftDown': 'Åk liften ner',
|
||||
'tours.ski.fix.technique': 'Byt teknik',
|
||||
'tours.ski.fix.discipline': 'Byt disciplin',
|
||||
'tours.ski.summerPath': 'Utanför pistdatan: via sommarleder, som kan ligga under snö.',
|
||||
'tours.ski.summary.title': 'Pistrutt',
|
||||
'tours.ski.summary.ski': 'Åkning',
|
||||
'tours.ski.summary.lift': 'I liftar',
|
||||
'tours.ski.summary.wait': 'Väntan',
|
||||
'tours.ski.summary.walk': 'Till fots',
|
||||
'tours.ski.summary.avoided': 'Undvek',
|
||||
'tours.ski.summary.unplanned': 'Vissa etapper kunde inte planeras, så det finns ingen skidtid än.',
|
||||
'tours.ski.summary.guessed':
|
||||
'Vissa delar bygger på en gissning från kartdatan, som en länk eller riktning som inte är kartlagd.',
|
||||
'tours.ski.summary.stale': 'Planerad på en äldre kopia av pistdatan, eftersom den aktuella inte går att nå.',
|
||||
'tours.ski.summary.source': 'Pistdata © OpenStreetMap-bidragsgivare',
|
||||
'tours.ski.chooseTechnique': 'Välj teknik, eller Okänd.',
|
||||
'tours.ski.infoLabel': 'Vad TREK gör med skidturer och skidspår',
|
||||
'tours.ski.info':
|
||||
'En planerad tur har varje etapp ritad rakt mellan sina punkter, ett importerat spår behålls som det spelades in. Inget av dem ruttplaneras längs backar, liftar eller leder. Höjderna kommer från terrängen eller filen. TREK räknar inte ut någon skidtid, och inget här, inte heller lutningsfärgerna i 3D, säger något om svårighet, snö, laviner eller om backar och spår är öppna.',
|
||||
'En planerad tur följer pister, liftar och spår från OpenStreetMap där pistdatan når, och går annars rakt mellan sina punkter. Ett importerat spår behålls som det spelades in. Höjder kommer från pistdatan, terrängen eller filen. Inget här, inte heller lutningsfärgerna i 3D, säger något om snö, laviner eller om pister, liftar och spår är öppna.',
|
||||
'tours.ski.time': 'Skidtid',
|
||||
'tours.ski.timeUnknown': 'Inte uträknad',
|
||||
'tours.ski.timeInfoLabel': 'Varför det inte finns någon skidtid',
|
||||
'tours.ski.timeInfoLabel': 'Hur skidtiden räknas fram',
|
||||
'tours.ski.timeInfo':
|
||||
'TREK räknar inte ut någon skidtid: etapperna ritas raka, inte planerade längs backar, liftar eller leder. Ange själv en planerad total tid om du vill.',
|
||||
'Pistroutern lägger ihop tiden i backar, i liftar, i väntan vid dalstationerna och till fots, för din disciplin och takt. Den ger en tid först när varje etapp är planerad. Ange en planerad totaltid själv om du vill ha en.',
|
||||
'tours.ski.classifyTitle': 'Klassa skidspår',
|
||||
'tours.ski.saved': 'Klassning sparad',
|
||||
'tours.ski.saveFailed': 'Klassningen kunde inte sparas.',
|
||||
|
||||
@@ -639,6 +639,9 @@ const admin: TranslationStrings = {
|
||||
'admin.toursCycleMap.title': 'แผนที่จักรยาน (CyclOSM)',
|
||||
'admin.toursCycleMap.subtitle':
|
||||
'ให้ CyclOSM เป็นแผนที่พื้นฐานสำหรับทริปจักรยานในตัววางแผนทัวร์ OpenStreetMap France ให้บริการสำหรับการใช้งานปานกลาง การติดตั้งที่ใช้งานหนักหรือเชิงพาณิชย์ควรปิดไว้หรือทำข้อตกลงกับผู้ให้บริการก่อน',
|
||||
'admin.toursSkiRouting.title': 'ตัววางเส้นทางลานสกี',
|
||||
'admin.toursSkiRouting.subtitle':
|
||||
'วางแผนทัวร์สกีตามลานสกี ลิฟต์ และเส้นทางด้วยข้อมูลลานสกีจากเซิร์ฟเวอร์ Places เมื่อปิด ช่วงทางของทัวร์สกีจะวาดเป็นเส้นตรง',
|
||||
'admin.addons.noAddons': 'ไม่มีส่วนเสริมให้ใช้งาน',
|
||||
'admin.weather.title': 'ข้อมูลสภาพอากาศ',
|
||||
'admin.weather.badge': 'ตั้งแต่วันที่ 24 มีนาคม 2569',
|
||||
|
||||
@@ -391,16 +391,89 @@ const tours: TranslationStrings = {
|
||||
'tours.ski.technique.unknown': 'ไม่ทราบ',
|
||||
'tours.ski.chooseDiscipline': 'เลือกประเภทเพื่อนำเข้าเส้นทางเป็นสกี',
|
||||
'tours.ski.chooseDisciplineToSave': 'เลือกประเภทเพื่อบันทึกทริป',
|
||||
'tours.ski.drawnFreely': 'แต่ละช่วงจะวาดเป็นเส้นตรงระหว่างจุดของคุณ ไม่มีการตรวจสอบกับลานสกี ลิฟต์ หรือเส้นทาง',
|
||||
'tours.ski.plannedHint':
|
||||
'ช่วงทางจะไปตามลานสกี ลิฟต์ และเส้นทางจาก OpenStreetMap ในที่ที่ข้อมูลลานสกีครอบคลุม และเป็นเส้นตรงในที่อื่น ที่นี่ไม่ได้บอกว่าเปิดอยู่หรือไม่',
|
||||
'tours.ski.options.maxDifficulty': 'ลานที่ยากที่สุด',
|
||||
'tours.ski.options.priority': 'ให้ความสำคัญกับ',
|
||||
'tours.ski.options.pace': 'จังหวะ',
|
||||
'tours.ski.options.useLifts': 'ใช้ลิฟต์',
|
||||
'tours.ski.options.freeride': 'อนุญาตเส้นทางฟรีไรด์',
|
||||
'tours.ski.options.liftDown': 'ลงด้วยลิฟต์เมื่อจำเป็น',
|
||||
'tours.ski.options.groomedOnly': 'เฉพาะเส้นทางที่บดอัดแล้ว',
|
||||
'tours.ski.options.litOnly': 'เฉพาะเส้นทางที่มีไฟ',
|
||||
'tours.ski.options.uphillOnPistes': 'ขึ้นตามขอบลานสกี',
|
||||
'tours.ski.options.avoidLinks': 'หลีกเลี่ยงทางเชื่อมที่คาดเดา',
|
||||
'tours.ski.options.avoidLifts': 'ไม่ใช้',
|
||||
'tours.ski.difficulty.novice': 'มือใหม่',
|
||||
'tours.ski.difficulty.easy': 'ง่าย',
|
||||
'tours.ski.difficulty.intermediate': 'ปานกลาง',
|
||||
'tours.ski.difficulty.advanced': 'ยาก',
|
||||
'tours.ski.difficulty.expert': 'ผู้เชี่ยวชาญ',
|
||||
'tours.ski.difficulty.freeride': 'ฟรีไรด์',
|
||||
'tours.ski.difficulty.extreme': 'สุดขีด',
|
||||
'tours.ski.lift.drag': 'ลิฟต์ลาก',
|
||||
'tours.ski.lift.open_chair': 'ลิฟต์เก้าอี้แบบเปิด',
|
||||
'tours.ski.lift.gondola': 'กอนโดลา',
|
||||
'tours.ski.lift.cable_car': 'กระเช้าลอยฟ้า',
|
||||
'tours.ski.lift.rail': 'รถรางไต่เขาและรถไฟ',
|
||||
'tours.ski.priority.fastest': 'เร็วที่สุด',
|
||||
'tours.ski.priority.most_skiing': 'ได้สกีมากที่สุด',
|
||||
'tours.ski.priority.easiest': 'ง่ายที่สุด',
|
||||
'tours.ski.pace.relaxed': 'สบาย ๆ',
|
||||
'tours.ski.pace.normal': 'ปกติ',
|
||||
'tours.ski.pace.sporty': 'สปอร์ต',
|
||||
'tours.ski.kind.run': 'ลานสกี',
|
||||
'tours.ski.kind.lift': 'ลิฟต์',
|
||||
'tours.ski.kind.rail': 'รถรางไต่เขา',
|
||||
'tours.ski.kind.lift_down': 'ลงด้วยลิฟต์',
|
||||
'tours.ski.kind.nordic': 'เส้นทางสกีครอสคันทรี',
|
||||
'tours.ski.kind.skitour': 'เส้นทางสกีทัวริง',
|
||||
'tours.ski.kind.hike': 'ทางเดินฤดูหนาว',
|
||||
'tours.ski.kind.connection': 'ลานเชื่อม',
|
||||
'tours.ski.kind.area': 'ลาดเปิด',
|
||||
'tours.ski.kind.link': 'ทางเชื่อมที่ไม่มีในแผนที่',
|
||||
'tours.ski.kind.crossing': 'ทางข้าม',
|
||||
'tours.ski.kind.walk': 'เดินเท้า',
|
||||
'tours.ski.fail.not_on_piste': 'จุดนี้อยู่ไกลเกินไปจากลานสกี เส้นทาง หรือลิฟต์ที่คุณใช้ได้',
|
||||
'tours.ski.fail.no_area': 'ไม่มีพื้นที่สกีในข้อมูลลานสกีที่ครอบคลุมจุดนี้',
|
||||
'tours.ski.fail.loading': 'ข้อมูลลานสกีของพื้นที่นี้ยังดาวน์โหลดอยู่',
|
||||
'tours.ski.fail.places_unavailable': 'ตอนนี้เข้าถึงข้อมูลลานสกีไม่ได้',
|
||||
'tours.ski.fail.disabled': 'การวางเส้นทางลานสกีถูกปิดอยู่',
|
||||
'tours.ski.fail.blocked_by_difficulty': 'มีเพียงลานที่ยากกว่าขีดจำกัดของคุณที่เชื่อมจุดเหล่านี้',
|
||||
'tours.ski.fail.blocked_by_lift_avoid': 'มีเพียงลิฟต์ที่คุณไม่ใช้ที่เชื่อมจุดเหล่านี้',
|
||||
'tours.ski.fail.blocked_by_technique': 'มีเพียงเส้นทางสำหรับเทคนิคอีกแบบที่เชื่อมจุดเหล่านี้',
|
||||
'tours.ski.fail.needs_ride_down': 'ทางกลับต้องลงด้วยลิฟต์',
|
||||
'tours.ski.fail.uphill_only': 'มีเพียงทางขึ้นที่เชื่อมจุดเหล่านี้ และไม่มีลิฟต์ให้ขึ้น',
|
||||
'tours.ski.fail.disconnected': 'ไม่มีสิ่งใดในข้อมูลลานสกีที่เชื่อมจุดเหล่านี้',
|
||||
'tours.ski.fail.cross_area': 'จุดเหล่านี้อยู่ในพื้นที่สกีสองแห่งที่ไม่เชื่อมกัน',
|
||||
'tours.ski.fail.busy': 'ตัววางเส้นทางลานสกีไม่ว่าง ลองใหม่อีกครู่',
|
||||
'tours.ski.fail.timeout': 'การวางแผนช่วงนี้ใช้เวลานานเกินไป',
|
||||
'tours.ski.fail.offline': 'คุณออฟไลน์อยู่ ช่วงนี้จึงวาดเป็นเส้นตรงไปก่อน',
|
||||
'tours.ski.fix.raiseDifficulty': 'อนุญาตลานที่ยากขึ้น',
|
||||
'tours.ski.fix.allowLift': 'อนุญาตลิฟต์นี้',
|
||||
'tours.ski.fix.liftDown': 'ลงด้วยลิฟต์',
|
||||
'tours.ski.fix.technique': 'เปลี่ยนเทคนิค',
|
||||
'tours.ski.fix.discipline': 'เปลี่ยนประเภท',
|
||||
'tours.ski.summerPath': 'นอกข้อมูลลานสกี: ไปตามทางเดินฤดูร้อน ซึ่งอาจอยู่ใต้หิมะ',
|
||||
'tours.ski.summary.title': 'เส้นทางลานสกี',
|
||||
'tours.ski.summary.ski': 'เล่นสกี',
|
||||
'tours.ski.summary.lift': 'บนลิฟต์',
|
||||
'tours.ski.summary.wait': 'รอ',
|
||||
'tours.ski.summary.walk': 'เดินเท้า',
|
||||
'tours.ski.summary.avoided': 'หลีกเลี่ยง',
|
||||
'tours.ski.summary.unplanned': 'บางช่วงวางแผนไม่ได้ จึงยังไม่มีเวลาเล่นสกี',
|
||||
'tours.ski.summary.guessed': 'บางส่วนอาศัยการคาดเดาจากข้อมูลแผนที่ เช่น ทางเชื่อมหรือทิศทางที่ไม่มีในแผนที่',
|
||||
'tours.ski.summary.stale': 'วางแผนบนสำเนาข้อมูลลานสกีที่เก่ากว่า เพราะเข้าถึงฉบับปัจจุบันไม่ได้',
|
||||
'tours.ski.summary.source': 'ข้อมูลลานสกี © ผู้ร่วมพัฒนา OpenStreetMap',
|
||||
'tours.ski.chooseTechnique': 'เลือกเทคนิค หรือไม่ทราบ',
|
||||
'tours.ski.infoLabel': 'TREK ทำอะไรกับทริปและเส้นทางสกี',
|
||||
'tours.ski.info':
|
||||
'ทริปที่วางแผนจะวาดแต่ละช่วงเป็นเส้นตรงระหว่างจุด ส่วนแทร็กที่นำเข้าจะเก็บไว้ตามที่บันทึก ทั้งสองแบบไม่ได้คำนวณเส้นทางตามลานสกี ลิฟต์ หรือเส้นทางเดิน ความสูงมาจากภูมิประเทศหรือไฟล์ TREK ไม่คำนวณเวลาเล่นสกี และไม่มีสิ่งใดในที่นี้ รวมถึงสีความชันในมุมมอง 3D ที่บอกอะไรเกี่ยวกับความยาก หิมะ หิมะถล่ม หรือว่าลานสกีเปิดอยู่หรือไม่',
|
||||
'ทัวร์ที่วางแผนจะไปตามลานสกี ลิฟต์ และเส้นทางจาก OpenStreetMap ในที่ที่ข้อมูลลานสกีครอบคลุม และเชื่อมจุดด้วยเส้นตรงในที่อื่น เส้นทางที่นำเข้าจะคงไว้ตามที่บันทึก ความสูงมาจากข้อมูลลานสกี ภูมิประเทศ หรือไฟล์ ไม่มีสิ่งใดที่นี่ รวมถึงสีความชันในมุมมอง 3D ที่บอกอะไรเกี่ยวกับหิมะ หิมะถล่ม หรือว่าลานสกี ลิฟต์ และเส้นทางเปิดอยู่หรือไม่',
|
||||
'tours.ski.time': 'เวลาเล่นสกี',
|
||||
'tours.ski.timeUnknown': 'ไม่ได้คำนวณ',
|
||||
'tours.ski.timeInfoLabel': 'ทำไมจึงไม่มีเวลาเล่นสกี',
|
||||
'tours.ski.timeInfoLabel': 'วิธีคำนวณเวลาเล่นสกี',
|
||||
'tours.ski.timeInfo':
|
||||
'TREK ไม่คำนวณเวลาเล่นสกี เพราะแต่ละช่วงวาดเป็นเส้นตรง ไม่ได้วางแผนตามลานสกี ลิฟต์ หรือเส้นทาง หากต้องการ ให้กรอกเวลารวมที่วางแผนไว้เอง',
|
||||
'ตัววางเส้นทางลานสกีจะรวมเวลาบนลานสกี บนลิฟต์ การรอที่สถานีล่าง และการเดิน ตามประเภทและจังหวะของคุณ จะให้เวลาเมื่อวางแผนครบทุกช่วงแล้วเท่านั้น หากต้องการเวลารวม ให้กรอกเอง',
|
||||
'tours.ski.classifyTitle': 'จัดประเภทเส้นทางสกี',
|
||||
'tours.ski.saved': 'บันทึกการจัดประเภทแล้ว',
|
||||
'tours.ski.saveFailed': 'บันทึกการจัดประเภทไม่สำเร็จ',
|
||||
|
||||
@@ -20,7 +20,7 @@ const toursModules = import.meta.glob<{ default: TranslationStrings }>('./*/tour
|
||||
const TOUR_DOMAINS: { file: string; generalKeyCount: number; modules: DomainModules }[] = [
|
||||
{
|
||||
file: 'tours',
|
||||
generalKeyCount: 1189,
|
||||
generalKeyCount: 1261,
|
||||
modules: toursModules,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -727,6 +727,9 @@ const admin: TranslationStrings = {
|
||||
'admin.toursCycleMap.title': 'Bisiklet haritası (CyclOSM)',
|
||||
'admin.toursCycleMap.subtitle':
|
||||
"Tur planlayıcısında bisiklet turları için CyclOSM'i temel harita olarak sunar. OpenStreetMap France onu ölçülü kullanım için sağlar; yoğun veya ticari bir kurulum onu kapatır ya da önce işletmeciyle anlaşır",
|
||||
'admin.toursSkiRouting.title': 'Pist rotalayıcı',
|
||||
'admin.toursSkiRouting.subtitle':
|
||||
'Kayak turlarını Places sunucusunun pist verileriyle pistler, liftler ve parkurlar boyunca planlar. Kapalıyken kayak turunun etapları düz çizilir',
|
||||
'admin.addons.noAddons': 'Kullanılabilir eklenti yok',
|
||||
'admin.weather.title': 'Hava Durumu Verileri',
|
||||
'admin.weather.badge': "24 Mart 2026'dan beri",
|
||||
|
||||
@@ -411,17 +411,90 @@ const tours: TranslationStrings = {
|
||||
'tours.ski.technique.unknown': 'Bilinmiyor',
|
||||
'tours.ski.chooseDiscipline': 'İzi kayak olarak içe aktarmak için bir branş seç.',
|
||||
'tours.ski.chooseDisciplineToSave': 'Turu kaydetmek için bir branş seç.',
|
||||
'tours.ski.drawnFreely':
|
||||
'Her bölüm noktaların arasında düz çizilir. Hiçbir şey onu pistler, teleferikler veya parkurlarla karşılaştırmaz.',
|
||||
'tours.ski.plannedHint':
|
||||
'Etaplar, pist verilerinin ulaştığı yerde OpenStreetMap pistlerini, teleferiklerini ve parkurlarını izler, ulaşmadığı yerde düz gider. Buradaki hiçbir şey açık olup olmadıklarını söylemez.',
|
||||
'tours.ski.options.maxDifficulty': 'En zor pistler',
|
||||
'tours.ski.options.priority': 'Tercih et',
|
||||
'tours.ski.options.pace': 'Tempo',
|
||||
'tours.ski.options.useLifts': 'Teleferik kullan',
|
||||
'tours.ski.options.freeride': 'Freeride rotalarına izin ver',
|
||||
'tours.ski.options.liftDown': 'Gerekirse teleferikle in',
|
||||
'tours.ski.options.groomedOnly': 'Yalnızca bakımlı parkurlar',
|
||||
'tours.ski.options.litOnly': 'Yalnızca ışıklı parkurlar',
|
||||
'tours.ski.options.uphillOnPistes': 'Pist kenarından tırman',
|
||||
'tours.ski.options.avoidLinks': 'Tahmin edilen bağlantılardan kaçın',
|
||||
'tours.ski.options.avoidLifts': 'Hariç tut',
|
||||
'tours.ski.difficulty.novice': 'Acemi',
|
||||
'tours.ski.difficulty.easy': 'Kolay',
|
||||
'tours.ski.difficulty.intermediate': 'Orta',
|
||||
'tours.ski.difficulty.advanced': 'Zor',
|
||||
'tours.ski.difficulty.expert': 'Uzman',
|
||||
'tours.ski.difficulty.freeride': 'Freeride', // same-as-en
|
||||
'tours.ski.difficulty.extreme': 'Aşırı zor',
|
||||
'tours.ski.lift.drag': 'Çekme lifleri',
|
||||
'tours.ski.lift.open_chair': 'Açık telesiyejler',
|
||||
'tours.ski.lift.gondola': 'Gondollar',
|
||||
'tours.ski.lift.cable_car': 'Teleferikler',
|
||||
'tours.ski.lift.rail': 'Füniküler ve trenler',
|
||||
'tours.ski.priority.fastest': 'En hızlı',
|
||||
'tours.ski.priority.most_skiing': 'En çok kayak',
|
||||
'tours.ski.priority.easiest': 'En kolay',
|
||||
'tours.ski.pace.relaxed': 'Rahat',
|
||||
'tours.ski.pace.normal': 'Normal', // same-as-en
|
||||
'tours.ski.pace.sporty': 'Sportif',
|
||||
'tours.ski.kind.run': 'Pist',
|
||||
'tours.ski.kind.lift': 'Lift', // same-as-en
|
||||
'tours.ski.kind.rail': 'Füniküler',
|
||||
'tours.ski.kind.lift_down': 'Liftle iniş',
|
||||
'tours.ski.kind.nordic': 'Kros kayağı parkuru',
|
||||
'tours.ski.kind.skitour': 'Dağ kayağı rotası',
|
||||
'tours.ski.kind.hike': 'Kış patikası',
|
||||
'tours.ski.kind.connection': 'Bağlantı pisti',
|
||||
'tours.ski.kind.area': 'Açık yamaç',
|
||||
'tours.ski.kind.link': 'Haritada olmayan bağlantı',
|
||||
'tours.ski.kind.crossing': 'Geçiş',
|
||||
'tours.ski.kind.walk': 'Yürüyerek',
|
||||
'tours.ski.fail.not_on_piste': 'Bu nokta kullanabileceğin her pistten, parkurdan veya liften çok uzakta.',
|
||||
'tours.ski.fail.no_area': 'Pist verilerindeki hiçbir kayak merkezi bu noktayı kapsamıyor.',
|
||||
'tours.ski.fail.loading': 'Bu bölgenin pist verileri hâlâ indiriliyor.',
|
||||
'tours.ski.fail.places_unavailable': 'Pist verilerine şu anda ulaşılamıyor.',
|
||||
'tours.ski.fail.disabled': 'Pist rotalama kapalı.',
|
||||
'tours.ski.fail.blocked_by_difficulty': 'Bu noktaları yalnızca sınırından zor pistler bağlıyor.',
|
||||
'tours.ski.fail.blocked_by_lift_avoid': 'Bu noktaları yalnızca hariç tuttuğun bir lift bağlıyor.',
|
||||
'tours.ski.fail.blocked_by_technique': 'Bu noktaları yalnızca diğer teknik için hazırlanmış parkurlar bağlıyor.',
|
||||
'tours.ski.fail.needs_ride_down': 'Dönüş yolu liftle inmeyi gerektiriyor.',
|
||||
'tours.ski.fail.uphill_only': 'Bu noktaları yalnızca bir yokuş yukarı yol bağlıyor ve binilecek lift yok.',
|
||||
'tours.ski.fail.disconnected': 'Pist verilerinde bu noktaları bağlayan hiçbir şey yok.',
|
||||
'tours.ski.fail.cross_area': 'Bu noktalar birbirine bağlı olmayan iki kayak merkezinde.',
|
||||
'tours.ski.fail.busy': 'Pist rotalayıcı meşgul. Birazdan tekrar dene.',
|
||||
'tours.ski.fail.timeout': 'Bu etabı planlamak çok uzun sürdü.',
|
||||
'tours.ski.fail.offline': 'Çevrimdışısın, bu yüzden bu etap şimdilik düz çiziliyor.',
|
||||
'tours.ski.fix.raiseDifficulty': 'Daha zor pistlere izin ver',
|
||||
'tours.ski.fix.allowLift': 'Bu lifte izin ver',
|
||||
'tours.ski.fix.liftDown': 'Liftle in',
|
||||
'tours.ski.fix.technique': 'Tekniği değiştir',
|
||||
'tours.ski.fix.discipline': 'Disiplini değiştir',
|
||||
'tours.ski.summerPath': 'Pist verilerinin dışında: karla kaplı olabilecek yaz patikalarından.',
|
||||
'tours.ski.summary.title': 'Pist rotası',
|
||||
'tours.ski.summary.ski': 'Kayak',
|
||||
'tours.ski.summary.lift': 'Liftlerde',
|
||||
'tours.ski.summary.wait': 'Bekleme',
|
||||
'tours.ski.summary.walk': 'Yürüyerek',
|
||||
'tours.ski.summary.avoided': 'Kaçınılan',
|
||||
'tours.ski.summary.unplanned': 'Bazı etaplar planlanamadı, bu yüzden henüz kayak süresi yok.',
|
||||
'tours.ski.summary.guessed':
|
||||
'Bazı kısımlar harita verilerinden bir tahmine dayanıyor, örneğin haritada olmayan bir bağlantı veya yön.',
|
||||
'tours.ski.summary.stale': 'Güncel kopyaya ulaşılamadığı için pist verilerinin eski bir kopyasıyla planlandı.',
|
||||
'tours.ski.summary.source': 'Pist verileri © OpenStreetMap katkıcıları',
|
||||
'tours.ski.chooseTechnique': 'Tekniği ya da Bilinmiyor seçeneğini seç.',
|
||||
'tours.ski.infoLabel': 'TREK kayak turları ve izleriyle ne yapar',
|
||||
'tours.ski.info':
|
||||
'Planlanan bir turda her bölüm noktaları arasında düz çizilir, içe aktarılan bir iz ise kaydedildiği gibi kalır. Hiçbiri pistler, teleferikler veya parkurlar boyunca rotalanmaz. Yükseklikler araziden ya da dosyadan gelir. TREK kayak süresi hesaplamaz; buradaki hiçbir şey, 3D eğim renkleri de dahil, zorluk, kar, çığ ya da pist ve parkurların açık olup olmadığı hakkında bir şey söylemez.',
|
||||
'Planlanan tur, pist verilerinin ulaştığı yerde OpenStreetMap pistlerini, teleferiklerini ve parkurlarını izler, ulaşmadığı yerde noktalarını düz bağlar. İçe aktarılan iz kaydedildiği gibi kalır. Yükseklikler pist verilerinden, araziden veya dosyadan gelir. Buradaki hiçbir şey, 3D eğim renkleri dahil, kar, çığ ya da pistlerin, teleferiklerin ve parkurların açık olup olmadığı hakkında bir şey söylemez.',
|
||||
'tours.ski.time': 'Kayak süresi',
|
||||
'tours.ski.timeUnknown': 'Hesaplanmadı',
|
||||
'tours.ski.timeInfoLabel': 'Neden kayak süresi yok',
|
||||
'tours.ski.timeInfoLabel': 'Kayak süresi nasıl hesaplanır',
|
||||
'tours.ski.timeInfo':
|
||||
'TREK kayak süresi hesaplamaz: bölümler düz çizilir, pistler, teleferikler veya parkurlar boyunca planlanmaz. İstersen planlanan toplam süreyi kendin gir.',
|
||||
'Pist rotalayıcı, disiplinine ve temposuna göre pistlerde, teleferiklerde, alt istasyonlarda beklerken ve yürüyerek geçen süreyi toplar. Süreyi ancak her etap planlandığında verir. İstersen planlanan toplam süreyi kendin gir.',
|
||||
'tours.ski.classifyTitle': 'Kayak izini sınıflandır',
|
||||
'tours.ski.saved': 'Sınıflandırma kaydedildi',
|
||||
'tours.ski.saveFailed': 'Sınıflandırma kaydedilemedi.',
|
||||
|
||||
@@ -693,6 +693,9 @@ const admin: TranslationStrings = {
|
||||
'admin.toursCycleMap.title': 'Велокарта (CyclOSM)',
|
||||
'admin.toursCycleMap.subtitle':
|
||||
'Пропонує CyclOSM як базову карту для велопоїздок у планувальнику. OpenStreetMap France надає її для помірного використання; навантажена або комерційна інсталяція вимикає її або спершу домовляється з оператором',
|
||||
'admin.toursSkiRouting.title': 'Маршрутизатор трас',
|
||||
'admin.toursSkiRouting.subtitle':
|
||||
'Планує лижні маршрути трасами, підйомниками й лижнями на даних трас сервера Places. Якщо вимкнено, ділянки лижного маршруту малюються прямими',
|
||||
'admin.addons.noAddons': 'Немає доступних доповнень',
|
||||
'admin.weather.title': 'Дані про погоду',
|
||||
'admin.weather.badge': 'З 24 березня 2026',
|
||||
|
||||
@@ -425,17 +425,90 @@ const tours: TranslationStrings = {
|
||||
'tours.ski.technique.unknown': 'Невідомо',
|
||||
'tours.ski.chooseDiscipline': 'Виберіть дисципліну, щоб імпортувати трек як лижі.',
|
||||
'tours.ski.chooseDisciplineToSave': 'Виберіть дисципліну, щоб зберегти маршрут.',
|
||||
'tours.ski.drawnFreely':
|
||||
'Кожна ділянка малюється прямою між вашими точками. Ніщо не звіряє її з трасами, підйомниками чи стежками.',
|
||||
'tours.ski.plannedHint':
|
||||
'Ділянки йдуть трасами, підйомниками й лижнями з OpenStreetMap там, де є дані трас, а деінде прямо. Ніщо тут не каже, чи вони відкриті.',
|
||||
'tours.ski.options.maxDifficulty': 'Найскладніші траси',
|
||||
'tours.ski.options.priority': 'Віддавати перевагу',
|
||||
'tours.ski.options.pace': 'Темп',
|
||||
'tours.ski.options.useLifts': 'Їздити підйомниками',
|
||||
'tours.ski.options.freeride': 'Дозволити фрірайд-маршрути',
|
||||
'tours.ski.options.liftDown': 'За потреби спускатися підйомником',
|
||||
'tours.ski.options.groomedOnly': 'Лише підготовлені лижні',
|
||||
'tours.ski.options.litOnly': 'Лише освітлені лижні',
|
||||
'tours.ski.options.uphillOnPistes': 'Підніматися краєм траси',
|
||||
'tours.ski.options.avoidLinks': 'Уникати припущених зв’язок',
|
||||
'tours.ski.options.avoidLifts': 'Виключити',
|
||||
'tours.ski.difficulty.novice': 'Новачок',
|
||||
'tours.ski.difficulty.easy': 'Легка',
|
||||
'tours.ski.difficulty.intermediate': 'Середня',
|
||||
'tours.ski.difficulty.advanced': 'Складна',
|
||||
'tours.ski.difficulty.expert': 'Експертна',
|
||||
'tours.ski.difficulty.freeride': 'Фрірайд',
|
||||
'tours.ski.difficulty.extreme': 'Екстремальна',
|
||||
'tours.ski.lift.drag': 'Бугельні підйомники',
|
||||
'tours.ski.lift.open_chair': 'Відкриті крісельні',
|
||||
'tours.ski.lift.gondola': 'Гондоли',
|
||||
'tours.ski.lift.cable_car': 'Канатні дороги',
|
||||
'tours.ski.lift.rail': 'Фунікулери й потяги',
|
||||
'tours.ski.priority.fastest': 'Найшвидше',
|
||||
'tours.ski.priority.most_skiing': 'Найбільше катання',
|
||||
'tours.ski.priority.easiest': 'Найлегше',
|
||||
'tours.ski.pace.relaxed': 'Спокійний',
|
||||
'tours.ski.pace.normal': 'Звичайний',
|
||||
'tours.ski.pace.sporty': 'Спортивний',
|
||||
'tours.ski.kind.run': 'Траса',
|
||||
'tours.ski.kind.lift': 'Підйомник',
|
||||
'tours.ski.kind.rail': 'Фунікулер',
|
||||
'tours.ski.kind.lift_down': 'Спуск підйомником',
|
||||
'tours.ski.kind.nordic': 'Лижня',
|
||||
'tours.ski.kind.skitour': 'Маршрут скітуру',
|
||||
'tours.ski.kind.hike': 'Зимова стежка',
|
||||
'tours.ski.kind.connection': 'Сполучна траса',
|
||||
'tours.ski.kind.area': 'Відкритий схил',
|
||||
'tours.ski.kind.link': 'Невідмічена зв’язка',
|
||||
'tours.ski.kind.crossing': 'Перехід',
|
||||
'tours.ski.kind.walk': 'Пішки',
|
||||
'tours.ski.fail.not_on_piste': 'Ця точка надто далеко від будь-якої доступної вам траси, лижні чи підйомника.',
|
||||
'tours.ski.fail.no_area': 'Жоден курорт у даних трас не охоплює цю точку.',
|
||||
'tours.ski.fail.loading': 'Дані трас для цього району ще завантажуються.',
|
||||
'tours.ski.fail.places_unavailable': 'Дані трас зараз недоступні.',
|
||||
'tours.ski.fail.disabled': 'Маршрутизацію трасами вимкнено.',
|
||||
'tours.ski.fail.blocked_by_difficulty': 'Ці точки з’єднують лише траси, складніші за ваш ліміт.',
|
||||
'tours.ski.fail.blocked_by_lift_avoid': 'Ці точки з’єднує лише виключений вами підйомник.',
|
||||
'tours.ski.fail.blocked_by_technique': 'Ці точки з’єднують лише лижні для іншої техніки.',
|
||||
'tours.ski.fail.needs_ride_down': 'Шлях назад потребує спуску підйомником.',
|
||||
'tours.ski.fail.uphill_only': 'Ці точки з’єднує лише шлях угору, а підйомника немає.',
|
||||
'tours.ski.fail.disconnected': 'Ніщо в даних трас не з’єднує ці точки.',
|
||||
'tours.ski.fail.cross_area': 'Ці точки лежать на двох непов’язаних курортах.',
|
||||
'tours.ski.fail.busy': 'Маршрутизатор трас зайнятий. Спробуйте трохи згодом.',
|
||||
'tours.ski.fail.timeout': 'Планування цієї ділянки тривало надто довго.',
|
||||
'tours.ski.fail.offline': 'Ви офлайн, тому ця ділянка поки намальована прямою.',
|
||||
'tours.ski.fix.raiseDifficulty': 'Дозволити складніші траси',
|
||||
'tours.ski.fix.allowLift': 'Дозволити цей підйомник',
|
||||
'tours.ski.fix.liftDown': 'Спуститися підйомником',
|
||||
'tours.ski.fix.technique': 'Змінити техніку',
|
||||
'tours.ski.fix.discipline': 'Змінити дисципліну',
|
||||
'tours.ski.summerPath': 'Поза даними трас: літніми стежками, які можуть бути під снігом.',
|
||||
'tours.ski.summary.title': 'Маршрут трасами',
|
||||
'tours.ski.summary.ski': 'Катання',
|
||||
'tours.ski.summary.lift': 'На підйомниках',
|
||||
'tours.ski.summary.wait': 'Очікування',
|
||||
'tours.ski.summary.walk': 'Пішки',
|
||||
'tours.ski.summary.avoided': 'Обійдено',
|
||||
'tours.ski.summary.unplanned': 'Деякі ділянки не вдалося спланувати, тому часу катання поки немає.',
|
||||
'tours.ski.summary.guessed':
|
||||
'Деякі відрізки спираються на здогад за картографічними даними, наприклад на невідмічену зв’язку чи напрямок.',
|
||||
'tours.ski.summary.stale': 'Сплановано за старішою копією даних трас, бо поточна недоступна.',
|
||||
'tours.ski.summary.source': 'Дані трас © учасники OpenStreetMap',
|
||||
'tours.ski.chooseTechnique': 'Виберіть техніку або «Невідомо».',
|
||||
'tours.ski.infoLabel': 'Що TREK робить з лижними маршрутами і треками',
|
||||
'tours.ski.info':
|
||||
'У запланованому маршруті кожна ділянка намальована прямою між його точками, імпортований трек лишається таким, як записано. Жоден не прокладається трасами, підйомниками чи стежками. Висоти взято з рельєфу або з файлу. TREK не розраховує час на лижах, і ніщо тут, зокрема кольори нахилу в 3D, не каже про складність, сніг, лавини чи про те, чи відкриті траси й лижні.',
|
||||
'Запланований маршрут іде трасами, підйомниками й лижнями з OpenStreetMap там, де є дані трас, а деінде з’єднує точки прямо. Імпортований трек лишається таким, яким його записано. Висоти беруться з даних трас, рельєфу або файлу. Ніщо тут, зокрема кольори нахилу в 3D, нічого не каже про сніг, лавини чи те, чи відкриті траси, підйомники й лижні.',
|
||||
'tours.ski.time': 'Час на лижах',
|
||||
'tours.ski.timeUnknown': 'Не розраховано',
|
||||
'tours.ski.timeInfoLabel': 'Чому немає часу на лижах',
|
||||
'tours.ski.timeInfoLabel': 'Як рахується час катання',
|
||||
'tours.ski.timeInfo':
|
||||
'TREK не розраховує час на лижах: ділянки намальовано прямими, а не прокладено трасами, підйомниками чи стежками. За бажання вкажіть загальний час самі.',
|
||||
'Маршрутизатор трас додає час на трасах, на підйомниках, в очікуванні біля нижніх станцій і пішки, з огляду на дисципліну й темп. Час він дає, лише коли сплановано кожну ділянку. Введіть запланований загальний час самі, якщо він потрібен.',
|
||||
'tours.ski.classifyTitle': 'Класифікувати лижний трек',
|
||||
'tours.ski.saved': 'Класифікацію збережено',
|
||||
'tours.ski.saveFailed': 'Не вдалося зберегти класифікацію.',
|
||||
|
||||
@@ -729,6 +729,9 @@ const admin: TranslationStrings = {
|
||||
'admin.toursCycleMap.title': 'Bản đồ xe đạp (CyclOSM)',
|
||||
'admin.toursCycleMap.subtitle':
|
||||
'Cung cấp CyclOSM làm bản đồ nền cho chuyến đạp xe trong trình lập kế hoạch. OpenStreetMap France phục vụ nó cho mức dùng vừa phải; bản cài đặt dùng nhiều hoặc thương mại cần tắt nó hoặc thỏa thuận trước với nhà vận hành',
|
||||
'admin.toursSkiRouting.title': 'Bộ định tuyến đường trượt',
|
||||
'admin.toursSkiRouting.subtitle':
|
||||
'Lên kế hoạch chuyến trượt tuyết theo đường trượt, cáp treo và đường mòn với dữ liệu đường trượt của máy chủ Places. Khi tắt, các chặng của chuyến trượt tuyết được vẽ thẳng',
|
||||
'admin.addons.noAddons': 'Không có tiện ích bổ sung nào',
|
||||
'admin.weather.title': 'Dữ liệu thời tiết',
|
||||
'admin.weather.badge': 'Kể từ ngày 24 tháng 3 năm 2026',
|
||||
|
||||
@@ -409,17 +409,91 @@ const tours: TranslationStrings = {
|
||||
'tours.ski.technique.unknown': 'Không rõ',
|
||||
'tours.ski.chooseDiscipline': 'Chọn bộ môn để nhập đường đi thành trượt tuyết.',
|
||||
'tours.ski.chooseDisciplineToSave': 'Chọn bộ môn để lưu chuyến đi.',
|
||||
'tours.ski.drawnFreely':
|
||||
'Mỗi chặng được vẽ thẳng giữa các điểm của bạn. Không có gì đối chiếu nó với đường trượt, cáp treo hay lối mòn.',
|
||||
'tours.ski.plannedHint':
|
||||
'Các chặng đi theo đường trượt, cáp treo và đường mòn từ OpenStreetMap ở nơi có dữ liệu đường trượt, và đi thẳng ở nơi khác. Không có gì ở đây cho biết chúng có mở hay không.',
|
||||
'tours.ski.options.maxDifficulty': 'Đường trượt khó nhất',
|
||||
'tours.ski.options.priority': 'Ưu tiên',
|
||||
'tours.ski.options.pace': 'Nhịp độ',
|
||||
'tours.ski.options.useLifts': 'Đi cáp treo',
|
||||
'tours.ski.options.freeride': 'Cho phép tuyến freeride',
|
||||
'tours.ski.options.liftDown': 'Đi cáp treo xuống khi cần',
|
||||
'tours.ski.options.groomedOnly': 'Chỉ đường đã được chăm sóc',
|
||||
'tours.ski.options.litOnly': 'Chỉ đường có đèn',
|
||||
'tours.ski.options.uphillOnPistes': 'Leo lên dọc mép đường trượt',
|
||||
'tours.ski.options.avoidLinks': 'Tránh các đoạn nối chỉ là phỏng đoán',
|
||||
'tours.ski.options.avoidLifts': 'Bỏ qua',
|
||||
'tours.ski.difficulty.novice': 'Người mới',
|
||||
'tours.ski.difficulty.easy': 'Dễ',
|
||||
'tours.ski.difficulty.intermediate': 'Trung bình',
|
||||
'tours.ski.difficulty.advanced': 'Khó',
|
||||
'tours.ski.difficulty.expert': 'Chuyên gia',
|
||||
'tours.ski.difficulty.freeride': 'Trượt tự do',
|
||||
'tours.ski.difficulty.extreme': 'Cực khó',
|
||||
'tours.ski.lift.drag': 'Cáp kéo',
|
||||
'tours.ski.lift.open_chair': 'Ghế treo hở',
|
||||
'tours.ski.lift.gondola': 'Cáp treo cabin',
|
||||
'tours.ski.lift.cable_car': 'Cáp treo lớn',
|
||||
'tours.ski.lift.rail': 'Đường sắt leo núi và tàu',
|
||||
'tours.ski.priority.fastest': 'Nhanh nhất',
|
||||
'tours.ski.priority.most_skiing': 'Trượt nhiều nhất',
|
||||
'tours.ski.priority.easiest': 'Dễ nhất',
|
||||
'tours.ski.pace.relaxed': 'Thong thả',
|
||||
'tours.ski.pace.normal': 'Bình thường',
|
||||
'tours.ski.pace.sporty': 'Thể thao',
|
||||
'tours.ski.kind.run': 'Đường trượt',
|
||||
'tours.ski.kind.lift': 'Cáp treo',
|
||||
'tours.ski.kind.rail': 'Đường sắt leo núi',
|
||||
'tours.ski.kind.lift_down': 'Xuống bằng cáp treo',
|
||||
'tours.ski.kind.nordic': 'Đường trượt băng đồng',
|
||||
'tours.ski.kind.skitour': 'Tuyến trượt tuyết leo núi',
|
||||
'tours.ski.kind.hike': 'Đường mòn mùa đông',
|
||||
'tours.ski.kind.connection': 'Đường trượt nối',
|
||||
'tours.ski.kind.area': 'Sườn dốc mở',
|
||||
'tours.ski.kind.link': 'Đoạn nối chưa có trên bản đồ',
|
||||
'tours.ski.kind.crossing': 'Đoạn băng qua',
|
||||
'tours.ski.kind.walk': 'Đi bộ',
|
||||
'tours.ski.fail.not_on_piste': 'Điểm này quá xa mọi đường trượt, đường mòn hoặc cáp treo bạn có thể dùng.',
|
||||
'tours.ski.fail.no_area': 'Không khu trượt tuyết nào trong dữ liệu đường trượt bao phủ điểm này.',
|
||||
'tours.ski.fail.loading': 'Dữ liệu đường trượt cho khu vực này vẫn đang được tải xuống.',
|
||||
'tours.ski.fail.places_unavailable': 'Hiện không thể truy cập dữ liệu đường trượt.',
|
||||
'tours.ski.fail.disabled': 'Định tuyến đường trượt đã tắt.',
|
||||
'tours.ski.fail.blocked_by_difficulty': 'Chỉ những đường trượt khó hơn giới hạn của bạn nối các điểm này.',
|
||||
'tours.ski.fail.blocked_by_lift_avoid': 'Chỉ một cáp treo bạn đã bỏ qua nối các điểm này.',
|
||||
'tours.ski.fail.blocked_by_technique': 'Chỉ những đường dành cho kỹ thuật kia nối các điểm này.',
|
||||
'tours.ski.fail.needs_ride_down': 'Đường về cần đi cáp treo xuống.',
|
||||
'tours.ski.fail.uphill_only': 'Chỉ có đường lên dốc nối các điểm này và không có cáp treo để đi.',
|
||||
'tours.ski.fail.disconnected': 'Không có gì trong dữ liệu đường trượt nối các điểm này.',
|
||||
'tours.ski.fail.cross_area': 'Các điểm này nằm ở hai khu trượt tuyết không nối với nhau.',
|
||||
'tours.ski.fail.busy': 'Bộ định tuyến đường trượt đang bận. Hãy thử lại sau giây lát.',
|
||||
'tours.ski.fail.timeout': 'Lên kế hoạch cho chặng này mất quá lâu.',
|
||||
'tours.ski.fail.offline': 'Bạn đang ngoại tuyến, nên chặng này tạm thời được vẽ thẳng.',
|
||||
'tours.ski.fix.raiseDifficulty': 'Cho phép đường trượt khó hơn',
|
||||
'tours.ski.fix.allowLift': 'Cho phép cáp treo này',
|
||||
'tours.ski.fix.liftDown': 'Đi cáp treo xuống',
|
||||
'tours.ski.fix.technique': 'Đổi kỹ thuật',
|
||||
'tours.ski.fix.discipline': 'Đổi môn',
|
||||
'tours.ski.summerPath': 'Ngoài dữ liệu đường trượt: đi theo đường mòn mùa hè, có thể nằm dưới tuyết.',
|
||||
'tours.ski.summary.title': 'Tuyến đường trượt',
|
||||
'tours.ski.summary.ski': 'Trượt tuyết',
|
||||
'tours.ski.summary.lift': 'Trên cáp treo',
|
||||
'tours.ski.summary.wait': 'Chờ',
|
||||
'tours.ski.summary.walk': 'Đi bộ',
|
||||
'tours.ski.summary.avoided': 'Đã tránh',
|
||||
'tours.ski.summary.unplanned': 'Một số chặng không thể lên kế hoạch, nên chưa có thời gian trượt tuyết.',
|
||||
'tours.ski.summary.guessed':
|
||||
'Một số đoạn dựa trên phỏng đoán từ dữ liệu bản đồ, như một đoạn nối hoặc hướng chưa được vẽ.',
|
||||
'tours.ski.summary.stale':
|
||||
'Được lên kế hoạch trên bản sao cũ hơn của dữ liệu đường trượt, vì không truy cập được bản hiện tại.',
|
||||
'tours.ski.summary.source': 'Dữ liệu đường trượt © cộng tác viên OpenStreetMap',
|
||||
'tours.ski.chooseTechnique': 'Chọn kỹ thuật, hoặc Không rõ.',
|
||||
'tours.ski.infoLabel': 'TREK làm gì với chuyến và đường trượt tuyết',
|
||||
'tours.ski.info':
|
||||
'Chuyến đi đã lập có mỗi chặng được vẽ thẳng giữa các điểm của nó, còn vết được nhập giữ nguyên như đã ghi. Không cái nào được định tuyến theo đường trượt, cáp treo hay lối mòn. Độ cao lấy từ địa hình hoặc từ tệp. TREK không tính thời gian trượt, và không gì ở đây, kể cả màu độ dốc trong 3D, nói gì về độ khó, tuyết, tuyết lở hay việc đường trượt có mở hay không.',
|
||||
'Một chuyến đi đã lên kế hoạch đi theo đường trượt, cáp treo và đường mòn từ OpenStreetMap ở nơi có dữ liệu đường trượt, và nối các điểm bằng đường thẳng ở nơi khác. Một tuyến đã nhập được giữ như khi ghi lại. Độ cao lấy từ dữ liệu đường trượt, địa hình hoặc tệp. Không có gì ở đây, kể cả màu độ dốc trong 3D, nói gì về tuyết, lở tuyết hay việc đường trượt, cáp treo và đường mòn có mở hay không.',
|
||||
'tours.ski.time': 'Thời gian trượt',
|
||||
'tours.ski.timeUnknown': 'Không tính',
|
||||
'tours.ski.timeInfoLabel': 'Vì sao không có thời gian trượt',
|
||||
'tours.ski.timeInfoLabel': 'Cách tính thời gian trượt tuyết',
|
||||
'tours.ski.timeInfo':
|
||||
'TREK không tính thời gian trượt: các chặng được vẽ thẳng, không lập theo đường trượt, cáp treo hay lối mòn. Nếu muốn, hãy tự nhập tổng thời gian dự kiến.',
|
||||
'Bộ định tuyến đường trượt cộng thời gian trên đường trượt, trên cáp treo, chờ ở ga dưới và đi bộ, theo môn và nhịp độ của bạn. Nó chỉ đưa ra thời gian khi mọi chặng đã được lên kế hoạch. Hãy tự nhập tổng thời gian dự kiến nếu bạn muốn.',
|
||||
'tours.ski.classifyTitle': 'Phân loại đường trượt tuyết',
|
||||
'tours.ski.saved': 'Đã lưu phân loại',
|
||||
'tours.ski.saveFailed': 'Không thể lưu phân loại.',
|
||||
|
||||
@@ -651,6 +651,8 @@ const admin: TranslationStrings = {
|
||||
'admin.toursCycleMap.title': '騎乘地圖(CyclOSM)',
|
||||
'admin.toursCycleMap.subtitle':
|
||||
'在行程規劃器中為騎乘提供 CyclOSM 作為底圖。OpenStreetMap France 為適度使用提供該地圖;使用量大或商業的安裝應關閉它,或先與營運方達成協議',
|
||||
'admin.toursSkiRouting.title': '雪道路線規劃器',
|
||||
'admin.toursSkiRouting.subtitle': '使用 Places 伺服器的雪道資料,沿雪道、纜車和越野雪道規劃滑雪路線。關閉後,滑雪路線的路段以直線繪製',
|
||||
'admin.addons.noAddons': '暫無可用擴充套件',
|
||||
'admin.weather.title': '天氣資料',
|
||||
'admin.weather.badge': '自 2026 年 3 月 24 日起',
|
||||
|
||||
@@ -376,16 +376,87 @@ const tours: TranslationStrings = {
|
||||
'tours.ski.technique.unknown': '未知',
|
||||
'tours.ski.chooseDiscipline': '請選擇項目,以便將軌跡匯入為滑雪。',
|
||||
'tours.ski.chooseDisciplineToSave': '請選擇項目以儲存路線。',
|
||||
'tours.ski.drawnFreely': '每段都在你的點之間以直線繪製,不會與雪道、纜車或步道進行核對。',
|
||||
'tours.ski.plannedHint': '在雪道資料涵蓋的地方,路段沿 OpenStreetMap 的雪道、纜車和越野雪道前進,其餘地方走直線。這裡不表示它們是否開放。',
|
||||
'tours.ski.options.maxDifficulty': '最難的雪道',
|
||||
'tours.ski.options.priority': '優先',
|
||||
'tours.ski.options.pace': '節奏',
|
||||
'tours.ski.options.useLifts': '搭乘纜車',
|
||||
'tours.ski.options.freeride': '允許野雪路線',
|
||||
'tours.ski.options.liftDown': '需要時搭纜車下山',
|
||||
'tours.ski.options.groomedOnly': '僅限壓雪道',
|
||||
'tours.ski.options.litOnly': '僅限有照明的雪道',
|
||||
'tours.ski.options.uphillOnPistes': '沿雪道邊緣上行',
|
||||
'tours.ski.options.avoidLinks': '避開推測的連接',
|
||||
'tours.ski.options.avoidLifts': '排除',
|
||||
'tours.ski.difficulty.novice': '入門',
|
||||
'tours.ski.difficulty.easy': '初級',
|
||||
'tours.ski.difficulty.intermediate': '中級',
|
||||
'tours.ski.difficulty.advanced': '高級',
|
||||
'tours.ski.difficulty.expert': '專家',
|
||||
'tours.ski.difficulty.freeride': '野雪',
|
||||
'tours.ski.difficulty.extreme': '極限',
|
||||
'tours.ski.lift.drag': '拖牽',
|
||||
'tours.ski.lift.open_chair': '開放式吊椅',
|
||||
'tours.ski.lift.gondola': '吊廂',
|
||||
'tours.ski.lift.cable_car': '索道',
|
||||
'tours.ski.lift.rail': '纜索鐵路和火車',
|
||||
'tours.ski.priority.fastest': '最快',
|
||||
'tours.ski.priority.most_skiing': '滑得最多',
|
||||
'tours.ski.priority.easiest': '最輕鬆',
|
||||
'tours.ski.pace.relaxed': '悠閒',
|
||||
'tours.ski.pace.normal': '正常',
|
||||
'tours.ski.pace.sporty': '運動',
|
||||
'tours.ski.kind.run': '雪道',
|
||||
'tours.ski.kind.lift': '纜車',
|
||||
'tours.ski.kind.rail': '纜索鐵路',
|
||||
'tours.ski.kind.lift_down': '搭纜車下山',
|
||||
'tours.ski.kind.nordic': '越野滑雪道',
|
||||
'tours.ski.kind.skitour': '登山滑雪路線',
|
||||
'tours.ski.kind.hike': '冬季步道',
|
||||
'tours.ski.kind.connection': '連接雪道',
|
||||
'tours.ski.kind.area': '開放坡面',
|
||||
'tours.ski.kind.link': '未標註的連接',
|
||||
'tours.ski.kind.crossing': '橫越',
|
||||
'tours.ski.kind.walk': '步行',
|
||||
'tours.ski.fail.not_on_piste': '這個點離你能用的任何雪道、越野雪道或纜車都太遠。',
|
||||
'tours.ski.fail.no_area': '雪道資料中沒有滑雪場涵蓋這個點。',
|
||||
'tours.ski.fail.loading': '該區域的雪道資料仍在下載。',
|
||||
'tours.ski.fail.places_unavailable': '現在無法存取雪道資料。',
|
||||
'tours.ski.fail.disabled': '雪道路線規劃已關閉。',
|
||||
'tours.ski.fail.blocked_by_difficulty': '只有超出你上限的雪道連接這些點。',
|
||||
'tours.ski.fail.blocked_by_lift_avoid': '只有你排除的纜車連接這些點。',
|
||||
'tours.ski.fail.blocked_by_technique': '只有為另一種技術壓出的雪道連接這些點。',
|
||||
'tours.ski.fail.needs_ride_down': '回程需要搭纜車下山。',
|
||||
'tours.ski.fail.uphill_only': '只有上坡路連接這些點,且沒有纜車可搭。',
|
||||
'tours.ski.fail.disconnected': '雪道資料中沒有任何東西連接這些點。',
|
||||
'tours.ski.fail.cross_area': '這些點位於兩個互不相連的滑雪場。',
|
||||
'tours.ski.fail.busy': '雪道路線規劃器忙碌中。請稍後再試。',
|
||||
'tours.ski.fail.timeout': '規劃這個路段耗時過長。',
|
||||
'tours.ski.fail.offline': '你目前離線,因此這個路段暫時以直線繪製。',
|
||||
'tours.ski.fix.raiseDifficulty': '允許更難的雪道',
|
||||
'tours.ski.fix.allowLift': '允許這條纜車',
|
||||
'tours.ski.fix.liftDown': '搭纜車下山',
|
||||
'tours.ski.fix.technique': '切換技術',
|
||||
'tours.ski.fix.discipline': '切換項目',
|
||||
'tours.ski.summerPath': '超出雪道資料:走夏季步道,可能被雪覆蓋。',
|
||||
'tours.ski.summary.title': '雪道路線',
|
||||
'tours.ski.summary.ski': '滑行',
|
||||
'tours.ski.summary.lift': '搭纜車',
|
||||
'tours.ski.summary.wait': '等候',
|
||||
'tours.ski.summary.walk': '步行',
|
||||
'tours.ski.summary.avoided': '已避開',
|
||||
'tours.ski.summary.unplanned': '有些路段無法規劃,因此還沒有滑雪時間。',
|
||||
'tours.ski.summary.guessed': '有些路段基於地圖資料的推測,例如未標註的連接或方向。',
|
||||
'tours.ski.summary.stale': '由於無法存取目前版本,使用較早的雪道資料副本規劃。',
|
||||
'tours.ski.summary.source': '雪道資料 © OpenStreetMap 貢獻者',
|
||||
'tours.ski.chooseTechnique': '請選擇技術,或選擇未知。',
|
||||
'tours.ski.infoLabel': 'TREK 如何處理滑雪路線與軌跡',
|
||||
'tours.ski.info':
|
||||
'規劃的路線中每段都在其點之間以直線繪製,匯入的軌跡保持記錄原樣。兩者都不會沿雪道、纜車或步道規劃路線。海拔來自地形或檔案。TREK 不計算滑雪時間,這裡的任何內容,包括 3D 中的坡度顏色,都不代表難度、積雪、雪崩或雪道是否開放。',
|
||||
'規劃的路線在雪道資料涵蓋的地方沿 OpenStreetMap 的雪道、纜車和越野雪道前進,其餘地方以直線連接各點。匯入的軌跡保持錄製時的樣子。海拔來自雪道資料、地形或檔案。這裡的任何內容,包括 3D 坡度顏色,都不說明積雪、雪崩或雪道、纜車和越野雪道是否開放。',
|
||||
'tours.ski.time': '滑雪時間',
|
||||
'tours.ski.timeUnknown': '未計算',
|
||||
'tours.ski.timeInfoLabel': '為什麼沒有滑雪時間',
|
||||
'tours.ski.timeInfo':
|
||||
'TREK 不計算滑雪時間:各段以直線繪製,並未沿雪道、纜車或步道規劃。如有需要,請自行填寫計畫總時長。',
|
||||
'tours.ski.timeInfoLabel': '滑雪時間如何計算',
|
||||
'tours.ski.timeInfo': '雪道路線規劃器依你的項目和節奏,加總在雪道上、纜車上、底站等候和步行的時間。只有每個路段都規劃好後才給出時間。如需總時長,請自行填寫。',
|
||||
'tours.ski.classifyTitle': '為滑雪軌跡分類',
|
||||
'tours.ski.saved': '分類已儲存',
|
||||
'tours.ski.saveFailed': '無法儲存分類。',
|
||||
|
||||
@@ -601,6 +601,8 @@ const admin: TranslationStrings = {
|
||||
'admin.toursCycleMap.title': '骑行地图(CyclOSM)',
|
||||
'admin.toursCycleMap.subtitle':
|
||||
'在行程规划器中为骑行提供 CyclOSM 作为底图。OpenStreetMap France 为适度使用提供该地图;使用量大或商业的安装应关闭它,或先与运营方达成协议',
|
||||
'admin.toursSkiRouting.title': '雪道路线规划器',
|
||||
'admin.toursSkiRouting.subtitle': '使用 Places 服务器的雪道数据,沿雪道、缆车和越野雪道规划滑雪路线。关闭后,滑雪路线的路段以直线绘制',
|
||||
'admin.addons.noAddons': '暂无可用扩展',
|
||||
'admin.weather.title': '天气数据',
|
||||
'admin.weather.badge': '自 2026 年 3 月 24 日起',
|
||||
|
||||
@@ -377,16 +377,87 @@ const tours: TranslationStrings = {
|
||||
'tours.ski.technique.unknown': '未知',
|
||||
'tours.ski.chooseDiscipline': '请选择项目,以便将轨迹导入为滑雪。',
|
||||
'tours.ski.chooseDisciplineToSave': '请选择项目以保存路线。',
|
||||
'tours.ski.drawnFreely': '每段都在你的点之间以直线绘制,不会与雪道、缆车或小径进行核对。',
|
||||
'tours.ski.plannedHint': '在雪道数据覆盖的地方,路段沿 OpenStreetMap 的雪道、缆车和越野雪道行进,其余地方走直线。这里不表示它们是否开放。',
|
||||
'tours.ski.options.maxDifficulty': '最难的雪道',
|
||||
'tours.ski.options.priority': '优先',
|
||||
'tours.ski.options.pace': '节奏',
|
||||
'tours.ski.options.useLifts': '乘坐缆车',
|
||||
'tours.ski.options.freeride': '允许野雪路线',
|
||||
'tours.ski.options.liftDown': '需要时乘缆车下山',
|
||||
'tours.ski.options.groomedOnly': '仅限压雪道',
|
||||
'tours.ski.options.litOnly': '仅限有照明的雪道',
|
||||
'tours.ski.options.uphillOnPistes': '沿雪道边缘上行',
|
||||
'tours.ski.options.avoidLinks': '避开推测的连接',
|
||||
'tours.ski.options.avoidLifts': '排除',
|
||||
'tours.ski.difficulty.novice': '入门',
|
||||
'tours.ski.difficulty.easy': '初级',
|
||||
'tours.ski.difficulty.intermediate': '中级',
|
||||
'tours.ski.difficulty.advanced': '高级',
|
||||
'tours.ski.difficulty.expert': '专家',
|
||||
'tours.ski.difficulty.freeride': '野雪',
|
||||
'tours.ski.difficulty.extreme': '极限',
|
||||
'tours.ski.lift.drag': '拖牵',
|
||||
'tours.ski.lift.open_chair': '开放式吊椅',
|
||||
'tours.ski.lift.gondola': '吊厢',
|
||||
'tours.ski.lift.cable_car': '索道',
|
||||
'tours.ski.lift.rail': '缆索铁路和火车',
|
||||
'tours.ski.priority.fastest': '最快',
|
||||
'tours.ski.priority.most_skiing': '滑得最多',
|
||||
'tours.ski.priority.easiest': '最轻松',
|
||||
'tours.ski.pace.relaxed': '悠闲',
|
||||
'tours.ski.pace.normal': '正常',
|
||||
'tours.ski.pace.sporty': '运动',
|
||||
'tours.ski.kind.run': '雪道',
|
||||
'tours.ski.kind.lift': '缆车',
|
||||
'tours.ski.kind.rail': '缆索铁路',
|
||||
'tours.ski.kind.lift_down': '乘缆车下山',
|
||||
'tours.ski.kind.nordic': '越野滑雪道',
|
||||
'tours.ski.kind.skitour': '登山滑雪路线',
|
||||
'tours.ski.kind.hike': '冬季步道',
|
||||
'tours.ski.kind.connection': '连接雪道',
|
||||
'tours.ski.kind.area': '开放坡面',
|
||||
'tours.ski.kind.link': '未标注的连接',
|
||||
'tours.ski.kind.crossing': '横穿',
|
||||
'tours.ski.kind.walk': '步行',
|
||||
'tours.ski.fail.not_on_piste': '这个点离你能用的任何雪道、越野雪道或缆车都太远。',
|
||||
'tours.ski.fail.no_area': '雪道数据中没有滑雪场覆盖这个点。',
|
||||
'tours.ski.fail.loading': '该区域的雪道数据仍在下载。',
|
||||
'tours.ski.fail.places_unavailable': '现在无法访问雪道数据。',
|
||||
'tours.ski.fail.disabled': '雪道路线规划已关闭。',
|
||||
'tours.ski.fail.blocked_by_difficulty': '只有超出你上限的雪道连接这些点。',
|
||||
'tours.ski.fail.blocked_by_lift_avoid': '只有你排除的缆车连接这些点。',
|
||||
'tours.ski.fail.blocked_by_technique': '只有为另一种技术压出的雪道连接这些点。',
|
||||
'tours.ski.fail.needs_ride_down': '返回需要乘缆车下山。',
|
||||
'tours.ski.fail.uphill_only': '只有上坡路连接这些点,且没有缆车可乘。',
|
||||
'tours.ski.fail.disconnected': '雪道数据中没有任何东西连接这些点。',
|
||||
'tours.ski.fail.cross_area': '这些点位于两个互不相连的滑雪场。',
|
||||
'tours.ski.fail.busy': '雪道路线规划器繁忙。请稍后再试。',
|
||||
'tours.ski.fail.timeout': '规划这个路段耗时过长。',
|
||||
'tours.ski.fail.offline': '你处于离线状态,因此这个路段暂时以直线绘制。',
|
||||
'tours.ski.fix.raiseDifficulty': '允许更难的雪道',
|
||||
'tours.ski.fix.allowLift': '允许这条缆车',
|
||||
'tours.ski.fix.liftDown': '乘缆车下山',
|
||||
'tours.ski.fix.technique': '切换技术',
|
||||
'tours.ski.fix.discipline': '切换项目',
|
||||
'tours.ski.summerPath': '超出雪道数据:走夏季步道,可能被雪覆盖。',
|
||||
'tours.ski.summary.title': '雪道路线',
|
||||
'tours.ski.summary.ski': '滑行',
|
||||
'tours.ski.summary.lift': '乘缆车',
|
||||
'tours.ski.summary.wait': '等候',
|
||||
'tours.ski.summary.walk': '步行',
|
||||
'tours.ski.summary.avoided': '已避开',
|
||||
'tours.ski.summary.unplanned': '有些路段无法规划,因此还没有滑雪时间。',
|
||||
'tours.ski.summary.guessed': '有些路段基于地图数据的推测,例如未标注的连接或方向。',
|
||||
'tours.ski.summary.stale': '由于无法访问当前版本,使用较早的雪道数据副本规划。',
|
||||
'tours.ski.summary.source': '雪道数据 © OpenStreetMap 贡献者',
|
||||
'tours.ski.chooseTechnique': '请选择技术,或选择未知。',
|
||||
'tours.ski.infoLabel': 'TREK 如何处理滑雪路线与轨迹',
|
||||
'tours.ski.info':
|
||||
'计划的路线中每段都在其点之间以直线绘制,导入的轨迹保持记录原样。两者都不会沿雪道、缆车或小径规划路线。海拔来自地形或文件。TREK 不计算滑雪用时,这里的任何内容,包括 3D 中的坡度颜色,都不代表难度、积雪、雪崩或雪道是否开放。',
|
||||
'规划的路线在雪道数据覆盖的地方沿 OpenStreetMap 的雪道、缆车和越野雪道行进,其余地方用直线连接各点。导入的轨迹保持录制时的样子。海拔来自雪道数据、地形或文件。这里的任何内容,包括 3D 坡度颜色,都不说明积雪、雪崩或雪道、缆车和越野雪道是否开放。',
|
||||
'tours.ski.time': '滑雪用时',
|
||||
'tours.ski.timeUnknown': '未计算',
|
||||
'tours.ski.timeInfoLabel': '为什么没有滑雪用时',
|
||||
'tours.ski.timeInfo':
|
||||
'TREK 不计算滑雪用时:各段以直线绘制,并未沿雪道、缆车或小径规划。如有需要,请自行填写计划总时长。',
|
||||
'tours.ski.timeInfoLabel': '滑雪时间如何计算',
|
||||
'tours.ski.timeInfo': '雪道路线规划器按你的项目和节奏,累加在雪道上、缆车上、底站等候和步行的时间。只有每个路段都规划好后才给出时间。如需总时长,请自行填写。',
|
||||
'tours.ski.classifyTitle': '为滑雪轨迹分类',
|
||||
'tours.ski.saved': '分类已保存',
|
||||
'tours.ski.saveFailed': '无法保存分类。',
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { SKI_DISCIPLINES, SKI_TECHNIQUES, skiIssues, tourSkiClassificationRequestSchema } from './ski.schema';
|
||||
import {
|
||||
SKI_DISCIPLINES,
|
||||
SKI_TECHNIQUES,
|
||||
skiIssues,
|
||||
skiOptionsReadBy,
|
||||
tourSkiClassificationRequestSchema,
|
||||
} from './ski.schema';
|
||||
import { tourGpxImportFieldsSchema } from './tour-gpx-import.schema';
|
||||
import {
|
||||
importableTourTypeSchema,
|
||||
@@ -183,4 +189,12 @@ describe('ski and winter tours (SKI-02)', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('SKI-SCHEMA-010: names the options each discipline reads, the downhill ones for skis and boards alike', () => {
|
||||
expect(skiOptionsReadBy('alpine')).toEqual(skiOptionsReadBy('snowboard'));
|
||||
expect(skiOptionsReadBy('alpine')).toContain('pace');
|
||||
expect(skiOptionsReadBy('cross_country')).toEqual(['groomed_only', 'lit_only', 'use_lifts', 'avoid_inferred_links']);
|
||||
expect(skiOptionsReadBy('touring')).toContain('touring_uphill_on_pistes');
|
||||
expect(skiOptionsReadBy('snowshoe')).not.toContain('max_difficulty');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -150,6 +150,11 @@ const OPTIONS_BY_DISCIPLINE: Record<SkiDiscipline, ReadonlyArray<keyof SkiRouteO
|
||||
winter_hike: ['allow_lift_down', 'use_lifts', 'avoid_inferred_links'],
|
||||
};
|
||||
|
||||
/** The options a discipline reads, the ones the planner offers for it. */
|
||||
export function skiOptionsReadBy(discipline: SkiDiscipline): ReadonlyArray<keyof SkiRouteOptions> {
|
||||
return OPTIONS_BY_DISCIPLINE[discipline];
|
||||
}
|
||||
|
||||
/**
|
||||
* The options as a discipline reads them: whatever belongs to another
|
||||
* discipline goes back to its default rather than being refused, so switching
|
||||
|
||||
Reference in New Issue
Block a user