feat(tours): fold the rail cards, remembered across reloads; trip totals as badges

The planner's cards fold to their head: waypoints, the paddle time estimate,
the wind and the checklist on the left, the open tour's summary on the right.
The title or the chevron folds a card, and the choice is kept in this
browser, so a reload finds the rails as they were left. A folded card still
says what matters in its head: the checklist what is open, the wind its day,
and the time estimate now its total as a badge, in the warning tone while
parts of it are unknown. The "planning values" badge and info dot of the
estimate go; what the starting values are is said once under its figures.
The obstacle entry dialog sits outside the checklist's body, so an obstacle
picked on the map still opens with the card folded.

The trip totals start folded to their counts and show each type's sums as
the row's white badges, each named in its tooltip, an incomplete one marked
as at least that much with how many tours it is known for. The info text is
shorter.

The Tours locale contract counts the 574 general keys the planned cards
brought in.
This commit is contained in:
Maurice
2026-10-19 14:18:28 +02:00
parent e4b49e1148
commit 9cd227fd81
40 changed files with 335 additions and 258 deletions
@@ -1,12 +1,27 @@
import type { TourListItem, TourTypeKey } from '@trek/shared';
import { ChartColumnBig, ChevronDown, Mountain, Route, Waves } from 'lucide-react';
import { useMemo, useState, type ComponentType } from 'react';
import {
ArrowDown,
ArrowUp,
CalendarClock,
ChartColumnBig,
ChevronDown,
Clock,
FileDown,
Mountain,
Route,
Ruler,
Waves,
type LucideIcon,
} from 'lucide-react';
import { useMemo, type ComponentType } from 'react';
import { useTranslation } from '../../i18n';
import { useSettingsStore } from '../../store/settingsStore';
import { useTripStore } from '../../store/tripStore';
import { formatDistance, formatElevation } from '../../utils/units';
import { fs } from '../shared/DialogShell';
import { InfoDot, RailCard, RailFacts, type TourMetricField } from './tourParts';
import { useRailFold } from './railFold';
import { TourDayRowPill } from './TourDayRowFacts';
import { InfoDot, RailCard } from './tourParts';
import { formatPlannedTourDuration } from './tourPresentation';
import { tourScopeSummary, type ScopeSum, type TourTypeScope } from './tourScope';
@@ -17,23 +32,6 @@ const TYPE_ICON: Partial<Record<TourTypeKey, ComponentType<{ size?: number; stro
kayak: Waves,
};
/** Whether the card is folded, kept per browser like other view conveniences; a blocked storage keeps it open. */
const FOLDED_KEY = 'trek.tours.scopeSummary.folded';
function readFolded(): boolean {
try {
return localStorage.getItem(FOLDED_KEY) === '1';
} catch {
return false;
}
}
function storeFolded(folded: boolean): void {
try {
localStorage.setItem(FOLDED_KEY, folded ? '1' : '0');
} catch {
// Private mode or blocked storage: the card simply opens again next time.
}
}
/** A count as a white badge of the card: the number, then what it counts. */
function CountBadge({ value, label, tone = 'neutral' }: { value: number; label: string; tone?: 'neutral' | 'faint' }) {
return (
@@ -47,53 +45,71 @@ function CountBadge({ value, label, tone = 'neutral' }: { value: number; label:
);
}
/** One measure of a type as a badge: its icon and sum, its name (and how complete it is) in the tooltip. */
interface ScopeBadge {
key: string;
icon: LucideIcon;
value: string;
label: string;
}
/**
* A sum as a fact: nothing when no tour has the value, else the sum, marked as
* at least that much with the reason in its info dot when some tours lack it.
* A sum as a badge: nothing when no tour has the value, else the sum, marked
* as at least that much when some tours lack it, the tooltip saying for how many.
*/
function sumFact(
label: string,
function sumBadge(
{ key, icon, name }: Pick<ScopeBadge, 'key' | 'icon'> & { name: string },
sum: ScopeSum,
format: (value: number) => string,
t: Translate
): TourMetricField | null {
): ScopeBadge | null {
if (sum.value === null) return null;
const partial = sum.known < sum.of;
return {
label,
value: partial ? `≥ ${format(sum.value)}` : format(sum.value),
info: partial
? {
label: t('tours.scope.partial'),
description: t('tours.scope.partialBody', { known: sum.known, of: sum.of }),
}
: undefined,
};
const value = partial ? `≥ ${format(sum.value)}` : format(sum.value);
const label = partial
? `${name}: ${value} (${t('tours.scope.partialBody', { known: sum.known, of: sum.of })})`
: `${name}: ${value}`;
return { key, icon, value, label };
}
function typeFacts(scope: TourTypeScope, unit: Parameters<typeof formatDistance>[1], t: Translate): TourMetricField[] {
function typeBadges(scope: TourTypeScope, unit: Parameters<typeof formatDistance>[1], t: Translate): ScopeBadge[] {
const distance = (km: number) => formatDistance(km, unit);
const height = (m: number) => formatElevation(m, unit);
return [
sumFact(t('tours.detail.distance'), scope.distanceKm, distance, t),
sumFact(t('tours.detail.ascent'), scope.ascentM, height, t),
sumFact(t('tours.detail.descent'), scope.descentM, height, t),
sumFact(t('tours.scope.routed'), scope.routedMinutes, formatPlannedTourDuration, t),
sumFact(t('tours.scope.imported'), scope.importedMinutes, formatPlannedTourDuration, t),
sumFact(t('tours.scope.planned'), scope.plannedMinutes, formatPlannedTourDuration, t),
].filter((fact): fact is TourMetricField => fact !== null);
sumBadge({ key: 'distance', icon: Ruler, name: t('tours.detail.distance') }, scope.distanceKm, distance, t),
sumBadge({ key: 'ascent', icon: ArrowUp, name: t('tours.detail.ascent') }, scope.ascentM, height, t),
sumBadge({ key: 'descent', icon: ArrowDown, name: t('tours.detail.descent') }, scope.descentM, height, t),
sumBadge(
{ key: 'routed', icon: Clock, name: t('tours.scope.routed') },
scope.routedMinutes,
formatPlannedTourDuration,
t
),
sumBadge(
{ key: 'imported', icon: FileDown, name: t('tours.scope.imported') },
scope.importedMinutes,
formatPlannedTourDuration,
t
),
sumBadge(
{ key: 'planned', icon: CalendarClock, name: t('tours.scope.planned') },
scope.plannedMinutes,
formatPlannedTourDuration,
t
),
].filter((badge): badge is ScopeBadge => badge !== null);
}
/** One tour type: its name with how many tours and how many are on a day, then its sums. */
/** One tour type: its name and how many tours, then its sums as a line of badges. */
function TypeScope({ scope }: { scope: TourTypeScope }) {
const { t } = useTranslation();
const unit = useSettingsStore((state) => state.settings.distance_unit) ?? 'metric';
const Icon = TYPE_ICON[scope.type] ?? Route;
const facts = typeFacts(scope, unit, t);
const badges = typeBadges(scope, unit, t);
return (
<section
aria-label={t(`tourTypes.${scope.type}`)}
className="flex flex-col gap-2.5"
className="flex flex-col gap-1.5"
data-testid={`tour-scope-${scope.type}`}
>
<div className="flex min-w-0 items-center gap-1.5">
@@ -107,10 +123,20 @@ function TypeScope({ scope }: { scope: TourTypeScope }) {
{scope.count}
</span>
</div>
{facts.length > 0 ? (
<RailFacts facts={facts} />
{badges.length > 0 ? (
<div className="flex flex-wrap gap-1.5 ps-[22px]">
{badges.map((badge) => (
<TourDayRowPill
key={badge.key}
icon={badge.icon}
value={badge.value}
label={badge.label}
testId={`tour-scope-${scope.type}-${badge.key}`}
/>
))}
</div>
) : (
<p className="m-0 text-content-faint" style={fs(11.5, 'body')}>
<p className="m-0 ps-[22px] text-content-faint" style={fs(11.5, 'body')}>
{t('tours.scope.noMeasures')}
</p>
)}
@@ -124,12 +150,13 @@ function TypeScope({ scope }: { scope: TourTypeScope }) {
* time apart (the router's, an import's, the planned one with breaks), never
* added up across types. It reads the list as filtered, so a type chosen in the
* filter narrows both. A value no tour has is left out rather than shown as
* zero; a sum some tours lack is marked as at least that much. Folding it is
* remembered in this browser.
* zero; a sum some tours lack is marked as at least that much. It starts folded
* to its counts, and opening it is remembered in this browser.
*/
export function TourScopeSummary({ tours }: { tours: readonly TourListItem[] }) {
const { t } = useTranslation();
const [folded, setFolded] = useState(readFolded);
// Folded until opened: the list is what the rail is for, the totals a look on demand.
const { folded, toggle } = useRailFold('trip-totals', true);
// On a day as the day lists say, not the list's flag, so a tour put on a day counts at once (as its row's badge does).
const assignments = useTripStore((state) => state.assignments);
const scope = useMemo(() => {
@@ -137,12 +164,6 @@ export function TourScopeSummary({ tours }: { tours: readonly TourListItem[] })
return tourScopeSummary(tours, (tour) => onDays.has(tour.place_id));
}, [assignments, tours]);
if (scope.count === 0) return null;
const toggle = () => {
setFolded((value) => {
storeFolded(!value);
return !value;
});
};
const toggleLabel = folded ? t('common.expand') : t('common.collapse');
return (
<RailCard
@@ -170,7 +191,7 @@ export function TourScopeSummary({ tours }: { tours: readonly TourListItem[] })
<CountBadge value={scope.count - scope.onDays} label={t('places.unplanned')} tone="faint" />
</div>
{!folded && (
<div className="mt-3.5 flex flex-col gap-4 border-t border-edge-faint pt-3.5">
<div className="mt-3 flex flex-col gap-3 border-t border-edge-faint pt-3">
{scope.types.map((typeScope) => (
<TypeScope key={typeScope.type} scope={typeScope} />
))}
@@ -367,21 +367,25 @@ export function PaddleCheckCard({ checklist }: { checklist: PaddleChecklist }) {
const notice = noticeOf(checklist);
const { summary, check } = checklist;
return (
<RailCard
icon={<ListChecks size={14} strokeWidth={2} />}
title={t('tours.paddle.check.title')}
aside={summary ? <PaddleCheckPill check={summary} /> : null}
>
<div data-testid="paddle-check-card" className="flex flex-col gap-2.5">
{notice && <CheckNoticeLine notice={notice} evaluatedAt={check?.evaluated_at ?? null} />}
{notice === 'new' && <PaddleObstacleNotice />}
{summary && <PaddleCheckDataLine check={summary} />}
{check && <CheckList checklist={checklist} check={check} />}
<p className="m-0 text-content-faint" style={fs(10.5, 'body')}>
{t('tours.paddle.check.notClearance')}
</p>
</div>
<>
<RailCard
icon={<ListChecks size={14} strokeWidth={2} />}
title={t('tours.paddle.check.title')}
foldId="paddle-check"
aside={summary ? <PaddleCheckPill check={summary} /> : null}
>
<div data-testid="paddle-check-card" className="flex flex-col gap-2.5">
{notice && <CheckNoticeLine notice={notice} evaluatedAt={check?.evaluated_at ?? null} />}
{notice === 'new' && <PaddleObstacleNotice />}
{summary && <PaddleCheckDataLine check={summary} />}
{check && <CheckList checklist={checklist} check={check} />}
<p className="m-0 text-content-faint" style={fs(10.5, 'body')}>
{t('tours.paddle.check.notClearance')}
</p>
</div>
</RailCard>
{/* Outside the card's body: an obstacle picked on the map opens its entry with the card folded too. */}
{checklist.dialogKey !== null && <ObstacleEntryDialog checklist={checklist} />}
</RailCard>
</>
);
}
@@ -144,9 +144,9 @@ describe('PaddleTimeCard (PAD-07)', () => {
renderCard({ breakMinutes: 30 });
const region = screen.getByRole('region', { name: 'Time estimate' });
expect(within(region).getByText('Planning values')).toBeInTheDocument();
const info = within(region).getByRole('button', { name: 'Planning values' });
expect(info).toHaveAccessibleDescription(/^TREK starts with 4 km\/h on the water/);
expect(within(region).getByText(/^TREK starts with 4 km\/h on the water/)).toBeInTheDocument();
// The estimate with its breaks rides in the head, so a folded card still says it.
expect(within(region).getByTestId('paddle-time-badge')).toHaveTextContent('3 h 50 min');
expect(within(region).getByText('Estimate')).toBeInTheDocument();
// 200 minutes of water, carrying, getting out and in and the lock, then 30 minutes of breaks.
expect(estimateText()).toBe('3 h 50 min');
@@ -15,7 +15,7 @@ import { SoftPill } from '../../Planner/planParts';
import { fs } from '../../shared/DialogShell';
import { paddlePlanPoints } from '../planner/paddlePlanOf';
import type { TourPlannerController } from '../planner/useTourPlanner';
import { InfoDot, RailCard, TourNotice } from '../tourParts';
import { RailCard, TourNotice } from '../tourParts';
import { formatPlannedTourDuration } from '../tourPresentation';
import { PaddleGlyph } from './PaddleGlyph';
import { PORTAGE_CSS_COLOR } from './paddlePaint';
@@ -364,7 +364,7 @@ export function PaddleEstimateBody({
return (
<div data-testid="paddle-time-card" className="flex flex-col gap-3">
<EstimateHead
total={estimate.estimateMinutes + (breakMinutes ?? 0)}
total={paddleTotal(estimate, breakMinutes).minutes}
complete={estimate.complete}
planned={planned}
t={t}
@@ -386,25 +386,47 @@ export function PaddleEstimateBody({
);
}
/** The card's head: its title, and that its values are planning values to adjust. */
export function PaddleTimeCardShell({ children }: { children: ReactNode }) {
/**
* The card: its title with the estimate as a badge (so a folded card still
* says it, in the warning tone while parts are unknown), the figures, and
* under them that its values are starting values to adjust.
*/
export function PaddleTimeCardShell({
children,
estimate,
breakMinutes,
}: {
children: ReactNode;
estimate: PaddleTimeEstimate;
breakMinutes: number | null;
}) {
const { t } = useTranslation();
const total = paddleTotal(estimate, breakMinutes);
return (
<RailCard
icon={<Timer size={14} strokeWidth={2} />}
title={t('tours.paddle.time.title')}
foldId="paddle-time"
aside={
<span className="flex flex-none items-center gap-1">
<SoftPill tone="info">{t('tours.paddle.time.assumption')}</SoftPill>
<InfoDot label={t('tours.paddle.time.assumption')} description={t('tours.paddle.time.assumptionInfo')} />
<span data-testid="paddle-time-badge" className="flex flex-none">
<SoftPill tone={total.complete ? 'neutral' : 'warning'}>{formatPlannedTourDuration(total.minutes)}</SoftPill>
</span>
}
>
{children}
{/* What the starting values are, said once under the figures rather than behind a dot. */}
<p className="m-0 mt-3 text-content-faint" style={fs(10.5, 'body')}>
{t('tours.paddle.time.assumptionInfo')}
</p>
</RailCard>
);
}
/** The estimate with the breaks added, as the card's head and its body say it. */
function paddleTotal(estimate: PaddleTimeEstimate, breakMinutes: number | null) {
return { minutes: estimate.estimateMinutes + (breakMinutes ?? 0), complete: estimate.complete };
}
/**
* PAD-07: how the time of a paddle tour comes about. Water at the paddling
* speed, portages at the carrying speed with their time out and in, locks at
@@ -416,7 +438,7 @@ export function PaddleTimeCard({ planner, locked }: { planner: TourPlannerContro
const estimate = planner.paddleEstimate;
if (!estimate || planner.waypoints.length < 2) return null;
return (
<PaddleTimeCardShell>
<PaddleTimeCardShell estimate={estimate} breakMinutes={planner.breakAdditionalMinutes}>
<PaddleEstimateBody
estimate={estimate}
layout={paddleLayoutOfPoints(paddlePlanPoints(planner.waypoints))}
@@ -266,6 +266,7 @@ export function PaddleWindCard({ wind, lakes }: { wind: TourWind; lakes?: LakeSt
<RailCard
icon={<Wind size={14} strokeWidth={2} />}
title={t('tours.paddle.wind.title')}
foldId="paddle-wind"
aside={
wind.tourDays.length > 1 ? (
<span className="flex flex-none items-center gap-1">
@@ -159,7 +159,7 @@ export function TrackPaddlePlanCards({ plan, tour }: { plan: TrackPaddlePlan; to
const locked = !plan.canEdit || plan.saving;
return (
<>
<PaddleTimeCardShell>
<PaddleTimeCardShell estimate={plan.estimate} breakMinutes={tour.break_additional_minutes ?? null}>
<PaddleEstimateBody
estimate={plan.estimate}
layout={plan.layout}
@@ -705,7 +705,7 @@ export function TourPlannerToursRail({ planner, tours, days, loading, onAssignTo
<div className="flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto px-3 py-3.5">
{hasRoute && (
<div data-testid="tour-planner-overview">
<RailCard icon={<Route size={14} strokeWidth={2} />} title={t('tours.planner.inspector.title')}>
<RailCard icon={<Route size={14} strokeWidth={2} />} title={t('tours.planner.inspector.title')} foldId="tour-summary">
<div className="flex flex-col gap-3">
<FerrataCrossingBadges />
<RailFacts facts={tourMetricFacts(routeAnalysis, distanceUnit, t, tourTimeFields)} />
@@ -820,6 +820,7 @@ export default function TourWaypointList({
<RailCard
icon={<MapPin size={14} strokeWidth={2} />}
title={t('tours.planner.waypoints')}
foldId="waypoints"
aside={
planner.waypoints.length > 0 && (
<span className="flex flex-none items-center gap-1.5">
@@ -0,0 +1,63 @@
import { act, renderHook } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { fireEvent, render, screen } from '../../../tests/helpers/render';
import { useRailFold } from './railFold';
import { RailCard } from './tourParts';
describe('useRailFold', () => {
beforeEach(() => localStorage.clear());
it('starts at its default, and a choice outlasts a reload', () => {
const first = renderHook(() => useRailFold('waypoints'));
expect(first.result.current.folded).toBe(false);
act(() => first.result.current.toggle());
expect(first.result.current.folded).toBe(true);
first.unmount();
expect(renderHook(() => useRailFold('waypoints')).result.current.folded).toBe(true);
// Another card keeps its own state.
expect(renderHook(() => useRailFold('paddle-wind')).result.current.folded).toBe(false);
});
it('a card folded by default stays open once opened', () => {
const first = renderHook(() => useRailFold('trip-totals', true));
expect(first.result.current.folded).toBe(true);
act(() => first.result.current.toggle());
first.unmount();
expect(renderHook(() => useRailFold('trip-totals', true)).result.current.folded).toBe(false);
});
it('falls back to the default where the storage is blocked', () => {
const spy = vi.spyOn(Storage.prototype, 'getItem').mockImplementation(() => {
throw new Error('blocked');
});
expect(renderHook(() => useRailFold('trip-totals', true)).result.current.folded).toBe(true);
spy.mockRestore();
});
});
describe('RailCard with a fold', () => {
beforeEach(() => localStorage.clear());
it('folds to its head by the chevron or the title, keeping the head aside', () => {
render(
<RailCard icon={null} title="Waypoints" foldId="waypoints" aside={<span>5</span>}>
<p>rows</p>
</RailCard>
);
fireEvent.click(screen.getByRole('button', { name: 'Collapse: Waypoints' }));
expect(screen.queryByText('rows')).toBeNull();
expect(screen.getByText('5')).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: 'Waypoints' }));
expect(screen.getByText('rows')).toBeInTheDocument();
});
it('a card without a fold id has no fold', () => {
render(
<RailCard icon={null} title="Tour summary">
<p>facts</p>
</RailCard>
);
expect(screen.queryByRole('button')).toBeNull();
expect(screen.getByText('facts')).toBeInTheDocument();
});
});
+38
View File
@@ -0,0 +1,38 @@
import { useCallback, useState } from 'react';
/**
* Whether a card of the tours rails is folded, kept per browser so a reload
* finds the rail as it was left. A view convenience, nothing shared: a blocked
* or cleared storage gives every card its default again.
*/
const keyOf = (id: string) => `trek.tours.fold.${id}`;
/** The stored choice, else the card's own default. */
function readFolded(id: string, byDefault = false): boolean {
try {
const stored = localStorage.getItem(keyOf(id));
return stored === null ? byDefault : stored === '1';
} catch {
return byDefault;
}
}
function storeFolded(id: string, folded: boolean): void {
try {
localStorage.setItem(keyOf(id), folded ? '1' : '0');
} catch {
// Private mode or blocked storage: the card takes its default again next time.
}
}
/** A card's folded state under `id`, remembered in this browser; `byDefault` until the user chose. */
export function useRailFold(id: string, byDefault = false) {
const [folded, setFolded] = useState(() => readFolded(id, byDefault));
const toggle = useCallback(() => {
setFolded((value) => {
storeFolded(id, !value);
return !value;
});
}, [id]);
return { folded, toggle };
}
+26 -5
View File
@@ -13,6 +13,7 @@ import { hikeSourceBadgeLabel, tourSource } from './tourPresentation'
import { type TourDay, dayMenuLabel } from './tourDays'
import { tourMetricFacts } from './tourFacts'
import type { TourDayAssignment } from './useTourDayAssignment'
import { useRailFold } from './railFold'
/**
* A tour's tile in a list: its T-scale grade in place of the mountain, named in
@@ -356,26 +357,46 @@ export function RailCard({
aside,
children,
className = '',
foldId,
}: {
icon: ReactNode
title: string
aside?: ReactNode
children: ReactNode
className?: string
/** Lets the card fold to its head, remembered in this browser under this id across reloads. */
foldId?: string
}) {
const { t } = useTranslation()
const bodyId = useId()
const { folded: storedFolded, toggle } = useRailFold(foldId ?? '')
const folded = !!foldId && storedFolded
return (
<section
className={`flex-none overflow-hidden rounded-2xl border border-edge-faint bg-surface-card ${className}`}
aria-label={title}
>
<div className="flex items-center gap-2 border-b border-edge-faint bg-surface-secondary px-3 py-2">
<div className={`flex items-center gap-2 bg-surface-secondary px-3 py-2 ${folded ? '' : 'border-b border-edge-faint'}`}>
<span className="grid h-4 w-4 flex-none place-items-center text-content-muted">{icon}</span>
<span className="min-w-0 flex-1 truncate font-semibold text-content" style={fs(12.5, 'body')}>
{title}
</span>
{foldId ? (
// The title folds the card too: a wide target, not only the chevron.
<button type="button" onClick={toggle} aria-expanded={!folded} aria-controls={bodyId}
className="min-w-0 flex-1 truncate text-start font-semibold text-content focus-visible:outline focus-visible:outline-2 focus-visible:outline-accent"
style={fs(12.5, 'body')}>
{title}
</button>
) : (
<span className="min-w-0 flex-1 truncate font-semibold text-content" style={fs(12.5, 'body')}>
{title}
</span>
)}
{aside}
{foldId && (
<FoldButton expanded={!folded} onToggle={toggle} controls={bodyId}
label={`${t(folded ? 'common.expand' : 'common.collapse')}: ${title}`} />
)}
</div>
<div className="px-3 py-3">{children}</div>
{!folded && <div id={bodyId} className="px-3 py-3">{children}</div>}
</section>
)
}
+16 -12
View File
@@ -1,6 +1,6 @@
import type { TourListItem } from '@trek/shared';
import { beforeEach, describe, expect, it } from 'vitest';
import { fireEvent, render, screen, within } from '../../../tests/helpers/render';
import { fireEvent, render, screen } from '../../../tests/helpers/render';
import { useTripStore } from '../../store/tripStore';
import { TourScopeSummary } from './TourScopeSummary';
import { tourScopeSummary } from './tourScope';
@@ -116,24 +116,28 @@ describe('TourScopeSummary', () => {
expect(counts).toHaveTextContent('3Tours');
expect(counts).toHaveTextContent('2Planned');
expect(counts).toHaveTextContent('1Unplanned');
const hike = screen.getByTestId('tour-scope-hike');
expect(within(hike).getByText('15.5 km')).toBeInTheDocument();
expect(within(hike).getByText('≥ 600 m')).toBeInTheDocument();
expect(within(hike).getByText('Routed time')).toBeInTheDocument();
expect(within(hike).queryByText('Time from imports')).toBeNull();
// The paddle tour has no heights: no ascent, rather than 0 m.
expect(within(screen.getByTestId('tour-scope-kayak')).queryByText('Ascent')).toBeNull();
fireEvent.click(screen.getByTestId('tour-scope-toggle'));
expect(screen.getByTestId('tour-scope-hike-distance')).toHaveTextContent('15.5 km');
const ascent = screen.getByTestId('tour-scope-hike-ascent');
expect(ascent).toHaveTextContent('≥ 600 m');
expect(ascent).toHaveAccessibleName('Ascent: ≥ 600 m (known for 1 of 2 tours)');
expect(screen.getByTestId('tour-scope-hike-routed')).toHaveAccessibleName('Routed time: 5 h');
expect(screen.queryByTestId('tour-scope-hike-imported')).toBeNull();
// Paddling sums no heights: no ascent badge, rather than 0 m.
expect(screen.queryByTestId('tour-scope-kayak-ascent')).toBeNull();
expect(screen.getByTestId('tour-scope-kayak-distance')).toHaveTextContent('12 km');
});
it('folds to its counts, and remembers that in this browser', () => {
it('starts folded to its counts, and remembers an opening in this browser', () => {
const { unmount } = render(<TourScopeSummary tours={[HIKE_A]} />);
fireEvent.click(screen.getByTestId('tour-scope-toggle'));
expect(screen.queryByTestId('tour-scope-hike')).toBeNull();
expect(screen.getByTestId('tour-scope-counts')).toBeInTheDocument();
fireEvent.click(screen.getByTestId('tour-scope-toggle'));
expect(screen.getByTestId('tour-scope-hike')).toBeInTheDocument();
unmount();
render(<TourScopeSummary tours={[HIKE_A]} />);
expect(screen.queryByTestId('tour-scope-hike')).toBeNull();
expect(screen.getByTestId('tour-scope-toggle')).toHaveAttribute('aria-expanded', 'false');
expect(screen.getByTestId('tour-scope-hike')).toBeInTheDocument();
expect(screen.getByTestId('tour-scope-toggle')).toHaveAttribute('aria-expanded', 'true');
});
it('shows nothing for a trip without tours', () => {
+2 -6
View File
@@ -278,11 +278,8 @@ const tours: TranslationStrings = {
'tours.filter.empty': 'لا توجد جولة تطابق هذه المرشحات.',
'tours.filter.reset': 'إعادة ضبط المرشحات',
'tours.scope.title': 'إجمالي الرحلة',
'tours.scope.info':
'ما تجمعه الجولات المعروضة أدناه حسب النوع، والفلتر يحدّ من الاثنين. الجولة في عدة أيام تُحسب مرة واحدة. تبقى أنواع الوقت الثلاثة منفصلة: ما حسبه التوجيه، وما جاء مع الاستيراد، وما خُطط له مع الاستراحات. لا يُجمع شيء عبر الأنواع.',
'tours.scope.partial': 'مجموع غير مكتمل',
'tours.scope.partialBody':
'معروف لـ {known} من {of} جولات؛ الباقي بلا قيمة ومستبعد، لذا المجموع على الأقل بهذا القدر.',
'tours.scope.info': 'كل جولة تُحسب مرة واحدة. تبقى الأوقات منفصلة حسب نوعها ولا تُجمع عبر الأنواع.',
'tours.scope.partialBody': 'معروف لـ {known} من {of} جولات',
'tours.scope.routed': 'وقت المسار المحسوب',
'tours.scope.imported': 'الوقت من الاستيراد',
'tours.scope.planned': 'المخطط مع الاستراحات',
@@ -426,7 +423,6 @@ const tours: TranslationStrings = {
'tours.paddle.lockTransit': 'العبور',
'tours.paddle.lockBypassed': 'يُحمل القارب حوله، فيُحتسب الحمل',
'tours.paddle.time.title': 'تقدير الوقت',
'tours.paddle.time.assumption': 'قيم التخطيط',
'tours.paddle.time.assumptionInfo':
'يبدأ TREK بسرعة 4 km/h على الماء، و2 km/h عند الحمل، و10 دقائق للخروج والعودة إلى الماء عند كل حمل للقارب. هذه قيم بداية قابلة للتعديل، لا معايير ولا قياسات. أوقات الأهوسة هي ما تدخله أنت فقط.',
'tours.paddle.time.water': 'على الماء',
+2 -6
View File
@@ -267,11 +267,8 @@ const tours: TranslationStrings = {
'tours.filter.empty': 'Bu filtrlərə uyğun tur yoxdur.',
'tours.filter.reset': 'Filtrləri sıfırla',
'tours.scope.title': 'Səfərin həcmi',
'tours.scope.info':
'Aşağıda göstərilən turların növə görə cəmi; filtr hər ikisini daraldır. Bir neçə gündə olan tur bir dəfə sayılır. Üç vaxt növü ayrı qalır: marşrutlaşdırmanın hesabladığı, idxalın gətirdiyi və fasilələrlə planlaşdırılan. Növlər arasında heç nə toplanmır.',
'tours.scope.partial': 'Natamam cəm',
'tours.scope.partialBody':
'{of} turdan {known} üçün məlumdur; qalanlarının dəyəri yoxdur və daxil edilmir, ona görə cəm ən azı bu qədərdir.',
'tours.scope.info': 'Hər tur bir dəfə sayılır. Vaxtlar növünə görə ayrı qalır və növlər arasında toplanmır.',
'tours.scope.partialBody': '{of} turdan {known} üçün məlumdur',
'tours.scope.routed': 'Marşrut vaxtı',
'tours.scope.imported': 'İdxaldan vaxt',
'tours.scope.planned': 'Fasilələrlə planlaşdırılan',
@@ -405,7 +402,6 @@ const tours: TranslationStrings = {
'tours.paddle.lockTransit': 'Keçid',
'tours.paddle.lockBypassed': 'Ətrafından daşınır, daşıma hesablanır',
'tours.paddle.time.title': 'Vaxt təxmini',
'tours.paddle.time.assumption': 'Planlaşdırma dəyərləri',
'tours.paddle.time.assumptionInfo':
'TREK suda 4 km/saat, daşımada 2 km/saat və hər daşımada sudan çıxıb yenidən suya girmək üçün 10 dəqiqə ilə başlayır. Bunlar dəyişdirmək üçün başlanğıc dəyərlərdir, nə norma, nə də ölçmədir. Şlüz vaxtları yalnız sənin daxil etdiklərindir.',
'tours.paddle.time.water': 'Suda',
+2 -6
View File
@@ -272,11 +272,8 @@ const tours: TranslationStrings = {
'tours.filter.empty': 'Nenhum passeio corresponde a esses filtros.',
'tours.filter.reset': 'Limpar filtros',
'tours.scope.title': 'Totais da viagem',
'tours.scope.info':
'O que os passeios mostrados abaixo somam, por tipo; o filtro restringe os dois. Um passeio em vários dias conta uma vez. Os três tipos de tempo ficam separados: o que o roteamento calculou, o que uma importação trouxe e o que está planejado com pausas. Nada é somado entre tipos.',
'tours.scope.partial': 'Soma incompleta',
'tours.scope.partialBody':
'Conhecido para {known} de {of} passeios; os outros não têm valor e ficam de fora, então o total é pelo menos isso.',
'tours.scope.info': 'Cada passeio conta uma vez. Os tempos ficam separados por tipo e nunca são somados entre tipos.',
'tours.scope.partialBody': 'conhecido para {known} de {of} passeios',
'tours.scope.routed': 'Tempo calculado',
'tours.scope.imported': 'Tempo das importações',
'tours.scope.planned': 'Planejado com pausas',
@@ -411,7 +408,6 @@ const tours: TranslationStrings = {
'tours.paddle.lockTransit': 'Passagem',
'tours.paddle.lockBypassed': 'Contornada por terra, conta o transbordo',
'tours.paddle.time.title': 'Estimativa de tempo',
'tours.paddle.time.assumption': 'Valores de planejamento',
'tours.paddle.time.assumptionInfo':
'O TREK começa com 4 km/h na água, 2 km/h carregando e 10 minutos para desembarcar e embarcar de novo em cada transbordo. São valores iniciais para ajustar, não normas nem medições. Os tempos de eclusa são apenas os que você informar.',
'tours.paddle.time.water': 'Na água',
+2 -5
View File
@@ -273,10 +273,8 @@ const tours: TranslationStrings = {
'tours.filter.reset': 'Restableix els filtres',
'tours.scope.title': 'Totals del viatge',
'tours.scope.info':
"El que sumen les rutes mostrades a sota, per tipus; el filtre limita totes dues coses. Una ruta en diversos dies compta una vegada. Els tres tipus de temps es mantenen separats: el que ha calculat l'encaminament, el que ha portat una importació i el que està planificat amb pauses. No se suma res entre tipus.",
'tours.scope.partial': 'Suma incompleta',
'tours.scope.partialBody':
'Conegut per a {known} de {of} rutes; les altres no tenen valor i queden fora, així que el total és com a mínim això.',
'Cada ruta compta una vegada. Els temps es mantenen separats per tipus i no se sumen entre tipus.',
'tours.scope.partialBody': 'conegut per a {known} de {of} rutes',
'tours.scope.routed': 'Temps calculat',
'tours.scope.imported': 'Temps de les importacions',
'tours.scope.planned': 'Planificat amb pauses',
@@ -411,7 +409,6 @@ const tours: TranslationStrings = {
'tours.paddle.lockTransit': 'Pas',
'tours.paddle.lockBypassed': 'Evitada per terra: compta el transport per terra',
'tours.paddle.time.title': 'Estimació de temps',
'tours.paddle.time.assumption': 'Valors de planificació',
'tours.paddle.time.assumptionInfo':
'TREK comença amb 4 km/h a l’aigua, 2 km/h portant l’embarcació i 10 minuts per sortir i tornar a embarcar a cada transport per terra. Són valors inicials per ajustar, no normes ni mesures. Els temps de les rescloses són només els que introdueixis.',
'tours.paddle.time.water': 'A l’aigua',
+2 -6
View File
@@ -267,11 +267,8 @@ const tours: TranslationStrings = {
'tours.filter.empty': 'Těmto filtrům neodpovídá žádná túra.',
'tours.filter.reset': 'Zrušit filtry',
'tours.scope.title': 'Rozsah cesty',
'tours.scope.info':
'Co dávají dohromady túry zobrazené níže, podle typu; filtr omezuje obojí. Túra na více dnech se počítá jednou. Tři druhy času zůstávají oddělené: co spočítalo trasování, co přinesl import a co je naplánováno s přestávkami. Napříč typy se nic nesčítá.',
'tours.scope.partial': 'Neúplný součet',
'tours.scope.partialBody':
'Známo pro {known} z {of} túr; ostatní hodnotu nemají a chybí, součet je tedy nejméně tolik.',
'tours.scope.info': 'Každá túra se počítá jednou. Časy zůstávají oddělené podle druhu a mezi typy se nesčítají.',
'tours.scope.partialBody': 'známo pro {known} z {of} túr',
'tours.scope.routed': 'Vypočtený čas',
'tours.scope.imported': 'Čas z importů',
'tours.scope.planned': 'Plán s přestávkami',
@@ -407,7 +404,6 @@ const tours: TranslationStrings = {
'tours.paddle.lockTransit': 'Proplutí',
'tours.paddle.lockBypassed': 'Přeneseno kolem, počítá se přenáška',
'tours.paddle.time.title': 'Odhad času',
'tours.paddle.time.assumption': 'Plánovací hodnoty',
'tours.paddle.time.assumptionInfo':
'TREK začíná se 4 km/h na vodě, 2 km/h při přenášení a 10 minutami na vystoupení a opětovné nastoupení u každé přenášky. Jsou to výchozí hodnoty k úpravě, ne normy ani měření. Časy zdymadel jsou jen to, co zadáte.',
'tours.paddle.time.water': 'Na vodě',
+2 -6
View File
@@ -275,11 +275,8 @@ const tours: TranslationStrings = {
'tours.filter.empty': 'Keine Tour passt zu diesen Filtern.',
'tours.filter.reset': 'Filter zurücksetzen',
'tours.scope.title': 'Umfang der Reise',
'tours.scope.info':
'Was die unten gezeigten Touren zusammen ergeben, nach Typ; der Filter grenzt beides ein. Eine Tour an mehreren Tagen zählt einmal. Die drei Zeitarten bleiben getrennt: was das Routing berechnet hat, was ein Import mitgebracht hat und was mit Pausen geplant ist. Über Typen hinweg wird nichts addiert.',
'tours.scope.partial': 'Unvollständige Summe',
'tours.scope.partialBody':
'Bekannt für {known} von {of} Touren; die übrigen haben keinen Wert und fehlen, die Summe ist also mindestens so groß.',
'tours.scope.info': 'Jede Tour zählt einmal. Zeiten bleiben nach Art getrennt und werden nicht über Typen addiert.',
'tours.scope.partialBody': 'bekannt für {known} von {of} Touren',
'tours.scope.routed': 'Routingzeit',
'tours.scope.imported': 'Zeit laut Import',
'tours.scope.planned': 'Geplant mit Pausen',
@@ -412,7 +409,6 @@ const tours: TranslationStrings = {
'tours.paddle.lockTransit': 'Durchfahrt',
'tours.paddle.lockBypassed': 'Umtragen, es zählt die Portage',
'tours.paddle.time.title': 'Zeitschätzung',
'tours.paddle.time.assumption': 'Planannahmen',
'tours.paddle.time.assumptionInfo':
'TREK beginnt mit 4 km/h auf dem Wasser, 2 km/h beim Tragen und 10 Minuten zum Aus- und Einsetzen je Portage. Das sind Startwerte zum Anpassen, keine Norm und keine Messung. Schleusenzeiten sind nur, was du einträgst.',
'tours.paddle.time.water': 'Auf dem Wasser',
+2 -6
View File
@@ -266,11 +266,8 @@ const tours: TranslationStrings = {
'tours.filter.empty': 'No tour matches these filters.',
'tours.filter.reset': 'Reset filters',
'tours.scope.title': 'Trip totals',
'tours.scope.info':
'What the tours shown below add up to, by type; the filter narrows both. A tour on several days counts once. The three kinds of time stay apart: what the router worked out, what an import brought, and what is planned with breaks. Nothing is added up across types.',
'tours.scope.partial': 'Incomplete sum',
'tours.scope.partialBody':
'Known for {known} of {of} tours; the others have no value and are left out, so the total is at least this much.',
'tours.scope.info': 'Each tour counts once. Times stay apart by kind and are never added across types.',
'tours.scope.partialBody': 'known for {known} of {of} tours',
'tours.scope.routed': 'Routed time',
'tours.scope.imported': 'Time from imports',
'tours.scope.planned': 'Planned with breaks',
@@ -404,7 +401,6 @@ const tours: TranslationStrings = {
'tours.paddle.lockTransit': 'Passage',
'tours.paddle.lockBypassed': 'Carried round, the portage counts',
'tours.paddle.time.title': 'Time estimate',
'tours.paddle.time.assumption': 'Planning values',
'tours.paddle.time.assumptionInfo':
'TREK starts with 4 km/h on the water, 2 km/h carrying and 10 minutes to get out and back in at each portage. These are starting values to adjust, not norms and not measurements. Lock times are only what you enter.',
'tours.paddle.time.water': 'On the water',
+2 -5
View File
@@ -272,10 +272,8 @@ const tours: TranslationStrings = {
'tours.filter.reset': 'Restablecer filtros',
'tours.scope.title': 'Totales del viaje',
'tours.scope.info':
'Lo que suman las rutas mostradas abajo, por tipo; el filtro acota ambos. Una ruta en varios días cuenta una vez. Los tres tipos de tiempo se mantienen separados: lo que calculó el enrutamiento, lo que trajo una importación y lo planificado con pausas. No se suma nada entre tipos.',
'tours.scope.partial': 'Suma incompleta',
'tours.scope.partialBody':
'Conocido para {known} de {of} rutas; las demás no tienen valor y quedan fuera, así que el total es al menos esto.',
'Cada ruta cuenta una vez. Los tiempos se mantienen separados por clase y no se suman entre tipos.',
'tours.scope.partialBody': 'conocido para {known} de {of} rutas',
'tours.scope.routed': 'Tiempo calculado',
'tours.scope.imported': 'Tiempo de las importaciones',
'tours.scope.planned': 'Planificado con pausas',
@@ -411,7 +409,6 @@ const tours: TranslationStrings = {
'tours.paddle.lockTransit': 'Tránsito',
'tours.paddle.lockBypassed': 'Rodeada a pie: cuenta el porteo',
'tours.paddle.time.title': 'Estimación de tiempo',
'tours.paddle.time.assumption': 'Valores de planificación',
'tours.paddle.time.assumptionInfo':
'TREK parte de 4 km/h en el agua, 2 km/h porteando y 10 minutos para desembarcar y volver a embarcar en cada porteo. Son valores iniciales para ajustar, no normas ni mediciones. Los tiempos de esclusa son solo los que introduces.',
'tours.paddle.time.water': 'En el agua',
+2 -6
View File
@@ -269,11 +269,8 @@ const tours: TranslationStrings = {
'tours.filter.empty': 'Ükski matk ei vasta neile filtritele.',
'tours.filter.reset': 'Lähtesta filtrid',
'tours.scope.title': 'Reisi kogumaht',
'tours.scope.info':
'Mida allpool näidatud matkad tüübi kaupa kokku annavad; filter kitsendab mõlemat. Mitmel päeval olev matk loetakse üks kord. Kolm ajaliiki jäävad eraldi: mida marsruutija arvutas, mida import kaasa tõi ja mis on pausidega planeeritud. Tüüpide vahel midagi kokku ei liideta.',
'tours.scope.partial': 'Mittetäielik summa',
'tours.scope.partialBody':
'Teada {known} matka puhul {of}-st; ülejäänutel väärtus puudub ja need jäävad välja, seega on summa vähemalt nii suur.',
'tours.scope.info': 'Iga matk loetakse üks kord. Ajad jäävad liigi kaupa eraldi ega liitu tüüpide vahel.',
'tours.scope.partialBody': 'teada {known} matka puhul {of}-st',
'tours.scope.routed': 'Arvutatud aeg',
'tours.scope.imported': 'Aeg importidest',
'tours.scope.planned': 'Planeeritud koos pausidega',
@@ -405,7 +402,6 @@ const tours: TranslationStrings = {
'tours.paddle.lockTransit': 'Läbimine',
'tours.paddle.lockBypassed': 'Ümber kantud, arvesse läheb ülekanne',
'tours.paddle.time.title': 'Ajahinnang',
'tours.paddle.time.assumption': 'Planeerimisväärtused',
'tours.paddle.time.assumptionInfo':
'TREK alustab väärtustega 4 km/h vee peal, 2 km/h kandes ja 10 minutit iga ülekande juures väljumiseks ja tagasi vette minekuks. Need on lähteväärtused, mida kohandada, mitte normid ega mõõtmised. Lüüsiajad on ainult need, mille ise sisestad.',
'tours.paddle.time.water': 'Vee peal',
+2 -5
View File
@@ -278,10 +278,8 @@ const tours: TranslationStrings = {
'tours.filter.reset': 'Réinitialiser les filtres',
'tours.scope.title': 'Totaux du voyage',
'tours.scope.info':
"Ce que totalisent les itinéraires affichés ci-dessous, par type ; le filtre limite les deux. Un itinéraire sur plusieurs jours compte une fois. Les trois sortes de temps restent séparées : ce que le calcul d'itinéraire a donné, ce qu'un import a apporté et ce qui est prévu avec les pauses. Rien n'est additionné entre les types.",
'tours.scope.partial': 'Somme incomplète',
'tours.scope.partialBody':
"Connu pour {known} itinéraires sur {of} ; les autres n'ont pas de valeur et sont laissés de côté, le total est donc au moins celui-ci.",
"Chaque itinéraire compte une fois. Les temps restent séparés par sorte et ne s'additionnent jamais entre types.",
'tours.scope.partialBody': 'connu pour {known} itinéraires sur {of}',
'tours.scope.routed': 'Temps calculé',
'tours.scope.imported': 'Temps des imports',
'tours.scope.planned': 'Prévu avec pauses',
@@ -417,7 +415,6 @@ const tours: TranslationStrings = {
'tours.paddle.lockTransit': 'Éclusage',
'tours.paddle.lockBypassed': 'Contournée, le portage compte',
'tours.paddle.time.title': 'Estimation du temps',
'tours.paddle.time.assumption': 'Valeurs de planification',
'tours.paddle.time.assumptionInfo':
'TREK part de 4 km/h sur l’eau, 2 km/h en portant et 10 minutes pour débarquer et rembarquer à chaque portage. Ce sont des valeurs de départ à ajuster, ni des normes ni des mesures. Les temps d’écluse sont uniquement ceux que vous saisissez.',
'tours.paddle.time.water': 'Sur l’eau',
+2 -5
View File
@@ -279,10 +279,8 @@ const tours: TranslationStrings = {
'tours.filter.reset': 'Επαναφορά φίλτρων',
'tours.scope.title': 'Σύνολα ταξιδιού',
'tours.scope.info':
'Τι αθροίζουν οι διαδρομές που φαίνονται παρακάτω, ανά τύπο· το φίλτρο περιορίζει και τα δύο. Μια διαδρομή σε πολλές μέρες μετρά μία φορά. Τα τρία είδη χρόνου μένουν χωριστά: όσα υπολόγισε η δρομολόγηση, όσα έφερε μια εισαγωγή και όσα σχεδιάστηκαν με διαλείμματα. Τίποτα δεν προστίθεται μεταξύ τύπων.',
'tours.scope.partial': 'Ελλιπές άθροισμα',
'tours.scope.partialBody':
'Γνωστό για {known} από {of} διαδρομές· οι υπόλοιπες δεν έχουν τιμή και μένουν έξω, άρα το σύνολο είναι τουλάχιστον τόσο.',
'Κάθε διαδρομή μετρά μία φορά. Οι χρόνοι μένουν χωριστά ανά είδος και δεν προστίθενται μεταξύ τύπων.',
'tours.scope.partialBody': 'γνωστό για {known} από {of} διαδρομές',
'tours.scope.routed': 'Υπολογισμένος χρόνος',
'tours.scope.imported': 'Χρόνος από εισαγωγές',
'tours.scope.planned': 'Με διαλείμματα',
@@ -417,7 +415,6 @@ const tours: TranslationStrings = {
'tours.paddle.lockTransit': 'Διέλευση',
'tours.paddle.lockBypassed': 'Παρακάμπτεται, μετράει η μεταφορά',
'tours.paddle.time.title': 'Εκτίμηση χρόνου',
'tours.paddle.time.assumption': 'Τιμές σχεδιασμού',
'tours.paddle.time.assumptionInfo':
'Το TREK ξεκινά με 4 km/h στο νερό, 2 km/h στη μεταφορά και 10 λεπτά για έξοδο και επανείσοδο σε κάθε μεταφορά σκάφους. Είναι αρχικές τιμές για προσαρμογή, όχι κανόνες ούτε μετρήσεις. Οι χρόνοι των υδατοφρακτών είναι μόνο όσοι καταχωρίσετε.',
'tours.paddle.time.water': 'Στο νερό',
+2 -5
View File
@@ -274,10 +274,8 @@ const tours: TranslationStrings = {
'tours.filter.reset': 'Szűrők törlése',
'tours.scope.title': 'Az út összesítése',
'tours.scope.info':
'Amit az alább mutatott túrák típusonként kiadnak; a szűrő mindkettőt szűkíti. A több napon szereplő túra egyszer számít. A háromféle idő külön marad: amit az útvonaltervező számolt, amit egy import hozott, és ami szünetekkel van tervezve. Típusok között semmi sem adódik össze.',
'tours.scope.partial': 'Hiányos összeg',
'tours.scope.partialBody':
'{of} túrából {known} esetén ismert; a többinek nincs értéke és kimarad, így az összeg legalább ennyi.',
'Minden túra egyszer számít. Az idők fajtánként külön maradnak, és típusok között nem adódnak össze.',
'tours.scope.partialBody': '{of} túrából {known} esetén ismert',
'tours.scope.routed': 'Számított idő',
'tours.scope.imported': 'Idő az importokból',
'tours.scope.planned': 'Tervezett, szünetekkel',
@@ -411,7 +409,6 @@ const tours: TranslationStrings = {
'tours.paddle.lockTransit': 'Zsilipelés',
'tours.paddle.lockBypassed': 'Megkerülve, az átemelés számít',
'tours.paddle.time.title': 'Időbecslés',
'tours.paddle.time.assumption': 'Tervezési értékek',
'tours.paddle.time.assumptionInfo':
'A TREK kiindulásként 4 km/h-val számol a vízen, 2 km/h-val hordozáskor és átemelésenként 10 perccel a ki- és visszaszállásra. Ezek állítható kiinduló értékek, nem normák és nem mérések. Zsilipidőként csak az számít, amit megadsz.',
'tours.paddle.time.water': 'A vízen',
+2 -6
View File
@@ -266,11 +266,8 @@ const tours: TranslationStrings = {
'tours.filter.empty': 'Tidak ada tur yang cocok dengan filter ini.',
'tours.filter.reset': 'Atur ulang filter',
'tours.scope.title': 'Total perjalanan',
'tours.scope.info':
'Jumlah tur yang ditampilkan di bawah, per jenis; filter mempersempit keduanya. Tur di beberapa hari dihitung sekali. Tiga jenis waktu tetap terpisah: yang dihitung perutean, yang dibawa impor, dan yang direncanakan dengan istirahat. Tidak ada yang dijumlahkan antar jenis.',
'tours.scope.partial': 'Jumlah tidak lengkap',
'tours.scope.partialBody':
'Diketahui untuk {known} dari {of} tur; yang lain tidak punya nilai dan tidak dihitung, jadi totalnya setidaknya sebesar ini.',
'tours.scope.info': 'Setiap tur dihitung sekali. Waktu tetap terpisah per jenis dan tidak dijumlahkan antar tipe.',
'tours.scope.partialBody': 'diketahui untuk {known} dari {of} tur',
'tours.scope.routed': 'Waktu perutean',
'tours.scope.imported': 'Waktu dari impor',
'tours.scope.planned': 'Rencana dengan istirahat',
@@ -401,7 +398,6 @@ const tours: TranslationStrings = {
'tours.paddle.lockTransit': 'Melintas',
'tours.paddle.lockBypassed': 'Diangkut memutar, dihitung sebagai angkut perahu',
'tours.paddle.time.title': 'Perkiraan waktu',
'tours.paddle.time.assumption': 'Nilai perencanaan',
'tours.paddle.time.assumptionInfo':
'TREK memulai dengan 4 km/h di air, 2 km/h saat mengangkut, dan 10 menit untuk keluar dan masuk kembali di setiap angkut perahu. Ini nilai awal untuk disesuaikan, bukan patokan dan bukan hasil pengukuran. Waktu pintu air hanya yang kamu masukkan.',
'tours.paddle.time.water': 'Di air',
+2 -5
View File
@@ -274,10 +274,8 @@ const tours: TranslationStrings = {
'tours.filter.reset': 'Reimposta filtri',
'tours.scope.title': 'Totali del viaggio',
'tours.scope.info':
"Quanto sommano i percorsi mostrati sotto, per tipo; il filtro limita entrambi. Un percorso su più giorni conta una volta. I tre tipi di tempo restano separati: quello calcolato dal routing, quello portato da un'importazione e quello pianificato con le pause. Nulla viene sommato tra tipi diversi.",
'tours.scope.partial': 'Somma incompleta',
'tours.scope.partialBody':
'Noto per {known} percorsi su {of}; gli altri non hanno un valore e restano fuori, quindi il totale è almeno questo.',
'Ogni percorso conta una volta. I tempi restano separati per tipo e non si sommano tra tipi diversi.',
'tours.scope.partialBody': 'noto per {known} percorsi su {of}',
'tours.scope.routed': 'Tempo calcolato',
'tours.scope.imported': 'Tempo dalle importazioni',
'tours.scope.planned': 'Pianificato con pause',
@@ -411,7 +409,6 @@ const tours: TranslationStrings = {
'tours.paddle.lockTransit': 'Passaggio',
'tours.paddle.lockBypassed': 'Aggirata a piedi, conta il trasbordo',
'tours.paddle.time.title': 'Stima dei tempi',
'tours.paddle.time.assumption': 'Valori di pianificazione',
'tours.paddle.time.assumptionInfo':
'TREK parte da 4 km/h in acqua, 2 km/h durante il trasporto e 10 minuti per sbarcare e reimbarcarsi a ogni trasbordo. Sono valori iniziali da adattare, non norme né misurazioni. I tempi delle chiuse sono solo quelli che inserisci.',
'tours.paddle.time.water': 'In acqua',
+2 -6
View File
@@ -260,11 +260,8 @@ const tours: TranslationStrings = {
'tours.filter.empty': 'この条件に合うツアーはありません。',
'tours.filter.reset': 'フィルターをリセット',
'tours.scope.title': '旅の合計',
'tours.scope.info':
'下に表示されたツアーの種類ごとの合計です。フィルターは両方に適用されます。複数の日にあるツアーは 1 回だけ数えます。3 種類の時間は分けたままです:ルーティングが計算した時間、インポートに含まれていた時間、休憩を含めて計画した時間。種類をまたいで合算はしません。',
'tours.scope.partial': '不完全な合計',
'tours.scope.partialBody':
'{of} 件中 {known} 件のツアーで判明しています。残りは値がないため含まれず、合計は少なくともこの値です。',
'tours.scope.info': '各ツアーは 1 回だけ数えます。時間は種類ごとに分け、種類をまたいで合算しません。',
'tours.scope.partialBody': '{of} 件中 {known} 件で判明',
'tours.scope.routed': 'ルート計算の時間',
'tours.scope.imported': 'インポートの時間',
'tours.scope.planned': '休憩込みの計画',
@@ -392,7 +389,6 @@ const tours: TranslationStrings = {
'tours.paddle.lockTransit': '通過',
'tours.paddle.lockBypassed': '運んで迂回、陸上運搬を計上',
'tours.paddle.time.title': '所要時間の見積もり',
'tours.paddle.time.assumption': '計画値',
'tours.paddle.time.assumptionInfo':
'TREK は水上 4 km/h、運搬 2 km/h、陸上運搬ごとの上陸と再出艇に 10 分を初期値とします。これは調整するための初期値で、基準でも測定値でもありません。閘門の時間は入力した値だけが使われます。',
'tours.paddle.time.water': '水上',
+2 -6
View File
@@ -258,11 +258,8 @@ const tours: TranslationStrings = {
'tours.filter.empty': '이 필터에 맞는 투어가 없습니다.',
'tours.filter.reset': '필터 초기화',
'tours.scope.title': '여행 합계',
'tours.scope.info':
'아래 표시된 투어의 유형별 합계입니다. 필터는 둘 다에 적용됩니다. 여러 날에 있는 투어는 한 번만 셉니다. 세 가지 시간은 따로 유지됩니다: 경로 계산이 낸 시간, 가져오기에 포함된 시간, 휴식을 포함해 계획한 시간. 유형 간에는 더하지 않습니다.',
'tours.scope.partial': '불완전한 합계',
'tours.scope.partialBody':
'{of}개 투어 중 {known}개만 값이 있습니다. 나머지는 값이 없어 빠졌으므로 합계는 최소 이만큼입니다.',
'tours.scope.info': '각 투어는 한 번만 셉니다. 시간은 종류별로 나뉘며 유형 간에 더하지 않습니다.',
'tours.scope.partialBody': '{of}개 중 {known}개 투어만 알려짐',
'tours.scope.routed': '경로 계산 시간',
'tours.scope.imported': '가져오기 시간',
'tours.scope.planned': '휴식 포함 계획',
@@ -390,7 +387,6 @@ const tours: TranslationStrings = {
'tours.paddle.lockTransit': '통과',
'tours.paddle.lockBypassed': '보트를 들고 우회, 육상 운반으로 계산',
'tours.paddle.time.title': '시간 추정',
'tours.paddle.time.assumption': '계획 기준값',
'tours.paddle.time.assumptionInfo':
'TREK은 물 위에서 4 km/h, 운반할 때 2 km/h, 육상 운반마다 출수와 재입수에 10분으로 시작합니다. 조정하라고 둔 시작값일 뿐 기준이나 측정값이 아닙니다. 갑문 시간은 직접 입력한 값만 반영됩니다.',
'tours.paddle.time.water': '물 위',
+2 -5
View File
@@ -274,10 +274,8 @@ const tours: TranslationStrings = {
'tours.filter.reset': 'Filters wissen',
'tours.scope.title': 'Totalen van de reis',
'tours.scope.info':
'Wat de hieronder getoonde tochten per type samen opleveren; het filter beperkt beide. Een tocht op meerdere dagen telt één keer. De drie soorten tijd blijven gescheiden: wat de routeplanner berekende, wat een import meebracht en wat met pauzes is gepland. Over types heen wordt niets opgeteld.',
'tours.scope.partial': 'Onvolledige som',
'tours.scope.partialBody':
'Bekend voor {known} van {of} tochten; de andere hebben geen waarde en blijven buiten beschouwing, dus het totaal is minstens zoveel.',
'Elke tocht telt één keer. Tijden blijven per soort gescheiden en worden niet over types opgeteld.',
'tours.scope.partialBody': 'bekend voor {known} van {of} tochten',
'tours.scope.routed': 'Berekende tijd',
'tours.scope.imported': 'Tijd uit imports',
'tours.scope.planned': 'Gepland met pauzes',
@@ -411,7 +409,6 @@ const tours: TranslationStrings = {
'tours.paddle.lockTransit': 'Doorvaart',
'tours.paddle.lockBypassed': 'Omgedragen, het overdragen telt',
'tours.paddle.time.title': 'Tijdschatting',
'tours.paddle.time.assumption': 'Planwaarden',
'tours.paddle.time.assumptionInfo':
'TREK begint met 4 km/h op het water, 2 km/h bij het dragen en 10 minuten om uit en weer in te stappen bij elke overdraagplaats. Dit zijn startwaarden om aan te passen, geen normen en geen metingen. Sluistijden zijn alleen wat je zelf invult.',
'tours.paddle.time.water': 'Op het water',
+2 -5
View File
@@ -281,10 +281,8 @@ const tours: TranslationStrings = {
'tours.filter.reset': 'Wyczyść filtry',
'tours.scope.title': 'Podsumowanie podróży',
'tours.scope.info':
'Ile dają razem trasy pokazane poniżej, według typu; filtr zawęża jedno i drugie. Trasa na kilku dniach liczy się raz. Trzy rodzaje czasu pozostają osobno: to, co obliczyło wyznaczanie trasy, to, co przyniósł import, i to, co zaplanowano z przerwami. Nic nie jest sumowane między typami.',
'tours.scope.partial': 'Niepełna suma',
'tours.scope.partialBody':
'Znane dla {known} z {of} tras; pozostałe nie mają wartości i są pominięte, więc suma wynosi co najmniej tyle.',
'Każda trasa liczy się raz. Czasy pozostają rozdzielone według rodzaju i nie są sumowane między typami.',
'tours.scope.partialBody': 'znane dla {known} z {of} tras',
'tours.scope.routed': 'Czas wyliczony',
'tours.scope.imported': 'Czas z importów',
'tours.scope.planned': 'Plan z przerwami',
@@ -426,7 +424,6 @@ const tours: TranslationStrings = {
'tours.paddle.lockTransit': 'Śluzowanie',
'tours.paddle.lockBypassed': 'Obnoszona, liczy się przenoska',
'tours.paddle.time.title': 'Szacowany czas',
'tours.paddle.time.assumption': 'Założenia do planowania',
'tours.paddle.time.assumptionInfo':
'TREK zaczyna od 4 km/h na wodzie, 2 km/h przy przenoszeniu i 10 minut na wyjście z wody i ponowne wodowanie przy każdej przenosce. To wartości początkowe do dostosowania, a nie normy ani pomiary. Czasy śluz to tylko to, co wpiszesz.',
'tours.paddle.time.water': 'Na wodzie',
+2 -6
View File
@@ -283,11 +283,8 @@ const tours: TranslationStrings = {
'tours.filter.empty': 'Ни один поход не подходит под эти фильтры.',
'tours.filter.reset': 'Сбросить фильтры',
'tours.scope.title': 'Итоги поездки',
'tours.scope.info':
'Сколько в сумме дают маршруты ниже, по типам; фильтр ограничивает и то, и другое. Маршрут на нескольких днях считается один раз. Три вида времени остаются раздельными: рассчитанное построением маршрута, взятое из импорта и запланированное с перерывами. Между типами ничего не складывается.',
'tours.scope.partial': 'Неполная сумма',
'tours.scope.partialBody':
'Известно для {known} из {of} маршрутов; у остальных значения нет, и они не учтены, поэтому сумма не меньше этой.',
'tours.scope.info': 'Каждый маршрут считается один раз. Время разделено по видам и не складывается между типами.',
'tours.scope.partialBody': 'известно для {known} из {of} маршрутов',
'tours.scope.routed': 'Расчётное время',
'tours.scope.imported': 'Время из импорта',
'tours.scope.planned': 'План с перерывами',
@@ -428,7 +425,6 @@ const tours: TranslationStrings = {
'tours.paddle.lockTransit': 'Шлюзование',
'tours.paddle.lockBypassed': 'Обходится по берегу, учитывается обнос',
'tours.paddle.time.title': 'Оценка времени',
'tours.paddle.time.assumption': 'Плановые значения',
'tours.paddle.time.assumptionInfo':
'TREK начинает с 4 км/ч на воде, 2 км/ч при переноске и 10 минут на выход и посадку на каждом обносе. Это стартовые значения для настройки, а не нормы и не измерения. Время шлюзов берётся только из того, что вы вводите.',
'tours.paddle.time.water': 'На воде',
+2 -6
View File
@@ -268,11 +268,8 @@ const tours: TranslationStrings = {
'tours.filter.empty': 'Týmto filtrom nezodpovedá žiadna túra.',
'tours.filter.reset': 'Zrušiť filtre',
'tours.scope.title': 'Rozsah cesty',
'tours.scope.info':
'Čo dávajú spolu túry zobrazené nižšie, podľa typu; filter obmedzuje oboje. Túra na viacerých dňoch sa počíta raz. Tri druhy času zostávajú oddelené: čo vypočítalo trasovanie, čo priniesol import a čo je naplánované s prestávkami. Naprieč typmi sa nič nesčítava.',
'tours.scope.partial': 'Neúplný súčet',
'tours.scope.partialBody':
'Známe pre {known} z {of} túr; ostatné hodnotu nemajú a chýbajú, súčet je teda najmenej toľko.',
'tours.scope.info': 'Každá túra sa počíta raz. Časy zostávajú oddelené podľa druhu a medzi typmi sa nesčítavajú.',
'tours.scope.partialBody': 'známe pre {known} z {of} túr',
'tours.scope.routed': 'Vypočítaný čas',
'tours.scope.imported': 'Čas z importov',
'tours.scope.planned': 'Plán s prestávkami',
@@ -410,7 +407,6 @@ const tours: TranslationStrings = {
'tours.paddle.lockTransit': 'Prejazd',
'tours.paddle.lockBypassed': 'Prenesené okolo, počíta sa prenáška',
'tours.paddle.time.title': 'Odhad času',
'tours.paddle.time.assumption': 'Plánovacie hodnoty',
'tours.paddle.time.assumptionInfo':
'TREK začína so 4 km/h na vode, 2 km/h pri prenášaní a 10 minútami na vystúpenie a opätovný nástup pri každej prenáške. Sú to východiskové hodnoty na úpravu, nie normy ani merania. Časy plavebných komôr sú len to, čo zadáte.',
'tours.paddle.time.water': 'Na vode',
+2 -6
View File
@@ -265,11 +265,8 @@ const tours: TranslationStrings = {
'tours.filter.empty': 'Ingen tur matchar de här filtren.',
'tours.filter.reset': 'Återställ filter',
'tours.scope.title': 'Resans omfattning',
'tours.scope.info':
'Vad turerna nedan blir tillsammans, per typ; filtret begränsar båda. En tur på flera dagar räknas en gång. De tre slagen av tid hålls isär: vad ruttplaneringen räknade fram, vad en import hade med sig och vad som är planerat med pauser. Inget läggs ihop mellan typer.',
'tours.scope.partial': 'Ofullständig summa',
'tours.scope.partialBody':
'Känt för {known} av {of} turer; de övriga saknar värde och räknas inte, så summan är minst så här mycket.',
'tours.scope.info': 'Varje tur räknas en gång. Tiderna hålls isär per slag och läggs aldrig ihop mellan typer.',
'tours.scope.partialBody': 'känt för {known} av {of} turer',
'tours.scope.routed': 'Beräknad tid',
'tours.scope.imported': 'Tid från importer',
'tours.scope.planned': 'Planerat med pauser',
@@ -402,7 +399,6 @@ const tours: TranslationStrings = {
'tours.paddle.lockTransit': 'Slussning',
'tours.paddle.lockBypassed': 'Bärs runt, lyftet räknas',
'tours.paddle.time.title': 'Tidsuppskattning',
'tours.paddle.time.assumption': 'Planeringsvärden',
'tours.paddle.time.assumptionInfo':
'TREK utgår från 4 km/h på vattnet, 2 km/h vid bärning och 10 minuter för att kliva ur och i igen vid varje lyft. Det är startvärden att justera, inte normer och inte mätningar. Slusstider är bara det du anger.',
'tours.paddle.time.water': 'På vattnet',
+2 -6
View File
@@ -254,11 +254,8 @@ const tours: TranslationStrings = {
'tours.filter.empty': 'ไม่มีทัวร์ที่ตรงกับตัวกรองเหล่านี้',
'tours.filter.reset': 'รีเซ็ตตัวกรอง',
'tours.scope.title': 'ภาพรวมของทริป',
'tours.scope.info':
'ผลรวมของทัวร์ที่แสดงด้านล่างแยกตามประเภท ตัวกรองมีผลกับทั้งสองส่วน ทัวร์ที่อยู่หลายวันนับครั้งเดียว เวลาสามแบบแยกกันเสมอ: เวลาที่การคำนวณเส้นทางได้ เวลาที่มากับการนำเข้า และเวลาที่วางแผนรวมพัก ไม่มีการรวมข้ามประเภท',
'tours.scope.partial': 'ผลรวมไม่ครบ',
'tours.scope.partialBody':
'ทราบค่าของทัวร์ {known} จาก {of} รายการ ส่วนที่เหลือไม่มีค่าและไม่ได้นับ ผลรวมจึงอย่างน้อยเท่านี้',
'tours.scope.info': 'แต่ละทัวร์นับครั้งเดียว เวลาแยกตามชนิดและไม่รวมข้ามประเภท',
'tours.scope.partialBody': 'ทราบสำหรับ {known} จาก {of} ทัวร์',
'tours.scope.routed': 'เวลาจากการคำนวณเส้นทาง',
'tours.scope.imported': 'เวลาจากการนำเข้า',
'tours.scope.planned': 'วางแผนรวมพัก',
@@ -383,7 +380,6 @@ const tours: TranslationStrings = {
'tours.paddle.lockTransit': 'ผ่านประตูน้ำ',
'tours.paddle.lockBypassed': 'แบกเรืออ้อมไป นับเวลาแบกเรือแทน',
'tours.paddle.time.title': 'การประมาณเวลา',
'tours.paddle.time.assumption': 'ค่าที่ใช้วางแผน',
'tours.paddle.time.assumptionInfo':
'TREK เริ่มต้นด้วย 4 km/h บนน้ำ, 2 km/h ขณะแบกเรือ และ 10 นาทีสำหรับขึ้นและลงเรือในแต่ละจุดแบกเรือ ค่าเหล่านี้เป็นค่าเริ่มต้นให้ปรับแก้ได้ ไม่ใช่มาตรฐานและไม่ใช่ค่าที่วัดได้ เวลาผ่านประตูน้ำมีเฉพาะที่คุณใส่เองเท่านั้น',
'tours.paddle.time.water': 'บนน้ำ',
+2 -2
View File
@@ -20,7 +20,7 @@ const tokens = (value: string) => [...value.matchAll(/\{[^{}]+\}/g)].map((match)
// placeholder spec; here only the general keys are.
const groups = pluralGroups(Object.keys(english));
const generalKeys = (keys: string[]) => keys.filter((key) => !pluralFormOf(key, groups));
const GENERAL_KEY_COUNT = 528;
const GENERAL_KEY_COUNT = 574;
function literalKeys(source: string): string[] {
const tree = ts.createSourceFile('locale.ts', source, ts.ScriptTarget.Latest, true);
@@ -34,7 +34,7 @@ function literalKeys(source: string): string[] {
}
describe('Tours locale contracts', () => {
it('covers all 27 supported locales with 528 general source keys', () => {
it('covers all 27 supported locales with 574 general source keys', () => {
expect(Object.keys(domains)).toHaveLength(27);
expect(Object.keys(domains).sort()).toEqual(SUPPORTED_LANGUAGE_CODES.map((code) => `./${code}/tours.ts`).sort());
expect(generalKeys(Object.keys(english))).toHaveLength(GENERAL_KEY_COUNT);
+2 -6
View File
@@ -267,11 +267,8 @@ const tours: TranslationStrings = {
'tours.filter.empty': 'Bu filtrelere uyan tur yok.',
'tours.filter.reset': 'Filtreleri sıfırla',
'tours.scope.title': 'Gezinin toplamı',
'tours.scope.info':
'Aşağıda gösterilen turların türe göre toplamı; filtre ikisini de daraltır. Birkaç günde olan bir tur bir kez sayılır. Üç zaman türü ayrı kalır: rotalamanın hesapladığı, bir içe aktarmanın getirdiği ve molalarla planlanan. Türler arasında hiçbir şey toplanmaz.',
'tours.scope.partial': 'Eksik toplam',
'tours.scope.partialBody':
'{of} turdan {known} için biliniyor; diğerlerinin değeri yok ve dışarıda kalıyor, bu yüzden toplam en az bu kadar.',
'tours.scope.info': 'Her tur bir kez sayılır. Süreler türüne göre ayrı kalır ve türler arasında toplanmaz.',
'tours.scope.partialBody': '{of} turdan {known} için biliniyor',
'tours.scope.routed': 'Hesaplanan süre',
'tours.scope.imported': 'İçe aktarmalardan süre',
'tours.scope.planned': 'Molalarla planlanan',
@@ -405,7 +402,6 @@ const tours: TranslationStrings = {
'tours.paddle.lockTransit': 'Geçiş',
'tours.paddle.lockBypassed': 'Etrafından taşınıyor, taşıma sayılır',
'tours.paddle.time.title': 'Süre tahmini',
'tours.paddle.time.assumption': 'Planlama değerleri',
'tours.paddle.time.assumptionInfo':
'TREK suda 4 km/sa, taşırken 2 km/sa ve her taşımada sudan çıkıp yeniden girmek için 10 dakika ile başlar. Bunlar ayarlanacak başlangıç değerleridir, norm ya da ölçüm değildir. Kilit süreleri yalnızca senin girdiğin değerlerdir.',
'tours.paddle.time.water': 'Suda',
+2 -6
View File
@@ -280,11 +280,8 @@ const tours: TranslationStrings = {
'tours.filter.empty': 'Жоден похід не відповідає цим фільтрам.',
'tours.filter.reset': 'Скинути фільтри',
'tours.scope.title': 'Підсумки подорожі',
'tours.scope.info':
'Скільки разом дають маршрути нижче, за типами; фільтр обмежує і те, і те. Маршрут на кількох днях рахується один раз. Три види часу лишаються окремо: розрахований побудовою маршруту, взятий з імпорту і запланований з перервами. Між типами нічого не додається.',
'tours.scope.partial': 'Неповна сума',
'tours.scope.partialBody':
'Відомо для {known} з {of} маршрутів; інші не мають значення й не враховані, тож сума щонайменше така.',
'tours.scope.info': 'Кожен маршрут рахується один раз. Час розділено за видами й не додається між типами.',
'tours.scope.partialBody': 'відомо для {known} з {of} маршрутів',
'tours.scope.routed': 'Розрахований час',
'tours.scope.imported': 'Час з імпорту',
'tours.scope.planned': 'План з перервами',
@@ -426,7 +423,6 @@ const tours: TranslationStrings = {
'tours.paddle.lockTransit': 'Проходження',
'tours.paddle.lockBypassed': 'Обнесено, рахується обнос',
'tours.paddle.time.title': 'Оцінка часу',
'tours.paddle.time.assumption': 'Планові значення',
'tours.paddle.time.assumptionInfo':
'TREK починає з 4 км/год на воді, 2 км/год під час перенесення та 10 хвилин на вихід і повторний спуск на кожному обносі. Це початкові значення для налаштування, а не норми й не вимірювання. Для шлюзів враховується лише той час, який ви вводите.',
'tours.paddle.time.water': 'На воді',
+2 -6
View File
@@ -267,11 +267,8 @@ const tours: TranslationStrings = {
'tours.filter.empty': 'Không có chuyến đi bộ nào khớp với bộ lọc này.',
'tours.filter.reset': 'Đặt lại bộ lọc',
'tours.scope.title': 'Tổng của chuyến đi',
'tours.scope.info':
'Tổng của các chuyến hiển thị bên dưới, theo loại; bộ lọc thu hẹp cả hai. Một chuyến ở nhiều ngày chỉ tính một lần. Ba loại thời gian luôn tách riêng: thời gian định tuyến tính ra, thời gian đi kèm bản nhập và thời gian đã lên kế hoạch kể cả nghỉ. Không cộng gộp giữa các loại.',
'tours.scope.partial': 'Tổng chưa đầy đủ',
'tours.scope.partialBody':
'Đã biết cho {known} trên {of} chuyến; các chuyến còn lại không có giá trị và bị bỏ qua, nên tổng ít nhất là chừng này.',
'tours.scope.info': 'Mỗi chuyến chỉ tính một lần. Thời gian tách theo loại và không cộng giữa các kiểu.',
'tours.scope.partialBody': 'đã biết cho {known} trên {of} chuyến',
'tours.scope.routed': 'Thời gian định tuyến',
'tours.scope.imported': 'Thời gian từ bản nhập',
'tours.scope.planned': 'Kế hoạch gồm nghỉ',
@@ -401,7 +398,6 @@ const tours: TranslationStrings = {
'tours.paddle.lockTransit': 'Qua âu',
'tours.paddle.lockBypassed': 'Khiêng vòng qua, tính theo đoạn khiêng thuyền',
'tours.paddle.time.title': 'Ước tính thời gian',
'tours.paddle.time.assumption': 'Giá trị lập kế hoạch',
'tours.paddle.time.assumptionInfo':
'TREK bắt đầu với 4 km/h trên nước, 2 km/h khi khiêng thuyền và 10 phút để lên bờ rồi xuống nước lại ở mỗi đoạn khiêng thuyền. Đây là giá trị ban đầu để bạn điều chỉnh, không phải tiêu chuẩn và không phải số đo. Thời gian qua âu thuyền chỉ là những gì bạn nhập.',
'tours.paddle.time.water': 'Trên nước',
+2 -5
View File
@@ -244,10 +244,8 @@ const tours: TranslationStrings = {
'tours.filter.empty': '沒有符合這些篩選條件的遊程。',
'tours.filter.reset': '重設篩選',
'tours.scope.title': '行程總覽',
'tours.scope.info':
'下方顯示的路線依類型的合計;篩選同時作用於兩者。在多天中的路線只計一次。三種時間分開顯示:路線規劃計算的時間、匯入自帶的時間,以及含休息的計畫時間。不同類型之間不相加。',
'tours.scope.partial': '不完整的合計',
'tours.scope.partialBody': '{of} 條路線中有 {known} 條有此值;其餘沒有數值未計入,因此合計至少為此數。',
'tours.scope.info': '每條路線只計一次。時間依種類分開,不跨類型相加。',
'tours.scope.partialBody': '{of} 條路線中 {known} 條已知',
'tours.scope.routed': '規劃時間',
'tours.scope.imported': '匯入時間',
'tours.scope.planned': '含休息的計畫',
@@ -369,7 +367,6 @@ const tours: TranslationStrings = {
'tours.paddle.lockTransit': '過閘',
'tours.paddle.lockBypassed': '繞行搬運,以陸上搬運計算',
'tours.paddle.time.title': '時間估算',
'tours.paddle.time.assumption': '規劃假設',
'tours.paddle.time.assumptionInfo':
'TREK 預設水上速度 4 km/h、搬運速度 2 km/h,每次陸上搬運另計 10 分鐘上岸與重新下水。這些是可調整的起始值,不是標準,也不是量測值。船閘時間只採用你輸入的數值。',
'tours.paddle.time.water': '水上划行',
+2 -5
View File
@@ -245,10 +245,8 @@ const tours: TranslationStrings = {
'tours.filter.empty': '没有符合这些筛选条件的路线。',
'tours.filter.reset': '重置筛选',
'tours.scope.title': '行程汇总',
'tours.scope.info':
'下方显示的路线按类型的合计;筛选同时作用于两者。在多天中的路线只计一次。三种时间分开显示:路线规划计算的时间、导入自带的时间,以及含休息的计划时间。不同类型之间不相加。',
'tours.scope.partial': '不完整的合计',
'tours.scope.partialBody': '{of} 条路线中有 {known} 条有此值;其余没有数值未计入,因此合计至少为此数。',
'tours.scope.info': '每条路线只计一次。时间按种类分开,不跨类型相加。',
'tours.scope.partialBody': '{of} 条路线中 {known} 条已知',
'tours.scope.routed': '规划时间',
'tours.scope.imported': '导入时间',
'tours.scope.planned': '含休息的计划',
@@ -370,7 +368,6 @@ const tours: TranslationStrings = {
'tours.paddle.lockTransit': '过闸',
'tours.paddle.lockBypassed': '已搬运绕过,按陆上搬运计',
'tours.paddle.time.title': '用时估算',
'tours.paddle.time.assumption': '规划假设',
'tours.paddle.time.assumptionInfo':
'TREK 的初始值为:水上 4 km/h,搬运 2 km/h,每处陆上搬运的上岸和重新下水共计 10 分钟。这些是可调整的初始值,不是标准,也不是实测值。船闸用时只取你输入的数值。',
'tours.paddle.time.water': '水上',