diff --git a/client/src/components/Tours/TourListFilterMenu.tsx b/client/src/components/Tours/TourListFilterMenu.tsx index ea702b629..54f352bc1 100644 --- a/client/src/components/Tours/TourListFilterMenu.tsx +++ b/client/src/components/Tours/TourListFilterMenu.tsx @@ -177,3 +177,26 @@ export default function TourListFilterMenu({ filters }: { filters: TourListFilte ); } + +/** What a list says when its filters leave nothing: so, and a way to clear them. */ +export function TourFilterEmpty({ filters }: { filters: TourListFilters }) { + const { t } = useTranslation(); + return ( +
+

+ {t('tours.filter.empty')} +

+ +
+ ); +} diff --git a/client/src/components/Tours/TourScopeSummary.tsx b/client/src/components/Tours/TourScopeSummary.tsx new file mode 100644 index 000000000..227e04af2 --- /dev/null +++ b/client/src/components/Tours/TourScopeSummary.tsx @@ -0,0 +1,175 @@ +import type { TourListItem, TourTypeKey } from '@trek/shared'; +import { ChartColumnBig, ChevronDown, Mountain, Route, Waves } from 'lucide-react'; +import { useMemo, useState, type ComponentType } from 'react'; +import { useTranslation } from '../../i18n'; +import { useSettingsStore } from '../../store/settingsStore'; +import { formatDistance, formatElevation } from '../../utils/units'; +import { fs } from '../shared/DialogShell'; +import { InfoDot, RailCard, RailFacts, type TourMetricField } from './tourParts'; +import { formatPlannedTourDuration } from './tourPresentation'; +import { tourScopeSummary, type ScopeSum, type TourTypeScope } from './tourScope'; + +type Translate = (key: string, params?: Record) => string; + +const TYPE_ICON: Partial>> = { + hike: Mountain, + 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 ( + + {value} + {label} + + ); +} + +/** + * 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. + */ +function sumFact( + label: string, + sum: ScopeSum, + format: (value: number) => string, + t: Translate +): TourMetricField | 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, + }; +} + +function typeFacts(scope: TourTypeScope, unit: Parameters[1], t: Translate): TourMetricField[] { + 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); +} + +/** One tour type: its name with how many tours and how many are on a day, then its sums. */ +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); + return ( +
+
+ + + + + {t(`tourTypes.${scope.type}`)} + + + {scope.count} + +
+ {facts.length > 0 ? ( + + ) : ( +

+ {t('tours.scope.noMeasures')} +

+ )} +
+ ); +} + +/** + * STA-01: what the trip's tours add up to, above their list, by type: how many, + * how many are on a day, distance, ascent and descent, and the three kinds of + * 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. + */ +export function TourScopeSummary({ tours }: { tours: readonly TourListItem[] }) { + const { t } = useTranslation(); + const [folded, setFolded] = useState(readFolded); + const scope = useMemo(() => tourScopeSummary(tours), [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 ( + } + title={t('tours.scope.title')} + aside={ + + + + + } + > +
+ + + +
+ {!folded && ( +
+ {scope.types.map((typeScope) => ( + + ))} +
+ )} +
+ ); +} diff --git a/client/src/components/Tours/ToursSidebar.tsx b/client/src/components/Tours/ToursSidebar.tsx index a289d8816..2f9f8d657 100644 --- a/client/src/components/Tours/ToursSidebar.tsx +++ b/client/src/components/Tours/ToursSidebar.tsx @@ -9,7 +9,7 @@ import { NEUTRAL_TINT, fs } from '../shared/DialogShell' import { Tooltip } from '../shared/Tooltip' import EmptyState from '../shared/EmptyState' import { TourDayMenu } from './tourParts' -import TourListFilterMenu from './TourListFilterMenu' +import TourListFilterMenu, { TourFilterEmpty } from './TourListFilterMenu' import { useTourListFilters } from './useTourListFilters' import TourGpxPreviewDialog from './TourGpxPreviewDialog' import { useTourGpxImport } from './useTourGpxImport' @@ -97,14 +97,7 @@ export default function ToursSidebar({ tripId, days, tours, loading = false, sel
{!loading && filters.emptyByFilter ? ( -
-

{t('tours.filter.empty')}

- -
+ ) : !loading && filters.filtered.length === 0 ? ( {t('tours.empty.body')}

} /> diff --git a/client/src/components/Tours/planner/TourPlannerPanels.test.tsx b/client/src/components/Tours/planner/TourPlannerPanels.test.tsx index 853e21ee0..3790eb439 100644 --- a/client/src/components/Tours/planner/TourPlannerPanels.test.tsx +++ b/client/src/components/Tours/planner/TourPlannerPanels.test.tsx @@ -151,8 +151,9 @@ describe('TourPlannerToursRail', () => { expect(screen.getByRole('complementary', { name: 'Tours of this trip' })).toBeInTheDocument() expect(screen.getByRole('option', { name: /Saved ridge walk/i })).toHaveTextContent('4 km') - expect(screen.getByText('Ascent')).toBeInTheDocument() - expect(screen.getByText('50 m')).toBeInTheDocument() + const overview = screen.getByTestId('tour-planner-overview') + expect(within(overview).getByText('Ascent')).toBeInTheDocument() + expect(within(overview).getByText('50 m')).toBeInTheDocument() expect(document.querySelector('svg[viewBox="0 0 440 100"]')).toBeInTheDocument() expect(screen.queryByText('Routing is planning assistance, not a safety guarantee.')).not.toBeInTheDocument() }) diff --git a/client/src/components/Tours/planner/TourPlannerPanels.tsx b/client/src/components/Tours/planner/TourPlannerPanels.tsx index e2d0fd51e..c2976bb2b 100644 --- a/client/src/components/Tours/planner/TourPlannerPanels.tsx +++ b/client/src/components/Tours/planner/TourPlannerPanels.tsx @@ -40,6 +40,9 @@ import { type AssignTourToDay, useTourDayAssignment } from '../useTourDayAssignm import WandererMark from '../wanderer/WandererMark' import { TourGpxExportBarButton } from '../TourGpxExportButton' import { TourRemoteChangeNotice, TourRevisionConflictDialog } from './TourRevisionNotice' +import TourListFilterMenu, { TourFilterEmpty } from '../TourListFilterMenu' +import { TourScopeSummary } from '../TourScopeSummary' +import { useTourListFilters } from '../useTourListFilters' const STATUS_KEY: Partial> = { dirty: 'tours.planner.status.dirty', @@ -645,6 +648,8 @@ export function TourPlannerToursRail({ planner, tours, days, loading, onAssignTo const distanceUnit = useSettingsStore(state => state.settings.distance_unit) const [pendingTour, setPendingTour] = useState(null) const dayAssignment = useTourDayAssignment({ canAssign, onAssignToDay }) + // STA-01: one filter for the list and the summary above it, the same as the plan's tour list. + const filters = useTourListFilters(tours) const isSaving = planner.isSaving const hasRoute = (planner.mode.type === 'new-draft' || planner.mode.type === 'edit-saved') && planner.route !== null // A paddle tour's heights are read off the banks, not the water: they stay unknown. @@ -723,17 +728,21 @@ export function TourPlannerToursRail({ planner, tours, days, loading, onAssignTo ) : tours.length === 0 ? (

{t('common.loading')}

) : ( + <> + + + {filters.emptyByFilter ? : ( } title={t('tours.mode.tours')} aside={ - {tours.length} + {filters.filtered.length} } label={t('tours.planner.tripTours')} > - {tours.map(tour => ( + {filters.filtered.map(tour => ( ))} + )} + )}
diff --git a/client/src/components/Tours/tourScope.test.tsx b/client/src/components/Tours/tourScope.test.tsx new file mode 100644 index 000000000..737129975 --- /dev/null +++ b/client/src/components/Tours/tourScope.test.tsx @@ -0,0 +1,130 @@ +import type { TourListItem } from '@trek/shared'; +import { beforeEach, describe, expect, it } from 'vitest'; +import { fireEvent, render, screen, within } from '../../../tests/helpers/render'; +import { TourScopeSummary } from './TourScopeSummary'; +import { tourScopeSummary } from './tourScope'; + +function tour(over: Partial & Pick): TourListItem { + return { + tour_type: 'hike', + name: `Tour ${over.place_id}`, + distance: null, + elevation_gain: null, + elevation_loss: null, + duration: null, + difficulty: null, + wanderer_ref: null, + match_confidence: null, + max_hiking_difficulty: 2, + planned: false, + caution: false, + has_waypoints: true, + origin: 'trek', + ...over, + } as TourListItem; +} + +const HIKE_A = tour({ + place_id: 1, + distance: 10, + elevation_gain: 600, + elevation_loss: 500, + duration: 200, + planned: true, +}); +const HIKE_B = tour({ + place_id: 2, + distance: 5.5, + elevation_gain: null, + elevation_loss: 100, + duration: 100, + break_additional_minutes: 30, +}); +const GPX_HIKE = tour({ + place_id: 3, + distance: 8, + elevation_gain: 300, + elevation_loss: 300, + duration: 150, + has_waypoints: false, + origin: 'gpx', +}); +const PADDLE = tour({ + place_id: 4, + tour_type: 'kayak', + distance: 12, + elevation_gain: 40, + elevation_loss: 40, + duration: 180, + planned: true, +}); + +describe('tourScopeSummary (STA-01)', () => { + it('counts each tour once, by type, the most tours first, with how many are on a day', () => { + const scope = tourScopeSummary([HIKE_A, PADDLE, HIKE_B, GPX_HIKE, HIKE_A]); + expect(scope.count).toBe(4); + expect(scope.onDays).toBe(2); + expect(scope.types.map((type) => [type.type, type.count, type.onDays])).toEqual([ + ['hike', 3, 1], + ['kayak', 1, 1], + ]); + }); + + it('sums what is known and says how much of the whole that is, never a zero for unknown', () => { + const [hike] = tourScopeSummary([HIKE_A, HIKE_B, GPX_HIKE]).types; + expect(hike.distanceKm).toEqual({ value: 23.5, known: 3, of: 3 }); + expect(hike.ascentM).toEqual({ value: 900, known: 2, of: 3 }); + const [none] = tourScopeSummary([tour({ place_id: 9 })]).types; + expect(none.distanceKm).toEqual({ value: null, known: 0, of: 1 }); + }); + + it('keeps the routed, the imported and the planned time apart', () => { + const [hike] = tourScopeSummary([HIKE_A, HIKE_B, GPX_HIKE]).types; + expect(hike.routedMinutes).toEqual({ value: 300, known: 2, of: 2 }); + expect(hike.importedMinutes).toEqual({ value: 150, known: 1, of: 1 }); + // Planned: each tour's own time plus its breaks (HIKE_B's 30 min). + expect(hike.plannedMinutes).toEqual({ value: 480, known: 3, of: 3 }); + }); + + it('sums no heights for paddling, not even those a recorded paddle track brought', () => { + const [paddle] = tourScopeSummary([PADDLE, { ...GPX_HIKE, place_id: 5, tour_type: 'kayak' }]).types; + expect(paddle.ascentM).toEqual({ value: null, known: 0, of: 0 }); + expect(paddle.descentM).toEqual({ value: null, known: 0, of: 0 }); + expect(paddle.distanceKm.value).toBe(20); + }); +}); + +describe('TourScopeSummary', () => { + beforeEach(() => localStorage.clear()); + + it('shows the counts and each type with its sums, an incomplete one marked as at least', () => { + render(); + const counts = screen.getByTestId('tour-scope-counts'); + 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(); + }); + + it('folds to its counts, and remembers that in this browser', () => { + const { unmount } = render(); + fireEvent.click(screen.getByTestId('tour-scope-toggle')); + expect(screen.queryByTestId('tour-scope-hike')).toBeNull(); + expect(screen.getByTestId('tour-scope-counts')).toBeInTheDocument(); + unmount(); + render(); + expect(screen.queryByTestId('tour-scope-hike')).toBeNull(); + expect(screen.getByTestId('tour-scope-toggle')).toHaveAttribute('aria-expanded', 'false'); + }); + + it('shows nothing for a trip without tours', () => { + const { container } = render(); + expect(container).toBeEmptyDOMElement(); + }); +}); diff --git a/client/src/components/Tours/tourScope.ts b/client/src/components/Tours/tourScope.ts new file mode 100644 index 000000000..5756ba372 --- /dev/null +++ b/client/src/components/Tours/tourScope.ts @@ -0,0 +1,89 @@ +import type { TourListItem, TourTypeKey } from '@trek/shared'; +import { heightsUnknown } from './tourFacts'; +import { tourPlannedTimes, tourSource } from './tourPresentation'; + +/** + * A sum over the tours that have the value, with how many have it of how many + * could: `value` is null when none has it, never a zero standing in for + * "unknown", and `known < of` says the sum is short of the whole. + */ +export interface ScopeSum { + value: number | null; + known: number; + of: number; +} + +/** One tour type's share of the trip: how many tours, how many on a day, and their measures. */ +export interface TourTypeScope { + type: TourTypeKey; + count: number; + onDays: number; + distanceKm: ScopeSum; + ascentM: ScopeSum; + descentM: ScopeSum; + /** The time the router worked out, for the tours planned in TREK. */ + routedMinutes: ScopeSum; + /** The time an import brought with it (a GPX file, a wanderer trail). */ + importedMinutes: ScopeSum; + /** The time planned for the day, breaks or the planner's own figure included. */ + plannedMinutes: ScopeSum; +} + +export interface TourScope { + count: number; + onDays: number; + /** By type, the most tours first; the time kinds are never added up across types. */ + types: TourTypeScope[]; +} + +/** A measure a type has no use for: no value, and nothing it is short of. */ +const NO_SUM: ScopeSum = { value: null, known: 0, of: 0 }; + +function sumOf(values: Array): ScopeSum { + const known = values.filter((value): value is number => value != null && Number.isFinite(value)); + return { + value: known.length ? known.reduce((total, value) => total + value, 0) : null, + known: known.length, + of: values.length, + }; +} + +function scopeOfType(type: TourTypeKey, tours: readonly TourListItem[]): TourTypeScope { + const planned = tours.filter((tour) => tourSource(tour) === 'trek'); + const imported = tours.filter((tour) => tourSource(tour) !== 'trek'); + // A paddle tour drawn in TREK has no true heights (they would be the banks'), so it does not count as having them. + const withHeights = tours.map((tour) => (heightsUnknown(tour) ? null : tour)); + // On the water a climb says nothing: paddling sums no heights, whatever a recorded track brought. + const heights = type !== 'kayak'; + return { + type, + count: tours.length, + onDays: tours.filter((tour) => tour.planned).length, + distanceKm: sumOf(tours.map((tour) => tour.distance)), + ascentM: heights ? sumOf(withHeights.map((tour) => tour?.elevation_gain)) : NO_SUM, + descentM: heights ? sumOf(withHeights.map((tour) => tour?.elevation_loss)) : NO_SUM, + routedMinutes: sumOf(planned.map((tour) => tour.duration)), + importedMinutes: sumOf(imported.map((tour) => tour.duration)), + plannedMinutes: sumOf(tours.map((tour) => tourPlannedTimes(tour).plannedTotalMinutes)), + }; +} + +/** + * STA-01: what the tours of a trip add up to, by type. Each tour counts once, + * however many days it is on; the list is the trip's tours, one row each, so + * a tour on three days is still one tour and one distance. + */ +export function tourScopeSummary(tours: readonly TourListItem[]): TourScope { + const byType = new Map(); + for (const tour of new Map(tours.map((tour) => [tour.place_id, tour])).values()) { + byType.set(tour.tour_type, [...(byType.get(tour.tour_type) ?? []), tour]); + } + const types = [...byType.entries()] + .map(([type, ofType]) => scopeOfType(type, ofType)) + .sort((a, b) => b.count - a.count || a.type.localeCompare(b.type)); + return { + count: types.reduce((total, scope) => total + scope.count, 0), + onDays: types.reduce((total, scope) => total + scope.onDays, 0), + types, + }; +} diff --git a/shared/src/i18n/ar/tours.ts b/shared/src/i18n/ar/tours.ts index 6b525af6b..9478272fa 100644 --- a/shared/src/i18n/ar/tours.ts +++ b/shared/src/i18n/ar/tours.ts @@ -277,6 +277,16 @@ const tours: TranslationStrings = { 'tours.filter.type': 'النوع', 'tours.filter.empty': 'لا توجد جولة تطابق هذه المرشحات.', 'tours.filter.reset': 'إعادة ضبط المرشحات', + 'tours.scope.title': 'إجمالي الرحلة', + 'tours.scope.info': + 'ما تجمعه الجولات المعروضة أدناه حسب النوع، والفلتر يحدّ من الاثنين. الجولة في عدة أيام تُحسب مرة واحدة. تبقى أنواع الوقت الثلاثة منفصلة: ما حسبه التوجيه، وما جاء مع الاستيراد، وما خُطط له مع الاستراحات. لا يُجمع شيء عبر الأنواع.', + 'tours.scope.partial': 'مجموع غير مكتمل', + 'tours.scope.partialBody': + 'معروف لـ {known} من {of} جولات؛ الباقي بلا قيمة ومستبعد، لذا المجموع على الأقل بهذا القدر.', + 'tours.scope.routed': 'وقت المسار المحسوب', + 'tours.scope.imported': 'الوقت من الاستيراد', + 'tours.scope.planned': 'المخطط مع الاستراحات', + 'tours.scope.noMeasures': 'لا مسافة أو ارتفاعات أو أوقات معروفة بعد.', 'tours.copy.action': 'حفظ كجولة جديدة', 'tours.copy.hint': 'ينسخ هذه الجولة كجولة جديدة مستقلة دون أيامها. يبقى الأصل كما هو.', 'tours.copy.name': '{name} (نسخة)', diff --git a/shared/src/i18n/az/tours.ts b/shared/src/i18n/az/tours.ts index 5e010a181..0988d18f4 100644 --- a/shared/src/i18n/az/tours.ts +++ b/shared/src/i18n/az/tours.ts @@ -266,6 +266,16 @@ const tours: TranslationStrings = { 'tours.filter.type': 'Növ', '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.routed': 'Marşrut vaxtı', + 'tours.scope.imported': 'İdxaldan vaxt', + 'tours.scope.planned': 'Fasilələrlə planlaşdırılan', + 'tours.scope.noMeasures': 'Hələ məsafə, hündürlük və ya vaxt məlum deyil.', 'tours.copy.action': 'Yeni tur kimi saxla', 'tours.copy.hint': 'Bu turu günləri olmadan yeni, müstəqil tur kimi kopyalayır. Orijinal olduğu kimi qalır.', 'tours.copy.name': '{name} (surət)', diff --git a/shared/src/i18n/br/tours.ts b/shared/src/i18n/br/tours.ts index eefc7e45d..8baff857a 100644 --- a/shared/src/i18n/br/tours.ts +++ b/shared/src/i18n/br/tours.ts @@ -271,6 +271,16 @@ const tours: TranslationStrings = { 'tours.filter.type': 'Tipo', '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.routed': 'Tempo calculado', + 'tours.scope.imported': 'Tempo das importações', + 'tours.scope.planned': 'Planejado com pausas', + 'tours.scope.noMeasures': 'Ainda sem distância, altitudes ou tempos conhecidos.', 'tours.copy.action': 'Salvar como novo passeio', 'tours.copy.hint': 'Copia este passeio como um novo passeio independente, sem os seus dias. O original fica como está.', diff --git a/shared/src/i18n/ca/tours.ts b/shared/src/i18n/ca/tours.ts index e964226bd..cb985f4dd 100644 --- a/shared/src/i18n/ca/tours.ts +++ b/shared/src/i18n/ca/tours.ts @@ -271,6 +271,16 @@ const tours: TranslationStrings = { 'tours.filter.type': 'Tipus', 'tours.filter.empty': 'Cap excursió no coincideix amb aquests filtres.', '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ò.', + 'tours.scope.routed': 'Temps calculat', + 'tours.scope.imported': 'Temps de les importacions', + 'tours.scope.planned': 'Planificat amb pauses', + 'tours.scope.noMeasures': 'Encara no hi ha distància, alçades ni temps coneguts.', 'tours.copy.action': 'Desa com a excursió nova', 'tours.copy.hint': 'Copia aquesta excursió com una excursió nova i independent, sense els seus dies. L’original es queda com està.', diff --git a/shared/src/i18n/cs/tours.ts b/shared/src/i18n/cs/tours.ts index bc8bf6ee9..6cdff071d 100644 --- a/shared/src/i18n/cs/tours.ts +++ b/shared/src/i18n/cs/tours.ts @@ -266,6 +266,16 @@ const tours: TranslationStrings = { 'tours.filter.type': 'Druh', '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.routed': 'Vypočtený čas', + 'tours.scope.imported': 'Čas z importů', + 'tours.scope.planned': 'Plán s přestávkami', + 'tours.scope.noMeasures': 'Zatím není známa vzdálenost, výšky ani časy.', 'tours.copy.action': 'Uložit jako novou túru', 'tours.copy.hint': 'Zkopíruje tuto túru jako novou, samostatnou túru bez jejích dnů. Originál zůstane, jak je.', 'tours.copy.name': '{name} (kopie)', diff --git a/shared/src/i18n/de/tours.ts b/shared/src/i18n/de/tours.ts index 98d1e3a55..40762e373 100644 --- a/shared/src/i18n/de/tours.ts +++ b/shared/src/i18n/de/tours.ts @@ -274,6 +274,16 @@ const tours: TranslationStrings = { 'tours.filter.type': 'Art', '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.routed': 'Routingzeit', + 'tours.scope.imported': 'Zeit laut Import', + 'tours.scope.planned': 'Geplant mit Pausen', + 'tours.scope.noMeasures': 'Noch keine Distanz, Höhen oder Zeiten bekannt.', 'tours.copy.action': 'Als neue Tour speichern', 'tours.copy.hint': 'Kopiert diese Tour als neue, eigenständige Tour ohne ihre Tage. Das Original bleibt, wie es ist.', 'tours.copy.name': '{name} (Kopie)', diff --git a/shared/src/i18n/en/tours.ts b/shared/src/i18n/en/tours.ts index 7319f7c72..e4a360063 100644 --- a/shared/src/i18n/en/tours.ts +++ b/shared/src/i18n/en/tours.ts @@ -265,6 +265,16 @@ const tours: TranslationStrings = { 'tours.filter.type': 'Type', '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.routed': 'Routed time', + 'tours.scope.imported': 'Time from imports', + 'tours.scope.planned': 'Planned with breaks', + 'tours.scope.noMeasures': 'No distance, heights or times known yet.', 'tours.copy.action': 'Save as new tour', 'tours.copy.hint': 'Copies this tour as a new, independent tour without its days. The original stays as it is.', 'tours.copy.name': '{name} (copy)', diff --git a/shared/src/i18n/es/tours.ts b/shared/src/i18n/es/tours.ts index ff859f017..0f9d9e958 100644 --- a/shared/src/i18n/es/tours.ts +++ b/shared/src/i18n/es/tours.ts @@ -270,6 +270,16 @@ const tours: TranslationStrings = { 'tours.filter.type': 'Tipo', 'tours.filter.empty': 'Ninguna excursión coincide con estos filtros.', '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.', + 'tours.scope.routed': 'Tiempo calculado', + 'tours.scope.imported': 'Tiempo de las importaciones', + 'tours.scope.planned': 'Planificado con pausas', + 'tours.scope.noMeasures': 'Aún no se conocen distancia, alturas ni tiempos.', 'tours.copy.action': 'Guardar como nueva excursión', 'tours.copy.hint': 'Copia esta excursión como una nueva excursión independiente, sin sus días. El original queda como está.', diff --git a/shared/src/i18n/et/tours.ts b/shared/src/i18n/et/tours.ts index c5c09cccd..6fb649fea 100644 --- a/shared/src/i18n/et/tours.ts +++ b/shared/src/i18n/et/tours.ts @@ -268,6 +268,16 @@ const tours: TranslationStrings = { 'tours.filter.type': 'Liik', '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.routed': 'Arvutatud aeg', + 'tours.scope.imported': 'Aeg importidest', + 'tours.scope.planned': 'Planeeritud koos pausidega', + 'tours.scope.noMeasures': 'Vahemaa, kõrgused ja ajad pole veel teada.', 'tours.copy.action': 'Salvesta uue matkana', 'tours.copy.hint': 'Kopeerib selle matka uue, iseseisva matkana ilma päevadeta. Originaal jääb samaks.', 'tours.copy.name': '{name} (koopia)', diff --git a/shared/src/i18n/fr/tours.ts b/shared/src/i18n/fr/tours.ts index d2f0f5019..b4086740e 100644 --- a/shared/src/i18n/fr/tours.ts +++ b/shared/src/i18n/fr/tours.ts @@ -276,6 +276,16 @@ const tours: TranslationStrings = { 'tours.filter.type': 'Type', // same-as-en 'tours.filter.empty': 'Aucune sortie ne correspond à ces filtres.', '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.", + 'tours.scope.routed': 'Temps calculé', + 'tours.scope.imported': 'Temps des imports', + 'tours.scope.planned': 'Prévu avec pauses', + 'tours.scope.noMeasures': "Aucune distance, altitude ni durée connue pour l'instant.", 'tours.copy.action': 'Enregistrer comme nouvelle sortie', 'tours.copy.hint': 'Copie cette sortie en une nouvelle sortie indépendante, sans ses jours. L’original reste tel quel.', diff --git a/shared/src/i18n/gr/tours.ts b/shared/src/i18n/gr/tours.ts index 6a5fc06c8..f0711efa4 100644 --- a/shared/src/i18n/gr/tours.ts +++ b/shared/src/i18n/gr/tours.ts @@ -277,6 +277,16 @@ const tours: TranslationStrings = { 'tours.filter.type': 'Είδος', 'tours.filter.empty': 'Καμία εκδρομή δεν ταιριάζει με αυτά τα φίλτρα.', 'tours.filter.reset': 'Επαναφορά φίλτρων', + 'tours.scope.title': 'Σύνολα ταξιδιού', + 'tours.scope.info': + 'Τι αθροίζουν οι διαδρομές που φαίνονται παρακάτω, ανά τύπο· το φίλτρο περιορίζει και τα δύο. Μια διαδρομή σε πολλές μέρες μετρά μία φορά. Τα τρία είδη χρόνου μένουν χωριστά: όσα υπολόγισε η δρομολόγηση, όσα έφερε μια εισαγωγή και όσα σχεδιάστηκαν με διαλείμματα. Τίποτα δεν προστίθεται μεταξύ τύπων.', + 'tours.scope.partial': 'Ελλιπές άθροισμα', + 'tours.scope.partialBody': + 'Γνωστό για {known} από {of} διαδρομές· οι υπόλοιπες δεν έχουν τιμή και μένουν έξω, άρα το σύνολο είναι τουλάχιστον τόσο.', + 'tours.scope.routed': 'Υπολογισμένος χρόνος', + 'tours.scope.imported': 'Χρόνος από εισαγωγές', + 'tours.scope.planned': 'Με διαλείμματα', + 'tours.scope.noMeasures': 'Δεν είναι ακόμη γνωστά απόσταση, υψόμετρα ή χρόνοι.', 'tours.copy.action': 'Αποθήκευση ως νέα εκδρομή', 'tours.copy.hint': 'Αντιγράφει αυτή την εκδρομή ως νέα, ανεξάρτητη εκδρομή χωρίς τις μέρες της. Το πρωτότυπο μένει ως έχει.', diff --git a/shared/src/i18n/hu/tours.ts b/shared/src/i18n/hu/tours.ts index 047c02ea1..122fc3a73 100644 --- a/shared/src/i18n/hu/tours.ts +++ b/shared/src/i18n/hu/tours.ts @@ -272,6 +272,16 @@ const tours: TranslationStrings = { 'tours.filter.type': 'Fajta', 'tours.filter.empty': 'Egy túra sem felel meg ezeknek a szűrőknek.', '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.', + 'tours.scope.routed': 'Számított idő', + 'tours.scope.imported': 'Idő az importokból', + 'tours.scope.planned': 'Tervezett, szünetekkel', + 'tours.scope.noMeasures': 'Még nem ismert távolság, magasság vagy idő.', 'tours.copy.action': 'Mentés új túraként', 'tours.copy.hint': 'Ezt a túrát új, önálló túraként másolja a napjai nélkül. Az eredeti változatlan marad.', 'tours.copy.name': '{name} (másolat)', diff --git a/shared/src/i18n/id/tours.ts b/shared/src/i18n/id/tours.ts index 771afb08d..a2d4ff0b1 100644 --- a/shared/src/i18n/id/tours.ts +++ b/shared/src/i18n/id/tours.ts @@ -265,6 +265,16 @@ const tours: TranslationStrings = { 'tours.filter.type': 'Jenis', '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.routed': 'Waktu perutean', + 'tours.scope.imported': 'Waktu dari impor', + 'tours.scope.planned': 'Rencana dengan istirahat', + 'tours.scope.noMeasures': 'Belum ada jarak, ketinggian, atau waktu yang diketahui.', 'tours.copy.action': 'Simpan sebagai tur baru', 'tours.copy.hint': 'Menyalin tur ini sebagai tur baru yang mandiri tanpa hari-harinya. Aslinya tetap seperti semula.', 'tours.copy.name': '{name} (salinan)', diff --git a/shared/src/i18n/it/tours.ts b/shared/src/i18n/it/tours.ts index 6a5ab638a..ec17ac577 100644 --- a/shared/src/i18n/it/tours.ts +++ b/shared/src/i18n/it/tours.ts @@ -272,6 +272,16 @@ const tours: TranslationStrings = { 'tours.filter.type': 'Tipo', 'tours.filter.empty': 'Nessun tour corrisponde a questi filtri.', '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.', + 'tours.scope.routed': 'Tempo calcolato', + 'tours.scope.imported': 'Tempo dalle importazioni', + 'tours.scope.planned': 'Pianificato con pause', + 'tours.scope.noMeasures': 'Nessuna distanza, quota o tempo ancora noto.', 'tours.copy.action': 'Salva come nuovo tour', 'tours.copy.hint': 'Copia questo tour come nuovo tour indipendente, senza i suoi giorni. L’originale resta com’è.', 'tours.copy.name': '{name} (copia)', diff --git a/shared/src/i18n/ja/tours.ts b/shared/src/i18n/ja/tours.ts index 2c0f12242..eec8e2324 100644 --- a/shared/src/i18n/ja/tours.ts +++ b/shared/src/i18n/ja/tours.ts @@ -259,6 +259,16 @@ const tours: TranslationStrings = { 'tours.filter.type': '種類', 'tours.filter.empty': 'この条件に合うツアーはありません。', 'tours.filter.reset': 'フィルターをリセット', + 'tours.scope.title': '旅の合計', + 'tours.scope.info': + '下に表示されたツアーの種類ごとの合計です。フィルターは両方に適用されます。複数の日にあるツアーは 1 回だけ数えます。3 種類の時間は分けたままです:ルーティングが計算した時間、インポートに含まれていた時間、休憩を含めて計画した時間。種類をまたいで合算はしません。', + 'tours.scope.partial': '不完全な合計', + 'tours.scope.partialBody': + '{of} 件中 {known} 件のツアーで判明しています。残りは値がないため含まれず、合計は少なくともこの値です。', + 'tours.scope.routed': 'ルート計算の時間', + 'tours.scope.imported': 'インポートの時間', + 'tours.scope.planned': '休憩込みの計画', + 'tours.scope.noMeasures': '距離、高度、時間はまだわかりません。', 'tours.copy.action': '新しいツアーとして保存', 'tours.copy.hint': 'このツアーを日程なしの独立した新しいツアーとしてコピーします。元のツアーはそのままです。', 'tours.copy.name': '{name}(コピー)', diff --git a/shared/src/i18n/ko/tours.ts b/shared/src/i18n/ko/tours.ts index b15a1b7af..db996c209 100644 --- a/shared/src/i18n/ko/tours.ts +++ b/shared/src/i18n/ko/tours.ts @@ -257,6 +257,16 @@ const tours: TranslationStrings = { 'tours.filter.type': '종류', 'tours.filter.empty': '이 필터에 맞는 투어가 없습니다.', 'tours.filter.reset': '필터 초기화', + 'tours.scope.title': '여행 합계', + 'tours.scope.info': + '아래 표시된 투어의 유형별 합계입니다. 필터는 둘 다에 적용됩니다. 여러 날에 있는 투어는 한 번만 셉니다. 세 가지 시간은 따로 유지됩니다: 경로 계산이 낸 시간, 가져오기에 포함된 시간, 휴식을 포함해 계획한 시간. 유형 간에는 더하지 않습니다.', + 'tours.scope.partial': '불완전한 합계', + 'tours.scope.partialBody': + '{of}개 투어 중 {known}개만 값이 있습니다. 나머지는 값이 없어 빠졌으므로 합계는 최소 이만큼입니다.', + 'tours.scope.routed': '경로 계산 시간', + 'tours.scope.imported': '가져오기 시간', + 'tours.scope.planned': '휴식 포함 계획', + 'tours.scope.noMeasures': '아직 알려진 거리, 고도, 시간이 없습니다.', 'tours.copy.action': '새 투어로 저장', 'tours.copy.hint': '이 투어를 날짜 없이 독립된 새 투어로 복사합니다. 원본은 그대로입니다.', 'tours.copy.name': '{name} (사본)', diff --git a/shared/src/i18n/nl/tours.ts b/shared/src/i18n/nl/tours.ts index 6a3c7802b..c6ed9ad58 100644 --- a/shared/src/i18n/nl/tours.ts +++ b/shared/src/i18n/nl/tours.ts @@ -272,6 +272,16 @@ const tours: TranslationStrings = { 'tours.filter.type': 'Soort', 'tours.filter.empty': 'Geen tocht past bij deze filters.', '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.', + 'tours.scope.routed': 'Berekende tijd', + 'tours.scope.imported': 'Tijd uit imports', + 'tours.scope.planned': 'Gepland met pauzes', + 'tours.scope.noMeasures': 'Nog geen afstand, hoogtes of tijden bekend.', 'tours.copy.action': 'Opslaan als nieuwe tocht', 'tours.copy.hint': 'Kopieert deze tocht als nieuwe, zelfstandige tocht zonder zijn dagen. Het origineel blijft zoals het is.', diff --git a/shared/src/i18n/pl/tours.ts b/shared/src/i18n/pl/tours.ts index 92d9cd2da..c8016d048 100644 --- a/shared/src/i18n/pl/tours.ts +++ b/shared/src/i18n/pl/tours.ts @@ -279,6 +279,16 @@ const tours: TranslationStrings = { 'tours.filter.type': 'Rodzaj', 'tours.filter.empty': 'Żadna trasa nie pasuje do tych filtrów.', '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.', + 'tours.scope.routed': 'Czas wyliczony', + 'tours.scope.imported': 'Czas z importów', + 'tours.scope.planned': 'Plan z przerwami', + 'tours.scope.noMeasures': 'Brak jeszcze znanego dystansu, wysokości i czasów.', 'tours.copy.action': 'Zapisz jako nową trasę', 'tours.copy.hint': 'Kopiuje tę trasę jako nową, niezależną trasę bez jej dni. Oryginał zostaje bez zmian.', 'tours.copy.name': '{name} (kopia)', diff --git a/shared/src/i18n/ru/tours.ts b/shared/src/i18n/ru/tours.ts index 84a98a2a2..b03d56a60 100644 --- a/shared/src/i18n/ru/tours.ts +++ b/shared/src/i18n/ru/tours.ts @@ -282,6 +282,16 @@ const tours: TranslationStrings = { 'tours.filter.type': 'Тип', 'tours.filter.empty': 'Ни один поход не подходит под эти фильтры.', 'tours.filter.reset': 'Сбросить фильтры', + 'tours.scope.title': 'Итоги поездки', + 'tours.scope.info': + 'Сколько в сумме дают маршруты ниже, по типам; фильтр ограничивает и то, и другое. Маршрут на нескольких днях считается один раз. Три вида времени остаются раздельными: рассчитанное построением маршрута, взятое из импорта и запланированное с перерывами. Между типами ничего не складывается.', + 'tours.scope.partial': 'Неполная сумма', + 'tours.scope.partialBody': + 'Известно для {known} из {of} маршрутов; у остальных значения нет, и они не учтены, поэтому сумма не меньше этой.', + 'tours.scope.routed': 'Расчётное время', + 'tours.scope.imported': 'Время из импорта', + 'tours.scope.planned': 'План с перерывами', + 'tours.scope.noMeasures': 'Расстояние, высоты и время пока неизвестны.', 'tours.copy.action': 'Сохранить как новый поход', 'tours.copy.hint': 'Копирует этот поход как новый, самостоятельный поход без его дней. Оригинал остаётся как есть.', 'tours.copy.name': '{name} (копия)', diff --git a/shared/src/i18n/sk/tours.ts b/shared/src/i18n/sk/tours.ts index 62c7a9b2d..6eb8b7a7c 100644 --- a/shared/src/i18n/sk/tours.ts +++ b/shared/src/i18n/sk/tours.ts @@ -267,6 +267,16 @@ const tours: TranslationStrings = { 'tours.filter.type': 'Druh', '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.routed': 'Vypočítaný čas', + 'tours.scope.imported': 'Čas z importov', + 'tours.scope.planned': 'Plán s prestávkami', + 'tours.scope.noMeasures': 'Zatiaľ nie je známa vzdialenosť, výšky ani časy.', 'tours.copy.action': 'Uložiť ako novú túru', 'tours.copy.hint': 'Skopíruje túto túru ako novú, samostatnú túru bez jej dní. Originál zostane, ako je.', 'tours.copy.name': '{name} (kópia)', diff --git a/shared/src/i18n/sv/tours.ts b/shared/src/i18n/sv/tours.ts index 43a1ebd1c..ab294dfca 100644 --- a/shared/src/i18n/sv/tours.ts +++ b/shared/src/i18n/sv/tours.ts @@ -264,6 +264,16 @@ const tours: TranslationStrings = { 'tours.filter.type': 'Typ', '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.routed': 'Beräknad tid', + 'tours.scope.imported': 'Tid från importer', + 'tours.scope.planned': 'Planerat med pauser', + 'tours.scope.noMeasures': 'Ingen sträcka, höjd eller tid känd ännu.', 'tours.copy.action': 'Spara som ny tur', 'tours.copy.hint': 'Kopierar turen som en ny, fristående tur utan dess dagar. Originalet förblir som det är.', 'tours.copy.name': '{name} (kopia)', diff --git a/shared/src/i18n/th/tours.ts b/shared/src/i18n/th/tours.ts index 64ec0ac6b..d452534b3 100644 --- a/shared/src/i18n/th/tours.ts +++ b/shared/src/i18n/th/tours.ts @@ -253,6 +253,16 @@ const tours: TranslationStrings = { 'tours.filter.type': 'ประเภท', 'tours.filter.empty': 'ไม่มีทัวร์ที่ตรงกับตัวกรองเหล่านี้', 'tours.filter.reset': 'รีเซ็ตตัวกรอง', + 'tours.scope.title': 'ภาพรวมของทริป', + 'tours.scope.info': + 'ผลรวมของทัวร์ที่แสดงด้านล่างแยกตามประเภท ตัวกรองมีผลกับทั้งสองส่วน ทัวร์ที่อยู่หลายวันนับครั้งเดียว เวลาสามแบบแยกกันเสมอ: เวลาที่การคำนวณเส้นทางได้ เวลาที่มากับการนำเข้า และเวลาที่วางแผนรวมพัก ไม่มีการรวมข้ามประเภท', + 'tours.scope.partial': 'ผลรวมไม่ครบ', + 'tours.scope.partialBody': + 'ทราบค่าของทัวร์ {known} จาก {of} รายการ ส่วนที่เหลือไม่มีค่าและไม่ได้นับ ผลรวมจึงอย่างน้อยเท่านี้', + 'tours.scope.routed': 'เวลาจากการคำนวณเส้นทาง', + 'tours.scope.imported': 'เวลาจากการนำเข้า', + 'tours.scope.planned': 'วางแผนรวมพัก', + 'tours.scope.noMeasures': 'ยังไม่ทราบระยะทาง ความสูง หรือเวลา', 'tours.copy.action': 'บันทึกเป็นทัวร์ใหม่', 'tours.copy.hint': 'คัดลอกทัวร์นี้เป็นทัวร์ใหม่ที่แยกเป็นอิสระโดยไม่รวมวัน ต้นฉบับจะคงเดิม', 'tours.copy.name': '{name} (สำเนา)', diff --git a/shared/src/i18n/tr/tours.ts b/shared/src/i18n/tr/tours.ts index 164132c1d..04bd9d3c6 100644 --- a/shared/src/i18n/tr/tours.ts +++ b/shared/src/i18n/tr/tours.ts @@ -266,6 +266,16 @@ const tours: TranslationStrings = { 'tours.filter.type': 'Tür', '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.routed': 'Hesaplanan süre', + 'tours.scope.imported': 'İçe aktarmalardan süre', + 'tours.scope.planned': 'Molalarla planlanan', + 'tours.scope.noMeasures': 'Henüz bilinen mesafe, yükseklik veya süre yok.', 'tours.copy.action': 'Yeni tur olarak kaydet', 'tours.copy.hint': 'Bu turu günleri olmadan yeni ve bağımsız bir tur olarak kopyalar. Asıl tur olduğu gibi kalır.', 'tours.copy.name': '{name} (kopya)', diff --git a/shared/src/i18n/uk/tours.ts b/shared/src/i18n/uk/tours.ts index ef0325c55..00f93dd14 100644 --- a/shared/src/i18n/uk/tours.ts +++ b/shared/src/i18n/uk/tours.ts @@ -279,6 +279,16 @@ const tours: TranslationStrings = { 'tours.filter.type': 'Тип', 'tours.filter.empty': 'Жоден похід не відповідає цим фільтрам.', 'tours.filter.reset': 'Скинути фільтри', + 'tours.scope.title': 'Підсумки подорожі', + 'tours.scope.info': + 'Скільки разом дають маршрути нижче, за типами; фільтр обмежує і те, і те. Маршрут на кількох днях рахується один раз. Три види часу лишаються окремо: розрахований побудовою маршруту, взятий з імпорту і запланований з перервами. Між типами нічого не додається.', + 'tours.scope.partial': 'Неповна сума', + 'tours.scope.partialBody': + 'Відомо для {known} з {of} маршрутів; інші не мають значення й не враховані, тож сума щонайменше така.', + 'tours.scope.routed': 'Розрахований час', + 'tours.scope.imported': 'Час з імпорту', + 'tours.scope.planned': 'План з перервами', + 'tours.scope.noMeasures': 'Відстань, висоти й час поки невідомі.', 'tours.copy.action': 'Зберегти як новий похід', 'tours.copy.hint': 'Копіює цей похід як новий, самостійний похід без його днів. Оригінал лишається як є.', 'tours.copy.name': '{name} (копія)', diff --git a/shared/src/i18n/vi/tours.ts b/shared/src/i18n/vi/tours.ts index f8e5a5e79..284b8fc71 100644 --- a/shared/src/i18n/vi/tours.ts +++ b/shared/src/i18n/vi/tours.ts @@ -266,6 +266,16 @@ const tours: TranslationStrings = { 'tours.filter.type': 'Loại', '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.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ỉ', + 'tours.scope.noMeasures': 'Chưa biết quãng đường, độ cao hay thời gian.', 'tours.copy.action': 'Lưu thành chuyến đi bộ mới', 'tours.copy.hint': 'Sao chép chuyến này thành một chuyến đi bộ mới, độc lập, không kèm các ngày. Bản gốc giữ nguyên.', 'tours.copy.name': '{name} (bản sao)', diff --git a/shared/src/i18n/zh-TW/tours.ts b/shared/src/i18n/zh-TW/tours.ts index 5e9a891a9..1856d0a04 100644 --- a/shared/src/i18n/zh-TW/tours.ts +++ b/shared/src/i18n/zh-TW/tours.ts @@ -243,6 +243,15 @@ const tours: TranslationStrings = { 'tours.filter.type': '類型', 'tours.filter.empty': '沒有符合這些篩選條件的遊程。', 'tours.filter.reset': '重設篩選', + 'tours.scope.title': '行程總覽', + 'tours.scope.info': + '下方顯示的路線依類型的合計;篩選同時作用於兩者。在多天中的路線只計一次。三種時間分開顯示:路線規劃計算的時間、匯入自帶的時間,以及含休息的計畫時間。不同類型之間不相加。', + 'tours.scope.partial': '不完整的合計', + 'tours.scope.partialBody': '{of} 條路線中有 {known} 條有此值;其餘沒有數值未計入,因此合計至少為此數。', + 'tours.scope.routed': '規劃時間', + 'tours.scope.imported': '匯入時間', + 'tours.scope.planned': '含休息的計畫', + 'tours.scope.noMeasures': '尚無已知的距離、高度或時間。', 'tours.copy.action': '另存為新遊程', 'tours.copy.hint': '將此遊程複製為不含日程的獨立新遊程。原遊程保持不變。', 'tours.copy.name': '{name}(副本)', diff --git a/shared/src/i18n/zh/tours.ts b/shared/src/i18n/zh/tours.ts index d364f3385..3a851600b 100644 --- a/shared/src/i18n/zh/tours.ts +++ b/shared/src/i18n/zh/tours.ts @@ -244,6 +244,15 @@ const tours: TranslationStrings = { 'tours.filter.type': '类型', 'tours.filter.empty': '没有符合这些筛选条件的路线。', 'tours.filter.reset': '重置筛选', + 'tours.scope.title': '行程汇总', + 'tours.scope.info': + '下方显示的路线按类型的合计;筛选同时作用于两者。在多天中的路线只计一次。三种时间分开显示:路线规划计算的时间、导入自带的时间,以及含休息的计划时间。不同类型之间不相加。', + 'tours.scope.partial': '不完整的合计', + 'tours.scope.partialBody': '{of} 条路线中有 {known} 条有此值;其余没有数值未计入,因此合计至少为此数。', + 'tours.scope.routed': '规划时间', + 'tours.scope.imported': '导入时间', + 'tours.scope.planned': '含休息的计划', + 'tours.scope.noMeasures': '尚无已知的距离、高度或时间。', 'tours.copy.action': '另存为新路线', 'tours.copy.hint': '将此路线复制为不含日程的独立新路线。原路线保持不变。', 'tours.copy.name': '{name}(副本)',